C# Soluções e Vários Projetos

Um programa de verdade raramente é um projeto só. A lojinha da Ana vai ter o programa de console, uma parte com as regras do estoque (que um dia também vai servir a um site), e os testes. Cada parte é um projeto, com o seu .csproj. Para juntar tudo e compilar de uma vez, o .NET usa uma solução: um arquivo que lista os projetos que andam juntos. Nesta aula você vai criar uma solução no formato novo do .NET 10, o .slnx, pôr dois projetos nela e fazer um usar o outro.

Você vai aprender
  • O que é uma solução e para que ela serve.
  • A criar uma com dotnet new sln e a ler o arquivo .slnx .NET 10.
  • A organizar os projetos na pasta src e a pô-los na solução com dotnet sln add, list e remove.
  • A compilar tudo com um dotnet build e a rodar um projeto com dotnet run --project.
  • A fazer um projeto usar o outro com dotnet reference add.
  • O formato antigo .sln, o dotnet sln migrate e os erros MSB1011 e MSB4006.

Todas as saídas desta aula são reais, feitas com o .NET 10 SDK (10.0.112) na máquina fictícia da Ana, na pasta ~/projetos/Loja. Você precisa já conhecer Projetos e O Arquivo .csproj.


A ideia: o fichário e os cadernos

Na escola, a Ana tinha um caderno para cada matéria e um fichário que juntava todos. O fichário não tem conteúdo próprio: ele só diz quais cadernos fazem parte do ano. Para levar o ano inteiro para casa, ela pega o fichário.

Na escolaNo .NETArquivo
Um cadernoUm projeto: um programa ou uma bibliotecaLoja.App.csproj
O fichárioA solução: a lista dos projetosLoja.slnx
Levar o fichário para casadotnet build na pasta da solução compila todos os projetos

Solução, em inglês, é solution, e por isso a extensão .sln (a antiga) e .slnx (a nova, com o x de XML). Editores como o Visual Studio e o Rider abrem a solução para mostrar todos os projetos de uma vez; o VS Code, com a extensão C# Dev Kit, também.


1. Criar a solução: dotnet new sln

A Ana cria a pasta da lojinha e, dentro dela, a solução:

$ mkdir Loja && cd Loja
$ dotnet new sln
O modelo "Arquivo de Solução" foi criado com êxito.

$ ls
Loja.slnx
$ cat Loja.slnx
<Solution>
</Solution>
dotnet new sln
Cria uma solução a partir do modelo "Arquivo de Solução". O nome vem da pasta (Loja); para outro, use -n Nome.
Loja.slnx
No .NET 10, o formato padrão é o .slnx .NET 10. Até o .NET 9, o mesmo comando criava um .sln (seção 6).
<Solution> </Solution>
Uma solução vazia: ainda não há nenhum projeto no fichário. O .slnx é XML, como o .csproj.

2. Criar os projetos na pasta src

O costume é guardar os projetos numa pasta src (de source, "código-fonte"), e os testes numa pasta tests (veja Testes). Os nomes dos projetos começam com o nome da solução e um ponto: Loja.App (o programa) e Loja.Estoque (as regras do estoque, uma biblioteca, veja Bibliotecas de Classes):

$ dotnet new console -o src/Loja.App
O modelo "Aplicativo do Console" foi criado com êxito.

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


$ dotnet new classlib -o src/Loja.Estoque
O modelo "Biblioteca de Classes" foi criado com êxito.

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

O -o src/Loja.App cria as duas pastas de uma vez, e o nome do projeto vem da última (Loja.App). O modelo classlib ("biblioteca de classes") cria um projeto sem Program.cs, com um arquivo de exemplo Class1.cs. Por enquanto, os dois projetos só estão na mesma pasta: a solução ainda não sabe deles.


3. Pôr os projetos na solução: dotnet sln add

$ dotnet sln add src/Loja.App src/Loja.Estoque
O projeto ‘src/Loja.App/Loja.App.csproj’ foi adicionado à solução.
O projeto ‘src/Loja.Estoque/Loja.Estoque.csproj’ foi adicionado à solução.
$ cat Loja.slnx
<Solution>
  <Folder Name="/src/">
    <Project Path="src/Loja.App/Loja.App.csproj" />
    <Project Path="src/Loja.Estoque/Loja.Estoque.csproj" />
  </Folder>
