Desenvolver bibliotecas com a CLI do .NET

Esse artigo aborda como escrever bibliotecas para .NET usando a CLI do .NET. A CLI fornece uma experiência eficiente e de baixo nível que funciona em qualquer sistema operacional com suporte. Você ainda pode criar bibliotecas com o Visual Studio e, se essa for sua experiência preferida, consultar o guia do Visual Studio.

Pré-requisitos

Você precisa do SDK do .NET instalado no seu computador.

Para as seções deste documento que tratam de versões do .NET Framework, é necessário ter o .NET Framework instalado em um computador Windows.

Além disso, se você quiser dar suporte a destinos mais antigos do .NET Framework, precisará instalar pacotes de direcionamento/desenvolvedor na página de arquivos de download do .NET Framework. Consulte esta tabela:

Versão do .NET Framework O que baixar
4.6.1 Pacote de Direcionamento do .NET Framework 4.6.1
4,6 Pacote de Direcionamento do .NET Framework 4.6
4.5.2 Pacote de Desenvolvedor do .NET Framework 4.5.2
4.5.1 Pacote de Desenvolvedor do .NET Framework 4.5.1
4.5 Software Development Kit do Windows (SDK do Windows) para Windows 8
4,0 SDK do Windows para Windows 7 e .NET Framework 4
2.0, 3.0 e 3.5 Runtime do .NET Framework 3.5 SP1 (ou versão do Windows 8 ou superior)

Como direcionar o .NET 5+ ou o .NET Standard

Você controla a estrutura de destino do projeto adicionando-a ao arquivo de projeto (.csproj ou .fsproj). Para obter diretrizes sobre como escolher entre direcionar o .NET 5+ ou o .NET Standard, consulte .NET 5+ e .NET Standard.

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
  </PropertyGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>
</Project>

Se desejar direcionar para o .NET Framework versões 4.0 ou inferior, ou se desejar usar uma API disponível no .NET Framework, mas não no .NET Standard (por exemplo, System.Drawing), leia as seções a seguir para aprender a usar multiplataformas.

Como direcionar para o .NET Framework

Observação

Essas instruções pressupõem que você tenha o .NET Framework instalado no seu computador. Consulte os Pré-requisitos para obter as dependências instaladas.

Lembre que algumas das versões do .NET Framework usadas aqui não têm mais suporte. Consulte as Perguntas Frequentes de Política do Ciclo de Vida de Suporte do .NET Framework sobre versões sem suporte.

Se desejar alcançar o número máximo de projetos e desenvolvedores, use o .NET Framework 4.0 como seu destino de linha de base. Para direcionar para o .NET Framework, comece usando o TFM (Moniker da Estrutura de Destino) correto correspondente à versão do .NET Framework à qual você deseja dar suporte.

Versão do .NET Framework TFM
.NET Framework 2.0 net20
.NET Framework 3.0 net30
.NET Framework 3.5 net35
.NET Framework 4.0 net40
.NET Framework 4.5 net45
.NET Framework 4.5.1 net451
.NET Framework 4.5.2 net452
.NET Framework 4.6 net46
.NET Framework 4.6.1 net461
.NET Framework 4.6.2 net462
.NET Framework 4.7 net47
.NET Framework 4.8 net48

Em seguida, insira esse TFM na seção TargetFramework do arquivo de projeto. Por exemplo, aqui está como você escreveria uma biblioteca direcionada ao .NET Framework 4.0:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net40</TargetFramework>
  </PropertyGroup>
</Project>

Pronto! Embora seja compilada somente para o .NET Framework 4, você pode usar a biblioteca em versões mais recentes do .NET Framework.

Como usar multiplataformas

Observação

As instruções a seguir pressupõem que você tenha o .NET Framework instalado no seu computador. Consulte a seção Pré-requisitos para saber quais dependências você precisa instalar e de onde baixá-las.

Talvez seja necessário direcionar para versões mais antigas do .NET Framework quando seu projeto der suporte ao .NET Framework e ao .NET. Nesse cenário, se você quiser usar construções de linguagem e APIs mais recentes para os destinos, use as diretivas #if no seu código. Você também pode precisar adicionar diferentes pacotes e dependências para cada plataforma de destino para incluir as diferentes APIs necessárias para cada caso.

