Ir al contenido
learn

12. Un pequeño proyecto

Los programas de este curso eran archivos sueltos, y la lección 11 añadió una biblioteca y sus pruebas. Un programa real suele estar formado por varios proyectos: una biblioteca que contiene la lógica, una aplicación que la gente ejecuta, y pruebas. Esta última lección los reúne en una solución. Añade un paquete de NuGet, el gestor de paquetes de .NET, convierte la biblioteca en un paquete propio, y da un primer vistazo a async y await, la forma que tiene C# de esperar un archivo o la red sin bloquearse.

El código de esta lección está en code/csharp-beginner: la carpeta l12-project con la solución y sus tres proyectos, y los archivos l12_*.cs de examples, exercises y compile_fail. check.sh ejecuta las pruebas, compila la solución, ejecuta la aplicación de consola con unos cuantos comandos y empaqueta la biblioteca, y después compara cada salida con los archivos de expected/.

Una solución es un archivo que lista proyectos, para que un solo comando los compile o los pruebe todos. A partir de una carpeta vacía, estos comandos crean la solución, los tres proyectos y las referencias entre ellos:

Ventana de terminal
dotnet new sln -n Fretboard
dotnet new classlib -o Fretboard
dotnet new console -o Fretboard.App
dotnet new xunit -o Fretboard.Tests
dotnet sln add Fretboard Fretboard.App Fretboard.Tests
dotnet add Fretboard.App reference Fretboard
dotnet add Fretboard.Tests reference Fretboard

Con el SDK 10, dotnet new sln crea Fretboard.slnx, una solución escrita en XML. Los SDK anteriores creaban un archivo .sln en un formato de texto más antiguo, que dotnet sln migrate convierte. Después de dotnet sln add, el archivo lista los tres proyectos:

<Solution>
<Project Path="Fretboard.App/Fretboard.App.csproj" />
<Project Path="Fretboard.Tests/Fretboard.Tests.csproj" />
<Project Path="Fretboard/Fretboard.csproj" />
</Solution>

Una solución solo lista proyectos. Qué proyecto usa a cuál se escribe en los archivos de proyecto, con dotnet add reference, como en la lección 11. En la carpeta de la solución, dotnet build compila los tres proyectos. check.sh conserva sus nombres, ordenados, y los recuentos del final; la salida real da la ruta de cada archivo compilado:

Fretboard
Fretboard.App
Fretboard.Tests
0 Warning(s)
0 Error(s)

dotnet test en la misma carpeta ejecuta las pruebas del único proyecto de pruebas:

Passed! - Failed: 0, Passed: 6, Skipped: 0, Total: 6 - Fretboard.Tests.dll (net10.0)

dotnet test solo compila lo que necesitan las pruebas: el proyecto de pruebas y la biblioteca. Después de dotnet clean y dotnet test, check.sh no encuentra ningún Fretboard.App compilado. Con un error de compilación añadido al Program.cs de la aplicación, dotnet test siguió pasando y terminó con 0, mientras que dotnet build falló. Que las pruebas pasen no demuestra que toda la solución compile: ejecuta también dotnet build.

La biblioteca es el Fretboard de la lección 11, con Guitar sin cambios, Project sin su método Parse, y una clase nueva, ProjectFile, que se muestra más abajo. El proyecto de pruebas contiene dos pruebas de Guitar y dos de ProjectFile.

Fretboard.App es una pequeña herramienta de línea de comandos construida sobre la biblioteca. Su primer argumento nombra un comando, y los demás son los argumentos de ese comando:

