Mayo de 2017

Volumen 32, número 5

.NET Core: generación de código multiplataforma con Roslyn y .NET Core

Por Alessandro Del Del

.NET Core es el conjunto de herramientas modular, de código abierto y multiplataforma que permite compilar aplicaciones .NET de nueva generación, que se ejecuten en Windows, Linux y macOS (microsoft.com/net/core/platform). También se puede instalar en la distribución de Windows 10 para IoT y se ejecuta en dispositivos como Raspberry PI. .NET Core es una eficaz plataforma que incluye el tiempo de ejecución, las bibliotecas y los compiladores, además de total compatibilidad con lenguajes como, por ejemplo, C#, F# y Visual Basic. Esto significa que puede codificar con C# no solo en Windows, sino también en otros sistemas operativos, ya que la plataforma del compilador .NET (github.com/dotnet/roslyn), también denominada "proyecto Roslyn", proporciona compiladores multiplataforma de código abierto con API de análisis de código completas. Como implicación importante, puede aprovechar las API de Roslyn para realizar muchas operaciones relacionadas con el código en distintos sistemas operativos, tales como las de análisis, generación y compilación de código. En este artículo se explican los pasos necesarios para configurar un proyecto de C# en .NET Core con las API de Roslyn, y se explican algunos escenarios de generación y compilación de código interesantes. También se tratan algunas técnicas básicas de Reflection para invocar y ejecutar código compilado con Roslyn en .NET Core. Si no está familiarizado con Roslyn, puede leer primero los siguientes artículos:

Instalación del SDK de .NET Core

El primer paso es la instalación de .NET Core y del SDK. Si trabaja con Windows y tiene instalado Visual Studio 2017, .NET Core ya se incluye si se seleccionó la carga de trabajo de desarrollo multiplataforma de .NET Core durante la instalación en el instalador de Visual Studio. De lo contrario, abra el instalador de Visual Studio, seleccione la carga de trabajo y haga clic en Modificar. Si trabaja en Windows, pero no tiene Visual Studio 2017, o si trabaja con Linux o macOS, puede instalar .NET Core manualmente y usar Visual Studio Code como entorno de desarrollo (code.visualstudio.com). Este último es el escenario que explicaré en este artículo, ya que Visual Studio Code es multiplataforma; por tanto, es el complemento perfecto para .NET Core. Asimismo, recuerde instalar la extensión de C# para Visual Studio Code (bit.ly/29b1Ppl). Los pasos para instalar .NET Core son distintos según el sistema operativo. Siga las instrucciones de bit.ly/2mJArWx. Asegúrese de instalar la versión más reciente. Vale la pena mencionar que las versiones más recientes de .NET Core ya no admiten el formato de archivo project.json, pero admiten el formato de archivo más común .csproj con MSBuild en su lugar.

Scaffolding de una aplicación de .NET Core en C#

Con .NET Core, puede crear aplicaciones de consola y web. Para las aplicaciones web, Microsoft ofrece más plantillas, además de la plantilla de ASP.NET Core, ya que .NET Core avanza en su mapa de ruta. Dado que Visual Studio Code es un editor ligero, no proporciona plantillas de proyecto igual que Visual Studio. Esto significa que debe crear una aplicación desde la línea de comandos en una carpeta cuyo nombre será el de la aplicación. El ejemplo siguiente se basa en las instrucciones para Windows, pero los mismos conceptos se aplican a macOS y Linux. Para comenzar, abra un símbolo del sistema y diríjase a una carpeta en el disco. Por ejemplo, tiene una carpeta denominada C:\Apps, se dirige a esta y crea una nueva subcarpeta denominada RoslynCore con los siguientes comandos:

> cd C:\Apps
> md RoslynCore
> cd RoslynCore

Por tanto, RoslynCore será el nombre de la aplicación de muestra que se explica en este artículo. Será una aplicación de consola, que resulta ideal para fines pedagógicos y simplifica el enfoque de codificación con Roslyn. También puede usar las mismas técnicas en las aplicaciones web de ASP.NET Core. Para crear un nuevo proyecto vacío para una aplicación de consola, solo tiene que escribir la línea de comandos siguiente:

> dotnet new console

