Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 6 de junio de 2023
.NET

Hace unos días hablábamos de la serialización polimórfica en .NET 6, y vimos qué posibilidades teníamos para conseguirlo sin tener que escribir un custom converter o conversor personalizado. Y aunque realmente .NET 6 permite hacerlo, no es lo más elegante del mundo porque teníamos que operar sobre tipos object.

Pero por suerte, en .NET 7 la cosa ha mejorado y ya tenemos opciones razonables para conseguirlo basadas en los dos nuevos atributos [JsonDerivedType] y [JsonPolymorphic]. Veamos cómo utilizarlos.

Podemos aplicar [JsonDerivedType] a una clase o interfaz para especificar que uno de sus subtipos debe ser serializado de forma polimórfica. Siguiendo con el ejemplo del post anterior, donde teníamos una clase VideoFile que heredaba de File, habría que hacerlo de esta forma:

[JsonDerivedType(typeof(VideoFile))]
public class File
{
    public string FileName { get; set; }
    public ulong SizeBytes { get; set; }
}

public class VideoFile : File
{
    public string Codec { get; set; }
    public TimeSpan Duration { get; set; }
}

De esta forma, el resultado de la siguiente serialización sería correcto:

string Serialize(File file) => JsonSerializer.Serialize(file);

var videoFile = new VideoFile
{
    FileName = "video.mp4",
    SizeBytes = 1024 * 1024,
    Codec = "H264",
    Duration = TimeSpan.FromMinutes(3),
};
Console.WriteLine(Serialize(videoFile));

En el código de arriba, fijaos que aunque la función Serialize() recibe un objeto de tipo File, al serializarlo se está teniendo en cuenta la clase real que está llegando en tiempo de ejecución (VideoFile) y, por tanto, el resultado incluirá sus propiedades:

{"Codec":"H264","Duration":"00:03:00","FileName":"video.mp4","SizeBytes":1048576}

Igual que ocurría en .NET 6 y anteriores, la serialización polimórfica se aplica sólo al objeto raíz, pero no a sus propiedades. Por ejemplo, en el siguiente código, el resultado de la serialización on incluiría la propiedad LastPlayed, que es específica de la clase VideoProperties, dado que el miembro Properties es del tipo base FileProperties:

string Serialize(File file)
    => JsonSerializer.Serialize(file);

var videoFile = new VideoFile
{
    FileName = "video.mp4",
    SizeBytes = 1024 * 1024,
    Codec = "H264",
    Duration = TimeSpan.FromMinutes(3),
    Properties = new VideoProperties()
    {
        Flags = 0x01,
        LastModified = new DateTime(2020, 8, 4, 23, 12, 23),
        LastPlayed = new DateTime(2022, 11, 21, 14, 10, 0),
    }
};
Console.WriteLine(Serialize(videoFile));

// Definición de tipos:

[JsonDerivedType(typeof(VideoFile))]
public class File
{
    public string FileName { get; set; }
    public ulong SizeBytes { get; set; }
    public FileProperties Properties { get; set; } // Definido más abajo
}

public class VideoFile : File
{
    public string Codec { get; set; }
    public TimeSpan Duration { get; set; }
}

public class FileProperties
{
    public uint Flags { get; set; }
    public DateTime LastModified { get; set; }
}

public class VideoProperties : FileProperties
{
    public DateTime LastPlayed { get; set; }
}

La salida de este código sería:

{"Codec":"H264","Duration":"00:03:00","FileName":"video.mp4",
 "SizeBytes":1048576,"Properties":{"Flags":1,"LastModified":"2020-08-04T23:12:23"}}

Para solucionarlo, basta con aplicar el atributo [JsonDerivedType] también en la clase FileProperties para indicar cuáles de su subtipos pueden ser serializados polimórficamente:

[JsonDerivedType(typeof(VideoProperties))]
public class FileProperties
{
    ...
}

Y el resultado ya incluiría las propiedades de VideoProperties:

{"Codec":"H264","Duration":"00:03:00","FileName":"video.mp4",
 "SizeBytes":1048576,"Properties":{"LastPlayed":"2022-11-21T14:10:00",
 "Flags":1,"LastModified":"2020-08-04T23:12:23"}}

El atributo [JsonDerivedType] también puede ser utilizado para generar en el JSON de salida metadatos que permitirán luego realizar la deserialización polimórfica. En la práctica, esto consiste en la inserción de propiedades adicionales en el JSON que ayuden a identificar el tipo específico que fue serializado.

Por ejemplo, volvamos a los tipos File y VideoFile, pero esta vez vamos a insertar dos atributos [JsonDerivedType] para indicar el identificador o discriminador a usar en cada uno de ellos:

[JsonDerivedType(typeof(File), typeDiscriminator: nameof(File))]
[JsonDerivedType(typeof(VideoFile), typeDiscriminator: nameof(VideoFile))]
public class File
{
    public string FileName { get; set; }
    public ulong SizeBytes { get; set; }
}

public class VideoFile : File
{
    public string Codec { get; set; }
    public TimeSpan Duration { get; set; }
}

Y, si ahora creamos objetos de cada uno de los tipos y los serializamos polimórficamente, el resultado será el mostrado justo después:

string Serialize(File file) => JsonSerializer.Serialize(file);

var file = new File()
{
    FileName = "Test.txt",
    SizeBytes = 1024
};

var videoFile = new VideoFile
{
    FileName = "video.mp4",
    SizeBytes = 1024 * 1024,
    Codec = "H264",
    Duration = TimeSpan.FromMinutes(3)
};
Console.WriteLine(Serialize(file));
Console.WriteLine(Serialize(videoFile));
{"$type":"File","FileName":"Test.txt","SizeBytes":1024}
{"$type":"VideoFile","Codec":"H264","Duration":"00:03:00","FileName":"video.mp4",
 "SizeBytes":1048576}

Fijaos qué interesante: en el JSON se ha incluido una propiedad llamada $type con el valor del discriminador que hemos establecido en cada caso, por lo que más adelante, a la hora de deserializar los objetos, el framework podrá saber exactamente qué tipos de objeto debe crear en cada caso.

string Serialize(File file) => JsonSerializer.Serialize(file);

var serializedVideoFile = Serialize(videoFile);
// {"$type":"VideoFile","Codec":"H264","Duration":"00:03:00",
//  "FileName":"video.mp4","SizeBytes":1048576}

var newVideoFile = JsonSerializer.Deserialize<File>(serializedVideoFile);
Console.WriteLine("Type: " + newVideoFile.GetType()); // Type: VideoFile

Por último, es bueno saber que el atributo [JsonPolymorphic] permite personalizar estos comportamientos, definiendo, por ejemplo, el nombre de la propiedad usada como discriminador (por defecto $type) o cómo debe actuar en caso de que al deserializar se detecte un discriminador no reconocido, o un subtipo desconocido a la hora de serializar.

[JsonPolymorphic(
    TypeDiscriminatorPropertyName = "__type",
    UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType, 
    IgnoreUnrecognizedTypeDiscriminators = true
)]
[JsonDerivedType(typeof(File), typeDiscriminator: nameof(File))]
[JsonDerivedType(typeof(VideoFile), typeDiscriminator: nameof(VideoFile))]
public class File
{
    ...
}

Publicado en Variable not found.

Aún no hay comentarios, ¡sé el primero!