Map C# complex data type in mongo

Viewed 48

I have the following class hierarchy

public class Beneficiary
{
    public string Id { get; set; }

    public string Name { get; set; }

    public InvoiceNumber InvoiceNumber { get; set; }

    public ICollection<Invoice> Invoices { get; set; }
}

public class InvoiceNumber
{
    public string Current { get; set; }

    public DateTime IssueDate { get; }
}

public class Invoice
{
    public string Number { get; set; }
    public DateTime IssueDate { get; }

    public ICollection<InvoiceEntry> InvoiceEntries { get; set; }
}

public class InvoiceEntry
{
    public decimal BillableHours { get; set; }
}

Up till now I've used EF to configure relations but I would like to move to mongo (this is just for learning purposes). In entity frameowrk I know how to map this type of hierarchy, but i don't know in mongo.

As it is seen only Beneficiary has an Id attached and generated but the rest is just a dependent hierarchy, so if the beneficiary gets deleted everything else will get deleted.

Can I map such class structure in mongo ? I'm using fluent api for mapping

        BsonClassMap.RegisterClassMap<Beneficiary>(type =>
        {
            type.MapIdProperty(prop => prop.Id)
                .SetIdGenerator(new StringObjectIdGenerator())
                .SetSerializer(new StringSerializer(BsonType.ObjectId));

            type.MapProperty(prop => prop.Name)
                .SetIsRequired(true);

        });

but I'm stuck because I don't know how to continue mapping the complex data types.

1 Answers

TLDR; After having decided on the document structure, you add maps for the classes if you want to customize the way they are serialized as you did for Beneficiary. For the most part, the defaults work well and the driver will (de)serialize your class structure to a document just fine.

If your data model is more complicated, you will have several document types; relationships are not depicted using navigation properties like in Entity Framework, but need dedicated query and update statements.


Before mapping the data, you should decide how to model the data. In this regard, MongoDB offers more options than the relational "normalize-everthing" approach. When it comes to relationships between data types, the following factors come into play:

  • Which type of relationship is it? 1:1 as in Beneficiary <-> InvoiceNumber, 1:n (where n is a few) or 1:z (where z means a lot like zillions)? As regards the relationship Beneficiary <-> Invoices you have to check which of the latter is the case for most of the documents. Including a few invoices in Beneficiary is ok, but storing a huge number of invoices means putting them in a separate collection (or documents at least).
  • Do you query the data together? If you query all the data together in most of the cases, putting all of them in a single document is an option; if you do not query them together or query only a subset of invoices (e.g. the last ten), you can have a look at the Subset pattern where Beneficiary stores only the latest invoices and the others are put into a separate collection.
  • If you load a Beneficiary, do you need all data for the invoices or only a part of it? If for instance you want to show the Beneficiary with an overview of the latest ten invoices, you could opt for a combination of the Subset and Extended reference patterns. This means you store Invoices in a separate collection, but include the most important data (e.g. id, number, date, totals) in the Beneficiary document to be able to fill the overview from a single document.
  • Are there exceptions to the amount of invoices of a Beneficiary? Do most Beneficiaries have 10 invoices, but a few have several thousand? In this case have a look at the Outlier pattern that prepares your documents to store invoices in the Beneficiary document, but adds a flag that signals that more invoices can be found in a different collection.
  • How often do you update the documents? Do you need to update Beneficiary every time you add an invoice?
  • How important are atomic updates to you? If all of the data are in a single document, all updates to this document are atomic whereas you need transactions if consistency is as important as it is in the relational world. I could imagine that InvoiceNumber is updated every time an Invoice is created and that consistency matters in this aspect.
  • Another option is to put Beneficiary and Invoices in separate documents in the same collection. You discern between the document types with a type discriminator (usually the _t property) but you have the advantage to be able to load both the Beneficiary and the corresponding Invoices with a single statement.

After having decided on the structure, mapping the data is usually not too complicated. In comparison to Entity Framework, you do not have to use navigation properties, but only create properties for the classes that are contained in your Beneficiary document. If your class contains subdocuments (like Beneficiary.InvoiceNumber), the driver will store the value as a subdocument. Also, in your current structure, if you have a collection like Beneficiary.Invoices, the driver will create an array when storing the data. Vice versa, when loading a Beneficiary, the properties will be deserialized from the BSON document, so Beneficiary.InvoiceNumber will contain an object of type InvoiceNumber (unless it is null) and Beneficiary.Invoices will contain a collection of Invoice objects, respectively. If you want to customize the way that the other classes are serialized, you create another mapping as you already did for Beneficiary.

However, if you decide to store invoices in separate documents, you do not have the same comfort as in Entity Framework when loading the data (e.g. Include), but have to create separate query and update statement for Invoices (maybe with transactions if - and only if - required).

Related