De este modo, .NET Core aplica scaffolding a un proyecto de C# de una aplicación de consola denominada RoslynCore. Ahora puede abrir la carpeta del proyecto con Visual Studio Code. La manera más fácil de hacerlo es escribir la línea de comandos siguiente:

> code .

Por supuesto, puede abrir Visual Studio Code desde el menú Inicio de Windows y, después, abrir una carpeta de proyecto manualmente. Cuando introduzca cualquier archivo de código de C#, le pedirá permiso para generar algunos recursos necesarios y para restaurar algunos paquetes NuGet (consulte la Figura 1).

El código de Visual Studio debe actualizar el proyecto
Figura 1 El código de Visual Studio debe actualizar el proyecto

El siguiente paso consiste en agregar los paquetes NuGet necesarios para trabajar con Roslyn.

Adición de paquetes NuGet de Roslyn

Como debe saber, las API de Roslyn se pueden usar mediante la instalación de algunos paquetes NuGet desde la jerarquía Microsoft.CodeAnalysis. Antes de instalar estos paquetes, es importante aclarar cómo encajan las API de Roslyn en el sistema .NET Core. Si ha trabajado alguna vez con Roslyn en .NET Framework, es posible que esté acostumbrado a usar el conjunto completo de API de Roslyn. .NET Core depende de las bibliotecas de .NET Standard, lo que significa que solo se pueden usar las bibliotecas de Roslyn compatibles con .NET Standard en .NET Core. Al redactar este artículo, la mayoría de las API de Roslyn ya están disponibles para .NET Core, incluidas (sin limitación) las API de compilador (con Emit API y Diagnostic API) y las API de áreas de trabajo. Solo algunas API no son aún portables, pero Microsoft está realizando grandes inversiones en Roslyn y .NET Core, por lo que se prevé que .NET Standard sea totalmente compatible en futuras versiones. Un ejemplo real de aplicación multiplataforma que se ejecuta en .NET Core es OmniSharp (bit.ly/2mpcZeF), que aprovecha las API de Roslyn para habilitar la mayoría de las características del editor de código, como las listas de finalización y el resaltado de sintaxis.

En este artículo, descubrirá cómo aprovechar las API de compilador y de diagnóstico. Para lograrlo, debe agregar el paquete NuGet Microsoft.CodeAnalysis.CSharp al proyecto. Con el nuevo sistema del proyectos de .NET Core basado en MSBuild, la lista de paquetes NuGet se incluye ahora en el archivo de proyecto .csproj. En Visual Studio 2017, puede usar la interfaz de usuario de cliente de NuGet para descargar, instalar y administrar paquetes, pero en Visual Studio Code no existe ninguna opción equivalente. Afortunadamente, puede abrir simplemente el archivo .csproj y ubicar el nodo <ItemGroup> que contiene los elementos <PackageReference>, cada uno de los cuales representa un paquete NuGet necesario. Modifique el nodo de la siguiente manera:

<ItemGroup>
  ...
  <PackageReference Include="Microsoft.CodeAnalysis.CSharp"
    Version="2.0.0 " />
  <PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
</ItemGroup>

Observe que, al agregar una referencia al paquete Microsoft.CodeAnalysis.C­Sharp, se puede acceder a las API de compilador de C# y que el paquete System.Runtime.Loader, que usaremos más adelante en este artículo, es necesario para Reflection.

Al guardar los cambios, Visual Studio Code detectará qué paquetes NuGet faltan y ofrecerá restaurarlos.

Análisis de código: análisis de texto de origen y generación de nodos de sintaxis

En el primer ejemplo, sobre el análisis de código, se demuestra cómo analizar el texto del código fuente y generar nuevos nodos de sintaxis. Por ejemplo, imagine que tiene el siguiente objeto de negocios simple y que quiere generar una clase View Model basada en este:

namespace Models
{
  public class Item
  {
    public string ItemName { get; set }
  }
}

El texto de este objeto de negocios puede proceder de distintos orígenes, como un archivo de código de C#, una cadena del código o incluso una entrada del usuario. Con las API de análisis de código, puede analizar el texto de origen, y generar un nuevo nodo de sintaxis que el compilador pueda comprender y manipular. Por ejemplo, considere el código que se muestra en la Figura 2, que analiza una cadena que contiene una definición de clase, obtiene el nodo de sintaxis correspondiente y llama a un nuevo método estático que genera una clase View Model a partir de dicho nodo.

