Una de las características que nos proporciona el driver de C# para MongoDB, es la serialización de documentos. De esta manera, y ayudándonos de genéricos, podemos convertir documentos de MongoDB en objetos de C#, que podemos utilizar directamente nuestras aplicaciones. Es lo que se conoce como mapeado de objetos o clases.
Aunque MongoDB hace estas operaciones de forma automática, hay veces en las que es necesario personalizar el proceso para ajustarlo a nuestras necesidades. Por ejemplo nos podemos encontrar un documento no tiene los mismos campos que su correspondiente clase, que los nombres varían o que los tipos de datos son diferentes.
Así que en esta entrada, vamos a ver cómo podemos personalizar este proceso para adaptarse a nuestras necesidades.
Mapeado automático
Como ya he comentado, el driver de MongoDB es capaz de mapear campos de forma directa, siempre que no sean documentos demasiado complicados.
{
"Nombre":"Rubén",
"Blog":"CharlasCylon",
"Articulos": 143
}
El documento anterior es un documento JSON que podríamos almacenar en MongoDB. Y una clase POCO que lo represente podría ser de la siguiente manera:
public class Usuario {
public string Nombre {get; set;}
public string Blog {get; set;}
public int Articulos {get; set;}
}
Como veis algo muy sencillo. Para devolver todos los usuarios con una consulta a MongoDB haríamos algo parecido a esto:
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);
}
Pero el código anterior, devuelve una excepción.
Element ’_id’ does not match any field or property of class Usuario.
Como ya sabemos, MongoDB siempre añade un campo _id a todos los documentos, pero este _id no se encuentra en nuestra clase. Y lo mismo pasaría con todos los posibles campos que tenga el documento, pero que no tenga la clase. Pero ¿cómo lo solucionamos?
Usando atributos
El espacio de nombres MongoDB.Bson.Serialization.Attributes nos proporciona atributos útiles para gestionar estas situaciones. Es una forma sencilla de decirle al serializador que debe tratar los datos de forma especial.
Para ver como se usan los atributos, primero que vamos a añadir un campo Id a nuestra clase. En este caso tendríamos dos opciones: que el campo sea del tipo ObjectId o que el campo sea un string que se convierta a ObjectId al almacenarse en la base de datos (y viceversa). Otra opción podría ser que el ObjectId se guardase en MongoDB como string, pero esa opción la vamos a dejar para otro artículo.
En el primer caso, el más sencillo, solo tenemos que modificar nuestra clase, y no es necesario añadir atributos. El serializador es lo suficientemente inteligente como para saber cómo realizar el mapeo. La clase quedaría así:
public class Usuario
{
public ObjectId Id { get; set; }
public string Nombre { get; set; }
public string Blog { get; set; }
public int Articulos { get; set; }
}
En el segundo caso, sí necesitamos usar un atributo. Lo haríamos de la siguiente manera:
public class Usuario
{
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
public string Nombre { get; set; }
public string Blog { get; set; }
public int Articulos { get; set; }
}
En este caso estamos diciendole a MongoDB que cuando guarde un objeto de la clase Usuario, debe guardar el Id como ObjectId. En cambio cuando los datos vengan de la base de datos, se deberá convertir dicho ObjectId en string.
Es importante tener en cuenta que el campo _id que se almacena en MongoDB tiene que ser un ObjectId. Se producirá una excepción si usamos otro valor distinto. Es decir, números enteros, strings que no se puedan convertir a ObjectId, GUIDS etc.
Con la anterior clase este documento se serializaría correctamente:
{
"_id" : ObjectId("543e49b085bef02eef995d18"),
"Nombre" : "Rubén",
"Blog" : "CharlasCylon",
"Articulos" : 143
}
Pero en cambio este otro documento, generará una excepción:
{
"_id" : 22,
"Nombre" : "Rubén",
"Blog" : "CharlasCylon",
"Articulos" : 143
}
De momento hemos solucionado el problema del Id, ¿pero qué pasa cuándo el documento tiene campos que no queremos mapear con nuestra clase? Imaginemos que nuestros documentos son como este:
{
"_id" : ObjectId("543e558f85bef02eef995d19"),
"Nombre" : "Rubén",
"Blog" : "CharlasCylon",
"Articulos" : 143,
"Activo" : true,
"Rol" : "administrator"
}
Si queremos mapear este documento con la clase que hemos usado antes, se produciría un error, ya que los campos Activo y Rol, no tienen correspondencia en la clase. Para solucionar este problema, podemos utilizar los atributos de clase. Por ejemplo, nuestra clase estaría codificada de la siguiente manera:
[BsonIgnoreExtraElements()]
public class Usuario
{
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
public string Nombre { get; set; }
public string Blog { get; set; }
public int Articulos { get; set; }
}
Con el atributo BsonIgnoreExtraElements le estamos diciendo al serializador, que debe ignorar aquellos elementos que no existan en la clase.
Si solo queremos hacer esa operación con alguno de los elementos, podemos usar el atributo BsonIgnoreIfNull, que se utiliza a nivel de propiedad o campo en lugar de a nivel de clase.
El caso contrario también se puede dar, si necesitamos asegurarnos de que un campo existe en el documento. Para ello usaremos el atributo BsonRequired. Por ejemplo:
[BsonIgnoreExtraElements()]
public class Usuario
{
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
public string Nombre { get; set; }
public string Blog { get; set; }
[BsonRequired()]
public int Articulos { get; set; }
}
En esta ocasión estamos ignorando todos los campos que no aparezcan en el documento, salvo el campo Articulos, que debe estar presente para que no se produzca una excepción.
Conclusiones
Los atributos son una herramienta sencilla, que nos permiten controlar de forma más activa la serialización de los documentos. Además de los atributos explicados en este artículo, existen algunos más que nos pueden ayudar en nuestros desarrollos.
Pero los atributos tienen también sus “peros”. Por ejemplo, que** debemos añadirlos para cada entidad o clase** POCO que tengamos, aunque el tratamiento sea el mismo en todas ellas (por ejemplo el tratamiento del Id). Otro problema es que estamos añadiendo elementos característicos de MongoDB a nuestras clases, lo cual puede significar un problema si queremos aislar nuestro modelo de datos de la implementación de la base de datos.
Para solucionar estos problemas, en el próximo artículo veremos como crear mapeos de clases con código C# y qué son las conventions. Aquí nos vemos.
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