UpToDate C#: Expresiones

C#12

(Actualizado a 10 de Agosto de 2024)

Aquí incluyo expresiones que son relativamente nuevas o no muy conocidas, no pretende ser un listado formal ni tampoco preciso en el término expresiones.

Propiedades

Se pueden usar expresiones para asociar código a varios elementos incluyendo propiedades (auto properties), en este ejemplo se asocia código a la propiedad Name usando una variable de clase (o variable miembro), este código devolverá el valor de la variable o lo establecerá con el valor proporcionado. También se puede usar para funciones y otros elementos de código.

Otro asunto diferente son los inicializadores de propiedad donde la propiedad simplemente coge un valor inicial por defecto como se ve en la propiedad Surname.

public class Sample
{
    private string name = "First";
    public string Name
    {
        get => name;
        set => name = value;
    }
    public string Surname { get; set; } = "Surname";
}

Modificador required

Este es un nuevo modificador que obliga a los consumidores de una clase a utilizar las propiedades marcadas como required esto evita tener que crear constructores personalizados para cada combinación y también evita tener que crear un excesivo código en las clases heredadas llamando a los constructores.

public class Person
{
// The default constructor requires that FirstName and LastName be set at construction time
    public required string FirstName { get; init; }
    public string MiddleName { get; init; } = "";
    public required string LastName { get; init; }
}

Condición ternaria por referencia

Es conocida la expresión: a = exp ? b : c; donde se asigna a la variable a el resultado de b o c dependiendo de la expresión exp, si es cierta se asignará b y si es falsa se asignará c. Por ejemplo:

var a = 12;
var b = 3;
var c = 6;
int res = (a > 10) ? b : c;

Tendría este equivalente usando la sentencia if:

int a = 12;
int res = 0;
if(a > 10)
{
  res = b;
}
else
{
  res = c;
}

También es posible hacerlo usando referencias en lugar de paso por valor, por ejemplo:

var a = 12;
var b = 3;
var c = 6;
ref int res = ref (a > 10) ? ref b : ref c;
res = -1; // Ahora c vale -1

En este caso se devuelve c, pero no el valor de c, es decir, se devuelve la referencia por lo tanto la última línea de código está modificando c no la variable res.

Null coalescing

Más conocida es esta expresión:

object b = a ?? new string("Null!");

En este caso si la variable a es null la variable b cogerá el valor de la expresión, en este caso new string(«Null!») esto es equivalente a:

if (a == null)
{
  b = new string("Null!");
}

También muy útil para verificar parámetros nulos en métodos y lanzar excepciones:

object a = null;
object b = a ?? throw new ArgumentNullException("Object null.");

También es posible asignar directamente el valor, por ejemplo esta expresion:

if (variable is null)
{
    variable = expression;
}

Es equivalente a:

variable ??= expression;

Parámetro out

Muchas veces sobre todo para hacer casting entre tipos se usa la variable out lo que obliga a declarla previamente, ya no es necesario hacer:

int i = -1;
if (int.TryParse(numericValue, out i))
    return i;
return -1;

Se puede hacer lo siguiente:

if (int.TryParse(numericValue, out int i))
    return i;
return -1;

Interpolación

Es conocida la posibilidad de usar de usar el operador $ para sustituir variables en cadenas de caracteres al estilo String.Format, por ejemplo:

var value = 100.435678M;
WriteLine($"You called with => {value:N3}"); // Saca 100.436
WriteLine($"\n\n{"*",-3} Hola {"*",3}"); // Imprime '*   Hola   *'

A partir de la versión 8, este operador se puede combinar con $ en cualquier orden, por lo que $@ y @$ son intercambiables.