Figura 2 Análisis de código fuente y recuperación de un nodo de sintaxis

using System;
using RoslynCore;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
class Program
{
  static void Main(string[] args)
  {
    GenerateSampleViewModel();
  }
  static void GenerateSampleViewModel()
  {
    const string models = @"namespace Models
{
  public class Item
  {
    public string ItemName { get; set }
  }
}
";
    var node = CSharpSyntaxTree.ParseText(models).GetRoot();
    var viewModel = ViewModelGeneration.GenerateViewModel(node);
    if(viewModel!=null)
      Console.WriteLine(viewModel.ToFullString());
    Console.ReadLine();
  }
}

El método GenerateViewModel se definirá en una clase estática denominada ViewModelGeneration, así que agregaremos un nuevo archivo denominado ViewModelGeneration.cs al proyecto. El método busca una definición de clase en el nodo de sintaxis de entrada (con fines de demostración, la primera instancia de un objeto ClassDeclarationSyntax) y, después, construye una nueva clase View Model basada en el nombre y los miembros de la clase. La Figura 3 lo demuestra.

Figura 3 Generación de un nuevo nodo de sintaxis

using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis;
using System.Linq;
using Microsoft.CodeAnalysis.CSharp;
namespace RoslynCore
{
  public static class ViewModelGeneration
  {
    public static SyntaxNode GenerateViewModel(SyntaxNode node)
    {
      // Find the first class in the syntax node
      var classNode = node.DescendantNodes()
       .OfType<ClassDeclarationSyntax>().FirstOrDefault();
      if(classNode!=null)
      {
        // Get the name of the model class
        string modelClassName = classNode.Identifier.Text;
        // The name of the ViewModel class
        string viewModelClassName = $"{modelClassName}ViewModel";
        // Only for demo purposes, pluralizing an object is done by
        // simply adding the "s" letter. Consider proper algorithms
        string newImplementation =
          $@"public class {viewModelClassName} : INotifyPropertyChanged
{{
public event PropertyChangedEventHandler PropertyChanged;
// Raise a property change notification
protected virtual void OnPropertyChanged(string propname)
{{
  PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propname));
}}
private ObservableCollection<{modelClassName}> _{modelClassName}s;
public ObservableCollection<{modelClassName}> {modelClassName}s
{{
  get {{ return _{modelClassName}s; }}
  set
  {{
    _{modelClassName}s = value;
    OnPropertyChanged(nameof({modelClassName}s));
  }}
}}
public {viewModelClassName}() {{
// Implement your logic to load a collection of items
}}
}}
";
          var newClassNode =
            CSharpSyntaxTree.ParseText(newImplementation).GetRoot()
            .DescendantNodes().OfType<ClassDeclarationSyntax>()
            .FirstOrDefault();
          // Retrieve the parent namespace declaration
          if(!(classNode.Parent is NamespaceDeclarationSyntax)) return null;
          var parentNamespace = (NamespaceDeclarationSyntax)classNode.Parent;
          // Add the new class to the namespace and adjust the white spaces
          var newParentNamespace =
            parentNamespace.AddMembers(newClassNode).NormalizeWhitespace();
          return newParentNamespace;
        }
      }
      else
      {
        return null;
      }
    }
  }
}

En la primera parte del código de la Figura 3, puede ver la representación inicial de la clase View Model como una cadena, con una interpolación de cadena que permite especificar nombres de objeto y miembro fácilmente basados en el nombre de clase original. En este escenario de ejemplo, los plurales se generan agregando simplemente una "s" al nombre de objeto o miembro; en el código real, se deben usar algoritmos de pluralización más específicos.

En la segunda parte de la Figura 3, el código invoca CSharpSyntaxTree.ParseText para analizar el texto de origen en un elemento SyntaxTree. GetRoot se invoca para recuperar la clase SyntaxNode del nuevo árbol; con DescendantNodes().OfType<ClassDeclarationSyntax>(), el código recupera solo los nodos de sintaxis que representan una clase y solo se selecciona el primero con FirstOrDefault. La recuperación de la primera clase del nodo de sintaxis es suficiente para obtener el espacio de nombres primario donde se insertará la nueva clase View Model. La obtención de un espacio de nombres es posible gracias a la conversión de la propiedad Parent de una clase ClassDeclarationSyntax en un objeto NamespaceDeclarationSyntax. Dado que una clase podría anidarse en otra, el código comprueba primero esta posibilidad, para lo cual confirma que el tipo de Parent sea NamespaceDeclarationSyntax. El fragmento final del código agrega el nuevo nodo de sintaxis de la clase View Model al espacio de nombres primario y lo devuelve como un nodo de sintaxis. Si ahora presiona F5, verá el resultado de la generación de código en la Consola de depuración, como se muestra en la Figura 4.

