Septiembre de 2017

Volumen 32, número 9

.NET Standard: desmitificación de .NET Core y .NET Standard

Por Immo Landwerth | Septiembre de 2017

Como incorporaciones más recientes de la familia .NET, existe gran confusión en relación con .NET Core y .NET Standard, y con sus diferencias respecto de .NET Framework. En este artículo, explicaré en qué consiste exactamente cada una de ellas y cuándo debe optar por una u otra.

Antes de entrar en detalles, resulta útil obtener una visión global de .NET para ver dónde encajan .NET Core y .NET Standard. Cuando .NET Framework se lanzó por primera vez hace 15 años, tenía una pila de .NET única que se podía usar para compilar aplicaciones web y de escritorio de Windows. Desde entonces, han surgido otras implementaciones de .NET, como Xamarin, que se pueden usar para compilar aplicaciones móviles para iOS y Android, así como aplicaciones de escritorio de macOS, tal como se muestra en la Figura 1.

Panorama de .NET

Figura 1 Panorama de .NET

.NET Core y .NET Standard encajan aquí de la siguiente manera:

  • .NET Core: Esta es la implementación de .NET más reciente. Se trata de código abierto y está disponible para varios sistemas operativos. Con .NET Core, puede compilar aplicaciones de consola multiplataforma y aplicaciones web y servicios en la nube de ASP.NET Core.
  • .NET Standard: Este es el conjunto de API fundamentales (que suele denominarse biblioteca de clases base o BCL) que las implementaciones de .NET deben implementar. Con .NET Standard como destino, puede compilar bibliotecas que se puedan compartir en todas sus aplicaciones de .NET, independientemente de la implementación de .NET o el sistema operativo donde se ejecuten.

Introducción a .NET Core

.NET Core es una nueva implementación de .NET multiplataforma de código completamente abierto que se bifurcó de .NET Framework y Silverlight. Está optimizada para cargas de trabajo de servidor y móviles gracias a la habilitación de implementaciones XCOPY autocontenidas.

Para mejorar la percepción de .NET Core, veamos más de cerca cómo es el desarrollo para .NET Core. Lo haremos mientras exploramos las nuevas herramientas basadas en la línea de comandos. También puede usar Visual Studio 2017 para el desarrollo de .NET Core, pero teniendo en cuenta que está leyendo este artículo, es probable que ya esté bastante familiarizado con Visual Studio, así que me centraré en la nueva experiencia.

Cuando se creó .NET, se optimizó enormemente para el desarrollo rápido de aplicaciones en Windows. En la práctica, esto significa que el desarrollo de .NET y Visual Studio eran amigos inseparables. Una cosa obvia: el uso de Visual Studio para el desarrollo es una bomba. Es superproductivo y el depurador es, sin lugar a dudas, el mejor que he usado.

No obstante, existen casos en que usar Visual Studio no es la opción más adecuada. Pongamos que solo quiere jugar con .NET para el aprendizaje de C#: No debería tener que descargar e instalar un IDE de varios gigabytes. O bien, pongamos que está accediendo a una máquina Linux a través de SSH, donde el uso de un IDE simplemente no es una opción. O bien, supongamos que es alguien que prefiere usar una interfaz de la línea de comandos (CLI).

Por eso se creó una CLI de primera clase, denominada CLI de .NET Core. El controlador principal de la CLI de .NET Core se denomina "dotnet". Puede usarlo para prácticamente todos los aspectos del desarrollo, tales como la creación, la compilación, las pruebas y el empaquetado de proyectos. Veamos cuál es su aspecto.

Para empezar, creamos y ejecutamos una aplicación de consola Hola mundo (uso PowerShell en Windows, aunque funcionará igual con Bash en macOS o Linux):

$ dotnet new console -o hello
$ cd hello
$ dotnet run
Hello World!

El comando "dotnet new" de la CLI es el equivalente de Archivo | Nuevo proyecto de Visual Studio. Puede crear varios tipos de proyectos diferentes. Escriba "dotnet new" para ver las distintas plantillas que vienen preinstaladas.

Ahora, vamos a extraer parte de la lógica en una biblioteca de clases. Para hacerlo, cree primero un proyecto de biblioteca de clases que sea paralelo a su proyecto Hola:

$ cd ..
$ dotnet new library -o logic
$ cd logic

La lógica que quiere encapsular es la construcción de un mensaje Hola mundo. Por tanto, cambie el contenido de Class1.cs por el código siguiente:

namespace logic
{
  public static class HelloWorld
  {
      public static string GetMessage(string name) => $"Hello {name}!";
  }
}

En este punto, también debe cambiar el nombre Class1.cs por HelloWorld.cs:

$ mv Class1.cs HelloWorld.cs

