An easy-to-configure, powerful repository for MongoDB with support for multi-tenancy
- Getting started
- Querying
- Inserting, updating and deleting
- InsertAsync, InsertManyAsync
- UpdateOneAsync, UpdateManyAsync
- UpdateOneBulkAsync
- FindOneAndUpdateAsync,
- [FindOneAndReplaceAsync](documentation under construction),
- [FindOneOrInsertAsync](documentation under construction),
- Aggregation
- Deleting
- Transactions
- Advanced features
- Configuration
- Contribute
PM> Install-Package JohnKnoop.MongoRepository
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();
var employeeRepository = mongoClient.GetRepository<Employee>();
var animalRepository = mongoClient.GetRepository<Animal>(tenantKey);
await repository.GetAsync("id");
await repository.GetAsync<SubType>("id");
// With projection
await repository.GetAsync("id", x => x.TheOnlyPropertyIWant);
await repository.GetAsync<SubType>("id", x => new
{
x.SomeProperty,
x.SomeOtherProperty
}
);
await repository.Find(x => x.SomeProperty == someValue);
await repository.Find<SubType>(x => x.SomeProperty == someValue);
await repository.Find(x => x.SomeProperty, regexPattern);
Returns an IFindFluent which offers methods like ToListAsync
, CountAsync
, Project
, Skip
and Limit
Examples:
var dottedAnimals = await repository
.Find(x => x.Coat == "dotted")
.Limit(10)
.Project(x => x.Species)
.ToListAsync()
repository.Query();
repository.Query<SubType>();
Returns an IMongoQueryable which offers async versions of all the standard LINQ methods.
Examples:
var dottedAnimals = await repository.Query()
.Where(x => x.Coat == "dotted")
.Take(10)
.Select(x => x.Species)
.ToListAsync()
await repository.InsertAsync(someObject);
await repository.InsertManyAsync(someCollectionOfObjects);
// Update one document
await repository.UpdateOneAsync("id", x => x.Set(y => y.SomeProperty, someValue), upsert: true);
await repository.UpdateOneAsync(x => x.SomeProperty == someValue, x => x.Push(y => y.SomeCollection, someValue));
await repository.UpdateOneAsync<SubType>(x => x.SomeProperty == someValue, x => x.Push(y => y.SomeCollection, someValue));
// Update all documents matched by filter
await repository.UpdateManyAsync(x => x.SomeProperty == someValue, x => x.Inc(y => y.SomeProperty, 5));
Perform multiple update operations with different filters in one db roundtrip.
await repository.UpdateOneBulkAsync(new List<UpdateOneCommand<MyEntity>> {
new UpdateOneCommand<MyEntity> {
Filter = x => x.SomeProperty = "foo",
Update = x => x.Set(y => y.SomeOtherProperty, 10)
},
new UpdateOneCommand<MyEntity> {
Filter = x => x.SomeProperty = "bar",
Update = x => x.Set(y => y.SomeOtherProperty, 20)
}
});
This is a really powerful feature of MongoDB, in that it lets you update and retrieve a document atomically.
var entityAfterUpdate = await repository.FindOneAndUpdateAsync(
filter: x => x.SomeProperty.StartsWith("Hello"),
update: x => x.AddToSet(y => y.SomeCollection, someItem)
);
var entityAfterUpdate = await repository.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
);
repository.Aggregate();
repository.Aggregate(options);
Returns an IAggregateFluent which offers methods like AppendStage
, Group
, Match
, Unwind
, Out
, Lookup
etc.
await repository.DeleteByIdAsync("id");
// or
await repository.DeleteManyAsync(x => x.SomeProperty === someValue);
// or
var deleted = await repository.FindOneAndDeleteAsync("id");
// or
var deleted = await repository.FindOneAndDeleteAsync<DerivedType>(x => x.SomeProp == someValue);
Soft-deleting an entity will move it to a different collection, preserving type-information.
await repository.DeleteByIdAsync("id", softDelete: true);
// or
var deleted = await repository.FindOneAndDeleteAsync("id", softDelete: true);
Listing soft-deleted entities:
await repository.ListTrashAsync();
Restoring one (or many) soft-deleten entities
await repository.RestoreSoftDeletedAsync("id");
await repository.RestoreSoftDeletedAsync(x => x.TimestampDeletedUtc > DateTime.Today);
Permanently delete soft-deleted documents
await repository.PermamentlyDeleteSoftDeletedAsync(x => x.Foo == "bar");
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 (var transaction = repository.StartTransaction()) {
// ...
await transaction.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 (var transaction = new TransactionScope(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 transaction
await repo.WithTransactionAsync(async () =>
{
// your code here
}, maxRetries: 3);
// Retry using TransactionScope
await repo.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.
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.
var value = await repository.GetCounterValueAsync();
var value = await repository.GetCounterValueAsync("MyNamedCounter");
Atomically increment and read the value of a counter:
var value = await repository.IncrementCounterAsync(); // Increment by 1
var value = await repository.IncrementCounterAsync(name: "MyNamedCounter", incrementBy: 5);
Reset a counter:
await repository.ResetCounterAsync(); // Reset to 1
await repository.ResetCounterAsync(name: "MyNamedCounter", newValue: 5);
Delete a property from a document:
await repository.DeletePropertyAsync(x => x.SomeProperty == someValue, x => x.PropertyToRemove);
Configuration is done once, when the application is started. Use MongoRepository.Configure()
as shown below.
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}".
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 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();
[To be documented]
[To be documented]
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.
this.Bind(typeof(IRepository<>)).ToMethod(context =>
{
Type entityType = context.GenericArguments[0];
var mongoClient = context.Kernel.Get<IMongoClient>();
var tenantKey = /* Pull out your tenent key from auth ticket or Owin context or what suits you best */;
var getRepositoryMethod = typeof(MongoConfiguration).GetMethod(nameof(MongoConfiguration.GetRepository));
var getRepositoryMethodGeneric = getRepositoryMethod.MakeGenericMethod(entityType);
return getRepositoryMethodGeneric.Invoke(this, new object[] { mongoClient, tenantKey });
});
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.