0

.Net 7 Preview 6

Hace unos días tenemos disponible la preview 6 correspondiente a la versión 7 de .Net. Esta versión incluye mejoras en los convertidores de tipos, personalización de contratos JSON, actualizaciones de System.Formats.Tar API, restricciones para la creación de plantillas .NET y mejoras de rendimiento en el área de CodeGen. Veamos las novedades

Convertidores de tipos

Los convertidores de tipo expuestos para los tipos primitivos recién agregados son DateOnly, TimeOnly, Int128, UInt128 y Half. Algunos de estos aparecieron como novedades en la Preview 5.

namespace System.ComponentModel
{
    public class DateOnlyConverter : System.ComponentModel.TypeConverter
    {
        public DateOnlyConverter() { }
    }

    public class TimeOnlyConverter : System.ComponentModel.TypeConverter
    {
        public TimeOnlyConverter() { }
    }

    public class Int128Converter : System.ComponentModel.BaseNumberConverter
    {
        public Int128Converter() { }
    }

    public class UInt128Converter : System.ComponentModel.BaseNumberConverter
    {
        public UInt128Converter() { }
    }

    public class HalfConverter : System.ComponentModel.BaseNumberConverter
    {
        public HalfConverter() { }
    }
}

Veamos algunos ejemplos de cómo se utilizan:

TypeConverter dateOnlyConverter = TypeDescriptor.GetConverter(typeof(DateOnly));
// produce DateOnly value of DateOnly(1940, 10, 9)
DateOnly? date = dateOnlyConverter.ConvertFromString("1940-10-09") as DateOnly?;

TypeConverter timeOnlyConverter = TypeDescriptor.GetConverter(typeof(TimeOnly));
// produce TimeOnly value of TimeOnly(20, 30, 50)
TimeOnly? time = timeOnlyConverter.ConvertFromString("20:30:50") as TimeOnly?;

TypeConverter halfConverter = TypeDescriptor.GetConverter(typeof(Half));
// produce Half value of -1.2
Half? half = halfConverter.ConvertFromString(((Half)(-1.2)).ToString()) as Half?;

TypeConverter Int128Converter = TypeDescriptor.GetConverter(typeof(Int128));
// produce Int128 value of Int128.MaxValue which equal 170141183460469231731687303715884105727
Int128? int128 = Int128Converter.ConvertFromString("170141183460469231731687303715884105727") as Int128?;

TypeConverter UInt128Converter = TypeDescriptor.GetConverter(typeof(UInt128));
// produce UInt128 value of UInt128.MaxValue which equal 340282366920938463463374607431768211455
UInt128? uint128 = UInt128Converter.ConvertFromString("340282366920938463463374607431768211455") as UInt128?;

Personalización de contratos JSON

En ciertas situaciones,serializamos o deserializamos JSON y no queremos o no podemos cambiar los tipos porque provienen de una biblioteca externa o ensucia riamos mucho nuestro el código, pero necesitamos hacer algunos cambios que influyen en la serialización, como eliminar propiedad, cambiar cómo números se serializan, cómo se crea el objeto, etc. Nos vemos obligados a escribir contenedores o convertidores personalizados que son una molestia y también hacen que la serialización sea más lenta.

La personalización del contrato JSON viene a solucionar estos inconvenientes. Permite al usuario un mayor control sobre qué y cómo se serializar o deserializar los tipos.

Optar por la personalización

Tenemos dos formas en que podemos «conectar» a la personalización, ambos terminan asignando JsonSerializerOptions.TypeInfoResolver y requieren la asignación de resolución:

Podemos usar DefaultJsonTypeInfoResolver y agregar su modificador, todos los modificadores serán llamado en serie:

{
    TypeInfoResolver = new DefaultJsonTypeInfoResolver()
    {
        Modifiers =
        {
            (JsonTypeInfo jsonTypeInfo) =>
            {
                // your modifications here, i.e.:
                if (jsonTypeInfo.Type == typeof(int))
                {
                    jsonTypeInfo.NumberHandling = JsonNumberHandling.AllowReadingFromString;
                }
            }
        }
    }
};