using System.Globalization;
using Fretboard;
// Una pequeña herramienta de línea de comandos sobre la biblioteca: el primer argumento es el comando
const string Usage = "usage: fret <open-string-hz> <fret> | transpose <semitones> <notes...> | projects <file.csv>";
if (args.Length == 0)
{
Console.Error.WriteLine(Usage);
return 1;
}
try
{
switch (args[0])
{
case "fret" when args.Length == 3:
double openString = double.Parse(args[1], CultureInfo.InvariantCulture);
int fret = Guitar.ParseFret(args[2]);
Console.WriteLine(Guitar.FretFrequency(openString, fret).ToString(CultureInfo.InvariantCulture));
return 0;
case "transpose" when args.Length >= 3:
List<string> notes = [.. args[2..]];
Console.WriteLine(string.Join(" ", Guitar.Transpose(notes, int.Parse(args[1]))));
return 0;
case "projects" when args.Length == 2:
List<Project> projects = await ProjectFile.ReadAsync(args[1]);
Console.WriteLine($"{projects.Count} projects, {projects.Sum(p => p.SourceFiles)} source files");
return 0;
default:
Console.Error.WriteLine(Usage);
return 1;
}
}
catch (Exception e) when (e is FormatException or ArgumentException or IOException)
{
// Un mensaje y el código de salida 1: un script que ejecuta la herramienta ve que falló
Console.Error.WriteLine($"error: {e.Message}");
return 1;
}

dotnet run la ejecuta: --project nombra el proyecto, y las palabras que van después de -- van al programa, en args. check.sh ejecuta seis comandos desde la carpeta del curso, como dotnet run --project l12-project/Fretboard.App -- fret 110 7, y muestra el código de salida después de cada uno. Quita la carpeta del nombre del archivo que falta; el mensaje real da su ruta completa:

> fret 110 7
164.81
-> exit code 0
> transpose 2 C E G
D F# A
-> exit code 0
> projects data/ga-projects.csv
12 projects, 632 source files
-> exit code 0
> fret 110 25
error: fret ('25') must be less than or equal to '24'. (Parameter 'fret')
Actual value was 25.
-> exit code 1
> projects missing.csv
error: Could not find file 'missing.csv'.
-> exit code 1
> (no arguments)
usage: fret <open-string-hz> <fret> | transpose <semitones> <notes...> | projects <file.csv>
-> exit code 1
  • El código de salida le dice al programa que inició este si tuvo éxito: 0 para el éxito, cualquier otro valor para un fallo. Las instrucciones de nivel superior lo devuelven con return, como lo haría Main (Main y su valor de retorno). Bash guarda el código del último comando en $?, PowerShell en $LASTEXITCODE; los scripts y la CI lo comprueban, como hace check.sh.
  • Los errores van a Console.Error, la salida de error estándar, para que no se mezclen con los resultados cuando la salida va a un archivo.
  • catch (Exception e) when (...) es un filtro de excepciones: solo captura las excepciones para las que la condición es verdadera. Cualquier otra excepción sigue deteniendo el programa, con un código distinto de cero.
  • case "fret" when args.Length == 3 es una protección de caso: el caso solo coincide si el texto es fret y hay tres argumentos.

La lección 10 leyó con TextFieldParser un valor CSV entre comillas. CsvHelper es una biblioteca para archivos CSV, publicada en nuget.org. Una aplicación basada en archivos nombra un paquete con una línea #:package, con la versión después de @ (aplicaciones basadas en archivos):

#:package CsvHelper@33.1.0
using System.Globalization;
using CsvHelper;
// La línea de la lección 10, leída esta vez por el paquete CsvHelper de NuGet
string line = "\"Crosby, Stills & Nash\",Suite: Judy Blue Eyes,1969";
using var parser = new CsvParser(new StringReader(line), CultureInfo.InvariantCulture);
parser.Read();
string[] fields = parser.Record!;
Console.WriteLine($"CsvHelper: {fields.Length} fields: {string.Join(" | ", fields)}");
CsvHelper: 3 fields: Crosby, Stills & Nash | Suite: Judy Blue Eyes | 1969

El primer dotnet run descarga el paquete en una carpeta que comparten todos tus proyectos, la carpeta global de paquetes, .nuget/packages en tu carpeta personal. La versión es obligatoria: sin @33.1.0, la restauración se detiene con NU1015. El .csproj del mensaje es el proyecto que el SDK compila detrás de una aplicación basada en archivos. El SDK 10.0.112 lo llama l12_package_no_version.csproj, y el SDK 10.0.401, el que usa la CI, l12_package_no_version.cs.csproj; check.sh escribe el primer nombre:

l12_package_no_version.csproj : error NU1015: The following PackageReference item(s) do not have a version specified: CsvHelper

En un proyecto, dotnet add package escribe un PackageReference en el archivo de proyecto. El 3 de octubre de 2026, dotnet add Fretboard package CsvHelper eligió la última versión estable y escribió esto en Fretboard.csproj:

<ItemGroup>
<PackageReference Include="CsvHelper" Version="33.1.0" />
</ItemGroup>

La biblioteca lo usa en ProjectFile, que lee el ga-projects.csv de la lección 10 y lo convierte en records Project:

using System.Globalization;
using CsvHelper;
namespace Fretboard;
// Lee un archivo CSV de proyectos con el paquete CsvHelper, que entiende los valores entre comillas
public static class ProjectFile
{
// Desde cualquier texto: un archivo, o una cadena en una prueba
public static List<Project> Read(TextReader text)
{
using var csv = new CsvReader(text, CultureInfo.InvariantCulture);
return csv.GetRecords<Project>().ToList();
}
public static List<Project> Read(string path)
{
using var reader = new StreamReader(path);
return Read(reader);
}
// Lo mismo, pero el programa puede hacer otra cosa mientras se lee el archivo
public static async Task<List<Project>> ReadAsync(string path)
{
string text = await File.ReadAllTextAsync(path);
return Read(new StringReader(text));
}
}

GetRecords<Project>() empareja las columnas de la cabecera con los parámetros del constructor del record, por nombre: Name, Language, CsFiles, FsFiles. No toca la propiedad calculada SourceFiles. Read recibe un TextReader, la clase base de StreamReader y StringReader: el programa le da un archivo, y una prueba puede darle una cadena. Esta prueba comprueba las comillas:

[Fact]
public void Read_KeepsACommaInsideQuotes()
{
string text = """
Name,Language,CsFiles,FsFiles
"Demos, music theory",C#,3,0
""";
List<Project> projects = ProjectFile.Read(new StringReader(text));
Assert.Equal([new Project("Demos, music theory", "C#", 3, 0)], projects);
}

El texto entre """ es un literal de cadena sin formato: puede contener comillas y saltos de línea, y se le quita la sangría hasta la columna de las """ de cierre (literales de cadena sin formato). Dos records con los mismos valores son iguales, como mostró la lección 6, así que Assert.Equal puede comparar listas de ellos.

Un paquete con una vulnerabilidad conocida. Cuando restaura los paquetes, NuGet los compara con una base de datos de avisos de seguridad (auditoría de paquetes). La versión 12.0.1 de Newtonsoft.Json, una biblioteca JSON muy usada, tiene uno:

#:package Newtonsoft.Json@12.0.1
using Newtonsoft.Json;
// Una versión antigua de un paquete muy usado: la restauración avisa de que tiene una vulnerabilidad conocida
Console.WriteLine(JsonConvert.SerializeObject(new { Chord = "Am7", Frets = "x02010" }));
l12_audit.csproj : warning NU1903: Package 'Newtonsoft.Json' 12.0.1 has a known high severity vulnerability, https://github.com/advisories/GHSA-5crp-9r3c-p9vr
l12_audit.csproj : warning NU1903: Package 'Newtonsoft.Json' 12.0.1 has a known high severity vulnerability, https://github.com/advisories/GHSA-5crp-9r3c-p9vr
{"Chord":"Am7","Frets":"x02010"}

La advertencia aparece dos veces, y el programa se ejecuta. NU1903 corresponde a una gravedad alta; NU1901, NU1902 y NU1904, a una gravedad baja, moderada y crítica (NU1901–NU1904). La página del aviso da la primera versión corregida: la corrección es usar esa versión o una posterior. En un proyecto, dotnet package list --vulnerable muestra una tabla de esos paquetes. Terminó con 0 incluso cuando la tabla listaba uno.

dotnet pack convierte una biblioteca en un paquete, un archivo .nupkg:

Ventana de terminal
dotnet pack Fretboard
The package Fretboard.1.0.0 is missing a readme. Go to https://aka.ms/nuget/authoring-best-practices/readme to learn why package readmes are important.
Successfully created package 'Fretboard.1.0.0.nupkg'.

check.sh también quita aquí la carpeta: el paquete está en Fretboard/bin/Release.

  • Sin opciones, dotnet pack compila en Release, no en Debug.
  • La versión es 1.0.0 porque el proyecto no fija ninguna. <Version> en el archivo de proyecto la cambia, y el paquete toma el nombre del proyecto salvo que <PackageId> le dé otro (propiedades de paquete).
  • El mensaje sobre el readme es un consejo, no un error: nuget.org muestra el readme de un paquete en su página.
  • Un .nupkg es un archivo zip. Contiene lib/net10.0/Fretboard.dll y Fretboard.nuspec, que describe el paquete y lista CsvHelper como dependencia: un proyecto que instala Fretboard recibe también CsvHelper. El ejercicio 4 mira dentro.

dotnet nuget push publica un paquete en nuget.org, con una cuenta y una clave de API. Este curso no lo publica.

Leer un archivo o llamar a un servicio web lleva tiempo, y el programa no hace más que esperar. Con await, espera sin bloquear su hilo, que puede hacer otro trabajo mientras tanto (programación asíncrona). Un método que devuelve un Task representa un trabajo que termina más tarde; await lo espera y da su resultado:

using System.Diagnostics;
// await: el programa espera el archivo sin bloquear su hilo
string[] lines = await File.ReadAllLinesAsync(Path.Combine("data", "ga-projects.csv"));
Console.WriteLine($"{lines.Length - 1} projects");
// Dos esperas de medio segundo, una después de otra
var watch = Stopwatch.StartNew();
await Task.Delay(500);
await Task.Delay(500);
Console.WriteLine($"One after the other, at least 1 s: {watch.ElapsedMilliseconds >= 1000}");
// Las mismas dos esperas iniciadas a la vez: Task.WhenAll termina cuando las dos han terminado
watch.Restart();
Task first = Task.Delay(500);
Task second = Task.Delay(500);
await Task.WhenAll(first, second);
Console.WriteLine($"Together, under 0.9 s: {watch.ElapsedMilliseconds < 900}");
12 projects
One after the other, at least 1 s: True
Together, under 0.9 s: True
  • File.ReadAllLinesAsync es el gemelo asíncrono del File.ReadAllLines de la lección 10; await da su string[].
  • Task.Delay(500) es una tarea que termina al cabo de 500 milisegundos, y Stopwatch mide el tiempo. Dos retardos esperados uno después de otro tardan un segundo. Iniciados primero y esperados juntos con Task.WhenAll, tardan medio segundo: las dos esperas se solapan. Cinco ejecuciones midieron de 1014 a 1023 ms, y después de 508 a 514 ms. El programa muestra True o False frente a límites fijos, porque los tiempos exactos cambian de una ejecución a otra.
  • Un método que usa await se marca async y devuelve Task, o Task<T> para un resultado de tipo T, igual que ProjectFile.ReadAsync devuelve Task<List<Project>>. Las instrucciones de nivel superior que contienen await se vuelven asíncronas sin ninguna palabra clave.

La aplicación de consola espera ReadAsync en su comando projects, y xUnit ejecuta una prueba asíncrona, un método de prueba que devuelve Task:

// Una prueba asíncrona: xUnit espera el Task antes de leer el resultado
[Fact]
public async Task ReadAsync_GivesTheSameProjectsAsRead()
{
string path = Path.Combine(AppContext.BaseDirectory, "data", "ga-projects.csv");
List<Project> projects = await ProjectFile.ReadAsync(path);
Assert.Equal(ProjectFile.Read(path), projects);
Assert.Equal(12, projects.Count);
Assert.Equal(632, projects.Sum(p => p.SourceFiles));
}

Un await olvidado. Llamar a un método asíncrono lo inicia, pero sin await nada espera a que termine:

Console.WriteLine("Before");
WriteLater(); // sin await: el programa sigue sin esperar
Console.WriteLine("After");
async Task WriteLater()
{
await Task.Delay(100);
Console.WriteLine("Later");
}
l12_forgotten_await.cs(2,1): warning CS4014: Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.
Before
After

El compilador avisa con CS4014. WriteLater se ejecuta hasta su primer await, y después devuelve una tarea que nadie espera. El programa muestra After y termina, y Later nunca se escribe. El ejercicio 3 lo corrige.

Un constructor no puede esperar. Un constructor no puede ser async, así que no puede contener await:

var index = new VoicingIndex();
class VoicingIndex
{
public VoicingIndex()
{
await Task.Delay(100); // un constructor no puede ser async
}
}
l12_constructor_await.cs(7,9): error CS4033: The 'await' operator can only be used within an async method. Consider marking this method with the 'async' modifier and changing its return type to 'Task'.

Escribir public async VoicingIndex() no ayuda: el compilador lee entonces async como un tipo de retorno y VoicingIndex() como un método, e informa de CS0246 y CS0542. El remedio habitual es un método estático asíncrono que hace la espera y después llama a un constructor que no espera, como static async Task<VoicingIndex> CreateAsync().

Las mediciones se hicieron en el commit 5c3a52a, con un script que lee el commit con git, y con compilaciones de los proyectos de GA.

Proyectos fuera de la solución. La solución de GA, AllProjects.slnx, lista 75 de los 111 archivos de proyecto del repositorio. Su CI compila y prueba esa solución. Un proyecto que queda fuera solo se compila si lo referencia un proyecto listado, como les pasa a 7, o si lo nombra un workflow, y sus pruebas solo se ejecutan si lo nombra un workflow. Seis proyectos de pruebas están fuera de la solución y ningún workflow los nombra; sus archivos contienen 102 líneas con un atributo de prueba. Uno de ellos, GA.Business.Core.Graphiti.Tests, no compila: sus pruebas usan el espacio de nombres GA.Business.Graphiti, y su archivo de proyecto no referencia ningún proyecto. La compilación se detiene con CS0234 en sus dos líneas using GA.Business.Graphiti....

Versiones de paquetes. Cada archivo de proyecto de GA da sus propias versiones de paquetes, y 35 paquetes aparecen con dos versiones o más; MongoDB.Driver tiene cinco. Directory.Build.props es un archivo que MSBuild importa en cada proyecto situado bajo su carpeta (personalizar la compilación por carpeta). Bajo el comentario «Centralized package version alignment and vulnerability remediation» (alineación centralizada de las versiones de paquetes y corrección de vulnerabilidades), intenta alinear algunas versiones con líneas como <PackageReference Update="Microsoft.Extensions.Http" Version="10.0.0"/>. Update cambia un elemento que ya existe (elementos de MSBuild), pero Directory.Build.props se importa al principio de cada proyecto, antes de que existan los elementos PackageReference del propio proyecto: todavía no hay nada que actualizar. Una sonda restauró GA.Business.Core.Graphiti.Tests, cuyo archivo de proyecto pide 9.0.10, y obtuvo 9.0.10. Con la misma línea en Directory.Build.targets, que MSBuild importa al final, obtuvo 10.0.0. En el conjunto de los archivos de proyecto de GA, 23 declaraciones de estos paquetes dan una versión distinta de la alineada, 13 de ellas en proyectos de la solución. La gestión centralizada de paquetes de NuGet guarda una sola versión por paquete en un solo archivo, Directory.Packages.props.

Advertencias silenciadas. El mismo archivo desactiva NU1902, NU1903 y NU1904 para todos los proyectos: la advertencia del ejemplo de Newtonsoft.Json, y sus vecinas moderada y crítica. La CI tiene un análisis de seguridad que ejecuta dotnet list AllProjects.slnx package --vulnerable --include-transitive con continue-on-error: true. Incluso sin esa línea no podría fallar, porque el comando termina con 0 cuando lista un paquete vulnerable.