</Solution>
$ dotnet sln list
Projetos
--------
src/Loja.App/Loja.App.csproj
src/Loja.Estoque/Loja.Estoque.csproj
dotnet sln add src/Loja.App src/Loja.Estoque
"Solução, acrescente": põe os projetos na solução da pasta atual. Pode ser a pasta do projeto (o dotnet acha o .csproj dentro dela) ou o caminho do próprio .csproj. Vários de uma vez, separados por espaço.
<Folder Name="/src/">
Uma "pasta da solução": um grupo que os editores mostram como uma pastinha. O dotnet sln add criou o grupo /src/ porque os projetos estão na pasta src.
<Project Path="…" />
Um projeto da solução: só o caminho do .csproj, a partir da pasta da solução. É tudo o que o .slnx precisa guardar.
dotnet sln list
Lista os projetos da solução.

A estrutura de pastas da lojinha ficou assim:

Loja/
├── Loja.slnx                  « a solução (o fichário)
└── src/
    ├── Loja.App/              « o programa
    │   ├── Loja.App.csproj
    │   └── Program.cs
    └── Loja.Estoque/          « a biblioteca
        ├── Loja.Estoque.csproj
        └── Class1.cs

4. Compilar tudo de uma vez

Na pasta da solução, o dotnet build encontra o Loja.slnx e compila todos os projetos dele:

$ dotnet build
  Determinando os projetos a serem restaurados...
  Todos os projetos estão atualizados para restauração.
  Loja.Estoque -> /home/ana/projetos/Loja/src/Loja.Estoque/bin/Debug/net10.0/Loja.Estoque.dll
  Loja.App -> /home/ana/projetos/Loja/src/Loja.App/bin/Debug/net10.0/Loja.App.dll

Compilação com êxito.
    0 Aviso(s)
    0 Erro(s)

Tempo Decorrido 00:00:08.43

Uma linha Projeto -> …dll para cada projeto. Cada um continua com a sua própria pasta bin. Repare que a biblioteca virou só um .dll (sem executável): ela não roda sozinha. O mesmo vale para dotnet test, dotnet clean e dotnet format: na pasta da solução, eles agem sobre todos os projetos.

Rodar: dotnet run --project

Já o dotnet run precisa saber qual programa rodar:

$ dotnet run
Não foi possível localizar um projeto para executar. Verifique se existe um projeto em /home/ana/projetos/Loja ou passe o caminho para o projeto usando --project.
$ dotnet run --project src/Loja.App
Hello, World!

Com --project, a Ana continua na pasta da solução. A outra forma é entrar na pasta do projeto (cd src/Loja.App) e rodar dotnet run lá.


5. Um projeto usar o outro: dotnet reference add

Agora o conteúdo. A Ana apaga o Class1.cs e escreve a classe Produto na biblioteca:

namespace Loja.Estoque;

public class Produto(string nome, decimal preco, int quantidade)
{
    public string Nome { get; } = nome;
    public decimal Preco { get; } = preco;
    public int Quantidade { get; } = quantidade;

    public decimal Total => Preco * Quantidade;
}

E o programa usa essa classe:

using Loja.Estoque;

Produto[] produtos =
[
    new("Caderno", 12.50m, 10),
    new("Lápis", 1.99m, 40),
];

foreach (Produto p in produtos)
{
    Console.WriteLine($"{p.Nome,-8} {p.Quantidade,3} x {p.Preco,8:C} = {p.Total,9:C}");
}
$ dotnet build 2>&1 | grep -E "error" | sort -u
/home/ana/projetos/Loja/src/Loja.App/Program.cs(1,7): error CS0246: O nome do tipo ou do namespace "Loja" não pode ser encontrado (está faltando uma diretiva using ou uma referência de assembly?) [/home/ana/projetos/Loja/src/Loja.App/Loja.App.csproj]

Estar na mesma solução não basta. A solução é só o fichário: cada projeto é compilado separado e só enxerga o que está nele mesmo. Para o Loja.App enxergar o Loja.Estoque, é preciso uma referência de projeto:

$ dotnet reference add src/Loja.Estoque --project src/Loja.App
A referência ‘..\Loja.Estoque\Loja.Estoque.csproj’ foi adicionada ao projeto.
$ cat src/Loja.App/Loja.App.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <ItemGroup>
    <ProjectReference Include="..\Loja.Estoque\Loja.Estoque.csproj" />
  </ItemGroup>

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>
$ dotnet build
  Determinando os projetos a serem restaurados...
  /home/ana/projetos/Loja/src/Loja.App/Loja.App.csproj restaurado (em 206 ms).
  1 de 2 projetos estão atualizados para restauração.
  Loja.Estoque -> /home/ana/projetos/Loja/src/Loja.Estoque/bin/Debug/net10.0/Loja.Estoque.dll
  Loja.App -> /home/ana/projetos/Loja/src/Loja.App/bin/Debug/net10.0/Loja.App.dll

Compilação com êxito.
    0 Aviso(s)
    0 Erro(s)

Tempo Decorrido 00:00:02.12
$ dotnet run --project src/Loja.App
Caderno   10 x R$ 12,50 = R$ 125,00
Lápis     40 x  R$ 1,99 =  R$ 79,60
dotnet reference add src/Loja.Estoque
"Referência, acrescente": o projeto a ser usado.
--project src/Loja.App
O projeto que vai usar. Leia assim: "o Loja.App passa a usar o Loja.Estoque". (A ordem antiga, dotnet add src/Loja.App reference src/Loja.Estoque, também funciona.)
<ProjectReference Include="..\Loja.Estoque\Loja.Estoque.csproj" />
A linha que o comando escreveu no .csproj do Loja.App. O caminho é a partir da pasta do próprio Loja.App (.. = "a pasta de cima"). A barra invertida \ é costume do Windows; no Linux o dotnet entende igual. O comando pôs o ItemGroup antes do PropertyGroup: a ordem não importa.
Loja.Estoque -> … antes de Loja.App -> …
O dotnet descobre a ordem sozinho: quem é usado é compilado primeiro.

A classe da biblioteca precisa ser public para o outro projeto enxergá-la (veja Modificadores de Acesso). Os detalhes de bibliotecas (o que fica visível, como organizar) estão em Bibliotecas de Classes.

No editor do guia não há solução nem projetos separados: os arquivos das abas são compilados juntos, como se fossem um projeto só. Mas o namespace e o using funcionam igual:

Missão: a aba Produto.cs está no namespace Loja.Estoque, e o Program.cs não o importou: rode e veja o CS0246. Acrescente a linha que falta no começo do Program.cs e rode de novo.

Produto[] produtos =
[
    new("Caderno", 12.50m, 10),
    new("Lápis", 1.99m, 40),
];

foreach (Produto p in produtos)
{
    Console.WriteLine($"{p.Nome,-8} {p.Quantidade,3} x {p.Preco,8:C} = {p.Total,9:C}");
}
namespace Loja.Estoque;

public class Produto(string nome, decimal preco, int quantidade)
{
    public string Nome { get; } = nome;
    public decimal Preco { get; } = preco;
    public int Quantidade { get; } = quantidade;

    public decimal Total => Preco * Quantidade;
}
using Loja.Estoque;

Produto[] produtos =
[
    new("Caderno", 12.50m, 10),
    new("Lápis", 1.99m, 40),
];

foreach (Produto p in produtos)
{
    Console.WriteLine($"{p.Nome,-8} {p.Quantidade,3} x {p.Preco,8:C} = {p.Total,9:C}");
}

6. O formato antigo: .sln

Quase todo projeto feito antes do .NET 10, e quase todo tutorial, usa o formato .sln. Para ver como ele é, a Ana criou um com --format sln:

$ dotnet new sln --format sln -n Antiga
O modelo "Arquivo de Solução" foi criado com êxito.

$ cat Antiga.sln

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Global
	GlobalSection(SolutionConfigurationPlatforms) = preSolution
		Debug|Any CPU = Debug|Any CPU
		Release|Any CPU = Release|Any CPU
	EndGlobalSection
	GlobalSection(SolutionProperties) = preSolution
		HideSolutionNode = FALSE
	EndGlobalSection
EndGlobal

São 14 linhas (a primeira, em branco) para uma solução vazia, num formato próprio do Visual Studio. Depois de dotnet sln Antiga.sln add src/Loja.Estoque, com um projeto, o arquivo foi para 39 linhas. Um pedaço:

$ cat Antiga.sln
«… as 5 primeiras linhas, iguais …»
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Loja.Estoque", "src\Loja.Estoque\Loja.Estoque.csproj", "{EB621F30-53E9-43A3-9021-E43CBC4574F9}"
EndProject
Global
	GlobalSection(SolutionConfigurationPlatforms) = preSolution
		Debug|Any CPU = Debug|Any CPU
		Debug|x64 = Debug|x64
