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.
- Um menu que repete com while e switch, chamando funções locais.
- Uma List<Contato> que cresce com
Adde diminui comRemove. - Buscar sem ligar para maiúsculas com
StringComparison.OrdinalIgnoreCase(Métodos de String). - Guardar e ler a lista num arquivo com System.Text.Json, sem estragar os acentos.
- Tratar um arquivo com defeito com try e catch.
- Trabalhar num projeto com dois arquivos:
Program.cseContato.cs.
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"
}
]
- O menu, a lista e o record (adicionar e listar).
- Buscar e remover.
- Salvar num arquivo JSON.
- 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
switchcompara 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, comocontatos. - (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
returnsai 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):
carachaCarla Souza. Sem o segundo argumento,ContainsdiferenciacdeC. - 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 guardarnull, 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.
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
JsonSerializere oJsonSerializerOptions. 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.
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
Deserializepode devolvernull(se o arquivo tiver só a palavranull, por exemplo). Nesse caso, lista vazia. - catch (JsonException)
- Se o texto não for um JSON válido, o
DeserializelançaJsonException. 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.
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
- Não perder o arquivo com defeito. No
catch, antes de começar vazio, copie o arquivo ruim comFile.Copy(Arquivo, Arquivo + ".bak", true)e avise a Ana. - Sem nomes repetidos. No
Adicionar, recuse um nome que já está na agenda (use o mesmoEqualscomOrdinalIgnoreCasedoRemover). - Em ordem alfabética. Liste os contatos em ordem de nome com
contatos.OrderBy(c => c.Nome)(veja Ordenar). - Mais um campo. Acrescente o e-mail ao
Contato. O que acontece ao carregar um arquivo antigo, sem o campoEmail? (Veja JSON.) - Editar. Uma opção 5 que troca o telefone de um contato. Dica: records não mudam, então use
withe troque o contato na lista:contatos[i] = contatos[i] with { Telefone = novo };. - Salvar a cada mudança. Chame
Salvar()depois de cadaAddeRemove: 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:
6. Complete o tipo da exceção de um JSON com defeito:
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
staticenxergam as variáveis do programa principal, como a lista. ContainseEqualscomStringComparison.OrdinalIgnoreCasecomparam sem ligar para maiúsculas.- Nunca mude uma lista dentro de um
foreachsobre ela: ache primeiro, remova depois (ou useRemoveAll). JsonSerializer.Serialize+File.WriteAllTextsalvam;File.ReadAllText+Deserializecarregam.WriteIndentedeUnsafeRelaxedJsonEscapingdeixam 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).