La herramienta de línea de comandos. La aplicación de consola de GA, GaCLI, tampoco está en la solución. Su propio workflow, Quality Check, la compila y ejecuta benchmark-quality --limit 50. En su Program.cs, 12 de los 28 comandos lanzan NotImplementedException, entre ellos benchmark-quality y analyze-chord, el primer ejemplo de su ayuda. En la ejecución 37167745677 de GA, sobre un commit posterior con el mismo workflow y el mismo Program.cs, el paso termina con una NotImplementedException no controlada y el código de salida 134, y la ejecución sale en verde: el paso tiene continue-on-error: true. El comando sync-mongodb captura sus excepciones, muestra Error during sync: Unable to resolve service for type 'GA.Data.MongoDB.Services.MusicalObjectsService'... y termina con 0, así que un script no puede saber que falló. La lección 10 señaló que GaCLI lee appsettings.yaml desde el directorio actual. Iniciado desde otra carpeta, se ejecuta, y en este commit el archivo no cambia nada: solo sync-mongodb lee una de sus dos secciones, en unos ajustes que ningún servicio usa.

Esperar tareas. GA no tiene ningún método async void. Su código compilado se bloquea a la espera de una tarea con .Wait(), .Result o GetAwaiter().GetResult() en 11 sitios, 3 de ellos en constructores, que no pueden hacer await: QdrantVectorIndex llama a InitializeCollectionAsync().Wait(). Otros siete .Result leen tareas después de await Task.WhenAll, cuando ya han terminado, como en ProductionOrchestrator: es el patrón del ejemplo de esta lección.

La tabla QA y la tabla de experimentos del diario registran estas mediciones.

Las soluciones son aplicaciones basadas en archivos en exercises; check.sh las ejecuta desde la carpeta del curso.

Ejercicio 1 — un paquete en una aplicación basada en archivos

Sección titulada «Ejercicio 1 — un paquete en una aplicación basada en archivos»

Muestra los proyectos F# de data/ga-projects.csv, con sus números de archivos F# y C#, desde una aplicación basada en archivos que lea el archivo con CsvHelper.

Solución
#:package CsvHelper@33.1.0
using System.Globalization;
using CsvHelper;
// Ejercicio 1: los proyectos F# de data/ga-projects.csv, leídos con CsvHelper en una aplicación basada en archivos
using var reader = new StreamReader(Path.Combine("data", "ga-projects.csv"));
using var csv = new CsvReader(reader, CultureInfo.InvariantCulture);
foreach (Project project in csv.GetRecords<Project>().Where(p => p.Language == "F#"))
{
Console.WriteLine($"{project.Name}: F# {project.FsFiles}, C# {project.CsFiles}");
}
record Project(string Name, string Language, int CsFiles, int FsFiles);
GA.Business.Config: F# 11, C# 8
GA.Business.DSL: F# 49, C# 0
GA.Business.ProbabilisticGrammar: F# 6, C# 0
GA.Business.Core.Generated: F# 1, C# 0

GetRecords lee el archivo a demanda, un record cada vez, a medida que foreach los pide: las líneas using mantienen el archivo abierto hasta el final del programa.

Escribe un método asíncrono que devuelva el número de líneas de un archivo. Llámalo sobre data/ga-projects.csv y sobre l12-project/Fretboard/Guitar.cs, y espera las dos llamadas juntas con Task.WhenAll.

Solución
// Ejercicio 2: contar las líneas de dos archivos leídos al mismo tiempo
string csv = Path.Combine("data", "ga-projects.csv");
string guitar = Path.Combine("l12-project", "Fretboard", "Guitar.cs");
// Task.WhenAll de Task<int> da un int[], en el orden de las tareas
int[] counts = await Task.WhenAll(CountLinesAsync(csv), CountLinesAsync(guitar));
Console.WriteLine($"{Path.GetFileName(csv)}: {counts[0]} lines");
Console.WriteLine($"{Path.GetFileName(guitar)}: {counts[1]} lines");
async Task<int> CountLinesAsync(string path)
{
string[] lines = await File.ReadAllLinesAsync(path);
return lines.Length;
}
ga-projects.csv: 13 lines
Guitar.cs: 36 lines