La clase View Model se generó correctamente
Figura 4 La clase View Model se generó correctamente

La clase View Model generada es un objeto SyntaxNode con el que el compilador de C# puede trabajar, de modo que se puede manipular, analizar su información de diagnóstico, compilar en un ensamblado con Emit API y usar a través de Reflection.

Obtención de información de diagnóstico

Tanto si el texto de origen procede de una cadena, un archivo, o una entrada del usuario, puede usar Diagnostic API para recuperar la información de diagnóstico sobre los problemas del código, tales como errores y advertencias. Recuerde que Diagnostic API no solo permite recuperar errores y advertencias, sino también escribir analizadores y refactorizaciones de código. Retomando el ejemplo anterior, es una buena idea comprobar los errores sintácticos en el texto de origen inicial antes de intentar generar una clase View Model. Para lograrlo, puede invocar el método SyntaxNode.GetDiagnostics, que devuelve un objeto <Microsoft.CodeAnalysis.Diagnostic>, si existe alguno. Eche un vistazo a la Figura 5, que ofrece una versión ampliada de la clase ViewModelGeneration. El código comprueba si el resultado de invocar GetDiagnostics contiene algún diagnóstico. De lo contrario, el código genera la clase View Model. Si, en su lugar, el resultado contiene una colección de diagnósticos, el código muestra información de cada uno de ellos y devuelve null. La clase Diagnostic proporciona información muy detallada sobre cada problema del código; por ejemplo, la propiedad Id devuelve un identificador de diagnóstico; el método GetMessage devuelve el mensaje de diagnóstico completo; GetLineSpan devuelve la posición del diagnóstico en el código fuente; y la propiedad Severity devuelve la gravedad del diagnóstico, como Error, Advertencia o Información.

Figura 5 Comprobación de problemas de código con Diagnostic API

using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis;
using System.Linq;
using Microsoft.CodeAnalysis.CSharp;
using System;
namespace RoslynCore
{
  public static class ViewModelGeneration
  {
    public static SyntaxNode GenerateViewModel(SyntaxNode node)
    {
      // Find the first class in the syntax node
      var classNode =
        node.DescendantNodes().OfType<ClassDeclarationSyntax>().FirstOrDefault();
      if(classNode!=null)
      {
        var codeIssues = node.GetDiagnostics();
        if(!codeIssues.Any())
        {
          // Get the name of the model class
          var modelClassName = classNode.Identifier.Text;
          // The name of the ViewModel class
          var viewModelClassName = $"{modelClassName}ViewModel";
          // Only for demo purposes, pluralizing an object is done by
          // simply adding the "s" letter. Consider proper algorithms
          string newImplementation =
            $@"public class {viewModelClassName} : INotifyPropertyChanged
{{
public event PropertyChangedEventHandler PropertyChanged;
// Raise a property change notification
protected virtual void OnPropertyChanged(string propname)
{{
  PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propname));
}}
private ObservableCollection<{modelClassName}> _{modelClassName}s;
public ObservableCollection<{modelClassName}> {modelClassName}s
{{
  get {{ return _{modelClassName}s; }}
  set
  {{
    _{modelClassName}s = value;
    OnPropertyChanged(nameof({modelClassName}s));
  }}
}}
public {viewModelClassName}() {{
// Implement your logic to load a collection of items
}}
}}
";
            var newClassNode =
              SyntaxFactory.ParseSyntaxTree(newImplementation).GetRoot()
              .DescendantNodes().OfType<ClassDeclarationSyntax>()
              .FirstOrDefault();
            // Retrieve the parent namespace declaration
            if(!(classNode.Parent is NamespaceDeclarationSyntax)) return null;
            var parentNamespace = (NamespaceDeclarationSyntax)classNode.Parent;
            // Add the new class to the namespace
            var newParentNamespace =
              parentNamespace.AddMembers(newClassNode).NormalizeWhitespace();
            return newParentNamespace;
          }
          else
          {
            foreach(Diagnostic codeIssue in codeIssues)
          {
            string issue = $"ID: {codeIssue.Id}, Message: {codeIssue.GetMessage()},
              Location: {codeIssue.Location.GetLineSpan()},
              Severity: {codeIssue.Severity}";
            Console.WriteLine(issue);
          }
          return null;
        }
      }
      else
      {
        return null;
      }
    }
  }
}

