Projeto Agenda de Contatos (JSON)

A Ana quer guardar os telefones dos clientes da lojinha num programa: cadastrar, listar, procurar, apagar e, o mais importante, não perder nada quando o programa fecha. É o primeiro projeto que lembra das coisas: a agenda fica num arquivo contatos.json, que o programa lê ao abrir e grava ao sair. No caminho, você monta um menu, trabalha com uma List de records e conhece o JSON, o formato que quase todo programa usa para guardar e trocar dados.

Você vai praticar

O que vamos construir

A agenda pronta. Na primeira vez, ela começa vazia, e a Ana cadastra dois contatos:

$ dotnet run
=== Agenda da Ana ===
Contatos carregados: 0

1 Adicionar  2 Listar  3 Buscar  4 Remover  0 Sair
Opção: 2
Nenhum contato.

1 Adicionar  2 Listar  3 Buscar  4 Remover  0 Sair
Opção: 1
Nome: José Carlos
Telefone: 31 2222-1111
José Carlos adicionado.

1 Adicionar  2 Listar  3 Buscar  4 Remover  0 Sair
Opção: 1
Nome: Conceição
Telefone: 11 91234-5678
Conceição adicionado.

1 Adicionar  2 Listar  3 Buscar  4 Remover  0 Sair
Opção: 0
Agenda salva em contatos.json. Até logo!

No dia seguinte, os dois continuam lá:

$ dotnet run
=== Agenda da Ana ===
Contatos carregados: 2

1 Adicionar  2 Listar  3 Buscar  4 Remover  0 Sair
Opção: 2
  José Carlos: 31 2222-1111
  Conceição: 11 91234-5678

1 Adicionar  2 Listar  3 Buscar  4 Remover  0 Sair
Opção: 1
Nome: Bruno Lima
Telefone: 11 98765-4321
Bruno Lima adicionado.

1 Adicionar  2 Listar  3 Buscar  4 Remover  0 Sair
Opção: 3
Buscar por: lima
  Bruno Lima: 11 98765-4321

1 Adicionar  2 Listar  3 Buscar  4 Remover  0 Sair
Opção: 0
Agenda salva em contatos.json. Até logo!
$ cat contatos.json
[
  {
    "Nome": "José Carlos",
    "Telefone": "31 2222-1111"
  },
  {
    "Nome": "Conceição",
    "Telefone": "11 91234-5678"
  },
  {
    "Nome": "Bruno Lima",
    "Telefone": "11 98765-4321"
  }
]
  1. O menu, a lista e o record (adicionar e listar).
  2. Buscar e remover.
  3. Salvar num arquivo JSON.
  4. Carregar ao abrir (e o arquivo com defeito).

Antes de começar

Você precisa ter visto List<T>, Records, switch, Funções Locais e, para as versões 3 e 4, JSON e try e catch.

Este projeto tem dois arquivos e usa JSON, por isso é um projeto, e não um arquivo solto. (No dotnet run arquivo.cs, o .NET 10 desliga o jeito simples de usar o JsonSerializer; a aula JSON explica por quê.) A Ana cria o projeto em ~/projetos:

$ mkdir -p projetos
$ cd projetos
$ dotnet new console -o Agenda
O modelo "Aplicativo do Console" foi criado com êxito.

Processando ações pós-criação...
Restaurando /home/ana/projetos/Agenda/Agenda.csproj:
  Determinando os projetos a serem restaurados...
  /home/ana/projetos/Agenda/Agenda.csproj restaurado (em 104 ms).
A restauração foi bem-sucedida.

$ cd Agenda

O dotnet new já cria um Program.cs com um "Hello, World!". A Ana troca o conteúdo dele pelo código da versão 1 e cria, ao lado, o arquivo Contato.cs, com uma linha só. Todos os .cs da pasta são compilados juntos, então o Program.cs enxerga o Contato. Nas caixas Experimente, os dois arquivos aparecem como abas.

record Contato(string Nome, string Telefone);

Versão 1 · O menu, a lista e o record

Um contato tem nome e telefone: um record resolve. Os contatos ficam numa List<Contato>, uma lista que cresce conforme a Ana cadastra (um array teria tamanho fixo). E o programa é um menu: mostra as opções, lê a escolha, faz a tarefa e volta para o menu, até a Ana escolher 0.

List<Contato> contatos = new();

Console.WriteLine("=== Agenda da Ana ===");
while (true)
{
    Console.WriteLine();
    Console.WriteLine("1 Adicionar  2 Listar  0 Sair");
    Console.Write("Opção: ");
    string opcao = Console.ReadLine() ?? "0";
    if (opcao == "0")
    {
        break;
    }

    switch (opcao)
    {
        case "1":
            Adicionar();
            break;
        case "2":
            Listar();
            break;
        default:
            Console.WriteLine("Opção inválida.");
            break;
    }
}
Console.WriteLine("Até logo!");

void Adicionar()
{
    Console.Write("Nome: ");
    string nome = (Console.ReadLine() ?? "").Trim();
    Console.Write("Telefone: ");
    string telefone = (Console.ReadLine() ?? "").Trim();
    if (nome == "")
    {
        Console.WriteLine("O nome não pode ficar vazio.");
        return;
    }
    contatos.Add(new Contato(nome, telefone));
    Console.WriteLine($"{nome} adicionado.");
}

void Listar()
{
    if (contatos.Count == 0)
    {
        Console.WriteLine("Nenhum contato.");
        return;
    }
    foreach (Contato contato in contatos)
    {
        Console.WriteLine($"  {contato.Nome}: {contato.Telefone}");
    }
}
List<Contato> contatos = new();
Uma lista vazia de contatos. O new() sem o nome do tipo funciona porque o tipo já está escrito à esquerda (target-typed new).
string opcao = Console.ReadLine() ?? "0";
A opção fica como texto, e o switch compara textos. Se a entrada acabar (null), vale como "0": sai do programa, em vez de repetir o menu para sempre.
case "1": Adicionar(); break;
Cada opção chama uma função local, declarada mais abaixo. Assim o menu fica curto e cada tarefa tem nome.
void Adicionar()
Uma função local: um método escrito direto no arquivo, depois das instruções. Sem static, ela enxerga as variáveis do programa principal, como contatos.
(Console.ReadLine() ?? "").Trim()
Lê o texto e tira os espaços das pontas: " Bruno " vira "Bruno".
if (nome == "") { …; return; }
Um contato sem nome não entra. O return sai só da função, e o programa volta ao menu.
contatos.Add(new Contato(nome, telefone));
Cria o contato e põe no fim da lista.
contatos.Count
Quantos contatos a lista tem agora.
$ dotnet run
=== Agenda da Ana ===

