C#MongoDB: ¿Cómo correlacionar correctamente un objeto de dominio?
domain-driven-design dto (5)
Me gustaría analizar los documentos BSON y mover la lógica de análisis a una fábrica.
Primero defina una clase base de fábrica, que contiene una clase de constructor. La clase de constructor actuará como DTO, pero con una validación adicional de los valores antes de construir el objeto de dominio.
public class TransportFactory<TSource>
{
public Transport Create(TSource source)
{
return Create(source, new TransportBuilder());
}
protected abstract Transport Create(TSource source, TransportBuilder builder);
protected class TransportBuilder
{
private TransportId transportId;
private PersonCapacity personCapacity;
internal TransportBuilder()
{
}
public TransportBuilder WithTransportId(TransportId value)
{
this.transportId = value;
return this;
}
public TransportBuilder WithPersonCapacity(PersonCapacity value)
{
this.personCapacity = value;
return this;
}
public Transport Build()
{
// TODO: Validate the builder''s fields before constructing.
return new Transport(this.transportId, this.personCapacity);
}
}
}
Ahora, crea una subclase de fábrica en tu repositorio. Esta fábrica construirá objetos de dominio a partir de los documentos BSON.
public class TransportRepository
{
public Transport GetMostPopularTransport()
{
// Query MongoDB for the BSON document.
BsonDocument transportDocument = mongo.Query(...);
return TransportFactory.Instance.Create(transportDocument);
}
private class TransportFactory : TransportFactory<BsonDocument>
{
public static readonly TransportFactory Instance = new TransportFactory();
protected override Transport Create(BsonDocument source, TransportBuilder builder)
{
return builder
.WithTransportId(new TransportId(source.GetString("transportId")))
.WithPersonCapacity(new PersonCapacity(source.GetInt("personCapacity")))
.Build();
}
}
}
Las ventajas de este enfoque:
- El constructor es responsable de construir el objeto de dominio. Esto le permite mover alguna validación trivial del objeto de dominio, especialmente si el objeto de dominio no expone ningún constructor público.
- La fábrica es responsable de analizar los datos fuente.
- El objeto de dominio puede enfocarse en las reglas de negocios. No le molesta el análisis ni la validación trivial.
La clase abstracta de fábrica define un contrato genérico, que puede implementarse para cada tipo de fuente de datos que necesite. Por ejemplo, si necesita interactuar con un servicio web que devuelve XML, simplemente crea una nueva subclase de fábrica:
public class TransportWebServiceWrapper { private class TransportFactory : TransportFactory<XDocument> { protected override Transport Create(XDocument source, TransportBuilder builder) { // Construct domain object from XML. } } }
La lógica de análisis de los datos fuente está cerca de donde se originan los datos, es decir, el análisis sintáctico de los documentos BSON está en el repositorio, el análisis sintáctico de XML está en el contenedor del servicio web. Esto mantiene la lógica relacionada agrupada.
Algunas desventajas:
- Todavía no he probado este enfoque en proyectos grandes y complejos, solo en proyectos de pequeña escala. Puede haber algunas dificultades en algunos escenarios que aún no he encontrado.
- Es bastante cierto código para algo aparentemente simple. Especialmente los constructores pueden crecer bastante grandes. Puede reducir la cantidad de código en los constructores convirtiendo todos los métodos
WithXxx()
en propiedades simples.
Recientemente comencé a leer el libro de diseño impulsado por el dominio de Evans y comencé un pequeño proyecto de muestra para obtener algo de experiencia en DDD. Al mismo tiempo, quería aprender más sobre MongoDB y comencé a reemplazar mis repositorios de SQL EF4 con MongoDB y el último controlador oficial de C #. Ahora esta pregunta es sobre el mapeo MongoDB. Veo que es bastante fácil mapear objetos simples con captadores y decodificadores públicos, sin dolor. Pero tengo dificultades para mapear entidades de dominio sin setters públicos. Como aprendí, el único enfoque realmente limpio para construir una entidad válida es pasar los parámetros requeridos al constructor. Considere el siguiente ejemplo:
public class Transport : IEntity<Transport>
{
private readonly TransportID transportID;
private readonly PersonCapacity personCapacity;
public Transport(TransportID transportID,PersonCapacity personCapacity)
{
Validate.NotNull(personCapacity, "personCapacity is required");
Validate.NotNull(transportID, "transportID is required");
this.transportID = transportID;
this.personCapacity = personCapacity;
}
public virtual PersonCapacity PersonCapacity
{
get { return personCapacity; }
}
public virtual TransportID TransportID
{
get { return transportID; }
}
}
public class TransportID:IValueObject<TransportID>
{
private readonly string number;
#region Constr
public TransportID(string number)
{
Validate.NotNull(number);
this.number = number;
}
#endregion
public string IdString
{
get { return number; }
}
}
public class PersonCapacity:IValueObject<PersonCapacity>
{
private readonly int numberOfSeats;
#region Constr
public PersonCapacity(int numberOfSeats)
{
Validate.NotNull(numberOfSeats);
this.numberOfSeats = numberOfSeats;
}
#endregion
public int NumberOfSeats
{
get { return numberOfSeats; }
}
}
Obviamente, la automatización no funciona aquí. Ahora puedo mapear esas tres clases a mano a través de BsonClassMaps
y se guardarán muy bien. El problema es que, cuando quiero cargarlos desde el DB, tengo que cargarlos como BsonDocuments
y analizarlos en mi objeto de dominio. Intenté muchas cosas, pero finalmente no conseguí una solución limpia. ¿Realmente tengo que producir DTOs con getters / setters públicos para MongoDB y asignarlos a mis objetos de dominio? Tal vez alguien me puede dar algunos consejos sobre esto.
Considere NoRM, un ORM de código abierto para MongoDB en C #.
Aquí hay algunos enlaces:
http://www.codevoyeur.com/Articles/20/A-NoRM-MongoDB-Repository-Base-Class.aspx
http://lukencode.com/2010/07/09/getting-started-with-mongodb-and-norm/
https://github.com/atheken/NoRM (descargar)
Es posible serializar / deserializar clases donde las propiedades son de solo lectura. Si intenta mantener la persistencia de objetos de su dominio ignorante, no querrá usar BsonAttributes para guiar la serialización, y como señaló que AutoMapping requiere propiedades de lectura / escritura, entonces tendría que registrar los mapas de clase usted mismo. Por ejemplo, la clase:
public class C {
private ObjectId id;
private int x;
public C(ObjectId id, int x) {
this.id = id;
this.x = x;
}
public ObjectId Id { get { return id; } }
public int X { get { return x; } }
}
Puede ser mapeado usando el siguiente código de inicialización:
BsonClassMap.RegisterClassMap<C>(cm => {
cm.MapIdField("id");
cm.MapField("x");
});
Tenga en cuenta que los campos privados no pueden ser de solo lectura. Tenga en cuenta también que la deserialización puentea su constructor e inicializa directamente los campos privados (la serialización .NET también funciona de esta manera).
Aquí hay un programa de muestra completo que prueba esto:
Niels tiene una solución interesante, pero propongo un enfoque muy diferente: Simplifique su modelo de datos.
Digo esto porque estás intentando convertir entidades de estilo RDBMS a MongoDB y no se correlaciona muy bien, como has encontrado.
Una de las cosas más importantes para pensar cuando se usa cualquier solución NoSQL es su modelo de datos. Necesita liberar su mente de gran parte de lo que sabe sobre SQL y las relaciones y pensar más acerca de los documentos integrados.
Y recuerde, MongoDB no es la respuesta correcta para cada problema, así que trate de no forzarlo. Los ejemplos que está siguiendo pueden funcionar muy bien con servidores SQL estándar, pero no te mates tratando de descubrir cómo hacer que funcionen con MongoDB, probablemente no. En cambio, creo que un buen ejercicio sería tratar de encontrar la forma correcta de modelar los datos de ejemplo con MongoDB.
Un mejor enfoque para manejar esto ahora es usar MapCreator
(que posiblemente se agregó después de que se escribieron la mayoría de estas respuestas).
por ejemplo, tengo una clase llamada Time
con tres propiedades de solo lectura: Hour
, Minute
y Second
. Así es como lo obtengo para almacenar esos tres valores en la base de datos y para construir nuevos objetos Time
durante la deserialización.
BsonClassMap.RegisterClassMap<Time>(cm =>
{
cm.AutoMap();
cm.MapCreator(p => new Time(p.Hour, p.Minute, p.Second));
cm.MapProperty(p => p.Hour);
cm.MapProperty(p => p.Minute);
cm.MapProperty(p => p.Second);
}