Ahora, si introduce algunos errores de manera intencionada en el texto de origen incluido en la variable models, dentro del método GenerateSample­ViewModel de Program.cs y, después, ejecuta la aplicación, podrá observar que el compilador de C# devuelve detalles completos sobre cada problema del código. En la Figura 6 se muestra un ejemplo.

Detección de problemas de código con Diagnostic API
Figura 6 Detección de problemas de código con Diagnostic API

Vale la pena destacar que el compilador de C# produce un árbol de sintaxis aunque contenga diagnósticos. No solo genera este resultado totalmente fiel al texto de origen, sino que también ofrece a los desarrolladores una opción para corregir los problemas con nuevos nodos de sintaxis.

Ejecución de código: Emit API

Emit API permite compilar código fuente en ensamblados. A continuación, Reflection permite invocar y ejecutar código. El ejemplo siguiente es una combinación de generación, emisión y detección de diagnóstico de código. Agregue un nuevo archivo denominado EmitDemo.cs al proyecto y, después, considere la lista de código que se muestra en la Figura 7. Como puede ver, se genera un elemento SyntaxTree a partir del texto de origen, que define una clase Helper que contiene un método estático que calcula el área de un círculo. El objetivo es producir un archivo .dll a partir de esta clase y ejecutar el método CalculateCircleArea, con el paso del radio como un argumento.

Figura 7 Compilación y ejecución de código con Emit API y Reflection