Las dos lecturas empiezan antes del primer await. Los resultados vuelven en el orden de las tareas, sea cual sea la lectura que termine primero.

Corrige el programa del await olvidado para que muestre Before, Later y After, sin ninguna advertencia.

Solución
// Ejercicio 3: el await olvidado, corregido
Console.WriteLine("Before");
await WriteLater(); // el programa espera aquí hasta que WriteLater haya terminado
Console.WriteLine("After");
async Task WriteLater()
{
await Task.Delay(100);
Console.WriteLine("Later");
}
Before
Later
After

Con await, las propias instrucciones de nivel superior se vuelven asíncronas, y el programa solo termina después de la última línea.

Después de dotnet pack Fretboard, escribe un programa que liste los archivos de Fretboard.1.0.0.nupkg con ZipFile, y que después muestre las líneas <dependency> de su .nuspec.

Solución
using System.IO.Compression;
// Ejercicio 4: lo que dotnet pack puso en el paquete de la biblioteca, que es un archivo zip
string package = Path.Combine("l12-project", "Fretboard", "bin", "Release", "Fretboard.1.0.0.nupkg");
using ZipArchive zip = ZipFile.OpenRead(package);
foreach (ZipArchiveEntry entry in zip.Entries)
{
// El nombre de este archivo de metadatos es aleatorio: se muestra un marcador en su lugar
string name = entry.FullName.EndsWith(".psmdcp") ? "package/services/metadata/core-properties/(random).psmdcp" : entry.FullName;
Console.WriteLine(name);
}
// Los paquetes que también recibe un proyecto que instala Fretboard
using StreamReader nuspec = new StreamReader(zip.GetEntry("Fretboard.nuspec")!.Open());
foreach (string line in nuspec.ReadToEnd().Split('\n').Where(line => line.Contains("<dependency ")))
{
Console.WriteLine(line.Trim());
}
_rels/.rels
Fretboard.nuspec
lib/net10.0/Fretboard.dll
[Content_Types].xml
package/services/metadata/core-properties/(random).psmdcp
<dependency id="CsvHelper" version="33.1.0" exclude="Build,Analyzers" />

En un .nuspec, version="33.1.0" significa 33.1.0 o posterior (intervalos de versiones). Los demás archivos son propios del formato de empaquetado: _rels/.rels, [Content_Types].xml y los metadatos .psmdcp.

  • Una solución lista proyectos; los archivos de proyecto dicen qué proyecto referencia a cuál. dotnet build en la carpeta de la solución los compila todos. dotnet test solo compila los proyectos de pruebas y lo que referencian, así que un error de compilación en la aplicación puede esconderse detrás de pruebas que pasan.
  • Una aplicación de consola lee sus argumentos en args e indica el éxito con el código de salida 0, y el fallo con otro código. Los errores van a Console.Error.
  • Un paquete de NuGet viene con una versión: #:package Name@version en una aplicación basada en archivos, un PackageReference escrito por dotnet add package en un proyecto. La restauración avisa de las vulnerabilidades conocidas (NU1901 a NU1904).
  • dotnet pack crea un .nupkg, un zip con la biblioteca compilada y un .nuspec que lista sus dependencias.
  • await espera un Task sin bloquear el hilo; un método que lo usa es async y devuelve Task o Task<T>. Task.WhenAll espera varias tareas iniciadas a la vez.
  • Sin await, una llamada asíncrona se ejecuta por su cuenta, y nadie ve su final ni sus excepciones; el compilador avisa con CS4014. Un constructor no puede hacer await.
  • En Directory.Build.props, un PackageReference Update llega antes que los elementos del proyecto y no cambia nada. Un paso con continue-on-error: true no puede hacer fallar una ejecución de CI.

Esta es la última lección del curso. El plan lista las doce. El curso C# avanzado continúa con la memoria, el recolector de basura, async por dentro y el rendimiento medido.