Observe que no tiene que actualizar el archivo de proyecto para este cambio. Los archivos del nuevo proyecto usados en .NET Core solo incluyen todos los archivos de origen del directorio del proyecto. De este modo, agregar y quitar archivos, así como cambiarles el nombre, ya no requiere la modificación del proyecto. Esto simplifica las operaciones de archivos desde la línea de comandos.

Para usar la clase HelloWorld, debe actualizar la aplicación Hola para que haga referencia a la biblioteca de lógica. Para hacerlo, puede editar el archivo de proyecto o usar el comando dotnet add reference:

$ cd ../hello
$ dotnet add reference ../logic/logic.csproj

Ahora, actualice el archivo Program.cs para usar la clase HelloWorld, como se muestra en la Figura 2.

Figura 2 Actualización del archivo Program.cs para usar la clase HelloWorld

using System;
using logic;
namespace hello
{
class Program
{
static void Main(string[] args)
{
Console.Write("What's your name: ");
var name = Console.ReadLine();
var message = HelloWorld.GetMessage(name);
Console.WriteLine(message);
}
}
}

Para compilar y ejecutar su aplicación, solo tiene que escribir dotnet run:

$ dotnet run
What's your name: Immo
Hello Immo!

También puede crear pruebas desde la línea de comandos. La CLI admite MSTest, así como el popular marco xUnit. Vamos a usar xUnit en este ejemplo:

$ cd ..
$ dotnet new xunit -o tests
$ cd tests
$ dotnet add reference ../logic/logic.csproj

Cambie el contenido de UnitTest1.cs, como se muestra en la Figura 3, para agregar una prueba.

Figura 3 Cambio del contenido de UnitTest1.cs para agregar una prueba

using System;
using Xunit;
using logic;
namespace tests
{
public class UnitTest1
{
[Fact]
public void Test1()
{
var expectedMessage = "Hello Immo!";
var actualMessage = HelloWorld.GetMessage("Immo");
Assert.Equal(expectedMessage, actualMessage);
}
}
}

Ahora puede invocar dotnet test para ejecutar las pruebas:

$ dotnet test
Total tests: 1. Passed: 1. Failed: 0. Skipped: 0.
Test Run Successful.

Para que todo resulte un poco más interesante, vamos a crear un sitio web simple de ASP.NET Core:

$ cd ..
$ dotnet new web -o web
$ cd web
$ dotnet add reference ../logic/logic.csproj

Edite el archivo Startup.cs y cambie la invocación de app.Run para usar la clase HelloWorld de la manera siguiente:

app.Run(async (context) =>
{
  var name = Environment.UserName;
  var message = logic.HelloWorld.GetMessage(name);
  await context.Response.WriteAsync(message);
});

Para iniciar el servidor web de desarrollo, solo tiene que volver a usar dotnet run:

$ dotnet run
Hosting environment: Production
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.

Examine la URL que se muestra, que debería ser http://localhost:5000.

En este punto, la estructura de su proyecto debería parecerse a la de la Figura 4.

Figura 4 Estructura del proyecto creado

$ tree /f
│
├───hello
│ hello.csproj
│ Program.cs
│
├───logic
│ HelloWorld.cs
│ logic.csproj
│
├───tests
│ tests.csproj
│ UnitTest1.cs
│
└───web
Program.cs
Startup.cs
web.csproj

Para facilitar la edición de los archivos mediante Visual Studio, también crearemos un archivo de solución y agregaremos todos los proyectos a la solución:

$ cd ..
$ dotnet new sln -n HelloWorld
$ ls -fi *.csproj -rec | % { dotnet sln add $_.FullName }

Como puede ver, la CLI de .NET Core es muy eficaz y ofrece una experiencia moderada que los desarrolladores de otros orígenes pueden encontrar bastante familiar. Aunque usara dotnet con PowerShell en Windows, la experiencia sería bastante parecida en Linux o macOS.

Otra enorme ventaja de .NET Core es que admite las implementaciones autocontenidas. Podría incluir su aplicación en contenedor mediante Docker de manera que tuviera su propia copia de .NET Core Runtime. Esto permite ejecutar distintas aplicaciones en la misma máquina con distintas versiones de .NET Core sin que interfieran entre sí. Dado que .NET Core es de código abierto, también puede incluir compilaciones nocturnas o incluso versiones que ha modificado o compilado usted mismo, que pueden incluir sus propias modificaciones. No obstante, eso está fuera del ámbito de este artículo.

Introducción a .NET Core

Al crear experiencias modernas, su aplicación suele abarcar varios factores de forma y, por tanto, varias implementaciones de .NET. En este día y esta época, los clientes esperan sobre todo poder usar su aplicación web desde sus teléfonos móviles y poder compartir esos datos a través de un back-end basado en la nube. En caso de usar un portátil, también quieren obtener acceso a través de un sitio web. Para su propia infraestructura, es posible que quiera usar herramientas de línea de comandos y quizás hasta aplicaciones de escritorio para que el personal pueda administrar el sistema. Consulte la Figura 5 para ver cómo funcionan aquí las distintas implementaciones de .NET.