[C# 10] Es posible también crear interpoladores personalizados, los valores de la interpolación llaman a String.Format() ahora este comportamiento se puede personalizar.

[C#10] También se puede hacer con constantes, solo con cadenas que también sean constantes y no con números.

[C#11] Ahora se pueden agregar líneas en la interpolación, por ejemplo:

string message = $"The usage policy for {safetyScore} is {
    safetyScore switch
    {
        > 90 => "Unlimited usage",
        > 80 => "General usage, with daily safety check",
        > 70 => "Issues must be addressed within 1 week",
        > 50 => "Issues must be addressed within 1 day",
        _ => "Issues must be addressed before continued use",
    }
    }";

[C# 11] Incluir literales en cadenas, como:

int X = 2;
int Y = 3;

var pointMessage = $"""The point "{X}, {Y}" is {Math.Sqrt(X * X + Y * Y):F3} from the origin""";

Console.WriteLine(pointMessage);

Raw string literals

Hay más control sobre los literales de cadena, se crea una nueva forma usando «»» (triple comilla) que colocará el texto exactamente como se indica, la triple comilla tiene que ir sola al principio y al final, el resto del texto puede ir indentado, las comillas finales marcan la primera columna, no puede haber texto colocado antes.

string longMessage = """
    This is a long message.
    It has several lines.
        Some are indented
                more than others.
    Some should start at the first column.
    Some have "quoted text" in them.
    """;

Indicar utf8

Ahora se puede indicar que una cadena tiene que ser utf8:

ReadOnlySpan<byte> AuthWithTrailingSpace = new byte[] { 0x41, 0x55, 0x54, 0x48, 0x20 };
ReadOnlySpan<byte> AuthStringLiteral = "AUTH "u8; AuthStringLiteral = "AUTH "u8;

Hay que usar ReadOnlySpan<T>

Null propagation

Muchas veces se escribe código para comprobar si hay valores nulos, por ejemplo:

object d = null;
var a = new { A = new { B = new { C = d } } };

if(a != null)
  if(a.A != null)
    if(a.A.B != null)
      if(a.A.B.C != null)
        Console.WriteLine($"{nameof(a.A.B.C)} = {C.ToString()}");

Esto se puede simplicar usando el operador ? el código anterior quedaría así (se incluye el opeador nameof() como ejemplo de uso, este operador coge el nombre de la variable para evitar tener que escribirlo). El código equivalente sería:

var a = new { A = new { B = new { C = d } } };
Console.WriteLine($"{nameof(C)} = {a?.A?.B?.C?.ToString()}");

Discard

A veces se crean variables temporales que no se van a usar o se pasan parámetros que no se necesitan en el contexto de una llamada, para este tipo de escenarios existe la opción de descartar esos valores y no usarlos.

 bool b = int.TryParse(integer, out int _);
if (b)
{
    Console.WriteLine("Can be casted.");
}
else
{
    Console.WriteLine("It can not be casted.");
}

También en la devolución de una función (lo que incluye también por ejemplo expresiones lambdas) pero teniendo en cuenta que no puede haber más de un discard en el mismo ámbito (sería como crear dos variables con el mismo nombre).

public double ReturnDiscard(double value)
{
     var _ = Math.Sqrt(value++);
     return Math.Ceiling(_);
}

Conversion de valores

Se puede comparar el tipo y castear un valor dentro de una estructura if directamente, se usará el primero casting que funcione.

public string ConvertUsingIf(object element)
{
    if (element is string s)
        return s.ToUpper();// Casting included in the same line
    else if (element is int i)
        return Math.Abs(i).ToString();
    else if (element is double d)
        return Math.Truncate(d).ToString();
    else
        return string.Empty;
}

Console.WriteLine(structureControl.ConvertUsingIf("Element"));
Console.WriteLine(structureControl.ConvertUsingIf(5));
Console.WriteLine(structureControl.ConvertUsingIf(4.98));

/* El resultado es:
ELEMENT
5
4
*/

Con la sentencia switch se puede conseguir el mismo resultado

public string ConvertToStringUsingSwitch(object element)
{
    switch (element)
    {
        case string s:
            return s.ToUpper();
        case int i:
            return Math.Abs(i).ToString();
        case double d:
            return Math.Truncate(d).ToString();
        default:
            return string.Empty;
    }
}

Console.WriteLine(structureControl.ConvertToStringUsingSwitch("Element"));
Console.WriteLine(structureControl.ConvertToStringUsingSwitch(5));
Console.WriteLine(structureControl.ConvertToStringUsingSwitch(4.98));

/* El resultado es:
ELEMENT
5
4
*/

De hecho con switch incluso podemos añadir algunas condiciones dentro de la conversion.

public string ConvertToStringUsingSwitchWhen(object element)
{
    switch (element)
    {
        case string s when s.Length > 0:
            return s.ToUpper();
        case int i when i > 5: // Entero y mayor de 5
            return Math.Abs(i).ToString();
        case int i: // Cualquier entero
            return Math.Sqrt(i).ToString();
        case double d:
            return Math.Truncate(d).ToString();
        case var o when (o?.ToString().Length ?? 0) == 0:// Usando 'var'
            return "Empty";
        case null: // Comprobando contra 'null'
            throw new ArgumentNullException(paramName: nameof(element), message: "Element must not be null");
        default:
            return string.Empty;
    }
}

Patrones recursivos

Se añade más potencia a los patrones, ahora los patrones de expresiones son recursivos por lo que se pueden aplicar expresiones de patrones a expresiones de patrones.

Para los siguientes ejemplos se usará esta clase:

public class Building : IDisposable
{
    public int Age { get; set; }
    public string State { get; set; }
    public void Deconstruct(out int age, out string state)
    {
        age = Age;
        state = State.ToLower();
    }
    public void Dispose()
    {
        Age = 0;
    }
}

Expresiones Switch

Ahora hay una forma más compacta de expresar un switch.

public Color ExpressionPatterns() =>
    Console.BackgroundColor switch
    {
        ConsoleColor.Red => Color.FromArgb(255, 0, 0),
        ConsoleColor.Green => Color.FromArgb(0, 255, 0),
        ConsoleColor.Blue => Color.FromArgb(0, 0, 255),
        _ => throw new ArgumentException("Invalid value")
    };

[C# 10] Es posible ahora hacer referencia a propiedades anidadas.

public string ExtendedPropertyPattern(Building building)
    => building switch
    {
        { Name.FirstName: "John" } => "ok",
        { Name.FirstName: "Doe" } => "ko",
        _ => "Unknown"
    };

Patrones de propiedad

Es posible usar switch a través de las propiedades de un objeto, por ejemplo con una clase Person que tenga una propiedad Age. Esto devolvería un resultado en función del valor de la propiedad Age.

public int PropertyExpression(Building p) =>
    p switch
    {
        { Age: 10 } => p.Age * 1,
        { Age: 20 } => p.Age * 2,
        _ => 30,
    };

Patrones posicionales

Se puede combinar la funcionalidad de deconstrucción con patrones posicionales dentro de sentencias switch, es decir, se crea el método Deconstruct y se usa dentro de una sentencia switch parecido al manejo de tuplas:

public string PositionalPattern(Building building)
    => building switch
    {
        (0, "") => "No born",
        (15, "A") => "Young female",
        (25, "B") => "Too much older man",
        (_, _) => "No human",
        _ => "Unknown"
    };

Patrones de Tupla

Es posible usar tuplas dentro de switchs:

public static string SwitchTuple(string first, string second)
    => (first, second) switch
    {
        ("one", "two") => "Wins second",
        ("two", "one") => "Wins first",
        (_, _) => "tie"
    };

Patrones avanzados (C# 9)

Poco a poco se ha ido mejorando el uso de patrones y ahora se permite simplificaciones adicionales, por ejemplo:

public bool PatternMatchFunction(string a) =>
    a[0] is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z' and not default(char));

El ejemplo anterior usamos paréntesis para remarcar la precedencia y además se permite también comprobación contra null (x is not null).

Patrones en listas

Ahora se puede hacer encajar un array o lista contra una secuencia de patrones.

int[] numbers = { 1, 2, 3 };

Console.WriteLine(numbers is [1, 2, 3]);  // True
Console.WriteLine(numbers is [1, 2, 4]);  // False
Console.WriteLine(numbers is [0 or 1, <= 2, >= 3]);  // True

O también se puede hacer

List<int> numbers = new() { 1, 2, 3 };

if (numbers is [var first, _, _])
{
    Console.WriteLine($"The first element of a three-item list is {first}.");
}
// Output:
// The first element of a three-item list is 1.

Excepciones

Es posible establecer condiciones dentro de las claúsulas catch para poder filtrar por condiciones concretas:

public void RaiseException(string code)
{
    try
    {
        throw new ArgumentException(code);
    }
    catch (ArgumentException ae) when (ae.Message.Contains("100"))
    {// Mismo tipo (ArgumentException) y valor ("100")
        System.Console.WriteLine($"Error => {ae.Message}");
    }
    catch (ArgumentException ae) when (ae.Message.Contains("200"))
    {
        System.Console.WriteLine($"Error => {ae.Message}");
    }
    catch (ArgumentException ae)
    {// Cualquier otro ArgumentException no incluido antes
        System.Console.WriteLine($"Error => {ae.Message}");
    }
}

Using

Ahora la sentencia using no está limitada por las llaves, durará lo mismo que el ámbito en el que esté contenida la variable creada por using.

El siguiente código devolverá un 0 en su propiedad Age debido al método Dispose de la clase Building.

public Building ShortUsing()
{
    using Building b = new Building()
    {
        Age = 20,
        State = "WA"
    };
    b.Age = 40;
    return b;
}

También se pueden crear using globales, no es más que una forma de declarar un using en un fichero que se aplicará de forma global en todo el proyecto. Una forma útil es crear un fichero solo para los using.

global using System;

[C# 12] Ahora se puede usar using para cualquier tipo no solo tipos con nombre, por ejemplo tuplas, arrays, punteros… por ejemplo:

// The alias is to `List<...>` which is itself not a nullable
// reference type itself, even though it contains one as a type argument.
using Y = System.Collections.Generic.List<string?>;
using Z = int?;

Namespace [C#10]

Ahora es posible eliminar las llaves asociadas a los namespace, así se gana un poco de tabulación y espacio hacia la izquierda. Tip: Solo con poner ; al final del namespace VS limpiará los corchetes.

namespace UpToDate.Demos;
internal class DemoExpression
{...}

Secuencias asincrónicas

Es posible usar await para un bucle que consume una función asincrónica, por ejemplo teniendo este método.

public static async IAsyncEnumerable<string> GenerateWord(string a)
{
    for (int i = 65; i < 97; i++)
    {
        await Task.Delay(100);
        a += (char)i;
        yield return a;
    }
}

Se podría consumir directamente de esta forma:

await foreach (var word in Expressions.GenerateWord("z"))
{
    Console.WriteLine(word);
}

Índices y rangos

Un índice define un elemento dentro de una secuencia, por ejemplo ^0 se refiere al primer elemento, mientras que un rango define una serie de elementos dentro de la secuencia, por ejemplo 0..8 indica los elementos del 0 al 7 ya que el primer elemento siempre se incluye y el último se omite.

var numbers = new string[]
{
    "One",
    "Two",
    "Three",
    "Four",
    "Five"
};

Esto funciona con los siguientes elementos

var last = numbers[^1]; // The last one ("Five)
var rangeFromBegin = numbers[0..3]; // The first three ("One", "Two", "Three")
var rangeFromEnd = numbers[^2..^1]; // The penultimate element ("Four")
var all = numbers[..]; // All ("One","Two","Three","Four","Five")
var fromBegin = numbers[..2]; // First two ("One","Two")
var middle = numbers[3..]; // From third till end ("Four", "Five")

Range range = 1..4;
var range = numbers[range]; // From second till third ("Two", "Three", "Four")

Index index = ^2;
var index = numbers[index]; // The penultimate element ("Four")

Expresiones en colecciones

Una forma sencilla de agrupar colecciones, sirve para tipos array, Span<> y ReadOnlySpan<> y tipos que se pueden inicializar como colecciones (por ejemplo List<T>), en este ejemplo se concatenan 3 arrays.

int[] row0 = [1, 2, 3];
int[] row1 = [4, 5, 6];
int[] row2 = [7, 8, 9];
int[] single = [.. row0, .. row1, .. row2];
foreach (var element in single)
{
    Console.Write($"{element}, ");
}

Init

Una forma de establecer valores a las propiedades durante la creación del objeto, es útil para inicializar en lugar de usar un constructor o establecer valores en propiedades desde clases derivadas.

Por ejemplo el siguiente código permite inicializar un objeto a través de sus propiedades y después cambiar algunas de ellas menos la marcada como init que dará un error.

public class Building
{
    ...
    public string Name { get; init; } = "None";
    ...
}
Building b = new () { Age = 5, Name = "Chrysler", State = "USA" };
b.Age = 40;
b.Name = "11";
 // CS8852

Scoped

Este es un modificador para tipos por valor (en concreto ref struct) que asegura que el código no extenderá el tiempo de vida de la variable. Solo se puede aplicar a variables locales y parámetros.

Span<char> values = stackalloc char[3] { 'T', 'o', 'm' };
new Test().TestMethod(values);

ref struct Test
{
    public void TestMethod(scoped ReadOnlySpan<char> characters)
    {
        // The body of the method must only use characters in the local scope, and cannot assign it directly to any field or classes unles they themselves would be scoped.
    }
}

Constructores (C# 9)

Se pueden realizar algunas simplificaciones en los constructores si el tipo está bien establecido, por ejemplo está función:

public Building IsNull(Building b)
{
    if(b is null)
        return new();
    else
        return b;
}

Se podría llamar usando:

if (expressions.IsNull(new()) is null)
    Console.WriteLine("Is null");
else
    Console.WriteLine("Is not null");

En todos estos casos donde se usa new() no hace falta indicar el tipo porque está bien definido ya sea a través de los parámetros, el valor de retorno o la propia construcción del objeto.

Constructores primarios

Los constructores primarios permiten declaraciones más compactas eliminando propiedades y añadiéndolas directamente en el constructor, ahora se admiten para estructuras, registros y clases. Por ejemplo lo que antes era:

public readonly struct Distance
{
    public readonly double Magnitude { get; }

    public readonly double Direction { get; }

    public Distance(double dx, double dy)
    {
        Magnitude = Math.Sqrt(dx * dx + dy * dy);
        Direction = Math.Atan2(dy, dx);
    }
}

Ahora se puede hacer asi:

public readonly struct Distance(double dx, double dy)
{
    public readonly double Magnitude { get; } = Math.Sqrt(dx * dx + dy * dy);
    public readonly double Direction { get; } = Math.Atan2(dy, dx);
}

Y podemos asimismo heredar estos constructores o crear varios constructores primarios.

public class EmpireState(decimal height, decimal weight) : Building("WA")
{
    public EmpireState(decimal height, decimal weight, string address) : this(height, weight) { }
}

Lambda

Se incorpora el concepto de tipo natural de tal forma que ya no hace falta indicar expresamente los tipos de parámetros o devolución, por ejemplo:

var parse = (string s) => int.Parse(s); // Equivalent to Func<string, int>

Aunque en algunos casos si se quiere indicar se puede indicar el parámetro de devolución:

var choose = object (bool b) => b ? 1 : "two"; // Func<bool, object>

Ahora también se pueden añadir atributos a la expresión lambda, a parámetros de entrada y al valor de retorno, por ejemplo:

var lambdaAttribute = [Custom("lambda attribute")][return: Custom("return attribute")]int ([Custom("parameter attribute")]string s) => int.Parse(s);

[C# 12] Ahora también se admite parámetros por defecto, por ejemplo:

var IncrementBy = (int source, int increment = 1) => source + increment;

Atributos Genéricos (C# 11)

Ahora se pueden crear atributos genéricos, por ejemplo:

public class GenericCustomAttribute<T> : Attribute { }

Que luego podemos usar como atributo a un método por ejemplo:

[GenericCustom<string>()]
public string ShowCustomAttribute()
{
    return "Custom operation string";
}

A la hora de usar al el atributo tiene que estar construido, (no puede volver a ser genérico) y hay algunas restricciones como que no se puede usar dynamic, string?, (int x, int y)… pero se pueden usar los sustitutos naturales (object, string, ValueTuple<int, int>).

Atributo Experimental

Se puede marcar código como experimental para que saque una advertencia, realmente el compilador mostrará un error que se puede evitar usando la directiva #pragma

[Experimental("DiagID", UrlFormat = "https://example.org/{0}")]
public class GenericCustomAttribute<T> : Attribute { }

#pragma warning disable DiagID
[GenericCustom<string>()]
public string ShowCustomAttribute()
{
    return "Custom operation string";
}
UpToDate C#: Expresiones

Configurando SourceLink

Cuando se crean aplicaciones es muy habitual usar paquetes NuGet, muchas veces estos paquetes son de código libre y están albergados en GitHub o puede que sean privados y estén depositados en servidores NuGet privados.

Cuando se está desarrollando una aplicación a veces es cómodo poder depurar paquetes NuGet para obtener información como si fuera código local, para esto sirve SourceLink para permitir que el desarrollador pueda acceder al código fuente de un paquete NuGet.

Para que esto funcione el paquete debe soportar SourceLink así que es posible que no todos los paquetes que existan actualmente puedan usar este método.

También hará falta una versión de Visual Studio 15.3 y se recomienda una 15.7 para obtener toda la funcionalidad.

En este ejemplo se va a utilizar .Net Core que viene preparado para SourceLink, aunque se soportan otros formatos como .Net Framework así como varios formatos de PDB (Portable, Embedded y Windows PDBs).

Configuración

Es recomendable que la información de depuración de los proyectos sea portable, otros formatos son dependientes de Windows y no siempre funcionan bien.

Editamos el archivo .csproj y añadimos la siguiente información que es la recomendada por SourceLink.

<Project Sdk="Microsoft.NET.Sdk">
 <PropertyGroup>
    <TargetFramework>netcoreapp2.1</TargetFramework>
 
    <!-- Optional: Publish the repository URL in the built .nupkg (in the NuSpec <Repository> element) -->
    <PublishRepositoryUrl>true</PublishRepositoryUrl>
 
    <!-- Optional: Embed source files that are not tracked by the source control manager in the PDB -->
    <EmbedUntrackedSources>true</EmbedUntrackedSources>
  
    <!-- Optional: Build symbol package (.snupkg) to distribute the PDB containing Source Link -->
    <IncludeSymbols>true</IncludeSymbols>
    <SymbolPackageFormat>snupkg</SymbolPackageFormat>
  </PropertyGroup>
  <ItemGroup>
    <!-- Add PackageReference specific for your source control provider (see below) --> 
  </ItemGroup>
</Project>
  • PublishRepositoryUrl: Indicamos que queremos que en la información del paquete aparezca la Url del repositorio.
  • EmbedUntrackedSources: Permite añadir cómo código fuente a usar durante la depuración código que no esté asociado al repositorio de este paquete o que esté fuera del control del código fuente (como un AssemblyAttribute.cs), hay que tener cuidado porque puede añadir mucho código que igual no nos interesa.
  • IncludeSymbols: Construye el paquete .snupkg asociado que contiene la información necesaria para la depuración, otra forma sería añadirlo al paquete principal pero esto aumentaría el tamaño.
  • SymbolPackageFormat: Formato del paquete de símbolos.

Si tenemos problemas a la hora de depurar por ejemplo al tener las librerías en un repositorio privado podemos probar a usar está configuración que nos incluirá los símbolos dentro de la librería en lugar de un paquete de símbolos aparte, para Nuget.org se recomienda crear un paquete aparte (*.snupkg) como se indica aparte, en ese caso se puede omitir la directiva SymbolPackageFormat.

<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>

La configuración que hemos establecido hasta ahora lo que hace es crear una etiqueta <repository> con la dirección del repositorio así como el commit asociado a esta versión de código.
A través de Nuget Package Explorer se vería así.

<repository>

Varios paquetes por proyecto

En el caso de que queramos usar el fichero .nuspec, por ejemplo si tenemos varios proyectos y queremos crear un solo paquete con varios, tenemos que añadir esta misma etiqueta manualmente y parametrizarla ya que dotnet pack no funciona igual cuando se usa .nuspec, así que dentro del .nuspec añadimos:

<repository type="$RepositoryType$" url="$RepositoryUrl$" branch="$RepositoryBranch$" commit="$SourceRevisionId$"/>

Y los parámetros los tenemos que referenciar a través del .csproj añadiendo esta etiqueta.

<NuspecProperties>version=$(PackageVersion);SourceRevisionId=$(SourceRevisionId);RepositoryUrl=$(RepositoryUrl);RepositoryType=$(RepositoryType);RepositoryBranch=$(RepositoryBranch)</NuspecProperties>

Si usasemos un comando como el siguiente se rellenarían los valores correctos dentro de la etiqueta <repository>, más adelante se indicará como hacerlo a través de Azure DevOps.

dotnet pack .\Folder\Project.csproj -p:NuspecFile=C:\Projects\Project\Folder\File.nuspec -p:PackageVersion=4.9 -p:SourceRevisionId=7aab58c9134r2146dfr595a335e1474f4861649c -p:RepositoryUrl=https://github.com/acme/coyote -p:RepositoryType=git -p:RepositoryBranch=develop -o C:\folder\packages

También hay que tener en cuenta que si se quieren añadir los símbolos de depuración dentro del paquete hay que usar la siguiente etiquetas en lugar de <IncludeSymbols> ya que si existen ambas no se guardarán los .pdb dentro del paquete.

<DebugSymbols>true</DebugSymbols>

Ahora vamos a añadir los paquetes que harán falta para habilitar la depuración, este paquete lo marcamos como PrivateAssets=»All» (normalmente no hará falta hacerlo), esto evita que los proyectos que usen este paquete intente también usar el propio paquete SourceLink,

Los paquetes a instalar dependen del repositorio donde esté el código, unos ejemplos serían:

GitHub

<ItemGroup>
  <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0-beta2-18618-05" PrivateAssets="All"/>
</ItemGroup>

Aquí incluimos la referencia a paquete que antes hemos dejado vacía, actualmente la librería se encuentra en prerelease así que tendremos que marcar el check de NuGet Include prerelease para poder descargarla.

Azure DevOps

En este caso la referencia a añadir sería esta, al igual que para GitHub esta librería está en modo prerelease.

<ItemGroup>
  <PackageReference Include="Microsoft.SourceLink.Vsts.Git" Version="1.0.0-beta2-18618-05" PrivateAssets="All"/>
</ItemGroup>

Otros

Existen otras alternativas (GitLab, TFS, Bitbucket, …) que se pueden consultar en la propia web.

Sino aparece el paquete exacto para el repositorio la opción es usar Microsoft.SourceLink.GitHub.

Puede suceder que un paquete referencie a otros paquetes de otros proveedores, por ejemplo un paquete de GitHub que referencia a uno de Azure DevOps, en este caso la aplicación debe incluir las referencias a los paquetes necesarios a todos ellos.

Verificando

Para estar seguro de que el paquete corresponde a la versión podemos verificar que el commit que aparece en el paquete es el adecuado usando el comando git log.

Para comprobar que todo está bien configurado podemos usar la herramienta SourceLink, ejecutando este comando desde una consola de desarrollador o desde el propio VS podremos instalarla de forma global.

dotnet tool install --global SourceLink --version 3.0.0 
Instalando SourceLink como herramienta global

Ahora podemos lanzar comandos, por ejemplo el mapeo del código para ver si el .json está dentro de los .pdb con:

sourcelink print-json paquete\nCubed.EFCore.pdb
print-json

O comprobar que los ficheros se pueden descargar con:

sourcelink test paquete\nCubed.EFCore.pdb

O para imprimir el código SHA para ver los ficheros que contiene el .pdb:

sourcelink print-documents paquete\nCubed.EFCore.pdb
print-documents

O incluso las URL donde se ubicarán los ficheros de código fuente, estas direcciones son reales y se debería poder ver el contenido del código:

sourcelink print-urls paquete\nCubed.EFCore.pdb
print-urls

Publicando

Si queremos crear el paquete usando el pipeline de compilación de Azure a través de una tarea dotnet tendremos que usar el comando personalizado y no el pack (la opción de especificar un fichero .nuspec en lugar de .csproj on parece funcionar), dentro de los parámetros podemos usar variables de entorno para rellenar los valores necesarios para .nuspec. En este caso la versión es una variable que va dentro del pipeline, no es de entorno.

De cara a la publicación de los paquetes será necesario una versión del cliente NuGet al menos 4.9.0 y usar la URL v3 para poder publicar los símbolos, por ejemplo para NuGet.org https://api.nuget.org/v3/index.json

Puede pasar un tiempo desde que se publican los símbolos en GitHub hasta que los símbolos estén disponibles (máximo 1 hora pero suele ser menos).

Si tenemos un error como:

C:\Windows\ServiceProfiles\NetworkService\.nuget\packages\sourcelink.create.commandline\2.8.3\build\SourceLink.Create.CommandLine.targets(30,5): error : unable to convert OriginUrl: https://{private}.visualstudio.com/dotNET%20Project/_git/company.cqrslite [C:\agent\_work\34\s\company.CQRSlite\company.CQRSlite.csproj]

En ese caso tenemos que eliminar este paquete SourceLink.Create.CommandLine

Consejo DevOps, es mejor publicar de forma separada el paquete y los símbolos, o al menos tener la opción, ya que por ejemplo NuGet.org no permite publicar la misma versión de nuevo lo que impediría subir los símbolos al fallar la tarea anterior.

Visual Studio

Solo queda un detalle final, deshabilitar la opción Just My Code en las opciones de Visual Studio Tools->Options->Debugging esto permite poder acceder a otros orígenes de código y no solo al propio del desarrollador.

Deshabilitando Just My Code

Si se esta depurando código fuente que ya existe localmente en la máquina sera ese el que se use, pero si el código no existe en la máquina (que suele ser lo normal) se mostrará un mensaje como el siguiente.

Aviso de advertencia antes de descargar código

Eligiendo la primera opción se descargará el código en una ruta como:

C:\Users\user\AppData\Local\SourceServer\ 1c6a834f2006f59a0cb6dd7101918efae8ad05b614b5fc36d11bfc9c6d9bcb9b \src\proyecto\carpeta\codigo.cs

Este tipo de rutas permite que se puedan bajar diferentes versiones del código para diferentes versiones del mismo.

Referencias

https://stackoverflow.com/questions/53485362/pass-variable-to-nuspec-with-dotnet-pack
https://docs.microsoft.com/es-es/nuget/reference/nuspec#replacement-tokens
https://cezarypiatek.github.io/post/setting-assembly-and-package-metadata/
https://devblogs.microsoft.com/nuget/introducing-source-code-link-for-nuget-packages/
https://github.com/ctaggart/SourceLink/issues/256

Configurando SourceLink

Cambiar nombres de propiedades de navegación en base de datos con EFCore

Es bastante común que la nomenclatura que se use en código, habitualmente Camel (CustomerId), no sea la misma que usemos en base de datos (CUSTOMER_ID) no voy a entrar a valorar cual es mejor o peor pero es conocido que es un hecho bastante habitual.

EFCore proporciona muchos mecanismos para realizar estos mapeos y poder elegir el nombre que se prefiera pero me he encontrado con el siguiente caso (muy habitual).

public class Project 
{
    public int ProjectId { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public DateTime? End { get; set; }
    public DateTime Start { get; set; }
    public Customer Customer { get; set; }
}

Soy fan de FluentAPI y no me gusta meter anotaciones dentro de las clases, tampoco voy a entrar a discutir esto, así que me creo una clase de mapeo tal que:

public class ProjectMapping : IEntityTypeConfiguration<Project>
{
    public void Configure(EntityTypeBuilder<Project> builder)
    {
        builder.ToTable("PROJECT");
        builder.HasKey(c => c.ProjectId);
        builder.Property(c => c.ProjectId).HasColumnName("PROJECT_ID");

        builder.Property(c => c.Name).IsRequired().HasColumnName("NAME");
        builder.Property(c => c.Description).HasColumnName("DESCRIPTION");
        builder.Property(c => c.End).HasColumnName("END_DATE");
        builder.Property(c => c.Start).IsRequired().HasColumnName("START_DATE");

        builder.HasOne(x => x.Customer)
         .WithMany(y => y.Projects)
         .OnDelete(DeleteBehavior.Cascade);
}

Haciendo las migraciones, etc… nos crearía un SQL como:

CREATE TABLE [PROJECT] (
    [PROJECT_ID] int NOT NULL IDENTITY,
    [NAME] nvarchar(max) NOT NULL,
    [DESCRIPTION] nvarchar(max) NULL,
    [END_DATE] datetime2 NULL,
    [START_DATE] datetime2 NOT NULL,
    [CustomerId] int NULL,
    CONSTRAINT [PK_PROJECT] PRIMARY KEY ([PROJECT_ID]),
    CONSTRAINT [FK_PROJECT_CUSTOMER_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [CUSTOMER] ([CUSTOMER_ID]) ON DELETE CASCADE
);

Vemos que todos los nombres son acordes a nuestra nomenclatura menos CustomerId que ha sido creada con el nombre por defecto de las convenciones, si quisiéramos cambiarlo podríamos añadir una propiedad CustomerId a la entidad mapearla como campo poniendo entonces el nombre que queramos y luego haciendo que la foreign key apunte a esta propiedad:

builder.Property(c => c.CustomerId).HasColumnName("CUSTOMER_ID");
builder.HasOne(x => x.Customer)
                .WithMany(y => y.Projects)
                .HasForeignKey(z=>z.CustomerId)
                .OnDelete(DeleteBehavior.Cascade);

Y esto nos sacaría el SQL que queremos:

CREATE TABLE [PROJECT] (
    [PROJECT_ID] int NOT NULL IDENTITY,
    [NAME] nvarchar(max) NOT NULL,
    [DESCRIPTION] nvarchar(max) NULL,
    [END_DATE] datetime2 NULL,
    [START_DATE] datetime2 NOT NULL,
    [CUSTOMER_ID] int NOT NULL,
    CONSTRAINT [PK_PROJECT] PRIMARY KEY ([PROJECT_ID]),
    CONSTRAINT [FK_PROJECT_CUSTOMER_CUSTOMER_ID] FOREIGN KEY ([CUSTOMER_ID]) REFERENCES [CUSTOMER] ([CUSTOMER_ID]) ON DELETE CASCADE
);

Ahora CustomerId tiene el nombre correcto que buscamos y además la clave foránea apunta a dicho campo. La verdad es que hasta aquí nada nuevo, el problema o la duda que me surgía era ¿se puede omitir crear la propiedad CustomerId? siendo realistas aunque puedo darle algún uso solo la he creado para poder mapear con un nombre acorde al resto de los campos de la tabla.

EFCore da más opciones con los llamados backing fields así que ahora si se puede hacer, lo que se hace es crear un campo shadow con el nombre que queramos y después usar dicho nombre en el mapeo, así que borramos la propiedad que hemos creado antes y hacemos lo siguiente:

  builder.Property<int>("CUSTOMER_ID"); // Genero un shadow
  builder.HasOne(x => x.Customer)
                .WithMany(y => y.Projects)
                .HasForeignKey("CUSTOMER_ID") // Lo uso aquí :)
                .OnDelete(DeleteBehavior.Cascade);

Esto nos genera exactamente el mismo SQL que en el caso anterior.

CREATE TABLE [PROJECT] (
    [PROJECT_ID] int NOT NULL IDENTITY,
    [NAME] nvarchar(max) NOT NULL,
    [DESCRIPTION] nvarchar(max) NULL,
    [END_DATE] datetime2 NULL,
    [START_DATE] datetime2 NOT NULL,
    [CUSTOMER_ID] int NOT NULL,
    CONSTRAINT [PK_PROJECT] PRIMARY KEY ([PROJECT_ID]),
    CONSTRAINT [FK_PROJECT_CUSTOMER_CUSTOMER_ID] FOREIGN KEY ([CUSTOMER_ID]) REFERENCES [CUSTOMER] ([CUSTOMER_ID]) ON DELETE CASCADE
);

Yendo un poco más lejos, ¿se podría hacer esto mismo con las tablas intermedias? así tendría solo objectos como propiedades de navegación y no tendría que tener propiedades extras para los mapeos de los campos de identidad.

En este caso hay un problema adicional, la clave primaria de la tabla intermedia suele estar compuestas por las claves primarias de las tablas a las que hace referencia y no se puede usar un objeto como clave primaria en el mapeo por lo tanto al quitarlas y dejar solo objetos no podría crear la clave primaria.

Resulta que ambos problemas se solucionan de la misma forma, para una entidad tal que:

    public class ProjectResource
    {
        public Resource Resource { get; set; }
        public Project Project { get; set; }
    }

Hacemos lo siguiente:

    public class ProjectResourceMapping : IEntityTypeConfiguration<ProjectResource>
    {
       public void Configure(EntityTypeBuilder<ProjectResource> builder)
       {
            builder.ToTable("PROJECT_RESOURCE");

            builder.Property<int>("PROJECT_ID"); // Campo que apunta a la tabla PROJECTS
            builder.Property<int>("RESOURCE_ID"); // Campo que apunta a la tabla RESOURCES

            builder.HasKey("PROJECT_ID", "RESOURCE_ID"); // Los uso para crear la clave primaria

            builder.HasOne(c => c.Resource)
                   .WithMany(d => d.ProjectResources)
                   .HasForeignKey("RESOURCE_ID"); // Y como clave foránea de RESOURCES
            builder.HasOne(c => c.Project)
                   .WithMany(d => d.ProjectResources)
                   .HasForeignKey("PROJECT_ID"); // y de PROJECTS
        }
    }

Ahora hay que hacer lo mismo en las tablas referenciadas para que sepan cual es el campo al que tienen que apuntar en la tabla intermedia, de otra forma crearían referencias duplicadas, cogemos de ejemplo la tabla RESOURCES (la otra tabla sería igual).

public class ResourceMapping : IEntityTypeConfiguration<Resource>
{
    public void Configure(EntityTypeBuilder<Resource> builder)
    {
        builder.ToTable("RESOURCE");
        builder.HasKey(c => c.ResourceId);

        builder.HasMany(c => c.ProjectResources)
               .WithOne(d => d.Resource)
               .HasForeignKey("RESOURCE_ID"); // El mismo nombre que hemos elegido antes
    }
}

Con esto estaría resuelto, además tendríamos todas las ventajas de los campos shadow.

Un detalle importante, las propiedades shadow  tienen que aparecer antes que las propiedades que las hacen referencia, ya sea una clave foránea o una clave primaria, de otro modo dará un error, supongo que son cosas del directo.

¡Hasta otra!

Cambiar nombres de propiedades de navegación en base de datos con EFCore