using System;
using System.IO;
using System.Reflection;
using System.Runtime.Loader;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
namespace RoslynCore
{
  public static class EmitDemo
  {
    public static void GenerateAssembly()
    {
      const string code = @"using System;
using System.IO;
namespace RoslynCore
{
 public static class Helper
 {
  public static double CalculateCircleArea(double radius)
  {
    return radius * radius * Math.PI;
  }
  }
}";
      var tree = SyntaxFactory.ParseSyntaxTree(code);
      string fileName="mylib.dll";
      // Detect the file location for the library that defines the object type
      var systemRefLocation=typeof(object).GetTypeInfo().Assembly.Location;
      // Create a reference to the library
      var systemReference = MetadataReference.CreateFromFile(systemRefLocation);
      // A single, immutable invocation to the compiler
      // to produce a library
      var compilation = CSharpCompilation.Create(fileName)
        .WithOptions(
          new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
        .AddReferences(systemReference)
        .AddSyntaxTrees(tree);
      string path = Path.Combine(Directory.GetCurrentDirectory(), fileName);
      EmitResult compilationResult = compilation.Emit(path);
      if(compilationResult.Success)
      {
        // Load the assembly
        Assembly asm =
          AssemblyLoadContext.Default.LoadFromAssemblyPath(path);
        // Invoke the RoslynCore.Helper.CalculateCircleArea method passing an argument
        double radius = 10;
        object result = 
          asm.GetType("RoslynCore.Helper").GetMethod("CalculateCircleArea").
          Invoke(null, new object[] { radius });
        Console.WriteLine($"Circle area with radius = {radius} is {result}");
      }
      else
      {
        foreach (Diagnostic codeIssue in compilationResult.Diagnostics)
        {
          string issue = $"ID: {codeIssue.Id}, Message: {codeIssue.GetMessage()},
            Location: {codeIssue.Location.GetLineSpan()},
            Severity: {codeIssue.Severity}";
          Console.WriteLine(issue);
        }
      }
    }
  }
}

En la primera parte, el código crea una nueva compilación que representa una sola invocación inmutable del compilador de C#. El objeto CSharpCompilation permite la creación de un ensamblado a través de su método Create, mientras que WithOptions permite especificar el tipo de salida que se va a producir, que en este caso es DynamicallyLinkedLibrary. AddReferences se usa para agregar cualquier referencia que el código pueda necesitar; para lograrlo, debe proporcionar un tipo que tenga las mismas referencias que necesita el código. En este caso concreto, solo necesita las mismas referencias de las que depende el tipo de objeto. Con Get­TypeInfo().Assembly.Location, recupera el nombre del ensamblado de la referencia y, después, MetadataReference.CreateFromFile crea una referencia al ensamblado dentro de la compilación. Al final, el árbol de sintaxis se agrega a la compilación con AddSyntaxTrees.

En la segunda parte del código, una invocación a CSharpCompilation.Emit intenta producir el binario y devuelve un objeto de tipo EmitResult. Este último es muy interesante: Expone una propiedad Success de tipo booleano, que indica si la compilación se realizó correctamente. También expone una propiedad denominada Diagnostics, que devuelve una matriz inmutable de objetos Diagnostic que pueden resultar muy útiles para comprender el motivo por el que no se realizó la compilación. En la Figura 7, se puede ver fácilmente cómo se itera la propiedad Diagnostics si la compilación no se realiza correctamente. Es importante mencionar que el ensamblado de salida es una biblioteca de .NET Standard, de modo que la compilación del texto de origen solo se realizará correctamente si el código analizado con Roslyn depende de las API que se incluyen en .NET Standard.

Ahora, veamos qué sucede si la compilación se realiza correctamente. El espacio de nombres System.Runtime.Loader, incluido en el paquete NuGet con el mismo nombre que importó al principio del artículo, expone una clase singleton denominada Assembly­LoadContext, que expone un método denominado LoadFromAssemblyPath. Este método devuelve una instancia de la clase Assembly, que permite usar Reflection para obtener primero una referencia a la clase Helper y, luego, para obtener una referencia al método CalculateCircleArea, que puede invocar pasando un valor para el parámetro de radio. El método MethodInfo.Invoke recibe null como el primer argumento porque CalculateCircleArea es un método estático; por lo tanto, no es necesario que pase ninguna instancia de tipo. Si ahora llama al método GenerateAssembly desde Main de Program.cs, verá el resultado de este trabajo, como se muestra en la Figura 8, donde el resultado del cálculo es visible en la consola de depuración.

Resultado de la invocación a través de Reflection de código generado por Roslyn
Figura 8 Resultado de la invocación a través de Reflection de código generado por Roslyn

Como puede imaginar, Emit API junto con Reflection de .NET Core proporcionan una eficacia y una flexibilidad excelentes, ya que puede generar, analizar y ejecutar código de C# independientemente del sistema operativo. De hecho, todos los ejemplos que se tratan en este artículo se ejecutarán no solo en Windows, sino también en macOS y en la mayoría de las distribuciones de Linux. Además, invocar código desde una biblioteca es posible mediante Scripting API de Roslyn, de modo que no está limitado a usar Reflection.

Resumen

.NET Core permite escribir código de C# para crear aplicaciones multiplataforma que se ejecuten en varios sistemas operativos y dispositivos, motivo por el cual los compiladores son multiplataforma. Roslyn, la plataforma del compilador .NET, habilita el compilador de C# en .NET Core y permite a los desarrolladores usar las completas API de análisis de código con la finalidad de generar, analizar y compilar código. Esto implica que puede automatizar tareas mediante la generación y ejecución de código sobre la marcha, analizar texto de origen para detectar problemas del código y realizar un gran número de actividades en el código fuente, ya sea en Windows, macOS o Linux.


Alessandro Del Sole ha sido MVP de Microsoft desde el año 2008. Alessandro ha sido proclamado MVP del año en cinco ocasiones y es el autor de numerosos libros, eBooks, vídeos didácticos y artículos sobre desarrollo .NET con Visual Studio. Del Sole trabaja como desarrollador senior de .NET, centrado en temas de desarrollo, aprendizaje y consultoría de .NET y de aplicaciones móviles. Puede seguirlo en Twitter: @progalex.

Gracias al siguiente experto técnico de Microsoft por revisar este artículo: Dustin Campbell
Dustin Campbell es ingeniero jefe en Microsoft y miembro del equipo de diseño del lenguaje C#. Dustin trabajó en Roslyn desde su origen y, actualmente, es responsable de la extensión de C# de Visual Studio Code.