Figura 5 Descripciones de implementaciones de .NET

  SO Código abierto Propósito
.NET Framework Windows No Se usa para compilar aplicaciones de escritorio de Windows y aplicaciones web de ASP.NET que se ejecutan en IIS.
.NET Core Windows, Linux, macOS Se usa para compilar aplicaciones de consola multiplataforma y aplicaciones web y servicios en la nube de ASP.NET Core.
Xamarin iOS, Android, macOS Se usa para compilar aplicaciones móviles para iOS y Android, así como aplicaciones de escritorio para macOS.
.NET Standard No disponible Se usa para compilar bibliotecas a las que se puede hacer referencia desde todas las implementaciones de .NET, como .NET Framework, .NET Core y Xamarin.

En un entorno de este tipo, el uso compartido de código es un reto importante. Debe comprender dónde están disponibles las API y asegurarse de que los componentes compartidos solo usen aquellas API que estén disponibles en todas las implementaciones de .NET que usa.

Aquí es donde entra .NET Standard, que es una especificación. Cada versión de .NET Standard define el conjunto de API que todas las implementaciones de .NET deben proporcionar para ajustarse a esa versión. Se puede considerar una pila más de .NET, excepto en que no puede compilar aplicaciones para esta, sino solo bibliotecas. Es la implementación de .NET que debería usar para las bibliotecas a las que quiere hacer referencia desde todas partes.

Es probable que se esté preguntando cuáles son las API que cubre .NET Standard. Si está familiarizado con .NET Framework, debería estarlo también con la BCL, que mencioné anteriormente. La BCL es el conjunto de API fundamentales que son independientes de los marcos de trabajo de la interfaz de usuario y los modelos de aplicaciones. Incluye tipos primitivos, E/S de archivo, redes, reflexión, serialización, XML, etc.

Todas las pilas de .NET implementan alguna versión de .NET Standard. La regla general es que, cuando se produce una nueva versión de una implementación de .NET, suele implementar la versión disponible más reciente de .NET Standard.

Una buena analogía son HTML y los exploradores: Piense en la especificación HTML como si fuese .NET Standard y en los distintos exploradores como si fuesen implementaciones de .NET, como .NET Framework, .NET Core y Xamarin.

Llegado este punto, es posible que tenga curiosidad sobre cómo puede usar .NET Standard. De hecho, ya lo hizo. ¿Recuerda cuando creamos la biblioteca de clases de lógica anteriormente? Analicemos con mayor profundidad el archivo de proyecto:

$ cd logic
$ cat logic.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>

</Project>

Vamos a compararlo con el archivo de proyecto de la aplicación de consola "hello":

$ cd ..\hello
$ cat hello.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <ItemGroup>
    <ProjectReference Include="..\logic\logic.csproj" />
  </ItemGroup>

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.0</TargetFramework>
  </PropertyGroup>

</Project>

Como puede ver, la biblioteca de lógica tiene un valor de netstandard2.0 para TargetFramework, mientras que la aplicación de consola tiene un valor de netcoreapp2.0. La propiedad TargetFramework indica la implementación de .NET de destino. Por tanto, la aplicación de consola está destinada a .NET Core 2.0, mientras que la biblioteca está destinada a .NET Standard 2.0. Esto significa que puede hacer referencia a la biblioteca de lógica no solo desde una aplicación de .NET Core, sino también desde una aplicación compilada para .NET Framework o Xamarin.

Lamentablemente, la mayoría de las bibliotecas disponibles actualmente no están destinadas aún a .NET Standard. Muchas de ellas están destinadas a .NET Framework. Evidentemente, no todas las bibliotecas pueden (ni deberían) destinarse a .NET Standard. Por ejemplo, una biblioteca que contenga controles de Windows Presentation Foundation (WPF) debe destinarse a .NET Framework porque la interfaz de usuario no forma parte de la versión Standard. No obstante, muchas de las bibliotecas de uso general solo están destinadas a .NET Framework porque se crearon cuando .NET Standard todavía no existía.

Con .NET Standard 2.0, el conjunto de API es lo bastante grande para que la mayoría de las bibliotecas de uso general, si no todas, puedan destinarse a .NET Standard. Como resultado, el 70 % de todas las bibliotecas que existen en NuGet actualmente solo usan las API que ya forman parte de .NET Standard. Aún así, solo una parte de estas están marcadas explícitamente como compatibles con .NET Standard.

