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

2019-03-08 11:26发布

问题:

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.

回答1:

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



回答2:

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.


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


回答4:

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)



回答5:

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.