Desenvolver bibliotecas com a CLI do .NET

Este 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 suportado. Você ainda pode criar bibliotecas com o Visual Studio e, se essa for sua experiência preferida, consulte o guia do Visual Studio.

Pré-requisitos

Você precisa do SDK do .NET instalado em sua máquina.

Para as seções deste documento que lidam com versões do .NET Framework, você precisa do .NET Framework instalado em uma máquina Windows.

Além disso, se você deseja oferecer suporte a destinos mais antigos do .NET Framework, você precisa instalar pacotes de segmentação ou pacotes de desenvolvedor da página de downloads do .NET Framework. Consulte esta tabela:

Versão do .NET Framework O que fazer o download
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 do desenvolvedor do .NET Framework 4.5.2
4.5.1 Pacote do desenvolvedor do .NET Framework 4.5.1
4,5 Kit de Desenvolvimento de Software do Windows para Windows 8
4.0 SDK do Windows para Windows 7 e .NET Framework 4
2.0, 3.0 e 3.5 Tempo de execução do .NET Framework 3.5 SP1 (ou versão Windows 8+)

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

Você controla a estrutura de destino do seu projeto adicionando-a ao seu arquivo de projeto (.csproj ou .fsproj). Para obter orientação sobre como escolher entre o direcionamento do .NET 5+ ou do .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 você deseja direcionar o .NET Framework versões 4.0 ou inferiores, ou deseja usar uma API disponível no .NET Framework mas não no .NET Standard (por exemplo, System.Drawing), leia as seções a seguir e saiba como multitarget.

Como direcionar o .NET Framework

Nota

Estas instruções pressupõem que você tenha o .NET Framework instalado em sua máquina. Consulte os Pré-requisitos para instalar dependências.

Lembre-se de que algumas das versões do .NET Framework usadas aqui não são mais suportadas. Consulte as Perguntas frequentes sobre a Política de Ciclo de Vida de Suporte do .NET Framework sobre versões sem suporte.

Se você quiser atingir o número máximo de desenvolvedores e projetos, use o .NET Framework 4.0 como seu destino de linha de base. Para direcionar o .NET Framework, comece usando o Target Framework Moniker (TFM) correto que corresponde à versão do .NET Framework que você deseja suportar.

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 TargetFramework seção do seu arquivo de projeto. Por exemplo, veja como você escreveria uma biblioteca destinada ao .NET Framework 4.0:

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

E já está! Embora isso tenha sido compilado apenas para o .NET Framework 4, você pode usar a biblioteca em versões mais recentes do .NET Framework.

Como multitarget

Nota

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

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

Por exemplo, digamos que você tenha uma biblioteca que execute operações de rede por HTTP. Para o .NET Standard e o .NET Framework versões 4.5 ou superiores, você pode usar a HttpClient classe do System.Net.Http namespace. No entanto, versões anteriores do .NET Framework não têm a HttpClient classe, então você pode usar a WebClientSystem.Net classe do namespace para esses em vez disso.

Seu arquivo de projeto pode ter esta aparência:

<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 grandes mudanças aqui:

  1. O TargetFramework nó foi substituído por TargetFrameworks, e três TFMs são expressos no interior.
  2. Há um <ItemGroup> nó para o net40 destino puxando em uma referência do .NET Framework.
  3. Há um <ItemGroup> nó para o net45 destino puxando duas referências do .NET Framework.

Símbolos do pré-processador

O sistema de compilação está ciente dos seguintes símbolos de pré-processador usados em #if diretivas:

Estruturas de destino Símbolos Símbolos adicionais
(disponível em SDKs do .NET 5+)
Símbolos da plataforma (disponível apenas
quando você especifica um TFM específico do sistema operacional)
.NET Framework NETFRAMEWORK, NET48, , NET472, , NET462NET452NET47NET46NET35NET461NET451NET45NET40NET471NET20 NET48_OR_GREATER, NET472_OR_GREATER, , NET471_OR_GREATER, , NET461_OR_GREATERNET45_OR_GREATERNET46_OR_GREATERNET35_OR_GREATERNET462_OR_GREATERNET452_OR_GREATERNET451_OR_GREATERNET40_OR_GREATERNET47_OR_GREATERNET20_OR_GREATER
.NET Standard NETSTANDARD, NETSTANDARD2_1, , NETSTANDARD2_0, NETSTANDARD1_6, NETSTANDARD1_4NETSTANDARD1_5, NETSTANDARD1_3, NETSTANDARD1_2NETSTANDARD1_1,NETSTANDARD1_0 NETSTANDARD2_1_OR_GREATER, NETSTANDARD2_0_OR_GREATER, NETSTANDARD1_6_OR_GREATER, , NETSTANDARD1_5_OR_GREATER, NETSTANDARD1_3_OR_GREATERNETSTANDARD1_4_OR_GREATER, NETSTANDARD1_2_OR_GREATER, NETSTANDARD1_1_OR_GREATER,NETSTANDARD1_0_OR_GREATER
.NET 5+ (e .NET Core) NET, NET8_0, , NET7_0, , NETCOREAPPNETCOREAPP2_1NETCOREAPP3_1NETCOREAPP1_1NET5_0NETCOREAPP3_0NETCOREAPP2_2NETCOREAPP2_0NET6_0NETCOREAPP1_0 NET8_0_OR_GREATER, NET7_0_OR_GREATER, , NET5_0_OR_GREATERNET6_0_OR_GREATER, NETCOREAPP3_1_OR_GREATER, NETCOREAPP3_0_OR_GREATER, NETCOREAPP2_2_OR_GREATER, NETCOREAPP2_1_OR_GREATERNETCOREAPP2_0_OR_GREATERNETCOREAPP1_1_OR_GREATERNETCOREAPP1_0_OR_GREATER ANDROID, BROWSER, , IOS, MACOSMACCATALYST, TVOS, WINDOWS,
[OS][version] (por exemplo IOS15_1),
[OS][version]_OR_GREATER (por exemplo IOS15_1_OR_GREATER)

Nota

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

Aqui está um exemplo que faz uso da 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 este projeto com dotnet buildo , notará três diretórios na bin/ pasta:

net40/
net45/
netstandard2.0/

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

Como testar bibliotecas no .NET

É importante poder testar em todas as plataformas. Você pode usar xUnit ou MSTest fora da caixa. Ambos são perfeitamente adequados para testar sua biblioteca no .NET. A forma como você configura 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 teste e de origem vivam no mesmo diretório de nível superior.

Nota

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

  1. Configure a sua solução. Você pode fazer isso com 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. Seu 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 pacotes e construa projetos:

    dotnet restore
    dotnet build
    
  4. Verifique se o xUnit é executado executando o dotnet test comando. Se você optar por usar o MSTest, o executor do console MSTest deverá ser executado.

E já está! Agora você pode testar sua biblioteca em todas as plataformas usando ferramentas de linha de comando. Para continuar a testar agora que tem tudo configurado, testar a sua biblioteca é muito simples:

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

Seu código será reconstruído automaticamente quando você invocar dotnet test o comando.

Como usar vários projetos

Uma necessidade comum de bibliotecas maiores é colocar a funcionalidade em diferentes projetos.

Imagine que você deseja construir uma biblioteca que possa ser consumida em C# e F# idiomáticos. Isso significaria que os consumidores de sua biblioteca a consomem de maneiras naturais para C# ou F#. Por exemplo, em C# você pode consumir a biblioteca assim:

using AwesomeLibrary.CSharp;

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

Em F#, pode ter esta aparência:

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 acessadas precisam ter uma estrutura diferente para C# e F#. Uma abordagem comum para fazer 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 seguintes nomes:

  • 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 seguintes comandos em seu terminal para produzir a mesma estrutura deste 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 construa projetos a partir de um nível superior.

Referência de 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. Nos diretórios de projeto 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 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.

Estruturação de uma solução

Outro aspeto importante das soluções multi-projeto é estabelecer uma boa estrutura geral do projeto. Você pode organizar o código como quiser, e desde que vincule cada projeto ao seu arquivo de solução com dotnet sln addo , você poderá executar dotnet restore e dotnet build no nível da solução.