Para desbloquear su uso por parte de los desarrolladores, se ha agregado un modo de compatibilidad. Si instala un paquete NuGet que no ofrece una biblioteca para su marco de destino, ni proporciona una para .NET Standard, NuGet intentará recurrir a .NET Framework. En otras palabras, puede hacer referencia a las bibliotecas de .NET Framework como si estuviesen destinadas a .NET Standard.

Mostraré qué aspecto tiene. En mi ejemplo, usaré una biblioteca de colección popular denominada PowerCollections, que se escribió en 2007. No se actualizó durante cierto tiempo y sigue estando destinada a .NET Framework 2.0. La instalaré desde NuGet en la aplicación Hola:

$ dotnet add package Huitian.PowerCollections

Esta biblioteca proporciona tipos de colección adicionales que la BCL no ofrece, como una colección de elementos, que no proporciona garantías de ordenación. Vamos a modificar la aplicación Hola para que la use, como se muestra en la Figura 6.

Figura 6 Aplicación de ejemplo con PowerCollections

using System;
using Wintellect.PowerCollections;
namespace hello
{
class Program
{
static void Main(string[] args)
{
var data = new Bag<int>() { 1, 2, 3 };
foreach (var element in data)
Console.WriteLine(element);
}
}
}

Si ejecuta el programa, verá lo siguiente:

$ dotnet run
hello.csproj : warning NU1701: Package 'Huitian.PowerCollections 1.0.0' was restored using '.NETFramework,Version=v4.6.1' instead of the project target framework '.NETCoreApp,Version=v2.0'. This may cause compatibility problems.
1
3
2

¿Qué sucedió? La aplicación Hola está destinada a .NET Core 2.0. Dado que .NET Core 2.0 implementa .NET Standard 2.0, también cuenta con el modo de compatibilidad para hacer referencia a las bibliotecas de .NET Framework. No obstante, no todas las bibliotecas de .NET Framework funcionarán en todas las implementaciones de .NET. Por ejemplo, podrían usar las API de Windows Forms o WPF. NuGet no tiene ninguna manera de saberlo, por lo que muestra un mensaje de advertencia para notificarle la situación y evitar que pierda tiempo solucionando los problemas que esta pudiera causar.

Tenga en cuenta que esta advertencia se mostrará con cada compilación. De este modo, se evita el problema que suponía omitir u olvidar una advertencia durante la instalación del paquete.

Evidentemente, no hay nada peor que las advertencias no accionables que se muestran con cada compilación. Por tanto, la idea es que, después de validar la aplicación, se puede desactivar la advertencia para el paquete. Dado que la aplicación se ejecuta correctamente (imprimió correctamente el contenido del contenedor creado), ya puede desactivar la advertencia. Para ello, edite el archivo hello.csproj y agregue el atributo NoWarn a la referencia del paquete:

<PackageReference Include="Huitian.PowerCollections" Version="1.0.0" 
  NoWarn="NU1701" />

Ahora, si vuelve a ejecutar la aplicación, la advertencia no debería mostrarse. Si instala otro paquete que usa el modo de compatibilidad, se mostrará la advertencia para dicho paquete, que también puede desactivar.

Las nuevas herramientas también permiten que los proyectos de biblioteca de clases produzcan paquetes NuGet como parte de la compilación. De este modo, resulta mucho más fácil compartir las bibliotecas con el mundo (mediante su inserción en nuget.org) o simplemente en su organización (mediante la inserción de la fuente del paquete en Visual Studio Team Services o MyGet). Los nuevos proyectos también ofrecen compatibilidad con múltiples versiones, lo que permite compilar un solo proyecto para varias implementaciones de .NET. Esto significa que puede usar la compilación condicional (#if) para adaptar la biblioteca a implementaciones de .NET específicas. También permite compilar contenedores de .NET Standard para las API específicas de la plataforma. No obstante, eso está fuera del ámbito de este artículo.

Conclusión

.NET Standard es una especificación de las API que todas las implementaciones de .NET deben proporcionar. Proporciona coherencia a la familia .NET y le permite compilar bibliotecas que puede usar desde cualquier implementación de .NET. Reemplaza los PCL para la compilación de componentes compartidos.

.NET Core es una implementación de .NET Standard optimizada para la compilación de aplicaciones de consola, aplicaciones web y servicios en la nube mediante ASP.NET Core. Su SDK incluye eficaces herramientas que, además de usarse en el desarrollo de Visual Studio, están al servicio de un flujo de trabajo de desarrollo completo basado en la línea de comandos. Puede obtener más información sobre estas en aka.ms/netstandardfaq y en aka.ms/netcore.


Immo Landwerth es administrador de programas en Microsoft y trabaja en .NET. Se centra en el diseño de .NET Standard, la BCL y la API.


Discuta sobre este artículo en el foro de MSDN Magazine