Point point = JsonSerializer.Deserialize<Point>(@"{""X"":""12"",""Y"":""3""}", options);
Console.WriteLine($"({point.X},{point.Y})"); // (12,3)

public class Point
{
    public int X { get; set; }
    public int Y { get; set; }
}
  • Escribiendo su propia resolución personalizada implementando System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver.
  • Cuando el tipo no se maneja, el código debe devolver nulo.
  • IJsonTypeInfoResolver se puede combinar con otros en una resolución efectiva que devolverá la primera respuesta no nula. Por ejemplo, JsonTypeInfoResolver.Combine(nuevo MyResolver(), nuevo DefaultJsonTypeInfoResolver()).

Personalizaciones

IJsonTypeInfoResolver es proporcionar JsonTypeInfo para cualquier solicitud de serializador de tipo; esto solo sucederá una vez por tipo por opción. JsonTypeInfo.Kind determinará qué podemos cambiar y se determina en función del convertidor, que se determina en función de los convertidores proporcionados a las opciones. Por ejemplo, JsonTypeInfoKind.Object significa que podemos agregar/modificar propiedades, mientras que JsonTypeInfoKind.None significa que no se garantiza el uso de ninguna de las perillas; eso puede suceder cuando el tipo tiene un convertidor personalizado.

JsonTypeInfo es creado por DefaultJsonTypeInfoResolver con perillas rellenadas previamente provenientes de, por ejemplo, atributos personalizados o puede ser creado desde cero por el usuario: JsonTypeInfo.CreateJsonTypeInfo: crear desde cero significa que el usuario también deberá configurar JsonTypeInfo.CreateObject.

Personalización de propiedades

Las propiedades son relevantes cuando JsonTypeInfo.Kind == JsonTypeInfoKind.Object y, en el caso de DefaultJsonTypeInfoResolver, se completarán previamente. Podemos modificar o crear utilizando JsonTypeInfo.CreateJsonPropertyInfo y podemos agregar a la lista de propiedades, es decir, digamos que obtuvimos una clase de una biblioteca separada que tiene API de diseño que no puede cambiar:

class MyClass
{
    private string _name = string.Empty;
    public string LastName { get; set; }

    public string GetName() => _name;
    public void SetName(string name)
    {
        _name = name;
    }
}

Antes de esta característica, necesitábamos envolver su jerarquía de tipos o crear un convertidor personalizado para ese tipo. Ahora simplemente podemos hacer lo siguiente:

JsonSerializerOptions options = new()
{
    TypeInfoResolver = new DefaultJsonTypeInfoResolver()
    {
        Modifiers = { ModifyTypeInfo }
    }
};

MyClass obj = new()
{
    LastName = "Doe"
};

obj.SetName("John");

string serialized = JsonSerializer.Serialize(obj, options); // {"LastName":"Doe","Name":"John"}

static void ModifyTypeInfo(JsonTypeInfo ti)
{
    if (ti.Type != typeof(MyClass))
        return;

    JsonPropertyInfo property = ti.CreateJsonPropertyInfo(typeof(string), "Name");
    property.Get = (obj) =>
    {
        MyClass myClass = (MyClass)obj;
        return myClass.GetName();
    };

    property.Set = (obj, val) =>
    {
        MyClass myClass = (MyClass)obj;
        string value = (string)val;
        myClass.SetName(value);
    };

    ti.Properties.Add(property);
}

Serialización condicional de propiedades

Algunos escenarios requieren que algunos valores predeterminados no se serialicen. En lugar de que aparezca 0 en JSON para ciertas propiedades no aparezcan. Era posible hacer que funcionara antes usando JsonIgnoreAttribute con JsonIgnoreCondition.WhenWritingDefault. El problema ocurre cuando su valor predeterminado no es 0 y es algo diferente, es decir, -1 o depende de alguna configuración externa.

Ahora podemos configurar su propio predicado ShouldSerialize con cualquier condición que deseemos. Es decir. digamos que tenemos una propiedad de string y desea que N/A no apareció en JSON:

// string property you'd like to customize
JsonPropertyInfo property = ...;

