{"id":1475,"date":"2019-10-22T10:00:31","date_gmt":"2019-10-22T08:00:31","guid":{"rendered":"http:\/\/tpodolak.com\/blog\/?p=1475"},"modified":"2019-10-20T22:19:37","modified_gmt":"2019-10-20T20:19:37","slug":"mongodb-driver-class-based-server-side-projection","status":"publish","type":"post","link":"https:\/\/tpodolak.com\/blog\/2019\/10\/22\/mongodb-driver-class-based-server-side-projection\/","title":{"rendered":"MongoDB.Driver &#8211; class-based server side projection"},"content":{"rendered":"<h3> 1. Introduction <\/h3>\n<p>When working with <i>NoSQL<\/i> databases, your documents might be quite heavy and in some cases, you would like to get only a slice of original data. For instance, let&#8217;s assume we have an <i>Account<\/i> document which among other things contains a list of transactions<\/p>\n<pre lagn=\"csharp\">\r\npublic class Account : IAccountDefinition\r\n{\r\n    public ObjectId Id { get; set; }\r\n\r\n    public string Name { get; set; }\r\n\r\n    public List<Transaction> Transactions { get; set; }\r\n}\r\n\r\npublic class Transaction\r\n{\r\n    public ObjectId Id { get; set; }\r\n        \r\n    public decimal Amount { get; set; }\r\n        \r\n    public DateTime CreatedAt { get; set; }\r\n        \r\n    public DateTime ModifiedAt { get; set; }\r\n}\r\n<\/pre>\n<p>As there might be hundreds of transaction in the account object, you might want to occasionally work on a subset of original data, say for instance <i>AccountSlim<\/i>  object to improve the performance<\/p>\n<pre lang=\"csharp\">\r\npublic class AccountSlim : IAccountDefinition\r\n{\r\n    public ObjectId Id { get; set; }\r\n        \r\n        public string Name { get; set; }\r\n    }\r\n<\/pre>\n<h3>2.Exploring existing options <\/h3>\n<p><i>MongoDB.Driver<\/i> has a couple of ways of defining projections so as you can operate on a slimmer object instead of a default one. For instance<\/p>\n<pre lang=\"csharp\">\r\nList<AccountSlim> accountSlims = await accountsCollection.Find(defaultAccountFilterDefinition)\r\n                .Project(Builders<Account>.Projection.As<AccountSlim>())\r\n                .ToListAsync();\r\n<\/pre>\n<pre lang=\"csharp\">\r\nIAsyncCursor<AccountSlim> asyncCursor = await accountsCollection.FindAsync(defaultAccountFilterDefinition,\r\n                new FindOptions<Account, AccountSlim>());\r\n<\/pre>\n<p>Unfortunately, those are a client-side projection. This means that the entire object is returned from the database and we just serialized it to a different class. You can check it on your own by examining request and result commands sent to <i>Mongo<\/i><\/p>\n<pre lang=\"csharp\">\r\nvar clientSettings = MongoClientSettings.FromUrl(new MongoUrl(\"mongodb:\/\/localhost:27017\")); \r\nclientSettings.ClusterConfigurator += builder => \r\n{ \r\n    builder.Subscribe<CommandStartedEvent>(OnCommandStarted); \r\n    builder.Subscribe<CommandSucceededEvent>(OnCommandSucceeded); \r\n}; \r\nvar client = new MongoClient(clientSettings); \r\n\r\nprivate static void OnCommandSucceeded(CommandSucceededEvent @event) \r\n{ \r\n    if (@event.CommandName == FindCommandName) \r\n    { \r\n        Console.WriteLine(\"Returned document:\"); \r\n        PrintDocument(@event.Reply); \r\n    } \r\n} \r\n \r\nprivate static void OnCommandStarted(CommandStartedEvent @event) \r\n{ \r\n    if (@event.CommandName == FindCommandName) \r\n    { \r\n        Console.WriteLine(\"Requested document:\"); \r\n        PrintDocument(@event.Command); \r\n    } \r\n} \r\n<\/pre>\n<p>In both cases requested query doesn&#8217;t contain &#8220;projection&#8221; section, so entire document is returned <\/p>\n<pre lang=\"javascript\">\r\n\/\/ query \r\n{ \r\n\r\n  \"find\" : \"Accounts\", \r\n\r\n  \"filter\" : { \r\n\r\n    \"_id\" : ObjectId(\"5dac6ab1ea342a254e1b7748\") \r\n\r\n  }, \r\n\r\n  \"$db\" : \"Accounting\", \r\n\r\n  \"lsid\" : { \r\n\r\n    \"id\" : CSUUID(\"c6b40361-c47f-4fe6-99bd-6a9ec1702576\") \r\n\r\n  } \r\n\r\n} \r\n\r\n\/\/ returned document \r\n\r\n{ \r\n\r\n  \"cursor\" : { \r\n\r\n    \"firstBatch\" : [{ \r\n\r\n        \"_id\" : ObjectId(\"5dac6ab1ea342a254e1b7748\"), \r\n\r\n        \"AccountName\" : \"WRKYDWKKJY\", \r\n\r\n        \"Transactions\" : [{ \r\n\r\n            \"_id\" : ObjectId(\"5dac6ab1ea342a254e1b7749\"), \r\n\r\n            \"Amount\" : \"413648280\", \r\n\r\n            \"CreatedAt\" : ISODate(\"2019-10-20T14:09:53.635Z\"), \r\n\r\n            \"ModifiedAt\" : ISODate(\"2019-10-20T14:09:53.635Z\") \r\n\r\n          }] \r\n\r\n      }], \r\n\r\n    \"id\" : NumberLong(0), \r\n\r\n    \"ns\" : \"Accounting.Accounts\" \r\n\r\n  }, \r\n\r\n  \"ok\" : 1.0 \r\n\r\n} \r\n<\/pre>\n<p>Of course, there is a possibility to manually create a server-side projection, for instance<\/p>\n<pre lang=\"csharp\">\r\nvar accountSlims = await accountsCollection.Find(defaultAccountFilterDefinition)\r\n                .Project(new ObjectProjectionDefinition<Account, AccountSlim>(new { id = 1, name = 1 }))\r\n                .ToListAsync();\r\n<\/pre>\n<p>or with a strongly typed version<\/p>\n<pre lang=\"csharp\">\r\nvar bsonDocument = Builders<Account>.Projection.Exclude(x => x.Transactions)\r\n                .Render(accountsCollection.DocumentSerializer, accountsCollection.Settings.SerializerRegistry);\r\n\r\nList<AccountSlim> accountSlim = await accountsCollection.Find(defaultAccountFilterDefinition)\r\n                .Project(new BsonDocumentProjectionDefinition<Account, AccountSlim>(bsonDocument))\r\n                .ToListAsync();\r\n<\/pre>\n<p>However, in my opinion, this is error-prone and it would be better to generate server-side projection automatically based on properties in a slim object. As I didn&#8217;t find anything like that in the official driver, here is my approach for handling this<\/p>\n<h3>3. Class-based server-side projection<\/h3>\n<p>In order to create a custom projection, all we have to do is to extend <i>ProjectionDefinition&lt;TSource, TResult&gt;<\/i> class and provide a <i>RenderedProjectionDefinition<\/i> with all properties which are in both &#8220;heavy&#8221; and &#8220;slim&#8221; object<\/p>\n<pre lang=\"csharp\">\r\nusing System.Collections.Generic;\r\nusing System.Collections.ObjectModel;\r\nusing System.Linq;\r\nusing System.Reflection;\r\nusing MongoDB.Bson.Serialization;\r\nusing MongoDB.Driver;\r\n\r\nnamespace MongoDBServerSideProjection.Projections\r\n{\r\n    public class ServerSideProjectionDefinition<TSource, TResult> : ProjectionDefinition<TSource, TResult>\r\n    {\r\n        private readonly ProjectionDefinition<TSource> _projectionDefinition;\r\n\r\n        public ServerSideProjectionDefinition()\r\n        {\r\n            _projectionDefinition = BuildProjectionDefinition();\r\n        }\r\n\r\n        public override RenderedProjectionDefinition<TResult> Render(IBsonSerializer<TSource> sourceSerializer, IBsonSerializerRegistry serializerRegistry)\r\n        {\r\n            var bsonDocument = _projectionDefinition.Render(sourceSerializer, serializerRegistry);\r\n\r\n            return new RenderedProjectionDefinition<TResult>(bsonDocument, serializerRegistry.GetSerializer<TResult>());\r\n        }\r\n\r\n        private static ProjectionDefinition<TSource> BuildProjectionDefinition()\r\n        {\r\n            var sourceProperties = ProjectionPropertyCache<TSource>.PropertyNames;\r\n            var resultProperties = ProjectionPropertyCache<TResult>.PropertyNames;\r\n\r\n            var projectionDefinitionBuilder = Builders<TSource>.Projection;\r\n\r\n            var projectionDefinitions = resultProperties.Intersect(sourceProperties).Select(\r\n                name => projectionDefinitionBuilder.Include(new StringFieldDefinition<TSource>(name)));\r\n\r\n            return projectionDefinitionBuilder.Combine(projectionDefinitions);\r\n        }\r\n\r\n        private class ProjectionPropertyCache<T>\r\n        {\r\n            static ProjectionPropertyCache()\r\n            {\r\n                PropertyNames = GetProperties();\r\n            }\r\n\r\n            public static IReadOnlyCollection<string> PropertyNames { get; }\r\n\r\n            private static ReadOnlyCollection<string> GetProperties()\r\n            {\r\n                return typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance)\r\n                    .Where(prop => !prop.IsSpecialName).Select(prop => prop.Name).ToList().AsReadOnly();\r\n            }\r\n        }\r\n    }\r\n}\r\n<\/pre>\n<p>As you can see we use <i>MongoDB.Driver<\/i> build-in projections to render our custom projection consisting of necessary properties. Note, that as we are using <i>StringFieldDefinition<\/i> instead of defining <i>Bson<\/i> document manually, the projection will take into account potential class mappings or attribute mappings applied to your object <\/p>\n<pre lang=\"csharp\">\r\nBsonClassMap.RegisterClassMap<Account>(cm =>\r\n            {\r\n                cm.AutoMap();\r\n                cm.MapProperty(account => account.Name).SetElementName(\"AccountName\");\r\n            });\r\n<\/pre>\n<p>Having the projection ready we can make it a bit easier to use by introducing some extensions. The first one looks as follows<\/p>\n<pre lang=\"csharp\">\r\nusing MongoDB.Driver;\r\nusing MongoDBServerSideProjection.Projections;\r\n\r\nnamespace MongoDBServerSideProjection.Extensions\r\n{\r\n    public static class ProjectionDefinitionBuilderExtensions\r\n    {\r\n        public static ServerSideProjectionDefinition<TSource, TResult> ServerSide<TSource, TResult>(this ProjectionDefinitionBuilder<TSource> builder)\r\n        {\r\n            return new ServerSideProjectionDefinition<TSource, TResult>();\r\n        }\r\n    }\r\n}\r\n<\/pre>\n<p>which allows you to use this projection similarly like others \u2013 so by accessing <i>Builders<\/i> class<\/p>\n<pre lang=\"csharp\">\r\nvar projection = Builders<Account>.Projection.ServerSide<Account, AccountSlim>();\r\nList<AccountSlim> slims = await accountsCollection.Find(defaultAccountFilterDefinition)\r\n                .Project(projection)\r\n                .ToListAsync();\r\n<\/pre>\n<p>The second method will extend <i>IFindFluent&lt;TDocument, TProjection&gt;<\/i> interface<\/p>\n<pre lang=\"csharp\">\r\nusing MongoDB.Driver;\r\n\r\nnamespace MongoDBServerSideProjection.Extensions\r\n{\r\n    public static class FindFluentExtensions\r\n    {\r\n        public static IFindFluent<TSource, TResult> ProjectTo<TSource, TResult>(this IFindFluent<TSource, TSource> findFluent)\r\n        {\r\n            var serverSideProjectionDefinition = Builders<TSource>.Projection.ServerSide<TSource, TResult>();\r\n            return findFluent.Project(serverSideProjectionDefinition);\r\n        }\r\n    }\r\n}\r\n<\/pre>\n<p>and thanks to it we will end up with an even better syntax<\/p>\n<pre lang=\"csharp\">\r\nList<AccountSlim> list = await accountsCollection.Find(defaultAccountFilterDefinition)\r\n                .ProjectTo<Account, AccountSlim>()\r\n                .ToListAsync();\r\n<\/pre>\n<p>One way or another we will end up with proper projection definition which will result in a smaller document returned from the database<\/p>\n<pre lang=\"javascript\">\r\n\/\/ query\r\n{ \r\n\r\n  \"find\" : \"Accounts\", \r\n\r\n  \"filter\" : { \r\n\r\n    \"_id\" : ObjectId(\"5dac846eea29403cb83f99bb\") \r\n\r\n  }, \r\n\r\n  \"projection\" : { \r\n\r\n    \"_id\" : 1, \r\n\r\n    \"AccountName\" : 1 \r\n\r\n  }, \r\n\r\n  \"$db\" : \"Accounting\", \r\n\r\n  \"lsid\" : { \r\n\r\n    \"id\" : CSUUID(\"cf5d5a88-2598-4110-9295-6417d1a5f7dd\") \r\n\r\n  } \r\n\r\n} \r\n\r\n\/\/ returned document\r\n\r\n{ \r\n\r\n  \"cursor\" : { \r\n\r\n    \"firstBatch\" : [{ \r\n\r\n        \"_id\" : ObjectId(\"5dac846eea29403cb83f99bb\"), \r\n\r\n        \"AccountName\" : \"CBBRRWQVKJ\" \r\n\r\n      }], \r\n\r\n    \"id\" : NumberLong(0), \r\n\r\n    \"ns\" : \"Accounting.Accounts\" \r\n\r\n  }, \r\n\r\n  \"ok\" : 1.0 \r\n}\r\n<\/pre>\n<p>Source code for this post can be found <a href=\"https:\/\/github.com\/tpodolak\/Blog\/tree\/master\/MongoDBServerSideProjection\">here<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>1. Introduction When working with NoSQL databases, your documents might be quite heavy and in some cases, you would like to get only a slice of original data. For instance, let&#8217;s assume we have an Account document which among other things contains a list of transactions public class Account : IAccountDefinition { public ObjectId Id [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[337],"tags":[338,339],"class_list":["post-1475","post","type-post","status-publish","format-standard","hentry","category-mongodb","tag-mongodb","tag-nosql"],"_links":{"self":[{"href":"https:\/\/tpodolak.com\/blog\/wp-json\/wp\/v2\/posts\/1475","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tpodolak.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tpodolak.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tpodolak.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/tpodolak.com\/blog\/wp-json\/wp\/v2\/comments?post=1475"}],"version-history":[{"count":10,"href":"https:\/\/tpodolak.com\/blog\/wp-json\/wp\/v2\/posts\/1475\/revisions"}],"predecessor-version":[{"id":1485,"href":"https:\/\/tpodolak.com\/blog\/wp-json\/wp\/v2\/posts\/1475\/revisions\/1485"}],"wp:attachment":[{"href":"https:\/\/tpodolak.com\/blog\/wp-json\/wp\/v2\/media?parent=1475"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tpodolak.com\/blog\/wp-json\/wp\/v2\/categories?post=1475"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tpodolak.com\/blog\/wp-json\/wp\/v2\/tags?post=1475"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}