Mapping inner collection property individually instead of retrieving the whole collection property with MongoDB driver and Automapper - C# .Net

I’ve been having some trouble to get a collection property to be projected property by property instead of the whole field from the bson document in the MongoDB.

Here’s an example of a document in the Evt collection:

	[
		{
			"Inner": {
				"RootName" : "FooBar",
				"RootExtra" : "abcdefghijlmnopqrstuvwxyz",
				"Items": [
					{
						"Val" : 1,
						"Name" : "Foo",
						"ExtraProperty" : "abcdefghijlmnopqrstuvwxyz",
						"InnerItems" : [
							{
								"InnerVal" : "Importante thing",
								"InnerExtra" : "Unimportant thing"
							}
						]
					},
					{
						"Val" : 2,
						"Name" : "Bar",
						"ExtraProperty" : "abcdefghijlmnopqrstuvwxyz",
						"InnerItems" : [
							{
								"InnerVal" : "Importante thing",
								"InnerExtra" : "Unimportant thing"
							}
						]
					}
				],
				"Singular" : {
					"Prop1" : "123ABC",
					"Prop2" : "ABC!@#",
					"Prop3" : "Unwanted"
				}
			}
		}
	]

And here’s my class model in C#. It is important to mention that my model is used only with a reading purpose, that is to say that the writing is done by another application. Also, the documents are denormalized, therefore, my model contains only the properties that I’m absolutely sure that will be where I’ve mapped them.

[DisplayName("Evt")]
public class Evt1
{
    public InnerEvt1 Inner { get; set; }
}

public class InnerEvt1
{
    public List<EvtItem1> Items { get; set; }
    public string RootName { get; set; }
    public Singular Singular { get; set; }
}

public class EvtItem1
{
    public int Val { get; set; }
    public int Name { get; set; }
    public List<InnerItem> InnerItems { get; set; }
}

public class InnerItem
{
    public int InnerVal { get; set; }
    public string InnerName { get; set; }
}

public class Singular
{
    public string Prop1 { get; set; }
    public string Prop2 { get; set; }
}

As you can see, there are properties that I’ve left off from my model (e.g. RootExtra, InnerExtra, ExtraProperty). All of this (document example and model) are merely examples, for in my actual situation, there are numerous and heavy properties that I’ve left out of my model, which is to say that I do not want them to be loaded in my query.

If I simply retrieve the whole collection using

database.GetCollection<Evt1>("Evt").AsQueryable()

my projection will look something like this:

databaseName.SchemaName.Evt.Aggregate(
	[
		{
			"$match": {
				"Disabled": {
					"$ne": true
				}
			}
		},
		{
			"$project": {
				"_id": "$_id",
				"Evt": "$Evt",
			}
		}
	]
)

That means that the whole document would be loaded into memory before my model is constructed and discard the remaining/extra properties. That would be very bad in performance.

So, I use Automapper to create the projection linq/query for me:

CreateMap<Evt1, Evt1>();
CreateMap<InnerEvt1, InnerEvt1>();
CreateMap<EvtItem1, EvtItem1>();
CreateMap<InnerItem, InnerItem>();
CreateMap<Singular, Singular>();

And the query itself looks something like this:

database.GetCollection<Evt1>("Evt").AsQueryable().ProjectTo<Evt1>(mapper.ConfigurationProvider);

Which makes my projection to look like this:

[
	{
		"$match": {
			"Disabled": {
				"$ne": true
			}
		}
	},
	{
		"$project": {
			"_id": "$_id",
			"Inner": {
				"$cond": {
					"if": {
						"$eq": [
							"$Inner",
							null
						]
					},
					"then": null,
					"else": {
						"Items": "$Evt.Items",
						"_id": "$Evt._id",
						"RootName": "$Evt.RootName",
						"Singular": {
							"$cond": {
								"if": {
									"$eq": [
										"$Evt.Singular",
										null
									]
								},
								"then": null,
								"else": {
									"Prop1": "$Evt.Singular.Prop1",
									"Prop2": "$Evt.Singular.Prop2"
								}
							}
						}
					}
				}
			}
		}
	}
]

The projection worked as expected concerning the value type properties in the class Inner (RootName) and the singular reference type properties (Singular), in other words, projected its individual values, instead of retrieving the whole block, and so, not getting the unwanted values from the database.

The problem is the behavior of the collection property (Items), that have been projected as a block, even though its maps have been added.

I’ve tried several alternatives, including:

  1. mapping List<EvtItem1> to List<EvtItem1>
  2. using ForMember(a => a.Items, opts.MapFrom(a.Items))
  3. creating a custom constructor in EvtItem, so each property is initialized individually
  4. creating a second model, identical to the first, so it can be mapped to

The only option that worked was the forth. Here’s the duplicated model:

[DisplayName("Evt")]
public class Evt2
{
    public InnerEvt2 Inner { get; set; }
}

public class InnerEvt2
{
    public List<EvtItem2> Items { get; set; }
    public string RootName { get; set; }
    public Singular Singular { get; set; }
}

public class EvtItem2
{
    public int Val { get; set; }
    public int Name { get; set; }
    public List<InnerItem> InnerItems { get; set; }
}

The maps:

CreateMap<EvtBasesTrab, EvtBasesTrab2>();
CreateMap<EvtBasesTrabRoot, EvtBasesTrabRoot2>();
CreateMap<InfoCp, InfoCp2>();

The query:

database.GetCollection<Evt1>("Evt").AsQueryable().ProjectTo<Evt2>(mapper.ConfigurationProvider);

And the projection:

[
	{
		"$match": {
			"Disabled": {
				"$ne": true
			}
		}
	},
	{
		"$project": {
			"_id": "$_id",
			"Inner": {
				"$cond": {
					"if": {
						"$eq": [
							"$Inner",
							null
						]
					},
					"then": null,
					"else": {
						"Items": {
							"$map": {
								"input": "$Inner.Items",
								"as": "dtoItems",
								"in": {
									"Val": "$$dtoItems.Val",
									"Name": "$$dtoItems.Name",
									"InnerItems": "$$dtoItems.InnerItems"
								}
							}
						},
						"_id": "$Evt._id",
						"RootName": "$Evt.RootName",
						"Singular": {
							"$cond": {
								"if": {
									"$eq": [
										"$Evt.Singular",
										null
									]
								},
								"then": null,
								"else": {
									"Prop1": "$Evt.Singular.Prop1",
									"Prop2": "$Evt.Singular.Prop2"
								}
							}
						}
					}
				}
			}
		}
	}
]

The main difference is that the Items property had its properties mapped individually, which is what I intended. The problem is that in the real world, there are way too many classes to be duplicated, so this solution is not viable.

Does anyone have a better solution?