32

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.

shA.t
  • 16,580
  • 5
  • 54
  • 111
hoetz
  • 2,368
  • 4
  • 26
  • 58

5 Answers5

19

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

Robert Stam
  • 12,039
  • 2
  • 39
  • 36
  • That works! I cant believe I missed this. I guess for not so large scale applications, removing "readonly" from the private fields is an acceptable tradeoff for not having to create an additional layer of DTOs. As you pointed out, one has to be careful to not construct invalid objects by bypassing the ctor. Nevertheless, I'd like to thank Bryan and Niels for their answers as well. All of you made me a bit smarter, thank you. – hoetz Apr 22 '11 at 17:31
  • 3
    Small point: removing the readonly field makes it equivalent to `public ObjectId Id { get; private set; }` auto-property and the field can be removed altogether. – Zaid Masud Sep 12 '12 at 09:58
  • 2
    While this is a possible solution, **it still influences your model**, in the concrete example you gave by requiring fields not to be `readonly`. Every team must decide for themselves whether or not this is acceptable for their project. – theDmi Oct 26 '15 at 16:36
  • @theDmi I've spent hours trying to figure out why my deserialization wasn't working... it was the `readonly` keyword... once I removed, it worked. That's sad... I don't want to create `DTO` classes to serialize/deserialize to Mongo... despite not liking to remove `readonly`, I think it worth instead of creating several DTO classes – JobaDiniz Aug 21 '21 at 19:34
4

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.
Niels van der Rest
  • 31,664
  • 16
  • 80
  • 86
  • Very interesting concept, I guess there's really no other way than to add one layer. In the c# DDD demo application of Evans' book, they used nHibernate and I just love the concept of NOT having to do this. You throw in your unaltered domain objects and you get them back without any additional classes (except the xml mapping of course) – hoetz Apr 21 '11 at 17:31
3

A better approach to handling this now is using MapCreator (which was possibly added after most of these answers were written).

e.g. I have a class called Time with three readonly properties: Hour, Minute and Second. Here's how I get it to store those three values in the database and to construct new Time objects during deserialization.

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);
}
Ian Mercer
  • 38,490
  • 8
  • 97
  • 133
0

Niels has an interesting solution but I propose a much different approach: Simplify your data model.

I say this because you are trying to convert RDBMS style entities to MongoDB and it doesnt map over very well, as you have found.

One of the most important things to think about when using any NoSQL solution is your data model. You need to free your mind of much of what you know about SQL and relationships and think more about embedded documents.

And remember, MongoDB is not the right answer for every problem so try not to force it to be. The examples you are following may work great with standard SQL servers but dont kill yourself trying to figure out how to make them work with MongoDB - they probably dont. Instead, I think a good excercise would be trying to figure out the correct way to model the example data with MongoDB.

Bryan Migliorisi
  • 8,982
  • 4
  • 34
  • 47
  • 5
    But with DDD, I shouldnt worry at all about persistence. I don't design my domain entities with a certain storage technology in mind. – hoetz Apr 22 '11 at 06:37
  • @Malkier: Correct, you shouldn't let persistence influence your domain design. That's why I think MongoDB is actually a very good choice, because its storage model is much more natural. With RDBMS, you have to tear your entities apart in order to store them in a normalized data model. With MongoDB, you can just store an entire aggregate root in a single document, perhaps with a few references to other child entities. – Niels van der Rest Apr 22 '11 at 10:46
  • Couldn't agree more with @Niels. But I think its worth mentioning that while in theory you shouldn't have to worry about your persistence layer, in the real world, its definitely something to think about - alot, especially when considering a NoSQL solution. – Bryan Migliorisi Apr 22 '11 at 14:00
0

Consider NoRM, an open-source ORM for MongoDB in C#.

Here are some links:

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 (download)

Roy Dictus
  • 32,551
  • 8
  • 60
  • 76