1 Adicionar  2 Listar  0 Sair
Opção: 1
Nome: Bruno Lima
Telefone: 11 98765-4321
Bruno Lima adicionado.

1 Adicionar  2 Listar  0 Sair
Opção: 1
Nome: Carla
Telefone: 21 3333-4444
Carla adicionado.

1 Adicionar  2 Listar  0 Sair
Opção: 2
  Bruno Lima: 11 98765-4321
  Carla: 21 3333-4444

1 Adicionar  2 Listar  0 Sair
Opção: 7
Opção inválida.

1 Adicionar  2 Listar  0 Sair
Opção: 1
Nome:
Telefone:
O nome não pode ficar vazio.

1 Adicionar  2 Listar  0 Sair
Opção: 0
Até logo!

Missão (versão 1): a opção 2 ainda não existe: o programa responde Opção inválida.. Acrescente o case "2": que chama Listar(); (com o break). A entrada cadastra o Bruno, pede a lista e sai.

List<Contato> contatos = new();

Console.WriteLine("=== Agenda da Ana ===");
while (true)
{
    Console.WriteLine();
    Console.WriteLine("1 Adicionar  2 Listar  0 Sair");
    Console.Write("Opção: ");
    string opcao = Console.ReadLine() ?? "0";
    if (opcao == "0")
    {
        break;
    }

    switch (opcao)
    {
        case "1":
            Adicionar();
            break;
        // acrescente aqui o case "2"
        default:
            Console.WriteLine("Opção inválida.");
            break;
    }
}
Console.WriteLine("Até logo!");

void Adicionar()
{
    Console.Write("Nome: ");
    string nome = (Console.ReadLine() ?? "").Trim();
    Console.Write("Telefone: ");
    string telefone = (Console.ReadLine() ?? "").Trim();
    if (nome == "")
    {
        Console.WriteLine("O nome não pode ficar vazio.");
        return;
    }
    contatos.Add(new Contato(nome, telefone));
    Console.WriteLine($"{nome} adicionado.");
}

void Listar()
{
    if (contatos.Count == 0)
    {
        Console.WriteLine("Nenhum contato.");
        return;
    }
    foreach (Contato contato in contatos)
    {
        Console.WriteLine($"  {contato.Nome}: {contato.Telefone}");
    }
}
record Contato(string Nome, string Telefone);
List<Contato> contatos = new();

Console.WriteLine("=== Agenda da Ana ===");
while (true)
{
    Console.WriteLine();
    Console.WriteLine("1 Adicionar  2 Listar  0 Sair");
    Console.Write("Opção: ");
    string opcao = Console.ReadLine() ?? "0";
    if (opcao == "0")
    {
        break;
    }

    switch (opcao)
    {
        case "1":
            Adicionar();
            break;
        case "2":
            Listar();
            break;
        default:
            Console.WriteLine("Opção inválida.");
            break;
    }
}
Console.WriteLine("Até logo!");

void Adicionar()
{
    Console.Write("Nome: ");
    string nome = (Console.ReadLine() ?? "").Trim();
    Console.Write("Telefone: ");
    string telefone = (Console.ReadLine() ?? "").Trim();
    if (nome == "")
    {
        Console.WriteLine("O nome não pode ficar vazio.");
        return;
    }
    contatos.Add(new Contato(nome, telefone));
    Console.WriteLine($"{nome} adicionado.");
}

void Listar()
{
    if (contatos.Count == 0)
    {
        Console.WriteLine("Nenhum contato.");
        return;
    }
    foreach (Contato contato in contatos)
    {
        Console.WriteLine($"  {contato.Nome}: {contato.Telefone}");
    }
}

Versão 2 · Buscar e remover

Com dezenas de contatos, a Ana precisa procurar ("quem era a Carla mesmo?") e apagar quem saiu da lista. O menu ganha as opções 3 e 4:

Console.WriteLine("1 Adicionar  2 Listar  3 Buscar  4 Remover  0 Sair");
…
        case "3":
            Buscar();
            break;
        case "4":
            Remover();
            break;

E duas funções novas, no fim do arquivo:

void Buscar()
{
    Console.Write("Buscar por: ");
    string termo = (Console.ReadLine() ?? "").Trim();
    int achados = 0;
    foreach (Contato contato in contatos)
    {
        if (contato.Nome.Contains(termo, StringComparison.OrdinalIgnoreCase))
        {
            Console.WriteLine($"  {contato.Nome}: {contato.Telefone}");
            achados++;
        }
    }
    if (achados == 0)
    {
        Console.WriteLine("Ninguém encontrado.");
    }
}

void Remover()
{
    Console.Write("Nome para remover: ");
    string nome = (Console.ReadLine() ?? "").Trim();
    Contato? alvo = null;
    foreach (Contato contato in contatos)
    {
        if (contato.Nome.Equals(nome, StringComparison.OrdinalIgnoreCase))
        {
            alvo = contato;
            break;
        }
    }
    if (alvo is null)
    {
        Console.WriteLine($"{nome} não está na agenda.");
        return;
    }
    contatos.Remove(alvo);
    Console.WriteLine($"{alvo.Nome} removido.");
}
contato.Nome.Contains(termo, StringComparison.OrdinalIgnoreCase)
"O nome contém o termo?", sem ligar para maiúsculas e minúsculas (ignore case): car acha Carla Souza. Sem o segundo argumento, Contains diferencia c de C.
int achados = 0;
Conta quantos combinaram, para dizer "Ninguém encontrado." quando for zero.
contato.Nome.Equals(nome, StringComparison.OrdinalIgnoreCase)
Para remover, o nome tem de ser igual (sem ligar para maiúsculas), não só conter: "Remover Ana" não deve apagar a "Ana Paula".
Contato? alvo = null;
O contato a remover. Começa como "nenhum" (o ? deixa guardar null, veja Referências Anuláveis).
break;
Achou: para de procurar.
contatos.Remove(alvo);
Tira esse contato da lista, depois do foreach (veja o cenário abaixo).
$ dotnet run
=== Agenda da Ana ===
…                                         « a Ana cadastrou Bruno Lima, Carla Souza e José Carlos
1 Adicionar  2 Listar  3 Buscar  4 Remover  0 Sair
Opção: 3
Buscar por: CAR
  Carla Souza: 21 3333-4444
  José Carlos: 31 2222-1111

