Skip to content

Repository files navigation

JohnKnoop.MongoRepository

An easy-to-configure extension to the MongoDB driver, adding support for:

✔️ Multi-tenancy
✔️ Simplified transaction handling, including support for TransactionScope
✔️ Soft-deletes

Install via NuGet

Install-Package JohnKnoop.MongoRepository

Configure mappings, indices, multitenancy etc with a few lines of code:

MongoRepository.Configure().Database("HeadOffice", db =>db.Map<Employee>()).DatabasePerTenant("Zoo", db =>db.Map<AnimalKeeper>().Map<Enclosure>("Enclosures").Map<Animal>("Animals", x =>x.WithIdProperty(animal =>animal.NonConventionalId).WithIndex(animal =>animal.Name,unique:true))).Build();

See more options

...then start hacking away

varemployeeRepository=mongoClient.GetRepository<Employee>();varanimalRepository=mongoClient.GetRepository<Animal>(tenantKey);

In the real world you'd typically resolve IRepository<T> through your dependency resolution system. See the section about DI frameworks for more info.

Getting started

Querying

Get by id

awaitrepository.GetAsync("id");awaitrepository.GetAsync<SubType>("id");// With projectionawaitrepository.GetAsync("id", x =>x.TheOnlyPropertyIWant);awaitrepository.GetAsync<SubType>("id", x =>new{x.SomeProperty,x.SomeOtherProperty});

Find by expression

awaitrepository.Find(x =>x.SomeProperty==someValue);awaitrepository.Find<SubType>(x =>x.SomeProperty==someValue);awaitrepository.Find(x =>x.SomeProperty,regexPattern);

Returns an IFindFluent which offers methods like ToListAsync, CountAsync, Project, Skip and Limit

Examples:

vardottedAnimals=awaitrepository.Find(x =>x.Coat=="dotted").Limit(10).Project(x =>x.Species).ToListAsync()

LINQ

repository.Query();repository.Query<SubType>();

Returns an IMongoQueryable which offers async versions of all the standard LINQ methods.

Examples:

vardottedAnimals=awaitrepository.Query().Where(x =>x.Coat=="dotted").Take(10).Select(x =>x.Species).ToListAsync()

Inserting, updating and deleting

InsertAsync, InsertManyAsync

awaitrepository.InsertAsync(someObject);awaitrepository.InsertManyAsync(someCollectionOfObjects);

UpdateOneAsync, UpdateManyAsync

// Update one documentawaitrepository.UpdateOneAsync("id", x =>x.Set(y =>y.SomeProperty,someValue),upsert:true);awaitrepository.UpdateOneAsync(x =>x.SomeProperty==someValue, x =>x.Push(y =>y.SomeCollection,someValue));awaitrepository.UpdateOneAsync<SubType>(x =>x.SomeProperty==someValue, x =>x.Push(y =>y.SomeCollection,someValue));// Update all documents matched by filterawaitrepository.UpdateManyAsync(x =>x.SomeProperty==someValue, x =>x.Inc(y =>y.SomeProperty,5));

UpdateOneBulkAsync

Perform multiple update operations with different filters in one db roundtrip.

awaitrepository.UpdateOneBulkAsync(newList<UpdateOneCommand<MyEntity>>{newUpdateOneCommand<MyEntity>{Filter= x =>x.SomeProperty="foo",Update= x =>x.Set(y =>y.SomeOtherProperty,10)},newUpdateOneCommand<MyEntity>{Filter= x =>x.SomeProperty="bar",Update= x =>x.Set(y =>y.SomeOtherProperty,20)}});

FindOneAndUpdateAsync

This is a really powerful feature of MongoDB, in that it lets you update and retrieve a document atomically.

varentityAfterUpdate=awaitrepository.FindOneAndUpdateAsync(filter: x =>x.SomeProperty.StartsWith("Hello"),update: x =>x.AddToSet(y =>y.SomeCollection,someItem));varentityAfterUpdate=awaitrepository.FindOneAndUpdateAsync(filter: x =>x.SomeProperty.StartsWith("Hello"),update: x =>x.PullFilter(y =>y.SomeCollection, y =>y.SomeOtherProperty==5),returnProjection: x =>new{x.SomeCollection},returnedDocumentState:ReturnedDocumentState.AfterUpdate,upsert:true);

