C#MongoDB:如何正确映射域对象?

我最近开始阅读Evans的Domain-Driven设计书,并开始了一个小样本项目,以获得DDD的一些经验。 同时我想了解更多有关MongoDB的知识,并开始用MongoDB和最新的官方C#驱动程序替换我的SQL EF4存储库。 现在这个问题是关于MongoDB映射的。 我看到用公共getter和setter映射简单对象非常容易 – 没有痛苦。 但是我在没有公共设置者的情况下映射域实体有困难。 据我所知,构建有效实体的唯一真正干净的方法是将所需的参数传递给构造函数。 请考虑以下示例:

public class Transport : IEntity { 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 { 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 { private readonly int numberOfSeats; #region Constr public PersonCapacity(int numberOfSeats) { Validate.NotNull(numberOfSeats); this.numberOfSeats = numberOfSeats; } #endregion public int NumberOfSeats { get { return numberOfSeats; } } } 

显然自动化在这里不起作用。 现在我可以通过BsonClassMaps手动映射这三个类,它们将被存储得很好。 问题是,当我想从DB加载它们时,我必须将它们作为BsonDocuments加载,并将它们解析为我的域对象。 我尝试了很多东西,但最终没能得到一个干净的解决方案。 我真的必须使用MongoDB的公共getter / setter生成DTO并将它们映射到我的域对象吗? 也许有人可以给我一些建议。

可以对属性为只读的类进行序列化/反序列化。 如果你试图让你的域对象持久无知,你不会想要使用BsonAttributes来指导序列化,并且正如你所指出的,AutoMapping需要读/写属性,所以你必须自己注册类映射。 例如,class级:

 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; } } } 

可以使用以下初始化代码进行映射:

 BsonClassMap.RegisterClassMap(cm => { cm.MapIdField("id"); cm.MapField("x"); }); 

请注意,私有字段不能只读。 另请注意,反序列化会绕过构造函数并直接初始化私有字段(.NET序列化也以这种方式工作)。

这是一个测试这个的完整示例程序:

http://www.pastie.org/1822994

我将解析BSON文档并将解析逻辑移至工厂。

首先定义一个工厂基类,它包含一个构建器类。 构建器类将充当DTO,但在构造域对象之前还要对值进行额外validation。

 public class TransportFactory { 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); } } } 

现在,在您的存储库中创建一个工厂子类。 该工厂将从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 { 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(); } } } 

这种方法的优点:

  • 构建器负责构建域对象。 这允许您将一些简单的validation移出域对象,特别是如果域对象不公开任何公共构造函数。
  • 工厂负责解析源数据。
  • 域对象可以专注于业务规则。 它不会受到解析或琐碎validation的困扰。
  • 抽象工厂类定义了一个通用契约,可以为您需要的每种类型的源数据实现该契约。 例如,如果需要与返回XML的Web服务进行交互,则只需创建一个新的工厂子类:

     public class TransportWebServiceWrapper { private class TransportFactory : TransportFactory { protected override Transport Create(XDocument source, TransportBuilder builder) { // Construct domain object from XML. } } } 
  • 源数据的解析逻辑接近数据源自的位置,即BSON文档的解析在存储库中,XML的解析在Web服务包装器中。 这使相关逻辑保持在一起。

一些缺点:

  • 我还没有在大型复杂项目中尝试过这种方法,只是在小规模项目中。 在我尚未遇到的某些情况下可能会遇到一些困难。
  • 对于看似简单的事情来说,这是相当多的代码。 特别是建筑商可以长得很大。 您可以通过将所有WithXxx()方法转换为简单属性来减少构建器中的代码量。

现在处理这个问题的一个更好的方法是使用MapCreator (可能在写完大部分答案后添加)。

例如,我有一个名为Time的类,有三个只读属性: HourMinuteSecond 。 以下是我如何将这三个值存储在数据库中并在反序列化期间构造新的Time对象。

 BsonClassMap.RegisterClassMap 

Niels有一个有趣的解决方案,但我提出了一个非常不同的方法: 简化您的数据模型。

我这样说是因为你正在尝试将RDBMS样式实体转换为MongoDB,并且它没有很好地映射,正如你所发现的那样。

使用任何NoSQL解决方案时要考虑的最重要的事情之一是您的数据模型。 您需要释放您对SQL和关系的大部分知识,并更多地考虑嵌入式文档。

请记住,MongoDB不是每个问题的正确答案,所以尽量不要强迫它。 您所遵循的示例可能适用于标准SQL服务器,但不要试图弄清楚如何使它们与MongoDB一起工作 – 他们可能不会。 相反,我认为一个好的练习将试图找出使用MongoDB建模示例数据的正确方法。