O mesmo Pedido vai servir ao console, à API, ao site, ao aplicativo e ao jogo, e para isso ele precisa sair do projeto de console e morar num lugar próprio.
A Livraria acumulou, ao longo da Fase 2, um modelo de domínio respeitável: a hierarquia de produtos de "Herança", as políticas de "Interfaces", o Isbn e o ItemPedido de "Classe, Struct ou Record" e o pedido com ciclo de vida de "Enums e Pattern Matching". Tudo isso vive num único projeto de console, lado a lado com o código que lê o teclado. Quando chegarmos à API, ao site e ao aplicativo, todos vão precisar exatamente das mesmas classes. Copiá-las criaria versões divergentes da mesma regra de negócio. Nesta aula, fechamos a Fase 2 aprendendo a organizar código como os projetos profissionais em .NET fazem: namespaces para dar sobrenome aos tipos, bibliotecas de classes para compartilhar código, uma solução que reúne os projetos e o NuGet para usar código escrito por outras pessoas.
Namespaces: sobrenomes para os tipos
À medida que um sistema cresce, os nomes começam a colidir. A Livraria tem um Pedido. Uma biblioteca de pagamentos que você venha a usar pode ter outro Pedido. Um namespace agrupa tipos sob um nome comum, como um sobrenome:
namespace Livraria.Dominio.Pedidos;
public sealed class Pedido
{
// ...
}
Essa forma, com ponto e vírgula no fim e sem chaves, é o namespace de arquivo (C# 10): vale para o arquivo inteiro e economiza um nível de indentação. Você ainda vai encontrar a forma antiga, com chaves envolvendo o conteúdo, que é equivalente.
O nome completo do tipo agora é Livraria.Dominio.Pedidos.Pedido. Para usá-lo de outro namespace sem escrever tudo isso, declare um using no topo do arquivo:
using Livraria.Dominio.Pedidos;
var pedido = new Pedido();
Você já usa using desde "Operadores, Texto e Conversa com o Usuário", com using System.Globalization;. Agora sabe o que ele faz: traz os tipos de um namespace para o alcance do arquivo. Três variações são úteis:
global using: vale para todos os arquivos do projeto. Os usings implícitos ligados no.csprojdesde "Do Código-Fonte ao Processo" são exatamente isso: o SDK geraglobal using System;,global using System.Collections.Generic;e mais alguns.- alias, como em
using Pag = Pagamentos.Gateway;: dá um apelido a um namespace ou a um tipo, útil para resolver colisões de nome. using static: traz os membros estáticos de um tipo, como fizemos comusing static StatusPedido;na aula anterior.
A convenção do .NET é que o namespace espelhe a estrutura de pastas: o arquivo Pedidos/Pedido.cs dentro do projeto Livraria.Dominio declara namespace Livraria.Dominio.Pedidos;. O compilador não exige isso, mas qualquer desenvolvedor .NET espera, e o editor gera o namespace assim ao criar um arquivo novo.
Projetos, assemblies e bibliotecas de classes
Cada projeto .csproj compila para um assembly: um arquivo .dll (ou um executável). Até agora criamos apenas projetos de console, que geram programas. Um projeto de biblioteca de classes gera um .dll feito para ser usado por outros projetos:
dotnet new classlib -n Livraria.Dominio
A biblioteca não tem Main nem instruções de nível superior. Ela só oferece tipos. Um projeto de console, uma API ou um jogo podem referenciá-la e usar tudo que ela torna público.
E aqui o modificador de acesso internal, apresentado em "Propriedades e Encapsulamento", deixa de ser teoria. Lembra que uma classe declarada sem modificador é internal? Até agora isso não fazia diferença, porque tudo estava no mesmo projeto. A partir de hoje, um tipo internal da biblioteca é invisível para os outros projetos. Tudo que o console precisa usar precisa ser public. Tudo que é detalhe de implementação deve continuar internal. É o encapsulamento das classes, agora no nível dos projetos: a biblioteca expõe um contrato público e esconde o resto.
A solução: vários projetos, um só lugar
Uma solução reúne projetos relacionados, e com ela dotnet build compila tudo de uma vez, na ordem certa. Vamos montar a estrutura que a Livraria vai usar pelo resto do curso:
mkdir Livraria
cd Livraria
dotnet new sln -n Livraria
dotnet new gitignore
dotnet new classlib -n Livraria.Dominio -o src/Livraria.Dominio
dotnet new console -n Livraria.Cli -o src/Livraria.Cli
dotnet sln add src/Livraria.Dominio src/Livraria.Cli
dotnet add src/Livraria.Cli reference src/Livraria.Dominio
O resultado:
Livraria/
├── Livraria.slnx
├── .gitignore
└── src/
├── Livraria.Dominio/
│ ├── Livraria.Dominio.csproj
│ ├── Produtos/ (Produto, LivroImpresso, Ebook, Audiolivro, Isbn)
│ ├── Pedidos/ (Pedido, ItemPedido, StatusPedido)
│ └── Descontos/ (IPoliticaDesconto, as políticas, ResultadoDesconto)
└── Livraria.Cli/
├── Livraria.Cli.csproj
└── Program.cs
Algumas observações:
- Formato da solução. No .NET 10,
dotnet new slngera o arquivo no formato.slnx, um XML simples e legível. Versões anteriores geram.sln, um formato antigo e verboso. Os dois funcionam com todas as ferramentas atuais. .gitignore. O comandodotnet new gitignorecria um.gitignoreapropriado, que exclui as pastasbin/eobj/do controle de versão.dotnet add reference. Declara queLivraria.Clidepende deLivraria.Dominio, e essa dependência fica registrada no.csprojdo console:
<ItemGroup>
<ProjectReference Include="..\Livraria.Dominio\Livraria.Dominio.csproj" />
</ItemGroup>
As dependências têm direção. O console conhece o domínio. O domínio não conhece o console, nem deve conhecer. O Pedido não sabe se está sendo usado por um terminal, uma API ou um jogo, e é exatamente isso que permite reaproveitá-lo em todos. O .NET inclusive proíbe referências circulares: se o domínio referenciasse o console, a compilação falharia.
Uma armadilha de nomes
Por que o projeto de console se chama Livraria.Cli, e não Livraria.Console? Porque um namespace Livraria.Console faria Console.WriteLine deixar de funcionar dentro dele. Ao encontrar o nome Console, o compilador procura primeiro nos namespaces mais próximos, acha o namespace Livraria.Console e reclama que um namespace não tem WriteLine. É uma das armadilhas mais comuns entre iniciantes. Existem contornos (global::System.Console.WriteLine), mas o melhor é não criar o problema: evite nomear namespaces com nomes de tipos muito usados da biblioteca padrão, como Console, Task, File e Action.
Configurações comuns com Directory.Build.props
Com vários projetos, repetir as mesmas configurações em cada .csproj é convite a divergências. Um arquivo Directory.Build.props na raiz da solução é aplicado automaticamente a todos os projetos abaixo dele:
<Project>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
</Project>
A última linha merece destaque: TreatWarningsAsErrors transforma avisos em erros. Ao longo do curso vimos vários avisos importantes: valores de enum não tratados, variáveis possivelmente nulas, exceções capturadas e ignoradas. Num projeto novo, tratar avisos como erros desde o início é barato e impede que eles se acumulem aos milhares, até ninguém mais lê-los.
NuGet: usando código de outras pessoas
O NuGet é o gerenciador de pacotes do .NET, equivalente ao npm do JavaScript, ao pip do Python ou ao crates.io do Rust. O repositório público, nuget.org, tem centenas de milhares de pacotes. Adicionar um é um comando:
dotnet add src/Livraria.Cli package Spectre.Console
O pacote escolhido, o Spectre.Console, desenha tabelas, painéis e cores no terminal. O comando registra a dependência no .csproj:
<ItemGroup>
<PackageReference Include="Spectre.Console" Version="..." />
</ItemGroup>
Na próxima compilação, o dotnet baixa o pacote e suas dependências e as disponibiliza para o projeto. Esse passo se chama restore e acontece automaticamente.
As versões seguem o versionamento semântico: MAIOR.MENOR.CORREÇÃO. Uma mudança na versão de correção conserta bugs sem mudar nada da API. Na menor, acrescenta recursos de forma compatível. Na maior, pode quebrar o código de quem usa. Na prática, atualizar da 1.4.2 para a 1.4.3 deve ser seguro. Da 1.x para a 2.x, leia as notas de lançamento antes.
Três comandos que valem o hábito:
dotnet list package # o que o projeto usa
dotnet list package --outdated # o que tem versão mais nova
dotnet list package --vulnerable # o que tem falha de segurança conhecida
E um aviso honesto: cada pacote é código de terceiros rodando com as permissões do seu programa. Antes de adicionar um, confira o número de downloads, quem publica, se é mantido (data da última versão) e a licença. Desconfie de nomes quase iguais aos de pacotes populares: publicar pacotes com nomes parecidos para enganar desenvolvedores apressados (o typosquatting) é uma forma real de ataque. Muitas dependências também são um custo: cada uma é algo a atualizar, auditar e, um dia, substituir. A biblioteca padrão do .NET é enorme, e muitas vezes ela já resolve o problema.
Juntando tudo: o console usando o domínio
Depois de mover as classes para Livraria.Dominio, com os namespaces por pasta e os tipos que o console usa marcados como public, o Program.cs do Livraria.Cli fica assim:
using System.Globalization;
using Livraria.Dominio.Descontos;
using Livraria.Dominio.Pedidos;
using Livraria.Dominio.Produtos;
using Spectre.Console;
CultureInfo.CurrentCulture = new CultureInfo("pt-BR");
List<Produto> catalogo =
[
new LivroImpresso("Dom Casmurro", "Machado de Assis", 59.90m, pesoGramas: 350, estoque: 10),
new Ebook("Vidas Secas", "Graciliano Ramos", 24.90m, tamanhoMb: 1.8),
new LivroImpresso("Grande Sertão: Veredas", "Guimarães Rosa", 89.00m, pesoGramas: 1200, estoque: 3),
];
var tabela = new Table().Title("Catálogo da Livraria");
tabela.AddColumn("Título");
tabela.AddColumn("Autor");
tabela.AddColumn("Formato");
tabela.AddColumn(new TableColumn("Preço").RightAligned());
foreach (Produto p in catalogo)
{
tabela.AddRow(
Markup.Escape(p.Titulo),
Markup.Escape(p.Autor),
p.Formato,
p.Preco.ToString("C"));
}
AnsiConsole.Write(tabela);
var pedido = new Pedido();
pedido.Adicionar(catalogo[0], 2);
pedido.Adicionar(catalogo[1], 1);
var politica = new MelhorDesconto(new DescontoPorVolume(), new CupomPercentual("LEITURA20", 0.20m));
ResultadoDesconto desconto = politica.Calcular(pedido.Subtotal, quantidadeTotal: 3);
AnsiConsole.MarkupLine($"Subtotal: [bold]{pedido.Subtotal:C}[/]");
AnsiConsole.MarkupLine($"Desconto aplicado: [green]{Markup.Escape(desconto.Descricao)}[/] ({desconto.Valor:C})");
AnsiConsole.MarkupLine($"Total: [bold yellow]{pedido.Subtotal - desconto.Valor:C}[/]");
Para rodar a partir da raiz da solução:
dotnet run --project src/Livraria.Cli
Um detalhe do Spectre.Console merece atenção: ele interpreta colchetes como marcação ([bold], [green]), então qualquer texto vindo de dados, como um título que por acaso contenha colchetes, precisa passar por Markup.Escape. É o mesmo princípio que vai reaparecer com força em SQL e em HTML: dados nunca devem ser interpretados como instruções. Aqui a consequência de esquecer é uma tabela quebrada. Em SQL, é uma invasão do banco. Guarde a ideia.
Com isso a Fase 2 está completa, e o salto em relação à primeira fase é grande. Você passou de programas que manipulavam variáveis soltas a um modelo de domínio: tipos com nome e regras próprias, que nascem válidos e protegem seus dados, organizados em hierarquias quando compartilham identidade e em contratos quando compartilham capacidades. Você sabe distinguir entidades de valores e escolher entre classe, struct e record, dar nomes a estados e verificar transições, e empacotar tudo numa biblioteca reutilizável. A Livraria de hoje já poderia ser o núcleo de um sistema real.
Ao longo do caminho, usamos sem parar tipos como List<T>, Dictionary<TKey, TValue> e IReadOnlyList<T>, cujo <T> pedimos que você lesse como "de quê". A Fase 3 começa por eles: vamos entender como a biblioteca padrão escreve um único List<T> que serve para qualquer tipo, com segurança verificada pelo compilador, e escrever os nossos próprios tipos genéricos.
Fontes e leituras recomendadas
- Namespaces (Microsoft Learn): https://learn.microsoft.com/dotnet/csharp/fundamentals/types/namespaces. Cobre declaração, namespaces de arquivo e
global using. - Comando
dotnet sln: https://learn.microsoft.com/dotnet/core/tools/dotnet-sln. É a referência para criar soluções e adicionar ou remover projetos, incluindo o formato.slnx. - O que é o NuGet: https://learn.microsoft.com/nuget/what-is-nuget. É a introdução oficial a pacotes, versões e restauração.
- Personalizar o build por diretório (
Directory.Build.props): https://learn.microsoft.com/visualstudio/msbuild/customize-by-directory. Explica como compartilhar configurações entre todos os projetos de uma solução. - Gerenciamento central de pacotes: https://learn.microsoft.com/nuget/consume-packages/central-package-management. Mostra como fixar as versões de todos os pacotes num único arquivo, útil quando a solução crescer.
- Documentação do Spectre.Console: https://spectreconsole.net. Traz tabelas, prompts interativos, barras de progresso e árvores para aplicações de terminal.
Exercícios
Exercício 1
Monte a estrutura de solução da aula (solução, Livraria.Dominio, Livraria.Cli e a referência entre eles) e rode dotnet build na raiz. O que o comando compila, e em que ordem?
Ver resposta
✓ Resposta: Na raiz, dotnet build encontra o arquivo da solução e compila todos os projetos listados nela. A ordem é decidida pelas dependências: como Livraria.Cli referencia Livraria.Dominio, o domínio é compilado primeiro, e o seu .dll fica disponível para a compilação do console. Você não precisa informar a ordem: ela é deduzida das ProjectReference. Se houvesse projetos independentes entre si, eles poderiam ser compilados em paralelo. Ao final, src/Livraria.Cli/bin/Debug/net10.0/ contém tanto Livraria.Cli.dll quanto uma cópia de Livraria.Dominio.dll, porque o programa precisa das duas para rodar.
Exercício 2
Depois de mover as classes para o domínio, o console passa a mostrar o erro CS0122 ("inacessível devido ao seu nível de proteção") ao usar Pedido. Explique a causa e a correção, e diga que tipos do domínio não deveriam ser públicos.
Ver resposta
✓ Resposta: Uma classe declarada sem modificador de acesso é internal, visível apenas dentro do próprio assembly. Enquanto tudo estava num só projeto, isso não importava. Com o Pedido em Livraria.Dominio, o console é outro assembly e não enxerga tipos internos. A correção é declarar public sealed class Pedido, e fazer o mesmo com todos os tipos que formam o contrato do domínio: Produto e derivados, ItemPedido, StatusPedido, IPoliticaDesconto e as políticas, ResultadoDesconto e Isbn. Devem continuar internal os tipos que são detalhe de implementação, como uma classe auxiliar que só o Pedido usa por dentro. Quanto menor a superfície pública, mais liberdade você tem para mudar a biblioteca sem quebrar quem a usa.
Exercício 3
Um colega criou o projeto de console com o nome Livraria.Console, e agora todo Console.WriteLine escrito dentro das classes do projeto dá erro, embora o Program.cs continue compilando. Explique o que aconteceu e dê duas formas de resolver.
Ver resposta
✓ Resposta: O dotnet new console -n Livraria.Console define Livraria.Console como namespace raiz do projeto, e cada arquivo novo de classe nasce com namespace Livraria.Console;. O Program.cs do modelo não declara namespace nenhum (as instruções de nível superior ficam no namespace global), e por isso continua funcionando. Quando o compilador encontra o nome Console dentro desse namespace, ele procura primeiro nos namespaces mais próximos, acha o namespace Livraria.Console e tenta usá-lo, e um namespace não tem um método WriteLine. A resolução de nomes favorece o que está mais perto, e o namespace do próprio projeto está mais perto que System. A primeira solução, que é a melhor, é renomear o projeto e o namespace (Livraria.Cli, Livraria.App). A segunda é qualificar totalmente o nome, global::System.Console.WriteLine(...), em que global:: manda o compilador começar a busca pela raiz. Ela funciona, mas espalha ruído pelo código inteiro. Prevenir, evitando nomes que colidem com a biblioteca padrão, é muito mais barato que remediar.
Exercício 4
Abra o .csproj do console depois de adicionar o Spectre.Console e explique o significado do número de versão registrado. Por que declarar Version="*" para "sempre pegar a mais nova" seria uma má ideia?
Ver resposta
✓ Resposta: A versão segue o formato MAIOR.MENOR.CORREÇÃO. A correção indica consertos compatíveis, a menor indica recursos novos compatíveis, e a maior indica possíveis quebras de compatibilidade. Fixar a versão exata garante que o projeto compila hoje, amanhã e na máquina de qualquer colega com o mesmo código de terceiros. Com Version="*", cada restauração pode baixar uma versão diferente: o build que funcionava ontem pode quebrar hoje sem nenhuma mudança sua, e dois desenvolvedores podem estar testando bibliotecas diferentes sem saber. Há ainda o risco de segurança: uma versão nova comprometida entraria no seu projeto automaticamente. O caminho certo é fixar as versões e atualizá-las deliberadamente, com dotnet list package --outdated, lendo as notas de lançamento e rodando os testes.
Exercício 5
Crie um terceiro projeto, Livraria.Relatorios (biblioteca de classes), com um método público que gere o texto de um relatório de pedido. Faça o console referenciá-lo. Depois tente fazer Livraria.Dominio referenciar Livraria.Relatorios, sabendo que Relatorios já referencia o domínio. O que acontece, e por que essa restrição é boa?
Ver resposta
✓ Resposta:
dotnet new classlib -n Livraria.Relatorios -o src/Livraria.Relatorios
dotnet sln add src/Livraria.Relatorios
dotnet add src/Livraria.Relatorios reference src/Livraria.Dominio
dotnet add src/Livraria.Cli reference src/Livraria.Relatorios
namespace Livraria.Relatorios;
using Livraria.Dominio.Pedidos;
public static class RelatorioPedido
{
public static string Gerar(Pedido pedido) =>
$"Pedido {pedido.Numero} ({pedido.Status}): {pedido.Itens.Count} item(ns), subtotal {pedido.Subtotal:C}";
}
A tentativa de adicionar a referência inversa, de Livraria.Dominio para Livraria.Relatorios, cria um ciclo: Relatórios depende do Domínio, que dependeria de Relatórios. O build falha com um erro de dependência circular, porque não existe uma ordem de compilação possível: cada um precisaria do .dll do outro pronto antes. A restrição é boa porque força uma arquitetura em camadas, com as dependências apontando numa única direção. O domínio fica no centro, sem depender de nada da Livraria, e relatórios, console, API e site dependem dele. Se o domínio "precisar" de algo de uma camada externa, é sinal de que falta uma interface no domínio, implementada pela camada externa. É a mesma inversão que usamos com IPoliticaDesconto, agora na escala de projetos, e vamos usá-la muito com bancos de dados na Fase 5.