property.ShouldSerialize = (obj, val) =>
{
    // in this specific example we don't use parent but it's available if needed
    MyClass parentObj = (MyClass)obj;
    string value = (string)val;
    return value != "N/A";
};

Ignorar propiedades con nombre o tipo específico

var modifier = new IgnorePropertiesWithNameOrType();
modifier.IgnorePropertyWithType(typeof(SecretHolder));
modifier.IgnorePropertyWithName("IrrelevantDetail");

JsonSerializerOptions options = new()
{
    TypeInfoResolver = new DefaultJsonTypeInfoResolver()
    {
        Modifiers = { modifier.ModifyTypeInfo }
    }
};

ExampleClass obj = new()
{
    Name = "Test",
    Secret = new SecretHolder() { Value = "MySecret" },
    IrrelevantDetail = 15,
};

string output = JsonSerializer.Serialize(obj, options); // {"Name":"Test"}

class ExampleClass
{
    public string Name { get; set; }
    public SecretHolder Secret { get; set; }
    public int IrrelevantDetail { get; set; }
}

class SecretHolder
{
    public string Value { get; set; }
}

class IgnorePropertiesWithNameOrType
{
    private List<Type> _ignoredTypes = new List<Type>();
    private List<string> _ignoredNames = new List<string>();

    public void IgnorePropertyWithType(Type type)
    {
        _ignoredTypes.Add(type);
    }

    public void IgnorePropertyWithName(string name)
    {
        _ignoredNames.Add(name);
    }

    public void ModifyTypeInfo(JsonTypeInfo ti)
    {
        JsonPropertyInfo[] props = ti.Properties.Where((pi) => !_ignoredTypes.Contains(pi.PropertyType) && !_ignoredNames.Contains(pi.Name)).ToArray();
        ti.Properties.Clear();

        foreach (var pi in props)
        {
            ti.Properties.Add(pi);
        }
    }
}

Actualización en System.Formats.Tar

La preview 4 vino con la novedad de System.Formats.Tar para el manejo de archivos TAR. En estas preview se realizaron algunos cambios.

Clase especializada de atributos extendidos globales

Originalmente se pensó que PAX TAR podían contener una única entrada de Atributos ampliados globales (GEA) en la primera posición, pero se descubrió que los archivos TAR pueden contener varias entradas GEA, lo que puede afectar a todas las entradas posteriores hasta encontrar un nuevo GEA. entrada o el final del archivo. Por otro lado,se descubrió que las entradas de GEA no deben esperarse solo en archivos que contienen entradas de PAX exclusivamente: pueden aparecer en archivos que entremezclan entradas de diferentes formatos. Entonces se agregó una nueva clase para describir una entrada de GEA:

 public sealed partial class PaxGlobalExtendedAttributesTarEntry : PosixTarEntry
 {
     public PaxGlobalExtendedAttributesTarEntry(IEnumerable<KeyValuePair<string, string>> globalExtendedAttributes) { }
     public IReadOnlyDictionary<string, string> GlobalExtendedAttributes { get { throw null; } }
 }

También se descubrió que las entradas de diferentes formatos se pueden mezclar en un solo archivo TAR, la enumeración TarFormat se renombró a TarEntryFormat:

Cambios en la escritura y la lectura.

La propiedad Formato se eliminó de TarReader porque no se espera que ningún archivo tenga todas sus entradas en un solo formato. Dado que las entradas de GEA ahora se describen con su propia clase especializada, y se pueden encontrar varias entradas de este tipo en un solo archivo, también se eliminó la propiedad del diccionario de TarReader:

public sealed partial class TarReader : IDisposable
{
    ...
-    public TarFormat Format { get { throw null; } }
-    public IReadOnlyDictionary<string, string>? GlobalExtendedAttributes { get { throw null; } }
    ...
}

La adición de la clase GEA especializada también afectó a TarWriter:

  • Se eliminó el constructor que solía tomar el diccionario para una sola entrada GEA de primera posición.
  • Se agregó un nuevo constructor que toma solo la secuencia y el booleano LeaveOpen.
  • Se mantuvo el constructor que toma TarFormat, pero se cambió el nombre de la enumeración y se estableció un valor predeterminado en Pax. La documentación del método se modificó para explicar que el parámetro de formato especificado solo se aplica al método TarWriter.WriteEntry que agrega una entrada desde un archivo.