Por exemplo, digamos que você tem uma biblioteca que executa operações de rede por meio de HTTP. Para .NET Standard e .NET Framework versões 4.5 ou posterior, você pode usar a classe HttpClient do namespace System.Net.Http. No entanto, versões anteriores do .NET Framework não tem a classe HttpClient, portanto, você pode usar a classe WebClient do namespace System.Net para elas.

O arquivo de projeto seria semelhante a:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFrameworks>netstandard2.0;net40;net45</TargetFrameworks>
  </PropertyGroup>

  <!-- Need to conditionally bring in references for the .NET Framework 4.0 target -->
  <ItemGroup Condition="'$(TargetFramework)' == 'net40'">
    <Reference Include="System.Net" />
  </ItemGroup>

  <!-- Need to conditionally bring in references for the .NET Framework 4.5 target -->
  <ItemGroup Condition="'$(TargetFramework)' == 'net45'">
    <Reference Include="System.Net.Http" />
    <Reference Include="System.Threading.Tasks" />
  </ItemGroup>
</Project>

Você notará três alterações importantes aqui:

  1. O nó TargetFramework foi substituído pelo TargetFrameworks e três TFMs são expressas dentro.
  2. Há um nó <ItemGroup> para o destino net40 extraindo um que o .NET Framework referencia.
  3. Há um nó <ItemGroup> para o destino net45 extraindo dois que o .NET Framework referencia.

Símbolos do pré-processador.

O sistema de build reconhece os seguintes símbolos do pré-processador usados nas diretivas #if:

Frameworks de destino Símbolos Símbolos adicionais
(disponível em SDKs do .NET 5+)
Símbolos de plataforma (disponíveis somente
quando você especifica um TFM específico do sistema operacional)
.NET Framework NETFRAMEWORK, NET48, NET472, NET471, NET47, NET462, NET461, NET46, NET452, NET451, NET45, NET40, NET35, NET20 NET48_OR_GREATER, NET472_OR_GREATER, NET471_OR_GREATER, NET47_OR_GREATER, NET462_OR_GREATER, NET461_OR_GREATER, NET46_OR_GREATER, NET452_OR_GREATER, NET451_OR_GREATER, NET45_OR_GREATER, NET40_OR_GREATER, NET35_OR_GREATER, NET20_OR_GREATER
.NET Standard NETSTANDARD, NETSTANDARD2_1, NETSTANDARD2_0, NETSTANDARD1_6, NETSTANDARD1_5, NETSTANDARD1_4, NETSTANDARD1_3, NETSTANDARD1_2, NETSTANDARD1_1, NETSTANDARD1_0 NETSTANDARD2_1_OR_GREATER, NETSTANDARD2_0_OR_GREATER, NETSTANDARD1_6_OR_GREATER, NETSTANDARD1_5_OR_GREATER, NETSTANDARD1_4_OR_GREATER, NETSTANDARD1_3_OR_GREATER, NETSTANDARD1_2_OR_GREATER, NETSTANDARD1_1_OR_GREATER, NETSTANDARD1_0_OR_GREATER
.NET 5+ (e .NET Core) NET, NET8_0, NET7_0, NET6_0, NET5_0, NETCOREAPP, NETCOREAPP3_1, NETCOREAPP3_0, NETCOREAPP2_2, NETCOREAPP2_1, NETCOREAPP2_0, NETCOREAPP1_1, NETCOREAPP1_0 NET8_0_OR_GREATER, NET7_0_OR_GREATER, NET6_0_OR_GREATER, NET5_0_OR_GREATER, NETCOREAPP3_1_OR_GREATER, NETCOREAPP3_0_OR_GREATER, NETCOREAPP2_2_OR_GREATER, NETCOREAPP2_1_OR_GREATER, NETCOREAPP2_0_OR_GREATER, NETCOREAPP1_1_OR_GREATER, NETCOREAPP1_0_OR_GREATER ANDROID, BROWSER, IOS, MACCATALYST, MACOS, TVOS, WINDOWS,
[OS][version] (por exemplo, IOS15_1),
[OS][version]_OR_GREATER (por exemplo, IOS15_1_OR_GREATER)