UpdateOrInsertOneAsync

Lets you upsert a document of type T using an instance of type T as default and then apply updates on top of that, in an atomic operation. If the filter is matched, the default instance will not be used, and only the updates will be applied.

The same result can be achieved with common UpdateOne/FindOneAndUpdate using upsert and a bunch of SetOnInserts, but the advantage of UpdateOrInsertOneAsync is you don't have to add a SetOnInsert for each property manually.

Aggregation

repository.Aggregate();repository.Aggregate(options);

Returns an IAggregateFluent which offers methods like AppendStage, Group, Match, Unwind, Out, Lookup etc.

Deleting

awaitrepository.DeleteByIdAsync("id");// orawaitrepository.DeleteManyAsync(x =>x.SomeProperty===someValue);// orvardeleted=awaitrepository.FindOneAndDeleteAsync("id");// orvardeleted=awaitrepository.FindOneAndDeleteAsync<DerivedType>(x =>x.SomeProp==someValue);

Soft-deleting

Soft-deleting an entity will move it to a different collection, preserving type-information.

awaitrepository.DeleteByIdAsync("id",softDelete:true);// orvardeleted=awaitrepository.FindOneAndDeleteAsync("id",softDelete:true);

Listing soft-deleted entities:

awaitrepository.ListTrashAsync();

Restoring one (or many) soft-deleten entities

awaitrepository.RestoreSoftDeletedAsync("id");awaitrepository.RestoreSoftDeletedAsync(x =>x.TimestampDeletedUtc>DateTime.Today);

Permanently delete soft-deleted documents

awaitrepository.PermamentlyDeleteSoftDeletedAsync(x =>x.Foo=="bar");

Transactions

MongoDB 4 introduced support for multi-document transactions. We provide a simplified interface: you don't have to pass around the session object. Instead we detect any ambient transaction and uses it for all write/update/delete operations.:

