Desarrollo de bibliotecas con la CLI de .NET

En este artículo se explica cómo escribir bibliotecas para .NET con la CLI de .NET. La CLI proporciona una experiencia eficaz y de bajo nivel que funciona en todos los SO compatibles. De todos modos puede seguir compilando bibliotecas con Visual Studio; si esa es la experiencia de su preferencia, consulte la guía sobre Visual Studio.

Requisitos previos

Necesita tener instalado en la máquina el SDK de .NET.

En las secciones de este documento que se refieren a las versiones de .NET Framework, necesita tener instalado .NET Framework en una máquina con Windows.

Además, si desea admitir destinos de .NET Framework anteriores, deberá instalar paquetes de destino o desarrollador desde la página de descargas de .NET Framework. Consulte la tabla siguiente:

Versión de .NET Framework Qué debe descargar
4.6.1 Paquete de compatibilidad de .NET Framework 4.6.1
4.6 Paquete de compatibilidad de .NET Framework 4.6
4.5.2 Paquete de desarrollador de .NET Framework 4.5.2
4.5.1 Paquete de desarrollador de .NET Framework 4.5.1
4.5 Kit de desarrollo de software de Windows para Windows 8
4.0 Windows SDK para Windows 7 y .NET Framework 4
2.0, 3.0 y 3.5 Entorno de ejecución de .NET Framework 3.5 SP1 (o una versión posterior a Windows 8)

Establecimiento de .NET 5+ o .NET Standard como destino

Para controlar la plataforma de destino del proyecto, agréguela al archivo de proyecto ( .csproj o .fsproj). Para obtener instrucciones sobre cómo elegir entre los destinos .NET 5+ o .NET Standard consulte .NET 5+ y .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>

Si desea tener como destino .NET Framework versión 4.0 o inferior, o bien desea usar una API disponible en .NET Framework pero no disponible en el estándar .NET (por ejemplo, System.Drawing), lea las secciones siguientes y sepa cómo tener compatibilidad con múltiples versiones.

Uso de .NET Framework como destino

Nota:

En estas instrucciones se supone que tiene instalado .NET Framework en su máquina. Consulte los requisitos previos para instalar las dependencias.

Tenga en cuenta que algunas de las versiones de .NET Framework que se usan aquí ya no cuentan con soporte técnico. Consulte las P+F sobre la directiva del ciclo de vida de soporte técnico de .NET Framework sobre las versiones sin soporte técnico.

Si quiere llegar a la mayor cantidad posible de desarrolladores y proyectos, use .NET Framework 4.0 como el destino de línea base. Para tener .NET Framework como destino, comience utilizando el moniker de la plataforma de destino (TFM) correcto que corresponda a la versión de .NET Framework que desea admitir.

Versión de .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

Después, inserte el TFM en la sección TargetFramework de su archivo del proyecto. El siguiente es un ejemplo de cómo podría escribir una biblioteca que tenga como destino .NET Framework 4.0:

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

Y listo. Aunque esto solo hace la compilación para .NET Framework 4, puede usar la biblioteca en las versiones más recientes de .NET Framework.

Cómo lograr la compatibilidad con varias versiones

Nota:

Las instrucciones siguientes suponen que tiene instalado .NET Framework en su máquina. Consulte la sección de requisitos previos para información sobre las dependencias que debe instalar y dónde descargarlas.

Es posible que deba tener como destino versiones anteriores de .NET Framework cuando el proyecto admite .NET Framework y .NET. En este escenario, si desea usar API más recientes y construcciones de lenguaje para los destinos más recientes, use las directivas #if en el código. También es posible que tenga que agregar distintos paquetes y dependencias para cada plataforma que tiene como destino para incluir las distintas API necesarias para cada caso.

Por ejemplo, digamos que tiene una biblioteca que realiza operaciones de red a través de HTTP. En el estándar .NET y .NET Framework versión 4.5 o superiores, puede usar la clase HttpClient del espacio de nombres System.Net.Http. Sin embargo, las versiones anteriores de .NET Framework no tienen la clase HttpClient, por lo que, en su lugar, podría usar la clase WebClient del espacio de nombres System.Net para esas versiones.

Su archivo del proyecto podría tener la siguiente apariencia:

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

Observará tres cambios principales aquí:

  1. El nodo TargetFramework se ha reemplazado por TargetFrameworks, y se expresan tres TFM dentro.
  2. Existe un nodo <ItemGroup> para el destino net40 que se dirige a una referencia de .NET Framework.
  3. Existe un nodo <ItemGroup> para el destino net45 que se dirige a dos referencias de .NET Framework.

Símbolos de preprocesador

El sistema de compilación conoce los siguientes símbolos del preprocesador que se usan en las directivas #if:

Versiones de .NET Framework de destino Símbolos Símbolos adicionales
(disponible en SDK de .NET 5+)
Símbolos de plataforma (solo disponibles
cuando se especifica un TFM específico 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+ (y .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 ejemplo IOS15_1),
[OS][version]_OR_GREATER (por ejemplo, IOS15_1_OR_GREATER)

Nota:

  • Los símbolos sin versión se definen independientemente de la versión de destino.
  • Los símbolos específicos de la versión solo se definen para la versión de destino.
  • Los símbolos <framework>_OR_GREATER se definen para la versión de destino y todas las versiones anteriores. Por ejemplo, si tiene como destino .NET Framework 2.0, se definen los símbolos siguientes: NET20, NET20_OR_GREATER, NET11_OR_GREATER y NET10_OR_GREATER.
  • Los símbolos NETSTANDARD<x>_<y>_OR_GREATER solo se definen para destinos de .NET Standard y no para destinos que implementan .NET Standard, como .NET Core y .NET Framework.
  • Son diferentes de los monikers de la plataforma de destino (TFM) que usa la propiedad MSBuildTargetFramework y NuGet.

Aquí se muestra un ejemplo en el que se usa la compilación 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
    }
}

Si crea este proyecto con dotnet build, observará tres directorios en la carpeta bin/:

net40/
net45/
netstandard2.0/

Cada uno de ellos contiene los archivos .dll para cada destino.

Prueba de las bibliotecas en .NET

Es importante poder probar las plataformas. Puede usar xUnit o MSTest de fábrica. Ambos son perfectamente adecuados para las pruebas unitarias de su biblioteca en .NET. Cómo configurar la solución con proyectos de prueba dependerá de la estructura de la solución. En el ejemplo siguiente se presupone que los directorios de origen y de prueba residen en el mismo directorio de nivel superior.

Nota:

Esto usa algunos comandos de la CLI de .NET. Vea dotnet new y dotnet sln para obtener más información.

  1. Configure la solución. Puede hacerlo con los siguientes 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
    

    Esto creará proyectos y se vincularán conjuntamente en una solución. Su directorio para SolutionWithSrcAndTest debe tener el siguiente aspecto:

    /SolutionWithSrcAndTest
    |__SolutionWithSrcAndTest.sln
    |__MyProject/
    |__MyProject.Test/
    
  2. Vaya al directorio del proyecto de prueba y agregue una referencia a MyProject.Test desde MyProject.

    cd MyProject.Test
    dotnet add reference ../MyProject/MyProject.csproj
    
  3. Restaurar paquetes y crear proyectos:

    dotnet restore
    dotnet build
    
  4. Compruebe que xUnit se ejecuta mediante la ejecución del comando dotnet test. Si decide usar MSTest, entonces debe ejecutarse en su lugar el ejecutor de la consola de MSTest.

Y listo. Ahora puede probar la biblioteca en todas las plataformas; para ello, use herramientas de línea de comandos. Para seguir con las pruebas ahora que ya está todo configurado, probar la biblioteca es un proceso muy simple:

  1. Haga los cambios en la biblioteca.
  2. Ejecute las pruebas desde la línea de comandos, en el directorio de prueba, con el comando dotnet test.

El código se recompilará automáticamente cuando invoque el comando dotnet test.

Uso de varios proyectos

Una necesidad en común de las bibliotecas de mayor tamaño es ubicar la funcionalidad en distintos proyectos.

Imagine que desea compilar una biblioteca que se pudiera consumir en C# y F# idiomático. Eso significaría que los usuarios de las bibliotecas las consumirían de manera natural para C# o F#. Por ejemplo, en C#, podría consumir la biblioteca de la siguiente manera:

using AwesomeLibrary.CSharp;

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

En F#, sería de la siguiente manera:

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
}

Escenarios de consumo similares a este significan que las API a las que se tiene acceso deben tener una estructura distinta para C# y para F#. Un enfoque común para lograrlo es factorizar toda la lógica de una biblioteca en un proyecto central, con los proyectos de C# y F# definiendo los niveles de API que hacen llamadas a ese proyecto central. En el resto de la sección se usarán los siguientes nombres:

  • AwesomeLibrary.Core: un proyecto central que contiene toda la lógica de la biblioteca
  • AwesomeLibrary.CSharp: un proyecto con API públicas pensado para el consumo en C#
  • AwesomeLibrary.FSharp: un proyecto con API públicas pensado para el consumo en F#

Puede ejecutar los siguientes comandos en su terminal para generar la misma estructura de esta guía:

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

Esto agregará los tres proyectos anteriores y un archivo de solución que los vincula conjuntamente. Crear el archivo de solución y vincular los proyectos le permitirá restaurar y crear proyectos desde un nivel superior.

Referencias entre proyectos

La mejor manera de hacer referencia a un proyecto es usar la CLI de .NET para agregar una referencia de proyecto. Desde los directorios del proyecto AwesomeLibrary.CSharp y AwesomeLibrary.FSharp, puede ejecutar el siguiente comando:

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

Los archivos del proyecto para AwesomeLibrary.CSharp y AwesomeLibrary.FSharp ahora harán referencia a AwesomeLibrary.Core como un destino ProjectReference. Puede comprobar esto inspeccionando los archivos del proyecto y observando lo siguiente en ellos:

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

Puede agregar esta sección a cada archivo del proyecto manualmente si prefiere no usar la CLI de .NET.

Estructura de una solución

Otro aspecto importante de las soluciones de varios proyectos es establecer una buena estructura de proyecto general. Puede organizar el código de la manera que quiera, y siempre y cuando vincule cada proyecto a su archivo de solución con dotnet sln add, podrá ejecutar dotnet restore y dotnet build en el nivel de solución.