public sealed partial class TarWriter : IDisposable
{
    ...
-    public TarWriter(Stream archiveStream, IEnumerable<KeyValuePair<string, string>>? globalExtendedAttributes = null, bool leaveOpen = false) { }
+    public TarWriter(Stream archiveStream, bool leaveOpen = false) { }
-    public TarWriter(Stream archiveStream, TarFormat archiveFormat, bool leaveOpen = false) { }
+    public TarWriter(Stream archiveStream, TarEntryFormat format = TarEntryFormat.Pax, bool leaveOpen = false) { }
     public void WriteEntry(string fileName, string? entryName) { }
    ...
}

Template authoring

Constraints

Esta versión viene con el concepto de restricciones a las plantillas .NET. Las restricciones nos permiten definir el contexto en el que se permiten nuestras plantillas, lo que puede ayudar al motor de plantillas a determinar qué plantillas debe mostrar en comandos como dotnet new list. Para esta versión, sehan agregado soporte para tres tipos de restricciones:

  • Sistema operativo: limita las plantillas en función del sistema operativo del usuario.
  • Host del motor de plantillas: limita las plantillas en función del host que ejecuta el motor de plantillas; suele ser la propia CLI de .NET o escenarios integrados como el cuadro de diálogo Nuevo proyecto en Visual Studio/Visual Studio para Mac.
  • Cargas de trabajo instaladas: requiere que la carga de trabajo de .NET SDK especificada esté instalada antes de que la plantilla esté disponible


En todos los casos, describir estas restricciones es bastante sencillo como agregar una nueva sección de restricciones al archivo de configuración de su plantilla:

"constraints": {
       "web-assembly": {
           "type": "workload",
           "args": "wasm-tools"
       },
   }

Estas plantillas pueden tener un nombre, y usaremos ese nombre cuando le informemos al usuario por qué no pudo invocar su plantilla.

Actualmente, estas restricciones son compatibles con la CLI de .NET y estamos trabajando con nuestros socios en los equipos de Visual Studio para incorporarlas en las experiencias de creación de proyectos y elementos que ya conoce.

Parámetros Multi-choice 

Se agregó una nueva capacidad para los parámetros de elección: la capacidad de que un usuario especifique más de un valor en una sola selección. Esto lo podemos usar de la misma manera que se podría usar una enumeración de estilo Flags. Ejemplos comunes de este tipo de parámetro podrían ser:

  • Optar por múltiples formas de autenticación en la plantilla web
    Elegir múltiples plataformas de destino (ios, android, web) a la vez en las plantillas de maui

Optar por este comportamiento es tan simple como agregar «allowMultipleValues». Una vez que lo hagamos obtendremos acceso a una serie de funciones de ayuda para usar en el contenido de nuestra plantilla y también para ayudar a detectar valores específicos que el usuario eligió.

Unificación de códigos de salida y reporte

Se unificaron los códigos de salida informados por Template Engine. Nos ayudará a los a confiar en las secuencias de comandos en nuestro shell de elección para tener una experiencia de manejo de errores más consistente. Además, los errores informados por la CLI de .NET ahora incluyen un enlace para encontrar información detallada sobre cada código de salida:

➜ dotnet new unknown-template
No templates found matching: 'unknown-template'.

To list installed templates, run:
   dotnet new list
To search for the templates on NuGet.org, run:
   dotnet new search unknown-template

For details on the exit code, refer to https://aka.ms/templating-exit-codes#103

Conclusiones

En este post no hablamos de las cosas de más bajo nivel como CodeGen. Se han realizado mejoras de rendimiento nuevamente y una gran cantidad de reparaciones por parte de la comunidad. 

Tenemos varias novedades en el corazón de .net para investigar y tener presente en próximos desarrollos en próximos post veremos las siguiente vistas previas.

Fernando Sonego

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *