Sviluppare librerie con l'interfaccia della riga di comando di .NET

Questo articolo illustra come scrivere librerie per .NET usando l'interfaccia della riga di comando di .NET. L'interfaccia della riga di comando offre un'esperienza efficace e di basso livello per qualsiasi sistema operativo supportato. È comunque sempre possibile creare librerie con Visual Studio. Se si preferisce questo tipo di esperienza, consultare la Guida di Visual Studio.

Prerequisiti

È necessario che l’SDK .NET sia installato nel computer.

Per le sezioni di questo documento relative alle versioni di .NET Framework è necessario avere .NET Framework installato in un computer Windows.

Inoltre, se si desidera supportare destinazioni .NET Framework meno recenti, è necessario installare pacchetti di destinazione o pacchetti di sviluppo dalla pagina di download di .NET Framework. Fare riferimento a questa tabella:

Versione di .NET Framework Pacchetto da scaricare
4.6.1 .NET Framework 4.6.1 Targeting Pack
4.6 .NET Framework 4.6 Targeting Pack
4.5.2 .NET Framework 4.5.2 Developer Pack
4.5.1 .NET Framework 4.5.1 Developer Pack
4.5 Windows Software Development Kit per Windows 8
4.0 Windows SDK per Windows 7 e .NET Framework 4
2.0, 3.0 e 3.5 Runtime di .NET Framework 3.5 SP1 (o versione per Windows 8 o successiva)

Come usare .NET 5+ o .NET Standard

È possibile controllare il framework di destinazione del progetto aggiungendolo al file di progetto (.csproj o .fsproj). Per indicazioni su come scegliere tra .NET 5+ o .NET Standard, consultare .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 si vuole definire come destinazione .NET Framework 4.0 o versione precedente, o si desidera usare un'API disponibile in .NET Framework ma non in .NET Standard (ad esempio, System.Drawing), leggere le sezioni seguenti per apprendere come definire più destinazioni.

Come usare .NET Framework come destinazione

Nota

Queste istruzioni presuppongono che nel computer sia installato .NET Framework. Vedere la sezione Prerequisiti per informazioni sulle dipendenze da installare.

Tenere presente che alcune delle versioni di .NET Framework indicate in questo argomento non sono più supportate. Per informazioni sulle versioni non supportate, vedere Domande frequenti sui criteri relativi al ciclo di vita del supporto per Microsoft .NET Framework.

Se si vuole raggiungere il numero massimo di sviluppatori e progetti, usare .NET Framework 4.0 come destinazione di base. Per definire .NET Framework come destinazione, iniziare usando il TFM (moniker framework di destinazione) corretto, corrispondente alla versione di .NET Framework da supportare.

Versione di .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

Quindi inserire il TFM nella sezione TargetFramework del file di progetto. Ad esempio, ecco come scrivere una libreria con destinazione .NET Framework 4.0:

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

L'attività è terminata. Anche se compilata solo per .NET Framework 4, la libreria può essere usata nelle versioni più recenti di .NET Framework.

Come definire più destinazioni

Nota

Le istruzioni seguenti presuppongono che nel computer sia installato .NET Framework. Vedere la sezione Prerequisiti per informazioni sulle dipendenze da installare e sull'area da cui scaricarle.

Quando il progetto supporta sia .NET Framework che .NET, può essere opportuno definire come destinazione le versioni precedenti di .NET Framework. In questo scenario, per usare API e costrutti di linguaggio più recenti per le destinazioni più recenti, inserire direttive #if nel codice. Può anche essere necessario aggiungere diversi pacchetti e dipendenze per ogni piattaforma di destinazione per includere le diverse API necessarie per ogni caso.

Si supponga ad esempio di avere una libreria che esegue operazioni di rete tramite HTTP. Per .NET Standard e .NET Framework 4.5 o versioni successive, è possibile usare la classe HttpClient dello spazio dei nomi System.Net.Http. Tuttavia, le versioni precedenti di .NET Framework non dispongono della classe HttpClient e per tali versioni è quindi possibile usare in alternativa la classe WebClient dello spazio dei nomi System.Net.

Il file di progetto può avere un aspetto simile al seguente:

<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>

Si noteranno tre modifiche principali:

  1. Il nodo TargetFramework è stato sostituito da TargetFrameworks e all'interno sono espressi tre TFM.
  2. Un nodo <ItemGroup> per la destinazione net40 chiama un riferimento di .NET Framework.
  3. Un nodo <ItemGroup> per la destinazione net45 chiama due riferimenti di .NET Framework.

Simboli del preprocessore

Il sistema di compilazione è in grado di riconoscere i seguenti simboli di preprocessore usati nelle direttive #if:

Framework di destinazione Simboli Simboli aggiuntivi
(disponibile in .NET 5+ SDK)
Simboli della piattaforma (disponibile solo
quando si specifica un TFM specifico del sistema operativo)
.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] (ad esempio IOS15_1),
[OS][version]_OR_GREATER (ad esempio, IOS15_1_OR_GREATER)