using(vartransaction=repository.StartTransaction()){// ...awaittransaction.CommitAsync();}

Since version 5 we also support enlisting with a TransactionScope. This is useful to be able to put a transactional boundary around MongoDB operations and anything that is compatible with TransactionScopes.

using(vartransaction=newTransactionScope(TransactionScopeAsyncFlowOption.Enabled)){repository.EnlistWithCurrentTransactionScope();// ...transaction.Complete();}

If you configure the repository with .AutoEnlistWithTransactionScopes() then it will automatically enlist to any ambient TransactionScope without the need to do it explicitly like in the example above.

MongoDB replica sets sometimes encounter transient transaction errors, in which case the recommended course of action from the MongoDB team is to simply retry until it succeeds. We offer a shorthand for this:

// Retry using standard MongoDB transactionawaitrepo.WithTransactionAsync(async()=>{// your code here},maxRetries:3);// Retry using TransactionScopeawaitrepo.WithTransactionAsync(async()=>{// your code here},TransactionType.TransactionScope,maxRetries:3);

RetryAsync also comes with an overload that takes a number representing the max number of retries.

UnionWith

This library provides an extension method to IAggregateFluent<T> called UnionWith that accepts a repository and a projection expression.

usingJohnKnoop.MongoRepository.Extensions;varallContacts=awaitsoccerPlayersRepository.Aggregate().Project(x =>new{PlayerName=x.SoccerPlayerName,TeamName=x.SoccerTeamName}).UnionWith(rugbyPlayersRepository,
x =>new{PlayerName=x.RugbyPlayerName,TeamName=x.RugbyTeamName}).SortBy(x =>x.PlayerName).ToListAsync();

ArrayFilters helpers

Working with ArrayFilters using the MongoDB C# driver is an unpleasant experience in that it doesn't provide any compile-time checking. This library contains a few handy helpers that lets you replace this code:

await_repository.UpdateOneAsync(filter: x =>x.Title=="Game of Thrones",update: x =>x.Set("Seasons.$[a].Episodes.$[b].Title","Qarth"),options:newUpdateOptions{ArrayFilters=newList<ArrayFilterDefinition<Show>>{newBsonDocument("a.Year",newBsonDocument("$ne","2013")),newBsonDocument("b.Number",2),}});

...with this:

await_repository.UpdateOneAsync(filter: x =>x.Title=="Game of Thrones",update: x =>x.Set(ArrayFilters.CreateArrayFilterPath<Show>().SelectEnumerable(x =>x.Seasons,"a").SelectEnumerable(x =>x.Episodes,"b").SelectProperty(x =>x.Title).Build(),"Qarth"),options:newUpdateOptions{ArrayFilters=ArrayFilters.DefineFilters<Show>().AddFilter("a", show =>show.Seasons, f =>f.Eq(x =>x.Year,"2013")).ThenAddFilter("b", season =>season.Episodes, f =>f.Eq(x =>x.Number,2))});

This ensures already at design-time that you haven't misspelled any properties, or that you're using the wrong data type for any values.

Please note that this feature is still experimental.

Advanced features

Counters

Auto-incrementing fields is a feature of most relational databases that unfortunately isn't supported by MongoDB. To get around this, counters are a way to solve the problem of incrementing a number with full concurrency support.

varvalue=awaitrepository.GetCounterValueAsync();varvalue=awaitrepository.GetCounterValueAsync("MyNamedCounter");

Atomically increment and read the value of a counter:

varvalue=awaitrepository.IncrementCounterAsync();// Increment by 1varvalue=awaitrepository.IncrementCounterAsync(name:"MyNamedCounter",incrementBy:5);

Reset a counter:

awaitrepository.ResetCounterAsync();// Reset to 1awaitrepository.ResetCounterAsync(name:"MyNamedCounter",newValue:5);

Deleting properties

Delete a property from a document:

awaitrepository.DeletePropertyAsync(x =>x.SomeProperty==someValue, x =>x.PropertyToRemove);

Configuration

Configuration is done once, when the application is started. Use MongoRepository.Configure() as shown below.

Multi-tenancy

Database-per-tenant style multi-tenancy is supported. When defining a database, just use the DatabasePerTenant method:

MongoRepository.Configure()// Every tenant should have their own Sales database.DatabasePerTenant("Sales", db =>db.Map<Order>().Map<Customer>("Customers")).Build();

The name of the database will be "{tenant key}_{database name}".

Polymorphism

Mapping a type hierarchy to the same collection is easy. Just map the base type using MapAlongWithSubclassesInSameAssembly<MyBaseType>(). It takes all the same arguments as Map.

Indices

Indices are defined when mapping a type:

MongoRepository.Configure()// Every tenant should have their own Sales database.Database("Zoo", db =>db.Map<Animal>("Animals", x =>x.WithIndex(a =>a.Species).WithIndex(a =>a.EnclosureNumber,unique:true).WithIndex(a =>a.LastVaccinationDate,sparse:true)).Map<FeedingRoutine>("FeedingRoutines", x =>x// Composite index.WithIndex(new{Composite index }))).Build();

Capped collections

[To be documented]

Unconventional id properties

[To be documented]

DI frameworks

.NET Core

There is an extension package called JohnKnoop.MongoRepository.DotNetCoreDi that registers IRepository<T> as a dependency with the .NET Core dependency injection framework.

See the repository readme for more information.

Ninject

this.Bind(typeof(IRepository<>)).ToMethod(context =>{TypeentityType=context.GenericArguments[0];varmongoClient=context.Kernel.Get<IMongoClient>();vartenantKey=/* Pull out your tenent key from auth ticket or Owin context or what suits you best */;vargetRepositoryMethod=typeof(MongoConfiguration).GetMethod(nameof(MongoConfiguration.GetRepository));vargetRepositoryMethodGeneric=getRepositoryMethod.MakeGenericMethod(entityType);returngetRepositoryMethodGeneric.Invoke(this,newobject[]{mongoClient,tenantKey});});

Design philosophy

This library is an extension to the MongoDB C# driver, and thus I don't mind exposing types from the MongoDB.Driver namespace, like IFindFluent or the result types of the various operations.

Any contributions to this library should be in line with the philosophy of this primarily being an extension that makes it easy to write multi-tenant applications using the MongoDB driver. I'm not looking to widen the scope of this library.

About

An easy-to-configure, powerful repository for MongoDB with support for multi-tenancy

Topics

Resources

Stars

49 stars

Watchers

8 watching

Forks

Releases

Packages

Used by

Contributors

Languages