Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# MongoDB: How to correctly map a domain object?

I recently started reading Evans' Domain-Driven design book and started a small sample project to get some experience in DDD. At the same time I wanted to learn more about MongoDB and started to replace my SQL EF4 repositories with MongoDB and the latest official C# driver. Now this question is about MongoDB mapping. I see that it is pretty easy to map simple objects with public getters and setters - no pain there. But I have difficulties mapping domain entities without public setters. As I learnt, the only really clean approach to construct a valid entity is to pass the required parameters into the constructor. Consider the following example:

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

Obviously automapping does not work here. Now I can map those three classes by hand via BsonClassMaps and they will be stored just fine. The problem is, when I want to load them from the DB I have to load them as BsonDocuments, and parse them into my domain object. I tried lots of things but ultimately failed to get a clean solution. Do I really have to produce DTOs with public getters/setters for MongoDB and map those over to my domain objects? Maybe someone can give me some advice on this.

like image 646
hoetz Avatar asked Apr 21 '11 12:04

hoetz


2 Answers

It is possible to serialize/deserialize classes where the properties are read-only. If you are trying to keep your domain objects persistance ignorant, you won't want to use BsonAttributes to guide the serialization, and as you pointed out AutoMapping requires read/write properties, so you would have to register the class maps yourself. For example, the 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; } } } 

Can be mapped using the following initialization code:

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

Note that the private fields cannot be readonly. Note also that deserialization bypasses your constructor and directly initializes the private fields (.NET serialization works this way also).

Here's a full sample program that tests this:

http://www.pastie.org/1822994

like image 191
Robert Stam Avatar answered Sep 23 '22 03:09

Robert Stam


I'd go with parsing the BSON documents and move the parsing logic to a factory.

First define a factory base class, which contains a builder class. The builder class will act as the DTO, but with additional validation of the values before constructing the domain object.

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

Now, create a factory subclass in your repository. This factory will construct domain objects from the BSON documents.

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

The advantages of this approach:

  • The builder is responsible for building the domain object. This allows you to move some trivial validation out of the domain object, especially if the domain object doesn't expose any public constructors.
  • The factory is responsible for parsing the source data.
  • The domain object can focus on business rules. It's not bothered with parsing or trivial validation.
  • The abstract factory class defines a generic contract, which can be implemented for each type of source data you need. For example, if you need to interface with a web service that returns XML, you just create a new factory subclass:

    public class TransportWebServiceWrapper {     private class TransportFactory : TransportFactory<XDocument>     {         protected override Transport Create(XDocument source, TransportBuilder builder)         {             // Construct domain object from XML.         }     } } 
  • The parsing logic of the source data is close to where the data originates, i.e. the parsing of BSON documents is in the repository, the parsing of XML is in the web service wrapper. This keeps related logic grouped together.

Some disadvantages:

  • I haven't tried this approach in large and complex projects yet, only in small-scale projects. There may be some difficulties in some scenarios I haven't encountered yet.
  • It's quite some code for something seemingly simple. Especially the builders can grow quite large. You can reduce the amount of code in the builders by converting all the WithXxx() methods to simple properties.
like image 24
Niels van der Rest Avatar answered Sep 23 '22 03:09

Niels van der Rest