Nota

  • I simboli senza versione vengono definiti indipendentemente dalla versione di destinazione.
  • I simboli specifici della versione sono definiti solo per la versione di destinazione.
  • I simboli <framework>_OR_GREATER sono definiti per la versione di destinazione e per tutte le versioni precedenti. Ad esempio, se si ha come destinazione .NET Framework 2.0, vengono definiti i simboli seguenti: NET20, NET20_OR_GREATER, NET11_OR_GREATER e NET10_OR_GREATER.
  • I simboli NETSTANDARD<x>_<y>_OR_GREATER sono definiti solo per le destinazioni .NET Standard e non per le destinazioni che implementano .NET Standard, ad esempio .NET Core e .NET Framework.
  • Questi sono diversi dai moniker del framework di destinazione (TFMs) usati dalla proprietà TargetFramework MSBuild e Da NuGet.

Ecco un esempio che usa la compilazione condizionale basata sulla destinazione:

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 si compila il progetto con dotnet build si noteranno tre directory sotto la cartella bin/:

net40/
net45/
netstandard2.0/

Ognuno di questi contiene i file .dll per ogni destinazione.

Come testare le librerie in .NET

È importante essere in grado di eseguire test tra diverse piattaforme. È possibile usare xUnit o MSTest senza modifiche. Entrambi sono adatti per gli unit test della libreria in .NET. La modalità di configurazione della soluzione con progetti di test dipende dalla struttura della soluzione. Nell'esempio seguente si presuppone che le directory di test e di origine si trovino nella stessa directory di livello superiore.

Nota

Questa procedura usa alcuni comandi CLI .NET. Per altre informazioni, vedere dotnet new e dotnet sln.

  1. Configurare la soluzione. È possibile farlo con i comandi seguenti:

    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
    

    I progetti vengono creati e collegati in una soluzione. La directory per SolutionWithSrcAndTest ha un aspetto simile al seguente:

    /SolutionWithSrcAndTest
    |__SolutionWithSrcAndTest.sln
    |__MyProject/
    |__MyProject.Test/
    
  2. Passare alla directory del progetto di test e aggiungere un riferimento a MyProject.Test da MyProject.

    cd MyProject.Test
    dotnet add reference ../MyProject/MyProject.csproj
    
  3. Ripristinare i pacchetti e compilare i progetti:

    dotnet restore
    dotnet build
    
  4. Verificare che xUnit sia in esecuzione con il comando dotnet test. Se si sceglie di usare MSTest, va invece eseguita l'utilità di test delle console MSTest.

L'attività è terminata. È ora possibile testare la libreria su tutte le piattaforme usando gli strumenti da riga di comando. Una volta completata la configurazione, è possibile procedere con il testing della libreria in modo molto semplice:

  1. Eseguire le opportune modifiche nella libreria.
  2. Eseguire i test dalla riga di comando, nella directory di test, usando il comando dotnet test.

Il codice viene ricompilato automaticamente quando si richiama il comando dotnet test.

Come usare più progetti

Per le librerie di maggiori dimensioni, è spesso necessario inserire funzionalità in progetti diversi.

Si supponga di voler creare una libreria che potrebbe essere usata in C# e F#. Ciò significa che i consumer della libreria la usano in modi naturali per C# o F#. In C#, ad esempio, la libreria può essere utilizzata in un modo simile al seguente:

using AwesomeLibrary.CSharp;

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

In F#, invece, può assumere l'aspetto seguente:

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
}

Scenari di utilizzo come questo indicano che le API a cui si accede devono avere una struttura diversa per C# e F#. Un approccio comune per gestire questa situazione consiste nel definire l'intera logica di una libreria in un progetto di base, a cui eseguono chiamate i livelli di API definiti nei progetti C# e F#. Nella parte restante della sezione verranno usati i nomi seguenti:

  • AwesomeLibrary.Core: un progetto di base che contiene tutta la logica della libreria
  • AwesomeLibrary.CSharp: un progetto con API pubbliche destinato all'utilizzo in C#
  • AwesomeLibrary.FSharp: un progetto con API pubbliche destinato all'utilizzo in F#

Eseguire i seguenti comandi nel terminale in uso per ottenere la stessa struttura mostrata in questa Guida:

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

Verranno aggiunti i tre progetti precedenti e un file soluzione che li collega. La creazione del file di soluzione e il collegamento dei progetti consentono di ripristinare e compilare i progetti da un livello superiore.

Definizione di riferimenti da progetto a progetto

Il metodo migliore per creare un riferimento a un progetto è l'uso dell'interfaccia della riga di comando di .NET per aggiungere un riferimento al progetto. Dalle directory di progetto AwesomeLibrary.CSharp e AwesomeLibrary.FSharp è possibile eseguire il comando seguente:

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

I file di progetto per AwesomeLibrary.CSharp e AwesomeLibrary.FSharp includeranno ora un riferimento a AwesomeLibrary.Core come destinazione ProjectReference. È possibile verificare la presenza del riferimento cercando quanto segue nei file di progetto:

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

Se si preferisce non usare l'interfaccia della riga di comando .NET è possibile aggiungere manualmente questa sezione a ogni file di progetto.

Definizione della struttura di una soluzione

Un altro aspetto importante delle soluzioni basate su più progetti è quello di stabilire una buona struttura complessiva dei progetti. È possibile organizzare il codice come desiderato. A condizione che si colleghi ogni progetto al file di soluzione mediante dotnet sln add, sarà possibile eseguire dotnet restore e dotnet build a livello della soluzione.