Observação

  • Os símbolos sem versão são definidos independentemente da versão para a qual você está direcionando.
  • Os símbolos específicos à versão são definidos apenas para a versão que você está direcionando.
  • Os símbolos <framework>_OR_GREATER são definidos para a versão que você está direcionando e todas as versões anteriores. Por exemplo, se você estiver direcionando .NET Framework 2.0, os seguintes símbolos serão definidos: NET20, NET20_OR_GREATER, NET11_OR_GREATER e NET10_OR_GREATER.
  • Os símbolos NETSTANDARD<x>_<y>_OR_GREATER são definidos apenas para destinos .NET Standard, e não para destinos que implementam o .NET Standard, como o .NET Core e o .NET Framework.
  • Eles são diferentes dos TFMs (Moniker da Estrutura de Destino) usados pela propriedade MSBuild TargetFramework e pelo NuGet.

Aqui está um exemplo fazendo uso de compilação condicional por destino:

using System;
using System.Text.RegularExpressions;
#if NET40
// This only compiles for the .NET Framework 4 targets
using System.Net;
#else
 // This compiles for all other targets
using System.Net.Http;
using System.Threading.Tasks;
#endif

namespace MultitargetLib
{
    public class Library
    {
#if NET40
        private readonly WebClient _client = new WebClient();
        private readonly object _locker = new object();
#else
        private readonly HttpClient _client = new HttpClient();
#endif

#if NET40
        // .NET Framework 4.0 does not have async/await
        public string GetDotNetCount()
        {
            string url = "https://www.dotnetfoundation.org/";

            var uri = new Uri(url);

            string result = "";

            // Lock here to provide thread-safety.
            lock(_locker)
            {
                result = _client.DownloadString(uri);
            }

            int dotNetCount = Regex.Matches(result, ".NET").Count;

            return $"Dotnet Foundation mentions .NET {dotNetCount} times!";
        }
#else
        // .NET Framework 4.5+ can use async/await!
        public async Task<string> GetDotNetCountAsync()
        {
            string url = "https://www.dotnetfoundation.org/";

            // HttpClient is thread-safe, so no need to explicitly lock here
            var result = await _client.GetStringAsync(url);

            int dotNetCount = Regex.Matches(result, ".NET").Count;

            return $"dotnetfoundation.org mentions .NET {dotNetCount} times in its HTML!";
        }
#endif
    }
}

Se você compilar esse projeto com dotnet build, notará três diretórios sob a pasta bin/:

net40/
net45/
netstandard2.0/

Cada um deles contém arquivos .dll para cada destino.

Como testar bibliotecas no .NET

É importante ser capaz de testar em várias plataformas. Você pode usar o xUnit ou MSTest pronto para uso. Ambos são perfeitamente adequados para realizar o teste da unidade de biblioteca no .NET. A maneira de configurar sua solução com projetos de teste dependerá da estrutura da sua solução. O exemplo a seguir pressupõe que os diretórios de origem e de teste estão no mesmo diretório de nível superior.

Observação

Ele usa alguns comandos da CLI do .NET. Consulte dotnet new e dotnet sln para obter mais informações.

  1. Configure sua solução. Você pode fazer isso usando os seguintes comandos:

    mkdir SolutionWithSrcAndTest
    cd SolutionWithSrcAndTest
    dotnet new sln
    dotnet new classlib -o MyProject
    dotnet new xunit -o MyProject.Test
    dotnet sln add MyProject/MyProject.csproj
    dotnet sln add MyProject.Test/MyProject.Test.csproj
    

    Isso criará projetos e os conectará em uma solução. O diretório para SolutionWithSrcAndTest deve ter esta aparência:

    /SolutionWithSrcAndTest
    |__SolutionWithSrcAndTest.sln
    |__MyProject/
    |__MyProject.Test/
    
  2. Navegue até o diretório do projeto de teste e adicione uma referência a MyProject.Test de MyProject.

    cd MyProject.Test
    dotnet add reference ../MyProject/MyProject.csproj
    
  3. Restaure os pacotes e compile projetos:

    dotnet restore
    dotnet build
    
  4. Verifique se o xUnit é executado por meio da execução do comando dotnet test. Se você optar por usar o MSTest, o executor de console do MSTest deverá ser executado.

Pronto! Agora você pode testar sua biblioteca em todas as plataformas usando as ferramentas de linha de comando. É muito simples testar sua biblioteca agora que está tudo configurado:

  1. Faça alterações na sua biblioteca.
  2. Execute os testes de linha de comando no diretório de teste com o comando dotnet test.

Seu código será recriado automaticamente quando você invoca o comando dotnet test.

Como usar vários projetos

Uma necessidade comum das bibliotecas grandes é alocar funcionalidades em diferentes projetos.

Imagine que você deseja compilar uma biblioteca que poderia ser consumida em expressões idiomáticas de C# e F#. Isso significa que os consumidores da sua biblioteca o farão de maneiras naturais ao C# ou ao F#. Por exemplo, você poderia consumir a biblioteca em C# dessa forma:

using AwesomeLibrary.CSharp;

public Task DoThings(Data data)
{
    var convertResult = await AwesomeLibrary.ConvertAsync(data);
    var result = AwesomeLibrary.Process(convertResult);
    // do something with result
}

No F#, ela poderia assemelhar-se a:

open AwesomeLibrary.FSharp

let doWork data = async {
    let! result = AwesomeLibrary.AsyncConvert data // Uses an F# async function rather than C# async method
    // do something with result
}

Cenários de consumo como esse significam que as APIs que estão sendo acessadas devem ter uma estrutura diferente para C# e F#. Uma abordagem comum para realizar isso é fatorar toda a lógica de uma biblioteca em um projeto principal, com projetos C# e F# definindo as camadas de API que chamam esse projeto principal. O restante da seção usará os nomes a seguir:

  • AwesomeLibrary.Core – Um projeto principal que contém toda a lógica para a biblioteca
  • AwesomeLibrary.CSharp – Um projeto com APIs públicas destinadas ao consumo em C#
  • AwesomeLibrary.FSharp – Um projeto com APIs públicas destinadas ao consumo em F#

Você pode executar os comandos a seguir no seu terminal para produzir a mesma estrutura que este guia:

mkdir AwesomeLibrary && cd AwesomeLibrary
dotnet new sln
mkdir AwesomeLibrary.Core && cd AwesomeLibrary.Core && dotnet new classlib
cd ..
mkdir AwesomeLibrary.CSharp && cd AwesomeLibrary.CSharp && dotnet new classlib
cd ..
mkdir AwesomeLibrary.FSharp && cd AwesomeLibrary.FSharp && dotnet new classlib -lang "F#"
cd ..
dotnet sln add AwesomeLibrary.Core/AwesomeLibrary.Core.csproj
dotnet sln add AwesomeLibrary.CSharp/AwesomeLibrary.CSharp.csproj
dotnet sln add AwesomeLibrary.FSharp/AwesomeLibrary.FSharp.fsproj

Isso adicionará os três projetos acima e um arquivo de solução que os vincula. Criar o arquivo de solução e vincular projetos permitirá que você restaure e compile projetos de um nível superior.

Referência projeto a projeto

A melhor maneira de fazer referência a um projeto é usar a CLI do .NET para adicionar uma referência de projeto. Dos diretórios dos projetos AwesomeLibrary.CSharp e AwesomeLibrary.FSharp, você pode executar o seguinte comando:

dotnet add reference ../AwesomeLibrary.Core/AwesomeLibrary.Core.csproj

Os arquivos de projeto para AwesomeLibrary.CSharp e AwesomeLibrary.FSharp agora farão referência a AwesomeLibrary.Core como um destino ProjectReference. Você pode verificar isso inspecionando os arquivos de projeto e vendo o seguinte neles:

<ItemGroup>
  <ProjectReference Include="..\AwesomeLibrary.Core\AwesomeLibrary.Core.csproj" />
</ItemGroup>

Você pode adicionar esta seção a cada arquivo de projeto manualmente se preferir não usar a CLI do .NET.

Estruturar uma solução

Outro aspecto importante das soluções multiprojetos é estabelecer uma boa estrutura de projeto geral. Você pode organizar o código da maneira que desejar, e desde que vincule cada projeto ao arquivo de solução com dotnet sln add, você poderá executar dotnet restore e dotnet build no nível da solução.