1 Adicionar  2 Listar  3 Buscar  4 Remover  0 Sair
Opção: 4
Nome para remover: carla souza
Carla Souza removido.

1 Adicionar  2 Listar  3 Buscar  4 Remover  0 Sair
Opção: 4
Nome para remover: Diego
Diego não está na agenda.

1 Adicionar  2 Listar  3 Buscar  4 Remover  0 Sair
Opção: 2
  Bruno Lima: 11 98765-4321
  José Carlos: 31 2222-1111

1 Adicionar  2 Listar  3 Buscar  4 Remover  0 Sair
Opção: 0
Até logo!

A busca por CAR achou a Carla e o José Carlos, e a remoção aceitou carla souza em minúsculas.

Remover dentro do foreach: Collection was modified

A primeira versão do Remover da Ana tirava o contato da lista ali mesmo, dentro do laço:

void Remover()
{
    Console.Write("Nome para remover: ");
    string nome = (Console.ReadLine() ?? "").Trim();
    foreach (Contato contato in contatos)
    {
        if (contato.Nome.Equals(nome, StringComparison.OrdinalIgnoreCase))
        {
            contatos.Remove(contato);
            Console.WriteLine($"{contato.Nome} removido.");
        }
    }
}
$ dotnet run
…                                         « a Ana cadastrou Bruno Lima, Carla Souza e José Carlos
1 Adicionar  2 Listar  3 Buscar  4 Remover  0 Sair
Opção: 4
Nome para remover: bruno lima
Bruno Lima removido.
Unhandled exception. System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
   at System.Collections.Generic.List`1.Enumerator.MoveNext()
   at Program.<<Main>$>g__Remover|0_3(<>c__DisplayClass0_0&) in /home/ana/projetos/Agenda/Program.cs:line 87
   at Program.<Main>$(String[] args) in /home/ana/projetos/Agenda/Program.cs:line 27
$ echo $?
134

O que significa: "a coleção foi modificada; a enumeração não pode continuar". O Remove funcionou (apareceu "Bruno Lima removido."), mas o foreach da linha 87 estava no meio da lista: quando foi buscar o próximo contato, viu que a lista tinha mudado debaixo dele e lançou a exceção. O g__Remover|0_3 é o nome que o compilador dá à função local Remover, e a linha 27 é quem a chamou, no menu.

Como consertar: nunca mude uma lista enquanto um foreach passa por ela. Ache primeiro, remova depois, como na versão 2. Outra saída é contatos.RemoveAll(c => c.Nome.Equals(nome, StringComparison.OrdinalIgnoreCase)), que procura e remove de uma vez (é uma lambda).

Missão (versão 2): a busca por car não acha a Carla, porque o Buscar compara o nome inteiro, com maiúsculas: contato.Nome == termo. Troque por contato.Nome.Contains(termo, StringComparison.OrdinalIgnoreCase).

List<Contato> contatos = new();

Console.WriteLine("=== Agenda da Ana ===");
while (true)
{
    Console.WriteLine();
    Console.WriteLine("1 Adicionar  2 Listar  3 Buscar  4 Remover  0 Sair");
    Console.Write("Opção: ");
    string opcao = Console.ReadLine() ?? "0";
    if (opcao == "0")
    {
        break;
    }

    switch (opcao)
    {
        case "1":
            Adicionar();
            break;
        case "2":
            Listar();
            break;
        case "3":
            Buscar();
            break;
        case "4":
            Remover();
            break;
        default:
            Console.WriteLine("Opção inválida.");
            break;
    }
}
Console.WriteLine("Até logo!");

void Adicionar()
{
    Console.Write("Nome: ");
    string nome = (Console.ReadLine() ?? "").Trim();
    Console.Write("Telefone: ");
    string telefone = (Console.ReadLine() ?? "").Trim();
    if (nome == "")
    {
        Console.WriteLine("O nome não pode ficar vazio.");
        return;
    }
    contatos.Add(new Contato(nome, telefone));
    Console.WriteLine($"{nome} adicionado.");
}

void Listar()
{
    if (contatos.Count == 0)
    {
        Console.WriteLine("Nenhum contato.");
        return;
    }
    foreach (Contato contato in contatos)
    {
        Console.WriteLine($"  {contato.Nome}: {contato.Telefone}");
    }
}

void Buscar()
{
    Console.Write("Buscar por: ");
    string termo = (Console.ReadLine() ?? "").Trim();
    int achados = 0;
    foreach (Contato contato in contatos)
    {
        if (contato.Nome == termo)
        {
            Console.WriteLine($"  {contato.Nome}: {contato.Telefone}");
            achados++;
        }
    }
    if (achados == 0)
    {
        Console.WriteLine("Ninguém encontrado.");
    }
}

void Remover()
{
    Console.Write("Nome para remover: ");
    string nome = (Console.ReadLine() ?? "").Trim();
    Contato? alvo = null;
    foreach (Contato contato in contatos)
    {
        if (contato.Nome.Equals(nome, StringComparison.OrdinalIgnoreCase))
        {
            alvo = contato;
            break;
        }
    }
    if (alvo is null)
    {
        Console.WriteLine($"{nome} não está na agenda.");
        return;
    }
    contatos.Remove(alvo);
    Console.WriteLine($"{alvo.Nome} removido.");
}
record Contato(string Nome, string Telefone);
List<Contato> contatos = new();

Console.WriteLine("=== Agenda da Ana ===");
while (true)
{
    Console.WriteLine();
    Console.WriteLine("1 Adicionar  2 Listar  3 Buscar  4 Remover  0 Sair");
    Console.Write("Opção: ");
    string opcao = Console.ReadLine() ?? "0";
    if (opcao == "0")
    {
        break;
    }

    switch (opcao)
    {
        case "1":
            Adicionar();
            break;
        case "2":
            Listar();
            break;
        case "3":
            Buscar();
            break;
        case "4":
            Remover();
            break;
        default:
            Console.WriteLine("Opção inválida.");
            break;
    }
}
Console.WriteLine("Até logo!");

void Adicionar()
{
    Console.Write("Nome: ");
    string nome = (Console.ReadLine() ?? "").Trim();
    Console.Write("Telefone: ");
    string telefone = (Console.ReadLine() ?? "").Trim();
    if (nome == "")
    {
        Console.WriteLine("O nome não pode ficar vazio.");
        return;
    }
    contatos.Add(new Contato(nome, telefone));
    Console.WriteLine($"{nome} adicionado.");
}

void Listar()
{
    if (contatos.Count == 0)
    {
        Console.WriteLine("Nenhum contato.");
        return;
    }
    foreach (Contato contato in contatos)
    {
        Console.WriteLine($"  {contato.Nome}: {contato.Telefone}");
    }
}

void Buscar()
{
    Console.Write("Buscar por: ");
    string termo = (Console.ReadLine() ?? "").Trim();
    int achados = 0;
    foreach (Contato contato in contatos)
    {
        if (contato.Nome.Contains(termo, StringComparison.OrdinalIgnoreCase))
        {
            Console.WriteLine($"  {contato.Nome}: {contato.Telefone}");
            achados++;
        }
    }
    if (achados == 0)
    {
        Console.WriteLine("Ninguém encontrado.");
    }
}

void Remover()
{
    Console.Write("Nome para remover: ");
    string nome = (Console.ReadLine() ?? "").Trim();
    Contato? alvo = null;
    foreach (Contato contato in contatos)
    {
        if (contato.Nome.Equals(nome, StringComparison.OrdinalIgnoreCase))
        {
            alvo = contato;
            break;
        }
    }
    if (alvo is null)
    {
        Console.WriteLine($"{nome} não está na agenda.");
        return;
    }
    contatos.Remove(alvo);
    Console.WriteLine($"{alvo.Nome} removido.");
}

Versão 3 · Salvar num arquivo JSON

Até aqui, tudo o que a Ana cadastra some quando o programa fecha: a lista mora na memória. Para guardar, o programa escreve a lista num arquivo. Em que formato? O JSON (JavaScript Object Notation) é texto puro, fácil de ler por pessoas e por programas, e o .NET já sabe transformar objetos em JSON e de volta (veja JSON). Transformar um objeto em texto se chama serializar.

using System.Text.Encodings.Web;
using System.Text.Json;

const string Arquivo = "contatos.json";
JsonSerializerOptions opcoes = new JsonSerializerOptions
{
    WriteIndented = true,
    Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};

List<Contato> contatos = new();

Console.WriteLine("=== Agenda da Ana ===");
// … o menu continua igual …
Salvar();
Console.WriteLine($"Agenda salva em {Arquivo}. Até logo!");

// … Adicionar, Listar, Buscar e Remover continuam iguais …

void Salvar()
{
    string json = JsonSerializer.Serialize(contatos, opcoes);
    File.WriteAllText(Arquivo, json);
}
using System.Text.Json;
Traz o JsonSerializer e o JsonSerializerOptions. Esse namespace não vem incluído sozinho.
using System.Text.Encodings.Web;
Traz o JavaScriptEncoder, para os acentos (veja o cenário).
const string Arquivo = "contatos.json";
O nome do arquivo, num lugar só. Sem pasta no nome, ele fica na pasta em que o programa roda: ~/projetos/Agenda.
WriteIndented = true
"Escrever com recuo": um campo por linha, fácil de ler. Sem isso, sai tudo numa linha.
JsonSerializer.Serialize(contatos, opcoes)
Transforma a lista inteira num texto JSON: [ … ] é a lista, cada { … } é um contato, com os nomes das propriedades entre aspas.
File.WriteAllText(Arquivo, json);
Cria o arquivo (ou troca o conteúdo, se já existir) com esse texto (Arquivos).
Salvar(); depois do laço
A agenda é salva quando a Ana escolhe 0.
$ dotnet run
=== Agenda da Ana ===

1 Adicionar  2 Listar  3 Buscar  4 Remover  0 Sair
Opção: 1
Nome: José Carlos
Telefone: 31 2222-1111
José Carlos adicionado.

1 Adicionar  2 Listar  3 Buscar  4 Remover  0 Sair
Opção: 1
Nome: Conceição
Telefone: 11 91234-5678
Conceição adicionado.

1 Adicionar  2 Listar  3 Buscar  4 Remover  0 Sair
Opção: 0
Agenda salva em contatos.json. Até logo!
$ cat contatos.json
[
  {
    "Nome": "José Carlos",
    "Telefone": "31 2222-1111"
  },
  {
    "Nome": "Conceição",
    "Telefone": "11 91234-5678"
  }
]
"José": os acentos viraram códigos

Sem a linha do Encoder, o arquivo da Ana saiu assim:

$ cat contatos.json
[
  {
    "Nome": "José Carlos",
    "Telefone": "31 2222-1111"
  },
  {
    "Nome": "Conceição",
    "Telefone": "11 91234-5678"
  }
]

O que aconteceu: por segurança, o JsonSerializer escreve toda letra fora do inglês básico como um código: é é o é, ç o ç e ã o ã. Não é um erro: ao ler o arquivo de volta, o programa recebe "José" certinho. Mas o arquivo fica difícil de ler para uma pessoa.

Como consertar: Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping nas opções. O "Unsafe" ("inseguro") é porque um JSON assim não deve ser colado direto dentro de uma página da web; para um arquivo do seu programa, é tranquilo.

Esqueci o using: erros CS0246 e CS0103
$ dotnet run
/home/ana/projetos/Agenda/Program.cs(4,1): error CS0246: O nome do tipo ou do namespace "JsonSerializerOptions" não pode ser encontrado (está faltando uma diretiva using ou uma referência de assembly?) [/home/ana/projetos/Agenda/Agenda.csproj]
/home/ana/projetos/Agenda/Program.cs(4,36): error CS0246: O nome do tipo ou do namespace "JsonSerializerOptions" não pode ser encontrado (está faltando uma diretiva using ou uma referência de assembly?) [/home/ana/projetos/Agenda/Agenda.csproj]
/home/ana/projetos/Agenda/Program.cs(117,19): error CS0103: O nome "JsonSerializer" não existe no contexto atual [/home/ana/projetos/Agenda/Agenda.csproj]
/home/ana/projetos/Agenda/Program.cs(130,16): error CS0103: O nome "JsonSerializer" não existe no contexto atual [/home/ana/projetos/Agenda/Agenda.csproj]
/home/ana/projetos/Agenda/Program.cs(132,12): error CS0246: O nome do tipo ou do namespace "JsonException" não pode ser encontrado (está faltando uma diretiva using ou uma referência de assembly?) [/home/ana/projetos/Agenda/Agenda.csproj]

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

O que significa: cinco erros, uma causa só. Sem using System.Text.Json;, o compilador não conhece nenhum nome desse namespace: nem o tipo JsonSerializerOptions (CS0246, "tipo não encontrado"), nem a classe JsonSerializer (CS0103, "nome não existe"), nem o JsonException. Num projeto, cada linha termina com o nome do .csproj entre colchetes. A própria mensagem dá a dica: "está faltando uma diretiva using?".

Como consertar: a primeira linha do arquivo, using System.Text.Json;.

Missão (versão 3): o programa salva, mas o arquivo mostra "José Carlos" (a última linha do Program.cs mostra o arquivo, só para conferir). Nas opções, acrescente Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping (não esqueça a vírgula depois de WriteIndented = true) e o using System.Text.Encodings.Web; no topo.

using System.Text.Json;

const string Arquivo = "contatos.json";
JsonSerializerOptions opcoes = new JsonSerializerOptions
{
    WriteIndented = true
};

List<Contato> contatos = new();

Console.WriteLine("=== Agenda da Ana ===");
while (true)
{
    Console.WriteLine();
    Console.WriteLine("1 Adicionar  2 Listar  3 Buscar  4 Remover  0 Sair");
    Console.Write("Opção: ");
    string opcao = Console.ReadLine() ?? "0";
    if (opcao == "0")
    {
        break;
    }

    switch (opcao)
    {
        case "1":
            Adicionar();
            break;
        case "2":
            Listar();
            break;
        case "3":
            Buscar();
            break;
        case "4":
            Remover();
            break;
        default:
            Console.WriteLine("Opção inválida.");
            break;
    }
}
Salvar();
Console.WriteLine($"Agenda salva em {Arquivo}. Até logo!");
Console.WriteLine(File.ReadAllText(Arquivo)); // só para conferir o arquivo

void Adicionar()
{
    Console.Write("Nome: ");
    string nome = (Console.ReadLine() ?? "").Trim();
    Console.Write("Telefone: ");
    string telefone = (Console.ReadLine() ?? "").Trim();
    if (nome == "")
    {
        Console.WriteLine("O nome não pode ficar vazio.");
        return;
    }
    contatos.Add(new Contato(nome, telefone));
    Console.WriteLine($"{nome} adicionado.");
}

void Listar()
{
    if (contatos.Count == 0)
    {
        Console.WriteLine("Nenhum contato.");
        return;
    }
    foreach (Contato contato in contatos)
    {
        Console.WriteLine($"  {contato.Nome}: {contato.Telefone}");
    }
}

void Buscar()
{
    Console.Write("Buscar por: ");
    string termo = (Console.ReadLine() ?? "").Trim();
    int achados = 0;
    foreach (Contato contato in contatos)
    {
        if (contato.Nome.Contains(termo, StringComparison.OrdinalIgnoreCase))
        {
            Console.WriteLine($"  {contato.Nome}: {contato.Telefone}");
            achados++;
        }
    }
    if (achados == 0)
    {
        Console.WriteLine("Ninguém encontrado.");
    }
}

void Remover()
{
    Console.Write("Nome para remover: ");
    string nome = (Console.ReadLine() ?? "").Trim();
    Contato? alvo = null;
    foreach (Contato contato in contatos)
    {
        if (contato.Nome.Equals(nome, StringComparison.OrdinalIgnoreCase))
        {
            alvo = contato;
            break;
        }
    }
    if (alvo is null)
    {
        Console.WriteLine($"{nome} não está na agenda.");
        return;
    }
    contatos.Remove(alvo);
    Console.WriteLine($"{alvo.Nome} removido.");
}

void Salvar()
{
    string json = JsonSerializer.Serialize(contatos, opcoes);
    File.WriteAllText(Arquivo, json);
}
record Contato(string Nome, string Telefone);
using System.Text.Encodings.Web;
using System.Text.Json;

const string Arquivo = "contatos.json";
JsonSerializerOptions opcoes = new JsonSerializerOptions
{
    WriteIndented = true,
    Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};

List<Contato> contatos = new();

Console.WriteLine("=== Agenda da Ana ===");
while (true)
{
    Console.WriteLine();
    Console.WriteLine("1 Adicionar  2 Listar  3 Buscar  4 Remover  0 Sair");
    Console.Write("Opção: ");
    string opcao = Console.ReadLine() ?? "0";
    if (opcao == "0")
    {
        break;
    }

    switch (opcao)
    {
        case "1":
            Adicionar();
            break;
        case "2":
            Listar();
            break;
        case "3":
            Buscar();
            break;
        case "4":
            Remover();
            break;
        default:
            Console.WriteLine("Opção inválida.");
            break;
    }
}
Salvar();
Console.WriteLine($"Agenda salva em {Arquivo}. Até logo!");
Console.WriteLine(File.ReadAllText(Arquivo)); // só para conferir o arquivo

void Adicionar()
{
    Console.Write("Nome: ");
    string nome = (Console.ReadLine() ?? "").Trim();
    Console.Write("Telefone: ");
    string telefone = (Console.ReadLine() ?? "").Trim();
    if (nome == "")
    {
        Console.WriteLine("O nome não pode ficar vazio.");
        return;
    }
    contatos.Add(new Contato(nome, telefone));
    Console.WriteLine($"{nome} adicionado.");
}

void Listar()
{
    if (contatos.Count == 0)
    {
        Console.WriteLine("Nenhum contato.");
        return;
    }
    foreach (Contato contato in contatos)
    {
        Console.WriteLine($"  {contato.Nome}: {contato.Telefone}");
    }
}

void Buscar()
{
    Console.Write("Buscar por: ");
    string termo = (Console.ReadLine() ?? "").Trim();
    int achados = 0;
    foreach (Contato contato in contatos)
    {
        if (contato.Nome.Contains(termo, StringComparison.OrdinalIgnoreCase))
        {
            Console.WriteLine($"  {contato.Nome}: {contato.Telefone}");
            achados++;
        }
    }
    if (achados == 0)
    {
        Console.WriteLine("Ninguém encontrado.");
    }
}

void Remover()
{
    Console.Write("Nome para remover: ");
    string nome = (Console.ReadLine() ?? "").Trim();
    Contato? alvo = null;
    foreach (Contato contato in contatos)
    {
        if (contato.Nome.Equals(nome, StringComparison.OrdinalIgnoreCase))
        {
            alvo = contato;
            break;
        }
    }
    if (alvo is null)
    {
        Console.WriteLine($"{nome} não está na agenda.");
        return;
    }
    contatos.Remove(alvo);
    Console.WriteLine($"{alvo.Nome} removido.");
}

void Salvar()
{
    string json = JsonSerializer.Serialize(contatos, opcoes);
    File.WriteAllText(Arquivo, json);
}

Versão 4 · Carregar ao abrir

A agenda já é salva, mas o programa ainda começa com a lista vazia: o arquivo é escrito e nunca lido. Falta o caminho de volta: ao abrir, ler o contatos.json e transformar o texto numa lista (desserializar). Três situações precisam de cuidado: a primeira vez, quando o arquivo ainda não existe; o arquivo normal; e um arquivo com defeito.

using System.Text.Encodings.Web;
using System.Text.Json;

const string Arquivo = "contatos.json";
JsonSerializerOptions opcoes = new JsonSerializerOptions
{
    WriteIndented = true,
    Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};

Console.WriteLine("=== Agenda da Ana ===");
List<Contato> contatos = Carregar();
Console.WriteLine($"Contatos carregados: {contatos.Count}");
// … o menu, as outras funções e o Salvar continuam iguais …

List<Contato> Carregar()
{
    if (!File.Exists(Arquivo))
    {
        return [];
    }
    try
    {
        string json = File.ReadAllText(Arquivo);
        return JsonSerializer.Deserialize<List<Contato>>(json) ?? [];
    }
    catch (JsonException)
    {
        Console.WriteLine($"Aviso: {Arquivo} está com defeito. Começando com a agenda vazia.");
        return [];
    }
}
List<Contato> contatos = Carregar();
A lista agora começa com o que estava no arquivo.
if (!File.Exists(Arquivo)) return [];
Na primeira vez, não há arquivo: começa com uma lista vazia (o [] é uma expressão de coleção C# 12).
JsonSerializer.Deserialize<List<Contato>>(json)
Lê o texto JSON e monta uma List<Contato>. Entre < > vai o tipo que você espera receber.
?? []
O Deserialize pode devolver null (se o arquivo tiver só a palavra null, por exemplo). Nesse caso, lista vazia.
catch (JsonException)
Se o texto não for um JSON válido, o Deserialize lança JsonException. Em vez de cair, o programa avisa e começa vazio.

A saída é a do começo da página: na segunda vez, Contatos carregados: 2.

O arquivo cortado: JsonException

O notebook da Ana desligou no meio de uma gravação, e o contatos.json ficou pela metade:

$ cat contatos.json
[
  {
    "Nome": "José Carlos",
    "Telefone": "31 2222-1111"
  },
  {
    "Nome": "Concei

Sem o try/catch no Carregar, a agenda nem abriu:

$ dotnet run
=== Agenda da Ana ===
Unhandled exception. System.Text.Json.JsonException: '0x0A' is invalid within a JSON string. The string should be correctly escaped. Path: $[1].Nome | LineNumber: 6 | BytePositionInLine: 19.
 ---> System.Text.Json.JsonReaderException: '0x0A' is invalid within a JSON string. The string should be correctly escaped. LineNumber: 6 | BytePositionInLine: 19.
…                                         « mais 20 linhas de rastro
   at Program.<<Main>$>g__Carregar|0_5() in /home/ana/projetos/Agenda/Program.cs:line 129
   at Program.<Main>$(String[] args) in /home/ana/projetos/Agenda/Program.cs:line 12

O que significa: o leitor de JSON achou uma quebra de linha (0x0A) dentro de um texto que não foi fechado, o "Concei. A mensagem diz até onde: $[1].Nome (o Nome do segundo contato), linha 6 do arquivo (contando do 0), posição 19. Com o try/catch da versão 4, a agenda abre:

$ dotnet run
=== Agenda da Ana ===
Aviso: contatos.json está com defeito. Começando com a agenda vazia.
Contatos carregados: 0

1 Adicionar  2 Listar  3 Buscar  4 Remover  0 Sair
Opção: 0
Agenda salva em contatos.json. Até logo!
$ cat contatos.json
[]

Mas repare: ao sair, o programa salvou a agenda vazia por cima do arquivo com defeito, e o José Carlos, que ainda dava para recuperar à mão, se perdeu. Uma agenda de verdade guardaria uma cópia do arquivo ruim antes (é o desafio 1).

Missão (versão 4): o arquivo contatos.json (na terceira aba) já tem dois contatos, mas o Carregar sempre devolve a lista vazia. Complete-o: leia o texto com File.ReadAllText(Arquivo) e devolva JsonSerializer.Deserialize<List<Contato>>(json) ?? []. Se quiser, proteja com o try/catch (JsonException). A entrada pede a lista e sai.

using System.Text.Encodings.Web;
using System.Text.Json;

const string Arquivo = "contatos.json";
JsonSerializerOptions opcoes = new JsonSerializerOptions
{
    WriteIndented = true,
    Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};

Console.WriteLine("=== Agenda da Ana ===");
List<Contato> contatos = Carregar();
Console.WriteLine($"Contatos carregados: {contatos.Count}");
while (true)
{
    Console.WriteLine();
    Console.WriteLine("1 Adicionar  2 Listar  3 Buscar  4 Remover  0 Sair");
    Console.Write("Opção: ");
    string opcao = Console.ReadLine() ?? "0";
    if (opcao == "0")
    {
        break;
    }

    switch (opcao)
    {
        case "1":
            Adicionar();
            break;
        case "2":
            Listar();
            break;
        case "3":
            Buscar();
            break;
        case "4":
            Remover();
            break;
        default:
            Console.WriteLine("Opção inválida.");
            break;
    }
}
Salvar();
Console.WriteLine($"Agenda salva em {Arquivo}. Até logo!");

void Adicionar()
{
    Console.Write("Nome: ");
    string nome = (Console.ReadLine() ?? "").Trim();
    Console.Write("Telefone: ");
    string telefone = (Console.ReadLine() ?? "").Trim();
    if (nome == "")
    {
        Console.WriteLine("O nome não pode ficar vazio.");
        return;
    }
    contatos.Add(new Contato(nome, telefone));
    Console.WriteLine($"{nome} adicionado.");
}

void Listar()
{
    if (contatos.Count == 0)
    {
        Console.WriteLine("Nenhum contato.");
        return;
    }
    foreach (Contato contato in contatos)
    {
        Console.WriteLine($"  {contato.Nome}: {contato.Telefone}");
    }
}

void Buscar()
{
    Console.Write("Buscar por: ");
    string termo = (Console.ReadLine() ?? "").Trim();
    int achados = 0;
    foreach (Contato contato in contatos)
    {
        if (contato.Nome.Contains(termo, StringComparison.OrdinalIgnoreCase))
        {
            Console.WriteLine($"  {contato.Nome}: {contato.Telefone}");
            achados++;
        }
    }
    if (achados == 0)
    {
        Console.WriteLine("Ninguém encontrado.");
    }
}

void Remover()
{
    Console.Write("Nome para remover: ");
    string nome = (Console.ReadLine() ?? "").Trim();
    Contato? alvo = null;
    foreach (Contato contato in contatos)
    {
        if (contato.Nome.Equals(nome, StringComparison.OrdinalIgnoreCase))
        {
            alvo = contato;
            break;
        }
    }
    if (alvo is null)
    {
        Console.WriteLine($"{nome} não está na agenda.");
        return;
    }
    contatos.Remove(alvo);
    Console.WriteLine($"{alvo.Nome} removido.");
}

void Salvar()
{
    string json = JsonSerializer.Serialize(contatos, opcoes);
    File.WriteAllText(Arquivo, json);
}

List<Contato> Carregar()
{
    if (!File.Exists(Arquivo))
    {
        return [];
    }
    // leia o texto do arquivo e converta numa List<Contato>
    return [];
}
record Contato(string Nome, string Telefone);
[
  {
    "Nome": "José Carlos",
    "Telefone": "31 2222-1111"
  },
  {
    "Nome": "Conceição",
    "Telefone": "11 91234-5678"
  }
]
using System.Text.Encodings.Web;
using System.Text.Json;

const string Arquivo = "contatos.json";
JsonSerializerOptions opcoes = new JsonSerializerOptions
{
    WriteIndented = true,
    Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};

Console.WriteLine("=== Agenda da Ana ===");
List<Contato> contatos = Carregar();
Console.WriteLine($"Contatos carregados: {contatos.Count}");
while (true)
{
    Console.WriteLine();
    Console.WriteLine("1 Adicionar  2 Listar  3 Buscar  4 Remover  0 Sair");
    Console.Write("Opção: ");
    string opcao = Console.ReadLine() ?? "0";
    if (opcao == "0")
    {
        break;
    }

    switch (opcao)
    {
        case "1":
            Adicionar();
            break;
        case "2":
            Listar();
            break;
        case "3":
            Buscar();
            break;
        case "4":
            Remover();
            break;
        default:
            Console.WriteLine("Opção inválida.");
            break;
    }
}
Salvar();
Console.WriteLine($"Agenda salva em {Arquivo}. Até logo!");

void Adicionar()
{
    Console.Write("Nome: ");
    string nome = (Console.ReadLine() ?? "").Trim();
    Console.Write("Telefone: ");
    string telefone = (Console.ReadLine() ?? "").Trim();
    if (nome == "")
    {
        Console.WriteLine("O nome não pode ficar vazio.");
        return;
    }
    contatos.Add(new Contato(nome, telefone));
    Console.WriteLine($"{nome} adicionado.");
}

void Listar()
{
    if (contatos.Count == 0)
    {
        Console.WriteLine("Nenhum contato.");
        return;
    }
    foreach (Contato contato in contatos)
    {
        Console.WriteLine($"  {contato.Nome}: {contato.Telefone}");
    }
}

void Buscar()
{
    Console.Write("Buscar por: ");
    string termo = (Console.ReadLine() ?? "").Trim();
    int achados = 0;
    foreach (Contato contato in contatos)
    {
        if (contato.Nome.Contains(termo, StringComparison.OrdinalIgnoreCase))
        {
            Console.WriteLine($"  {contato.Nome}: {contato.Telefone}");
            achados++;
        }
    }
    if (achados == 0)
    {
        Console.WriteLine("Ninguém encontrado.");
    }
}

void Remover()
{
    Console.Write("Nome para remover: ");
    string nome = (Console.ReadLine() ?? "").Trim();
    Contato? alvo = null;
    foreach (Contato contato in contatos)
    {
        if (contato.Nome.Equals(nome, StringComparison.OrdinalIgnoreCase))
        {
            alvo = contato;
            break;
        }
    }
    if (alvo is null)
    {
        Console.WriteLine($"{nome} não está na agenda.");
        return;
    }
    contatos.Remove(alvo);
    Console.WriteLine($"{alvo.Nome} removido.");
}

void Salvar()
{
    string json = JsonSerializer.Serialize(contatos, opcoes);
    File.WriteAllText(Arquivo, json);
}

List<Contato> Carregar()
{
    if (!File.Exists(Arquivo))
    {
        return [];
    }
    try
    {
        string json = File.ReadAllText(Arquivo);
        return JsonSerializer.Deserialize<List<Contato>>(json) ?? [];
    }
    catch (JsonException)
    {
        Console.WriteLine($"Aviso: {Arquivo} está com defeito. Começando com a agenda vazia.");
        return [];
    }
}

O programa completo

A versão final tem dois arquivos. O Contato.cs:

record Contato(string Nome, string Telefone);

E o Program.cs:

using System.Text.Encodings.Web;
using System.Text.Json;

const string Arquivo = "contatos.json";
JsonSerializerOptions opcoes = new JsonSerializerOptions
{
    WriteIndented = true,
    Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};

Console.WriteLine("=== Agenda da Ana ===");
List<Contato> contatos = Carregar();
Console.WriteLine($"Contatos carregados: {contatos.Count}");
while (true)
{
    Console.WriteLine();
    Console.WriteLine("1 Adicionar  2 Listar  3 Buscar  4 Remover  0 Sair");
    Console.Write("Opção: ");
    string opcao = Console.ReadLine() ?? "0";
    if (opcao == "0")
    {
        break;
    }

    switch (opcao)
    {
        case "1":
            Adicionar();
            break;
        case "2":
            Listar();
            break;
        case "3":
            Buscar();
            break;
        case "4":
            Remover();
            break;
        default:
            Console.WriteLine("Opção inválida.");
            break;
    }
}
Salvar();
Console.WriteLine($"Agenda salva em {Arquivo}. Até logo!");

void Adicionar()
{
    Console.Write("Nome: ");
    string nome = (Console.ReadLine() ?? "").Trim();
    Console.Write("Telefone: ");
    string telefone = (Console.ReadLine() ?? "").Trim();
    if (nome == "")
    {
        Console.WriteLine("O nome não pode ficar vazio.");
        return;
    }
    contatos.Add(new Contato(nome, telefone));
    Console.WriteLine($"{nome} adicionado.");
}

void Listar()
{
    if (contatos.Count == 0)
    {
        Console.WriteLine("Nenhum contato.");
        return;
    }
    foreach (Contato contato in contatos)
    {
        Console.WriteLine($"  {contato.Nome}: {contato.Telefone}");
    }
}

void Buscar()
{
    Console.Write("Buscar por: ");
    string termo = (Console.ReadLine() ?? "").Trim();
    int achados = 0;
    foreach (Contato contato in contatos)
    {
        if (contato.Nome.Contains(termo, StringComparison.OrdinalIgnoreCase))
        {
            Console.WriteLine($"  {contato.Nome}: {contato.Telefone}");
            achados++;
        }
    }
    if (achados == 0)
    {
        Console.WriteLine("Ninguém encontrado.");
    }
}

void Remover()
{
    Console.Write("Nome para remover: ");
    string nome = (Console.ReadLine() ?? "").Trim();
    Contato? alvo = null;
    foreach (Contato contato in contatos)
    {
        if (contato.Nome.Equals(nome, StringComparison.OrdinalIgnoreCase))
        {
            alvo = contato;
            break;
        }
    }
    if (alvo is null)
    {
        Console.WriteLine($"{nome} não está na agenda.");
        return;
    }
    contatos.Remove(alvo);
    Console.WriteLine($"{alvo.Nome} removido.");
}

void Salvar()
{
    string json = JsonSerializer.Serialize(contatos, opcoes);
    File.WriteAllText(Arquivo, json);
}

List<Contato> Carregar()
{
    if (!File.Exists(Arquivo))
    {
        return [];
    }
    try
    {
        string json = File.ReadAllText(Arquivo);
        return JsonSerializer.Deserialize<List<Contato>>(json) ?? [];
    }
    catch (JsonException)
    {
        Console.WriteLine($"Aviso: {Arquivo} está com defeito. Começando com a agenda vazia.");
        return [];
    }
}

O que o projeto juntou: um record, uma List<T>, um menu com while e switch, funções locais, Contains e Equals com StringComparison, JSON, File e try e catch, num projeto com dois arquivos.


Desafios extras

  1. Não perder o arquivo com defeito. No catch, antes de começar vazio, copie o arquivo ruim com File.Copy(Arquivo, Arquivo + ".bak", true) e avise a Ana.
  2. Sem nomes repetidos. No Adicionar, recuse um nome que já está na agenda (use o mesmo Equals com OrdinalIgnoreCase do Remover).
  3. Em ordem alfabética. Liste os contatos em ordem de nome com contatos.OrderBy(c => c.Nome) (veja Ordenar).
  4. Mais um campo. Acrescente o e-mail ao Contato. O que acontece ao carregar um arquivo antigo, sem o campo Email? (Veja JSON.)
  5. Editar. Uma opção 5 que troca o telefone de um contato. Dica: records não mudam, então use with e troque o contato na lista: contatos[i] = contatos[i] with { Telefone = novo };.
  6. Salvar a cada mudança. Chame Salvar() depois de cada Add e Remove: assim, se o programa cair, nada se perde.

Exercícios

1. Por que a agenda usa uma List<Contato> e não um array Contato[]?

2. O que "Carla Souza".Contains("car", StringComparison.OrdinalIgnoreCase) devolve?

3. O que acontece ao chamar contatos.Remove(c) dentro de um foreach (var c in contatos) e o laço continuar?

4. No arquivo salvo, "José" apareceu no lugar de "José". O que isso quer dizer?

5. Complete a linha que transforma a lista em texto JSON:

string json = JsonSerializer.(contatos, opcoes);

6. Complete o tipo da exceção de um JSON com defeito:

catch ()

Resumo

  • Um menu é um while (true) que mostra as opções, lê a escolha e chama uma função para cada uma; ?? "0" faz o fim da entrada valer como "sair".
  • Funções locais sem static enxergam as variáveis do programa principal, como a lista.
  • Contains e Equals com StringComparison.OrdinalIgnoreCase comparam sem ligar para maiúsculas.
  • Nunca mude uma lista dentro de um foreach sobre ela: ache primeiro, remova depois (ou use RemoveAll).
  • JsonSerializer.Serialize + File.WriteAllText salvam; File.ReadAllText + Deserialize carregam. WriteIndented e UnsafeRelaxedJsonEscaping deixam o arquivo legível.
  • Confira se o arquivo existe e trate JsonException: um arquivo com defeito não pode derrubar o programa.

Próximo projeto: Conta Bancária (classes).