Tutorial de MongoDB y C#. Modificando el mapeo de clases con Class Maps y Conventions

En el anterior artículo contaba como podíamos tener un mayor control sobre la serialización de los elementos de un documento. Ayudados por los atributos, podíamos modificar el comportamiento. Así podíamos mapear un documento con una clase aunque los campos fueran de distinto tipo, no existieran o tuvieran nombres diferentes.

Pero como comentaba en el final del artículo, es posible que el uso de atributos no sea la solución más indicada para nosotros. Por ejemplo, puede pasar que no queramos mezclar nuestro modelo de clases de datos con la implementación específica que hace MongoDB. O que no queramos especificar los atributos por cada clase generada.

En este artículo vamos a ver como podemos cambiar el comportamiento de la serialización de objetos sin utilizar atributos.

Personalizando el mapeo de clases

Una de las opciones que tenemos, es cambiar el comportamiento del mapeo de clases. Esta es una operación que deberemos hacer una sola vez en nuestra aplicación, por lo que será conveniente hacerla en algún evento de inicio. Por ejemplo en MVC podemos hacerlo en la clase Startup.

Vamos a suponer que nuestros documentos siguen el siguiente modelo:

{
  "_id" : ObjectId("543e558f85bef02eef995d19"),
  "Nombre" : "Rubén",
  "Blog" : "CharlasCylon",
  "Articulos" : 143,
  "Activo" : true,
  "Rol" : "administrator"
}

Y que nuestra clase POCO tiene el siguiente formato:

public class Usuario
{
    public string Id { get; set; }
    public string Nombre { get; set; }
    public string Blog { get; set; }
    public int Articulos { get; set; }
}

Y que vamos a realizar una consulta desde C# de la siguiente manera:

var client = new MongoClient("mongodb://localhost:27666");
var server = client.GetServer();
var database = server.GetDatabase("test");
var collection = database.GetCollection<usuario>("usuarios");

var lista = collection.FindAllAs<usuario>();

foreach (var item in lista)
{
    Console.WriteLine(item.Nombre);
    Console.WriteLine(item.Blog);
    Console.WriteLine(item.Articulos);
}

Como vimos en el anterior artículo, el código anterior produce varias excepciones. La primera porque un campo ObjectId no se puede convertir a string, y la segunda porque existen campos en el documento, que no están en la clase (Activo y Rol).

Para evitar esas excepciones, debemos modificar el mapeo que hacemos de los campos. Y eso se puede hacer de la siguiente manera:

BsonClassMap.RegisterClassMap<usuario>(cm =>
{
    cm.AutoMap();
    cm.IdMemberMap.SetRepresentation(BsonType.ObjectId);
    cm.SetIgnoreExtraElements(true);
});

La clase BsonClassMap del espacio de nombres MongoDB.Bson.Serialization, se encarga de registrar los mapeos personalizados. En este caso estamos haciendo que para la clase Usuario, se realice el mapeo automático para todos los campos, pero con dos excepciones. En la primera, que indicamos con IdMemberMap.SetRepresentation, estamos diciendo que el campo Id será del tipo ObjectId en base de datos. La segunda excepción la indicamos con SetIgnoreExtraElements, y decimos que se deben ignorar los campos que no tengan correspondencia entre la clase y el documento existente en MongoDB.

Como he comentado antes, este mapeo debemos hacerlo solo una vez, ya que si no se producirá una excepción. El mapeo solo se aplica para las clases de tipo Usuario, ya que el resto se seguirán comportando igual.

Este tipo de mapeo puede tener sentido para cosas muy específicas, pero si tenemos muchas clases, será una pérdida de tiempo aplicar las mismas opciones a todas ellas. Y para solucionar ese problema aparecen las Conventions.

Personalizando el mapeo de clases con Conventions

He intentado buscar una buena traducción para conventions, pero la verdad es que no me convence ninguna. Podríamos decir que las conventions son acuerdos preestablecidos para serializar los documentos de una determinada manera. La idea es parecida a la del ejemplo anterior, pero en este caso se aplicará a distintas clases según un filtro y no solo para las de un tipo determinado. Un ejemplo:

ConventionPack pack = new ConventionPack();
pack.Add(new IgnoreExtraElementsConvention(true));
pack.Add(new StringObjectIdConvention());

ConventionRegistry.Register("My Conventions", pack, t => true);

Al igual que con BsonClassMap, ConventionRegistry hay que usarlo una sola vez en la aplicación. Y además hay que hacerlo en un punto temprano de la ejecución, ya que si se mapea alguna clase antes, nuestras conventions podrían no funcionarán bien.

Si nos fijamos en el código anterior, podemos ver creamos una instancia de ConventionPack y luego utilizamos el método Add para añadir. Una vez tenemos creado nuestro ConventionPack, tendremos que registrarlo. Esto podemos hacerlo con la clase ConventionRegistry, y el método estático Register. Como se puede ver en el ejemplo, este método recibe tres parámetros: un nombre (puede ser cualquier string), el pack de conventions y un filtro para delimitar las clases a las que se aplica. En este caso estamos haciendo un t => true, lo que hace que se aplique a todas las clases. Aunque si quisiéramos podríamos filtrar, por ejemplo, por el nombre del ensamblado.

Si nos fijamos otra vez en las conventions que estamos añadiendo al ConventionPack, podemos ver que estamos añadiendo dos: IgnoreExtraElementsConvention y StringObjectIdConvention. La primera, es una convention que ya tenemos implementada en el espacio de nombres MongoDB.Bson.Serialization.Conventions. Existen varias conventions más que podremos usar en nuestras aplicaciones. La segunda es una convention personalizada. Veamos su código:

public class StringObjectIdConvention : ConventionBase, IPostProcessingConvention
{
    public void PostProcess(BsonClassMap classMap)
    {
        var idMap = classMap.IdMemberMap;

        if (idMap != null && idMap.MemberName == "Id" && idMap.MemberType == typeof(string) )
        {
            idMap.SetRepresentation(BsonType.ObjectId);
            idMap.SetIdGenerator(new StringObjectIdGenerator());
        }
    }
}

Esta convention es la que utilizamos para decir que un Id será de tipo string en las clases, pero que se almacenará como ObjectId en MongoDB. Por eso implementamos la interface IPostProcessingConvention, que nos sirve para realizar operaciones al finalizar la serialización (PostProcess). La serialización se realiza en varios pasos que siguen un orden, y hay definidas distintas interfaces para procesar los elementos en un momento u otro.

Volviendo a nuestra convention personalizada, vemos que tiene solo un método y que en él comprueba si en el mapeo de clase existe un campo Id de tipo string. Si es así, lo que hace es establecer la representación como ObjectId y el generador de Ids como StringObjectGenerator.

Conclusiones

Aunque el driver de C# para MongoDB es capaz de serializar automáticamente muchos de los documentos, en ocasiones es necesario tener un mayor control sobre la operación. Para ello, el driver de C#, nos proporciona de distintos métodos. Tenemos los atributos, los mapeos de clases y las conventions.

En nuestra mano está, usar una u otra alternativa, según nuestras necesidades.



Recuerda que puedes ver el índice del tutorial y acceder a todos los artículos de la serie desde aquí.



¿Quiéres que te avisemos cuando se publiquen nuevas entradas en el blog?

Suscríbete por correo electrónico o por RSS