using System.Linq.Expressions; using Elsa.Common.Entities; using Elsa.Common.Multitenancy; using Elsa.Extensions; using Elsa.MongoDb.Extensions; using JetBrains.Annotations; using MongoDB.Driver; using MongoDB.Driver.Linq; namespace Elsa.MongoDb.Common; /// /// A generic repository class around MongoDb for accessing documents. /// /// The type of the document. [PublicAPI] public class MongoDbStore(IMongoCollection collection, ITenantAccessor tenantAccessor) where TDocument : class { /// /// Returns a queryable collection of documents. /// public IMongoCollection GetCollection() => collection; /// /// Saves the document. /// /// The document to save. /// The cancellation token. public async Task AddAsync(TDocument document, CancellationToken cancellationToken = default) { ApplyTenantId(document); await collection.InsertOneAsync(document, new InsertOneOptions(), cancellationToken); return document; } /// /// Saves a list of documents. /// /// The documents to save. /// The cancellation token. public async Task AddManyAsync(IEnumerable documents, CancellationToken cancellationToken = default) { var documentsList = documents.ToList(); if (!documentsList.Any()) return; ApplyTenantId(documentsList); await collection.InsertManyAsync(documentsList, new InsertManyOptions(), cancellationToken); } /// /// Saves the document. /// /// The document to save. /// The cancellation token. public async Task SaveAsync(TDocument document, CancellationToken cancellationToken = default) { ApplyTenantId(document); return await collection.FindOneAndReplaceAsync(document.BuildIdFilter(), document, new FindOneAndReplaceOptions { ReturnDocument = ReturnDocument.After, IsUpsert = true }, cancellationToken); } /// /// Saves the document. /// /// The document to save. /// The selector to use. /// The cancellation token. public async Task SaveAsync(TDocument document, Expression> selector, CancellationToken cancellationToken = default) { ApplyTenantId(document); return await collection.FindOneAndReplaceAsync(document.BuildExpression(selector), document, new FindOneAndReplaceOptions { ReturnDocument = ReturnDocument.After, IsUpsert = true }, cancellationToken); } /// /// Saves the specified documents. /// /// The documents to save. /// The cancellation token. public async Task SaveManyAsync(IEnumerable documents, CancellationToken cancellationToken = default) { var documentsList = documents.ToList(); ApplyTenantId(documentsList); var writes = new List>(); foreach (var document in documentsList) { var replacement = new ReplaceOneModel(document.BuildIdFilter(), document) { IsUpsert = true }; writes.Add(replacement); } if (!writes.Any()) return; await collection.BulkWriteAsync(writes, cancellationToken: cancellationToken); } /// /// Saves the specified documents. /// /// The documents to save. /// The primary key to use. /// The cancellation token. public async Task SaveManyAsync(IEnumerable documents, string primaryKey = nameof(Entity.Id), CancellationToken cancellationToken = default) { var documentsList = documents.ToList(); ApplyTenantId(documentsList); var writes = new List>(); foreach (var document in documentsList) { var replacement = new ReplaceOneModel(document.BuildFilter(primaryKey), document) { IsUpsert = true }; writes.Add(replacement); } if (!writes.Any()) return; await collection.BulkWriteAsync(writes, cancellationToken: cancellationToken); } public async Task UpdatePartialAsync( string id, IDictionary updatedFields, string primaryKey = nameof(Entity.Id), CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(id)) throw new ArgumentNullException(nameof(id)); if (updatedFields == null || updatedFields.Count == 0) throw new ArgumentException("No fields to update were provided.", nameof(updatedFields)); var filter = Builders.Filter.Eq(primaryKey, id); var updateDefinition = Builders.Update.Combine( updatedFields.Select(field => Builders.Update.Set(field.Key, field.Value)) ); var updateResult = await collection.UpdateOneAsync(filter, updateDefinition, cancellationToken: cancellationToken); if (updateResult.MatchedCount == 0) throw new InvalidOperationException($"No document found with ID '{id}'."); } /// /// Finds the document matching the specified predicate /// /// The predicate to use. /// The cancellation token. /// The document if found, otherwise null. public async Task FindAsync(Expression> predicate, CancellationToken cancellationToken = default) { return await FindAsync(predicate, false, cancellationToken); } /// /// Finds the document matching the specified predicate /// /// The predicate to use. /// Whether to include results across tenants. /// The cancellation token. /// The document if found, otherwise null. public async Task FindAsync(Expression> predicate, bool tenantAgnostic = false, CancellationToken cancellationToken = default) { var queryable = GetQueryableCollection(tenantAgnostic); return await queryable.Where(predicate).FirstOrDefaultAsync(cancellationToken); } /// /// Finds a single document using a query /// /// The query to use /// The cancellation token /// The document if found, otherwise null public async Task FindAsync(Func, IQueryable> query, CancellationToken cancellationToken = default) { return await FindAsync(query, false, cancellationToken); } /// /// Finds a single document using a query /// /// The query to use /// Whether to include results across tenants /// The cancellation token /// The document if found, otherwise null public async Task FindAsync(Func, IQueryable> query, bool tenantAgnostic = false, CancellationToken cancellationToken = default) { var queryable = GetQueryableCollection(tenantAgnostic); return await query(queryable).FirstOrDefaultAsync(cancellationToken); } /// /// Finds a list of documents matching the specified predicate /// public async Task> FindManyAsync(Expression> predicate, CancellationToken cancellationToken = default) { return await FindManyAsync(predicate, false, cancellationToken); } /// /// Finds a list of documents matching the specified predicate /// public async Task> FindManyAsync(Expression> predicate, bool tenantAgnostic = false, CancellationToken cancellationToken = default) { var queryable = GetQueryableCollection(tenantAgnostic); return await queryable.Where(predicate).ToListAsync(cancellationToken); } /// /// Queries the database using a query and a selector. /// public async Task> FindManyAsync(Func, IQueryable> query, Expression> selector, CancellationToken cancellationToken = default) { return await FindManyAsync(query, selector, false, cancellationToken); } /// /// Queries the database using a query and a selector. /// public async Task> FindManyAsync(Func, IQueryable> query, Expression> selector, bool tenantAgnostic = false, CancellationToken cancellationToken = default) { var queryable = GetQueryableCollection(tenantAgnostic); return await query(queryable).Select(selector).ToListAsync(cancellationToken); } /// /// Finds a list of documents using a query /// public async Task> FindManyAsync(Func, IQueryable> query, CancellationToken cancellationToken = default) { return await FindManyAsync(query, false, cancellationToken); } /// /// Finds a list of documents using a query /// public async Task> FindManyAsync(Func, IQueryable> query, bool tenantAgnostic = false, CancellationToken cancellationToken = default) { var queryable = GetQueryableCollection(tenantAgnostic); return await query(queryable).ToListAsync(cancellationToken); } /// /// Queries the database using a query and a selector. /// public async Task> FindMany(Func, IQueryable> query, Expression> selector, CancellationToken cancellationToken = default) { return await FindMany(query, selector, false, cancellationToken); } /// /// Queries the database using a query and a selector. /// public async Task> FindMany(Func, IQueryable> query, Expression> selector, bool tenantAgnostic = false, CancellationToken cancellationToken = default) { var queryable = GetQueryableCollection(tenantAgnostic); return await query(queryable).Select(selector).ToListAsync(cancellationToken); } /// /// Counts documents in the collection using a filter. /// public async Task CountAsync(Func, IQueryable> query, CancellationToken cancellationToken = default) { return await CountAsync(query, false, cancellationToken); } /// /// Counts documents in the collection using a filter. /// public async Task CountAsync(Func, IQueryable> query, bool tenantAgnostic = false, CancellationToken cancellationToken = default) { var queryable = GetQueryableCollection(tenantAgnostic); return await query(queryable).LongCountAsync(cancellationToken); } /// /// Counts documents in the collection using a filter and distinct by a key selector. /// public async Task CountAsync(Func, IQueryable> query, Expression> propertySelector, CancellationToken cancellationToken = default) { return await CountAsync(query, propertySelector, false, cancellationToken); } /// /// Counts documents in the collection using a filter and distinct by a key selector. /// public async Task CountAsync(Func, IQueryable> query, Expression> propertySelector, bool tenantAgnostic = false, CancellationToken cancellationToken = default) { var queryable = GetQueryableCollection(tenantAgnostic); return await query(queryable.DistinctBy(propertySelector)).LongCountAsync(cancellationToken); } /// /// Lists all documents. /// public async Task> ListAsync(bool tenantAgnostic = false, CancellationToken cancellationToken = default) { var queryable = GetQueryableCollection(tenantAgnostic); return await queryable.ToListAsync(cancellationToken); } /// /// Checks if any documents exist. /// public async Task AnyAsync(Expression> predicate, CancellationToken cancellationToken = default) { return await AnyAsync(predicate, false, cancellationToken); } /// /// Checks if any documents exist. /// public async Task AnyAsync(Expression> predicate, bool tenantAgnostic = false, CancellationToken cancellationToken = default) { var queryable = GetQueryableCollection(tenantAgnostic); return await queryable.Where(predicate).AnyAsync(cancellationToken); } /// /// Deletes documents using a predicate. /// /// The number of documents deleted. public async Task DeleteWhereAsync(Expression> predicate, CancellationToken cancellationToken = default) { return await DeleteWhereAsync(predicate, false, cancellationToken); } /// /// Deletes documents using a predicate. /// /// The number of documents deleted. public async Task DeleteWhereAsync(Expression> predicate, bool tenantAgnostic = false, CancellationToken cancellationToken = default) { return await DeleteWhereAsync(predicate, nameof(Entity.Id), tenantAgnostic, cancellationToken); } /// /// Deletes documents using a predicate. /// /// The number of documents deleted. public async Task DeleteWhereAsync(Expression> predicate, string key, CancellationToken cancellationToken = default) { return await DeleteWhereAsync(predicate, key, false, cancellationToken); } /// /// Deletes documents using a predicate. /// /// The number of documents deleted. public async Task DeleteWhereAsync(Expression> predicate, string key, bool tenantAgnostic = false, CancellationToken cancellationToken = default) { var queryable = GetQueryableCollection(tenantAgnostic); var documentsToDelete = await queryable.Where(predicate).ToListAsync(cancellationToken); var count = documentsToDelete.LongCount(); var filter = documentsToDelete.BuildIdFilterForList(key); await collection.DeleteManyAsync(filter, cancellationToken); return count; } /// /// Deletes documents using a query. /// /// The number of documents deleted. public async Task DeleteWhereAsync(Func, IQueryable> query, Expression> keySelector, CancellationToken cancellationToken = default) { return await DeleteWhereAsync(query, keySelector, false, cancellationToken); } /// /// Deletes documents using a query. /// /// The number of documents deleted. public async Task DeleteWhereAsync(Func, IQueryable> query, Expression> keySelector, bool tenantAgnostic = false, CancellationToken cancellationToken = default) { var key = keySelector.GetPropertyName(); return await DeleteWhereAsync(query, key, tenantAgnostic, cancellationToken); } /// /// Deletes documents using a query. /// /// The number of documents deleted. public async Task DeleteWhereAsync(Func, IQueryable> query, string key = nameof(Entity.Id), CancellationToken cancellationToken = default) { return await DeleteWhereAsync(query, key, false, cancellationToken); } /// /// Deletes documents using a query. /// /// The number of documents deleted. public async Task DeleteWhereAsync(Func, IQueryable> query, string key = nameof(Entity.Id), bool tenantAgnostic = false, CancellationToken cancellationToken = default) { var queryable = GetQueryableCollection(tenantAgnostic); var documentsToDelete = await query(queryable).ToListAsync(cancellationToken); var count = documentsToDelete.LongCount(); var filter = documentsToDelete.BuildIdFilterForList(key); await collection.DeleteManyAsync(filter, cancellationToken); return count; } private IQueryable GetQueryableCollection(bool tenantAgnostic = false) { var queryable = collection.AsQueryable(); if (tenantAgnostic) return queryable; if (typeof(Entity).IsAssignableFrom(typeof(TDocument))) { var tenant = tenantAccessor.Tenant; var tenantId = tenant?.Id.EmptyToNull(); queryable = queryable.Where(x => (x as Entity)!.TenantId == tenantId); } return queryable; } private void ApplyTenantId(TDocument document) { var tenant = tenantAccessor.Tenant; var tenantId = tenant?.Id; if (document is Entity tenantDocument) tenantDocument.TenantId = tenantId.EmptyToNull(); } private void ApplyTenantId(IEnumerable documents) { var tenant = tenantAccessor.Tenant; var tenantId = tenant?.Id; foreach (var document in documents) { if (document is Entity tenantDocument) tenantDocument.TenantId = tenantId.EmptyToNull(); } } }