«…»
	GlobalSection(ProjectConfigurationPlatforms) = postSolution
		{EB621F30-53E9-43A3-9021-E43CBC4574F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{EB621F30-53E9-43A3-9021-E43CBC4574F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
«… mais 10 linhas como essas …»
EndGlobal
$ wc -l Antiga.sln
39 Antiga.sln

Cada projeto ganha um código de identificação longo (um GUID, como {EB621F30-…}), repetido em uma dúzia de linhas de configuração. Era difícil de ler e de juntar quando duas pessoas mexiam no mesmo arquivo. O .slnx guarda a mesma informação em XML simples, e foi por isso que virou o padrão. (Para seguir a aula, a Ana apagou essa Antiga.sln e criou outra vazia, logo abaixo.)

.sln (antigo).slnx (novo) .NET 10
FormatoTexto próprio do Visual Studio, com GUIDsXML simples
Solução vazia14 linhas2 linhas
Com um projeto39 linhas5 linhas
Padrão do dotnet new slnaté o .NET 9no .NET 10
Quem abreTodos os editores e versões do .NETSó as versões recentes do dotnet (a partir do SDK 9.0.200) e dos editores

Converter: dotnet sln migrate

Para passar um .sln para o formato novo (aqui, uma Antiga.sln vazia, criada de novo com dotnet new sln --format sln -n Antiga):

$ dotnet sln Antiga.sln add src/Loja.App
O projeto ‘src/Loja.App/Loja.App.csproj’ foi adicionado à solução.
O projeto ‘src/Loja.Estoque/Loja.Estoque.csproj’ foi adicionado à solução.
$ dotnet sln Antiga.sln migrate
Arquivo .slnx /home/ana/projetos/Loja/Antiga.slnx gerado.
$ ls
Antiga.sln
Antiga.slnx
Loja.slnx
src
dotnet sln Antiga.sln add …
Com duas soluções na pasta, diga qual logo depois do sln. Repare: a Ana pediu só o Loja.App, e o Loja.Estoque veio junto. É que o Loja.App usa o Loja.Estoque (a referência da seção 5), e o dotnet sln add acrescenta também os projetos usados.
dotnet sln Antiga.sln migrate
"Migre": cria o Antiga.slnx ao lado. O .sln original não é apagado: confira o novo e apague o velho você mesmo.

A Ana apagou as duas (rm Antiga.sln Antiga.slnx), porque a lojinha já tem a Loja.slnx. Uma pasta deve ter uma solução só (veja "Quando dá errado").


7. Tirar um projeto: dotnet sln remove

$ dotnet sln remove src/Loja.Estoque
O projeto `src/Loja.Estoque/Loja.Estoque.csproj` foi removido da solução.
$ dotnet sln list
Projetos
--------
src/Loja.App/Loja.App.csproj

O remove só tira o projeto da lista: a pasta src/Loja.Estoque e os arquivos continuam no disco. A Ana pôs de volta com dotnet sln add src/Loja.Estoque.


Quando dá errado

error MSB1011: duas soluções na mesma pasta
$ ls
Antiga.sln
Loja.slnx
src
$ dotnet build
MSBUILD : error MSB1011: Especifique o arquivo de solução ou de projeto a usar porque esta pasta contém mais de um arquivo de solução ou de projeto.

O que aconteceu: com duas soluções (ou uma solução e um .csproj) na pasta, o dotnet não sabe qual usar. Acontece muito logo depois de um migrate.

Como resolver: diga qual (dotnet build Loja.slnx) ou, melhor, deixe uma só. Nunca ponha a solução na mesma pasta de um .csproj: a solução fica em cima, os projetos em src/.

Não foi possível encontrar o projeto ou diretório e já contém o projeto
$ dotnet sln add src/Loja.Relatorios
Não foi possível encontrar o projeto ou diretório ‘src/Loja.Relatorios’.
$ dotnet sln add src/Loja.App
A solução /home/ana/projetos/Loja/Loja.slnx já contém o projeto src/Loja.App/Loja.App.csproj.

O que aconteceu: no primeiro, o projeto ainda não existe (ou o caminho está errado). No segundo, nada de errado: o projeto já estava na solução e não foi duplicado.

Como resolver: crie o projeto antes (dotnet new … -o src/Loja.Relatorios) e confira o caminho com ls src.

error CS0246: está na solução, mas o outro projeto não enxerga

O que aconteceu: é o erro da seção 5: O nome do tipo ou do namespace "Loja" não pode ser encontrado. A solução junta os projetos para compilar, mas não faz um usar o outro.

Como resolver: dotnet reference add src/Loja.Estoque --project src/Loja.App, o using Loja.Estoque; no arquivo e a classe public na biblioteca.

error MSB4006: Há uma dependência circular
$ dotnet reference add src/Loja.App --project src/Loja.Estoque
A referência ‘..\Loja.App\Loja.App.csproj’ foi adicionada ao projeto.
$ dotnet build 2>&1 | tail -5
/usr/lib/dotnet/sdk/10.0.112/NuGet.targets(1294,5): error MSB4006: Há uma dependência circular no elemento gráfico de dependência circular de destino envolvendo o destino "_GenerateRestoreProjectPathWalk". [/home/ana/projetos/Loja/src/Loja.Estoque/Loja.Estoque.csproj]
    0 Aviso(s)
    1 Erro(s)

Tempo Decorrido 00:00:00.95

O que aconteceu: o Loja.App já usava o Loja.Estoque, e a Ana fez o Loja.Estoque usar o Loja.App também. Agora cada um precisa do outro pronto para ser compilado: um círculo sem começo. A tradução está confusa, mas a palavra-chave é "circular" (em inglês, There is a circular dependency).

Como resolver: as referências só podem ir num sentido: o programa usa a biblioteca, nunca o contrário. Tire a referência errada passando o caminho do arquivo .csproj:

$ dotnet reference remove --project src/Loja.Estoque/Loja.Estoque.csproj src/Loja.App/Loja.App.csproj
Referência de projeto ‘../Loja.App/Loja.App.csproj’ removida.

Cuidado: com o caminho da pasta (dotnet reference remove src/Loja.App --project src/Loja.Estoque), o SDK 10.0.112 calcula o caminho errado e responde Não foi possível encontrar a referência de projeto ‘../../Loja.App/Loja.App.csproj’, sem remover nada. Se a biblioteca precisar de algo do programa, esse algo deve mudar para a biblioteca.


Referência rápida

ComandoO que faz
dotnet new slnCria a solução .slnx (o nome vem da pasta; -n Nome para outro).
dotnet new sln --format slnCria no formato antigo .sln.
dotnet sln add caminho…Põe projetos na solução (e os que eles usam).
dotnet sln listLista os projetos.
dotnet sln remove caminhoTira um projeto da lista (não apaga nada).
dotnet sln Antiga.sln migrateCria um .slnx a partir de um .sln.
dotnet build (na pasta da solução)Compila todos os projetos, na ordem certa.
dotnet run --project src/Loja.AppRoda um projeto da solução.
dotnet reference add A --project BO projeto B passa a usar o A.
dotnet reference list --project BLista os projetos que o B usa.
dotnet reference remove --project B.csproj A.csprojTira a referência (use os caminhos dos arquivos).

Exercícios

1. Criar a solução

Complete o comando que cria a solução na pasta atual:

dotnet new

2. Pôr um projeto

Complete para pôr o projeto src/Loja.Web na solução:

dotnet sln src/Loja.Web

3. Rodar da pasta da solução

Complete para rodar o Loja.App sem sair da pasta da solução:

dotnet run

4. Os projetos Loja.App e Loja.Estoque estão na mesma solução, mas o Loja.App não acha a classe Produto. Por quê?

5. O que tem dentro de um .slnx?

6. A biblioteca Loja.Estoque passou a usar o programa Loja.App, que já usava a biblioteca. O que acontece?


Resumo

  • Uma solução é o fichário: a lista dos projetos que andam juntos. Ela não tem código.
  • dotnet new sln cria um .slnx .NET 10: XML simples com <Project Path="…" />. O formato antigo é o .sln; dotnet sln migrate converte.
  • Estrutura comum: a solução em cima, os projetos em src/ e os testes em tests/.
  • dotnet sln add, list e remove mexem na lista.
  • Na pasta da solução, dotnet build compila tudo, na ordem certa; para rodar, dotnet run --project src/Loja.App.
  • Para um projeto usar outro: dotnet reference add A --project B, que escreve um <ProjectReference>. As referências vão num sentido só.