From 9a8988bf805d29d92fdf09fe38aceec75af3d093 Mon Sep 17 00:00:00 2001 From: Ahmed Fwela Date: Fri, 5 Nov 2021 06:06:33 +0200 Subject: [PATCH 01/26] . --- MongoDB.Entities/DB/DB.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MongoDB.Entities/DB/DB.cs b/MongoDB.Entities/DB/DB.cs index a37b098b6..96f44d4a5 100644 --- a/MongoDB.Entities/DB/DB.cs +++ b/MongoDB.Entities/DB/DB.cs @@ -32,6 +32,8 @@ static DB() _ => true); } + //TODO: refactor api + //key: FullDBName(including tenant prefix - ex: TenantX~DBName) private static readonly ConcurrentDictionary dbs = new(); private static IMongoDatabase defaultDb; From 218bb9e3a3a0ba842028dfa4d82eade041078175 Mon Sep 17 00:00:00 2001 From: Ahmed Fwela Date: Fri, 5 Nov 2021 06:59:32 +0200 Subject: [PATCH 02/26] Initial WIP --- MongoDB.Entities/DBContext/DBContext.cs | 2 +- .../MongoContext/MongoContext.IMongoClient.cs | 163 ++++++++++++++++++ MongoDB.Entities/MongoContext/MongoContext.cs | 52 ++++++ 3 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 MongoDB.Entities/MongoContext/MongoContext.IMongoClient.cs create mode 100644 MongoDB.Entities/MongoContext/MongoContext.cs diff --git a/MongoDB.Entities/DBContext/DBContext.cs b/MongoDB.Entities/DBContext/DBContext.cs index 6339f0f93..35be4b99c 100644 --- a/MongoDB.Entities/DBContext/DBContext.cs +++ b/MongoDB.Entities/DBContext/DBContext.cs @@ -16,7 +16,7 @@ public partial class DBContext /// Returns the session object used for transactions /// public IClientSessionHandle Session { get; protected set; } - + public MongoContext MongoContext { get; set; } /// /// The value of this property will be automatically set on entities when saving/updating if the entity has a ModifiedBy property /// diff --git a/MongoDB.Entities/MongoContext/MongoContext.IMongoClient.cs b/MongoDB.Entities/MongoContext/MongoContext.IMongoClient.cs new file mode 100644 index 000000000..ae7a4aa98 --- /dev/null +++ b/MongoDB.Entities/MongoContext/MongoContext.IMongoClient.cs @@ -0,0 +1,163 @@ +using MongoDB.Bson; +using MongoDB.Driver; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace MongoDB.Entities +{ + public partial class MongoContext + { + public void DropDatabase(string name, CancellationToken cancellationToken = default) + { + Client.DropDatabase(name, cancellationToken); + } + + public void DropDatabase(IClientSessionHandle session, string name, CancellationToken cancellationToken = default) + { + Client.DropDatabase(session, name, cancellationToken); + } + + public Task DropDatabaseAsync(string name, CancellationToken cancellationToken = default) + { + return Client.DropDatabaseAsync(name, cancellationToken); + } + + public Task DropDatabaseAsync(IClientSessionHandle session, string name, CancellationToken cancellationToken = default) + { + return Client.DropDatabaseAsync(session, name, cancellationToken); + } + + public IMongoDatabase GetDatabase(string name, MongoDatabaseSettings settings = null) + { + return Client.GetDatabase(name, settings); + } + + public IAsyncCursor ListDatabaseNames(CancellationToken cancellationToken = default) + { + return Client.ListDatabaseNames(cancellationToken); + } + + public IAsyncCursor ListDatabaseNames(ListDatabaseNamesOptions options, CancellationToken cancellationToken = default) + { + return Client.ListDatabaseNames(options, cancellationToken); + } + + public IAsyncCursor ListDatabaseNames(IClientSessionHandle session, CancellationToken cancellationToken = default) + { + return Client.ListDatabaseNames(session, cancellationToken); + } + + public IAsyncCursor ListDatabaseNames(IClientSessionHandle session, ListDatabaseNamesOptions options, CancellationToken cancellationToken = default) + { + return Client.ListDatabaseNames(session, options, cancellationToken); + } + + public Task> ListDatabaseNamesAsync(CancellationToken cancellationToken = default) + { + return Client.ListDatabaseNamesAsync(cancellationToken); + } + + public Task> ListDatabaseNamesAsync(ListDatabaseNamesOptions options, CancellationToken cancellationToken = default) + { + return Client.ListDatabaseNamesAsync(options, cancellationToken); + } + + public Task> ListDatabaseNamesAsync(IClientSessionHandle session, CancellationToken cancellationToken = default) + { + return Client.ListDatabaseNamesAsync(session, cancellationToken); + } + + public Task> ListDatabaseNamesAsync(IClientSessionHandle session, ListDatabaseNamesOptions options, CancellationToken cancellationToken = default) + { + return Client.ListDatabaseNamesAsync(session, options, cancellationToken); + } + + public IAsyncCursor ListDatabases(CancellationToken cancellationToken = default) + { + return Client.ListDatabases(cancellationToken); + } + + public IAsyncCursor ListDatabases(ListDatabasesOptions options, CancellationToken cancellationToken = default) + { + return Client.ListDatabases(options, cancellationToken); + } + + public IAsyncCursor ListDatabases(IClientSessionHandle session, CancellationToken cancellationToken = default) + { + return Client.ListDatabases(session, cancellationToken); + } + + public IAsyncCursor ListDatabases(IClientSessionHandle session, ListDatabasesOptions options, CancellationToken cancellationToken = default) + { + return Client.ListDatabases(session, options, cancellationToken); + } + + public Task> ListDatabasesAsync(CancellationToken cancellationToken = default) + { + return Client.ListDatabasesAsync(cancellationToken); + } + + public Task> ListDatabasesAsync(ListDatabasesOptions options, CancellationToken cancellationToken = default) + { + return Client.ListDatabasesAsync(options, cancellationToken); + } + + public Task> ListDatabasesAsync(IClientSessionHandle session, CancellationToken cancellationToken = default) + { + return Client.ListDatabasesAsync(session, cancellationToken); + } + + public Task> ListDatabasesAsync(IClientSessionHandle session, ListDatabasesOptions options, CancellationToken cancellationToken = default) + { + return Client.ListDatabasesAsync(session, options, cancellationToken); + } + + public IClientSessionHandle StartSession(ClientSessionOptions options = null, CancellationToken cancellationToken = default) + { + return Client.StartSession(options, cancellationToken); + } + + public Task StartSessionAsync(ClientSessionOptions options = null, CancellationToken cancellationToken = default) + { + return Client.StartSessionAsync(options, cancellationToken); + } + + public IChangeStreamCursor Watch(PipelineDefinition, TResult> pipeline, ChangeStreamOptions options = null, CancellationToken cancellationToken = default) + { + return Client.Watch(pipeline, options, cancellationToken); + } + + public IChangeStreamCursor Watch(IClientSessionHandle session, PipelineDefinition, TResult> pipeline, ChangeStreamOptions options = null, CancellationToken cancellationToken = default) + { + return Client.Watch(session, pipeline, options, cancellationToken); + } + + public Task> WatchAsync(PipelineDefinition, TResult> pipeline, ChangeStreamOptions options = null, CancellationToken cancellationToken = default) + { + return Client.WatchAsync(pipeline, options, cancellationToken); + } + + public Task> WatchAsync(IClientSessionHandle session, PipelineDefinition, TResult> pipeline, ChangeStreamOptions options = null, CancellationToken cancellationToken = default) + { + return Client.WatchAsync(session, pipeline, options, cancellationToken); + } + + public IMongoClient WithReadConcern(ReadConcern readConcern) + { + return Client.WithReadConcern(readConcern); + } + + public IMongoClient WithReadPreference(ReadPreference readPreference) + { + return Client.WithReadPreference(readPreference); + } + + public IMongoClient WithWriteConcern(WriteConcern writeConcern) + { + return Client.WithWriteConcern(writeConcern); + } + } +} diff --git a/MongoDB.Entities/MongoContext/MongoContext.cs b/MongoDB.Entities/MongoContext/MongoContext.cs new file mode 100644 index 000000000..64dd22d9e --- /dev/null +++ b/MongoDB.Entities/MongoContext/MongoContext.cs @@ -0,0 +1,52 @@ +using MongoDB.Driver; +using MongoDB.Driver.Core.Clusters; +using System.Collections.Generic; + +namespace MongoDB.Entities +{ + /// + /// MongoContext is simply a wrapper around a + /// + public partial class MongoContext : IMongoClient + { + /// + /// Creates a new from an existing + /// + public MongoContext(IMongoClient client) + { + Client = client; + } + + /// + /// Creates a new and a new from a object + /// + public MongoContext(MongoClientSettings settings) : this(new MongoClient(settings)) + { + } + + /// + /// Creates a new , and from a object + /// + public MongoContext(MongoUrl url) : this(MongoClientSettings.FromUrl(url)) + { + } + + + + /// + /// The backing client + /// + public IMongoClient Client { get; } + + /// + /// Stores the s managed by this + /// This maps Database name to the matching + /// + public IDictionary Contexts { get; } = new Dictionary(); + + public ICluster Cluster => Client.Cluster; + + public MongoClientSettings Settings => Client.Settings; + } + +} From ff8b6fb0750376352d668b5b475b989364cd3a5a Mon Sep 17 00:00:00 2001 From: Ahmed Fwela Date: Fri, 5 Nov 2021 07:34:43 +0200 Subject: [PATCH 03/26] added MongoContextOptions --- MongoDB.Entities/Core/MongoContextOptions.cs | 22 ++++++ MongoDB.Entities/DB/DB.cs | 5 +- MongoDB.Entities/DBContext/DBContext.cs | 26 ++++++- .../MongoContext/MongoContext.IMongoClient.cs | 73 +++++++++++-------- MongoDB.Entities/MongoContext/MongoContext.cs | 39 +++------- 5 files changed, 101 insertions(+), 64 deletions(-) create mode 100644 MongoDB.Entities/Core/MongoContextOptions.cs diff --git a/MongoDB.Entities/Core/MongoContextOptions.cs b/MongoDB.Entities/Core/MongoContextOptions.cs new file mode 100644 index 000000000..8403c13eb --- /dev/null +++ b/MongoDB.Entities/Core/MongoContextOptions.cs @@ -0,0 +1,22 @@ +using MongoDB.Driver; +using System; +using System.Collections.Generic; +using System.Text; + +#nullable enable + +namespace MongoDB.Entities +{ + public class MongoContextOptions + { + public MongoContextOptions(ModifiedBy? modifiedBy = null) + { + ModifiedBy = modifiedBy; + } + + /// + /// The value of this property will be automatically set on entities when saving/updating if the entity has a ModifiedBy property + /// + public ModifiedBy? ModifiedBy { get; set; } + } +} diff --git a/MongoDB.Entities/DB/DB.cs b/MongoDB.Entities/DB/DB.cs index 96f44d4a5..de631e0cc 100644 --- a/MongoDB.Entities/DB/DB.cs +++ b/MongoDB.Entities/DB/DB.cs @@ -33,8 +33,9 @@ static DB() } //TODO: refactor api - - //key: FullDBName(including tenant prefix - ex: TenantX~DBName) + public static DBContext Context { get; set; } + + //key: FullDBName(including tenant prefix - ex: TenantX~DBName) private static readonly ConcurrentDictionary dbs = new(); private static IMongoDatabase defaultDb; diff --git a/MongoDB.Entities/DBContext/DBContext.cs b/MongoDB.Entities/DBContext/DBContext.cs index 35be4b99c..84472cecd 100644 --- a/MongoDB.Entities/DBContext/DBContext.cs +++ b/MongoDB.Entities/DBContext/DBContext.cs @@ -16,16 +16,36 @@ public partial class DBContext /// Returns the session object used for transactions /// public IClientSessionHandle Session { get; protected set; } - public MongoContext MongoContext { get; set; } + + + public MongoContext MongoContext { get; } + /// - /// The value of this property will be automatically set on entities when saving/updating if the entity has a ModifiedBy property + /// wrapper around so that we don't break the public api /// - public ModifiedBy ModifiedBy { get; set; } + public ModifiedBy ModifiedBy + { + get + { + return MongoContext.ModifiedBy; + } + set + { + MongoContext.Options.ModifiedBy = value; + } + } + + protected string tenantPrefix; private static Type[] allEntitiyTypes; private Dictionary globalFilters; + public DBContext(MongoContext mongoContext) + { + MongoContext = mongoContext; + } + /// /// Initializes a DBContext instance with the given connection parameters. /// TIP: network connection is deferred until the first actual operation. diff --git a/MongoDB.Entities/MongoContext/MongoContext.IMongoClient.cs b/MongoDB.Entities/MongoContext/MongoContext.IMongoClient.cs index ae7a4aa98..879166f26 100644 --- a/MongoDB.Entities/MongoContext/MongoContext.IMongoClient.cs +++ b/MongoDB.Entities/MongoContext/MongoContext.IMongoClient.cs @@ -1,5 +1,6 @@ using MongoDB.Bson; using MongoDB.Driver; +using MongoDB.Driver.Core.Clusters; using System; using System.Collections.Generic; using System.Text; @@ -8,154 +9,166 @@ namespace MongoDB.Entities { + //Make these interface implmentation explicit, so we can fine-tune the api return result public partial class MongoContext { - public void DropDatabase(string name, CancellationToken cancellationToken = default) + public ICluster Cluster => Client.Cluster; + + public MongoClientSettings Settings => Client.Settings; + + void IMongoClient.DropDatabase(string name, CancellationToken cancellationToken) { Client.DropDatabase(name, cancellationToken); } - public void DropDatabase(IClientSessionHandle session, string name, CancellationToken cancellationToken = default) + void IMongoClient.DropDatabase(IClientSessionHandle session, string name, CancellationToken cancellationToken) { Client.DropDatabase(session, name, cancellationToken); } - public Task DropDatabaseAsync(string name, CancellationToken cancellationToken = default) + Task IMongoClient.DropDatabaseAsync(string name, CancellationToken cancellationToken) { return Client.DropDatabaseAsync(name, cancellationToken); } - public Task DropDatabaseAsync(IClientSessionHandle session, string name, CancellationToken cancellationToken = default) + Task IMongoClient.DropDatabaseAsync(IClientSessionHandle session, string name, CancellationToken cancellationToken) { return Client.DropDatabaseAsync(session, name, cancellationToken); } - public IMongoDatabase GetDatabase(string name, MongoDatabaseSettings settings = null) + IMongoDatabase IMongoClient.GetDatabase(string name, MongoDatabaseSettings settings) { return Client.GetDatabase(name, settings); } - public IAsyncCursor ListDatabaseNames(CancellationToken cancellationToken = default) + IAsyncCursor IMongoClient.ListDatabaseNames(CancellationToken cancellationToken) { return Client.ListDatabaseNames(cancellationToken); } - public IAsyncCursor ListDatabaseNames(ListDatabaseNamesOptions options, CancellationToken cancellationToken = default) + IAsyncCursor IMongoClient.ListDatabaseNames(ListDatabaseNamesOptions options, CancellationToken cancellationToken) { return Client.ListDatabaseNames(options, cancellationToken); } - public IAsyncCursor ListDatabaseNames(IClientSessionHandle session, CancellationToken cancellationToken = default) + IAsyncCursor IMongoClient.ListDatabaseNames(IClientSessionHandle session, CancellationToken cancellationToken) { return Client.ListDatabaseNames(session, cancellationToken); } - public IAsyncCursor ListDatabaseNames(IClientSessionHandle session, ListDatabaseNamesOptions options, CancellationToken cancellationToken = default) + IAsyncCursor IMongoClient.ListDatabaseNames(IClientSessionHandle session, ListDatabaseNamesOptions options, CancellationToken cancellationToken) { return Client.ListDatabaseNames(session, options, cancellationToken); } - public Task> ListDatabaseNamesAsync(CancellationToken cancellationToken = default) + public async Task> AllDatabaseNamesAsync() + { + return await (await + ((IMongoClient)this) + .ListDatabaseNamesAsync().ConfigureAwait(false)) + .ToListAsync().ConfigureAwait(false); + } + Task> IMongoClient.ListDatabaseNamesAsync(CancellationToken cancellationToken) { return Client.ListDatabaseNamesAsync(cancellationToken); } - public Task> ListDatabaseNamesAsync(ListDatabaseNamesOptions options, CancellationToken cancellationToken = default) + Task> IMongoClient.ListDatabaseNamesAsync(ListDatabaseNamesOptions options, CancellationToken cancellationToken) { return Client.ListDatabaseNamesAsync(options, cancellationToken); } - public Task> ListDatabaseNamesAsync(IClientSessionHandle session, CancellationToken cancellationToken = default) + Task> IMongoClient.ListDatabaseNamesAsync(IClientSessionHandle session, CancellationToken cancellationToken) { return Client.ListDatabaseNamesAsync(session, cancellationToken); } - public Task> ListDatabaseNamesAsync(IClientSessionHandle session, ListDatabaseNamesOptions options, CancellationToken cancellationToken = default) + Task> IMongoClient.ListDatabaseNamesAsync(IClientSessionHandle session, ListDatabaseNamesOptions options, CancellationToken cancellationToken) { return Client.ListDatabaseNamesAsync(session, options, cancellationToken); } - public IAsyncCursor ListDatabases(CancellationToken cancellationToken = default) + IAsyncCursor IMongoClient.ListDatabases(CancellationToken cancellationToken) { return Client.ListDatabases(cancellationToken); } - public IAsyncCursor ListDatabases(ListDatabasesOptions options, CancellationToken cancellationToken = default) + IAsyncCursor IMongoClient.ListDatabases(ListDatabasesOptions options, CancellationToken cancellationToken) { return Client.ListDatabases(options, cancellationToken); } - public IAsyncCursor ListDatabases(IClientSessionHandle session, CancellationToken cancellationToken = default) + IAsyncCursor IMongoClient.ListDatabases(IClientSessionHandle session, CancellationToken cancellationToken) { return Client.ListDatabases(session, cancellationToken); } - public IAsyncCursor ListDatabases(IClientSessionHandle session, ListDatabasesOptions options, CancellationToken cancellationToken = default) + IAsyncCursor IMongoClient.ListDatabases(IClientSessionHandle session, ListDatabasesOptions options, CancellationToken cancellationToken) { return Client.ListDatabases(session, options, cancellationToken); } - public Task> ListDatabasesAsync(CancellationToken cancellationToken = default) + Task> IMongoClient.ListDatabasesAsync(CancellationToken cancellationToken) { return Client.ListDatabasesAsync(cancellationToken); } - public Task> ListDatabasesAsync(ListDatabasesOptions options, CancellationToken cancellationToken = default) + Task> IMongoClient.ListDatabasesAsync(ListDatabasesOptions options, CancellationToken cancellationToken) { return Client.ListDatabasesAsync(options, cancellationToken); } - public Task> ListDatabasesAsync(IClientSessionHandle session, CancellationToken cancellationToken = default) + Task> IMongoClient.ListDatabasesAsync(IClientSessionHandle session, CancellationToken cancellationToken) { return Client.ListDatabasesAsync(session, cancellationToken); } - public Task> ListDatabasesAsync(IClientSessionHandle session, ListDatabasesOptions options, CancellationToken cancellationToken = default) + Task> IMongoClient.ListDatabasesAsync(IClientSessionHandle session, ListDatabasesOptions options, CancellationToken cancellationToken) { return Client.ListDatabasesAsync(session, options, cancellationToken); } - public IClientSessionHandle StartSession(ClientSessionOptions options = null, CancellationToken cancellationToken = default) + IClientSessionHandle IMongoClient.StartSession(ClientSessionOptions options, CancellationToken cancellationToken) { return Client.StartSession(options, cancellationToken); } - public Task StartSessionAsync(ClientSessionOptions options = null, CancellationToken cancellationToken = default) + Task IMongoClient.StartSessionAsync(ClientSessionOptions options, CancellationToken cancellationToken) { return Client.StartSessionAsync(options, cancellationToken); } - public IChangeStreamCursor Watch(PipelineDefinition, TResult> pipeline, ChangeStreamOptions options = null, CancellationToken cancellationToken = default) + IChangeStreamCursor IMongoClient.Watch(PipelineDefinition, TResult> pipeline, ChangeStreamOptions options, CancellationToken cancellationToken) { return Client.Watch(pipeline, options, cancellationToken); } - public IChangeStreamCursor Watch(IClientSessionHandle session, PipelineDefinition, TResult> pipeline, ChangeStreamOptions options = null, CancellationToken cancellationToken = default) + IChangeStreamCursor IMongoClient.Watch(IClientSessionHandle session, PipelineDefinition, TResult> pipeline, ChangeStreamOptions options, CancellationToken cancellationToken) { return Client.Watch(session, pipeline, options, cancellationToken); } - public Task> WatchAsync(PipelineDefinition, TResult> pipeline, ChangeStreamOptions options = null, CancellationToken cancellationToken = default) + Task> IMongoClient.WatchAsync(PipelineDefinition, TResult> pipeline, ChangeStreamOptions options, CancellationToken cancellationToken) { return Client.WatchAsync(pipeline, options, cancellationToken); } - public Task> WatchAsync(IClientSessionHandle session, PipelineDefinition, TResult> pipeline, ChangeStreamOptions options = null, CancellationToken cancellationToken = default) + Task> IMongoClient.WatchAsync(IClientSessionHandle session, PipelineDefinition, TResult> pipeline, ChangeStreamOptions options, CancellationToken cancellationToken = default) { return Client.WatchAsync(session, pipeline, options, cancellationToken); } - public IMongoClient WithReadConcern(ReadConcern readConcern) + IMongoClient IMongoClient.WithReadConcern(ReadConcern readConcern) { return Client.WithReadConcern(readConcern); } - public IMongoClient WithReadPreference(ReadPreference readPreference) + IMongoClient IMongoClient.WithReadPreference(ReadPreference readPreference) { return Client.WithReadPreference(readPreference); } - public IMongoClient WithWriteConcern(WriteConcern writeConcern) + IMongoClient IMongoClient.WithWriteConcern(WriteConcern writeConcern) { return Client.WithWriteConcern(writeConcern); } diff --git a/MongoDB.Entities/MongoContext/MongoContext.cs b/MongoDB.Entities/MongoContext/MongoContext.cs index 64dd22d9e..7a705074e 100644 --- a/MongoDB.Entities/MongoContext/MongoContext.cs +++ b/MongoDB.Entities/MongoContext/MongoContext.cs @@ -1,52 +1,33 @@ using MongoDB.Driver; -using MongoDB.Driver.Core.Clusters; using System.Collections.Generic; - +#nullable enable namespace MongoDB.Entities { /// - /// MongoContext is simply a wrapper around a + /// MongoContext is a wrapper around an /// public partial class MongoContext : IMongoClient { /// - /// Creates a new from an existing + /// Creates a new context /// - public MongoContext(IMongoClient client) + /// The backing client, usually a + /// The options to configure the context + public MongoContext(IMongoClient client, MongoContextOptions? options = null) { Client = client; + Options = options ?? new(); } - /// - /// Creates a new and a new from a object - /// - public MongoContext(MongoClientSettings settings) : this(new MongoClient(settings)) - { - } - - /// - /// Creates a new , and from a object - /// - public MongoContext(MongoUrl url) : this(MongoClientSettings.FromUrl(url)) - { - } - - - /// /// The backing client /// public IMongoClient Client { get; } - /// - /// Stores the s managed by this - /// This maps Database name to the matching - /// - public IDictionary Contexts { get; } = new Dictionary(); - - public ICluster Cluster => Client.Cluster; + public MongoContextOptions Options { get; set; } - public MongoClientSettings Settings => Client.Settings; + /// + public ModifiedBy? ModifiedBy => Options.ModifiedBy; } } From 2db41d00b10326a91fc3b0c87769ac67ac2e0558 Mon Sep 17 00:00:00 2001 From: Ahmed Fwela Date: Fri, 5 Nov 2021 09:40:02 +0200 Subject: [PATCH 04/26] WIP moving static implmentations to MongoContext and DBContext --- MongoDB.Entities/Core/Cache.cs | 62 ++--- MongoDB.Entities/Core/DBContextOptions.cs | 17 ++ MongoDB.Entities/Core/Entity.cs | 2 +- MongoDB.Entities/Core/FileEntity.cs | 10 +- MongoDB.Entities/DB/DB.Collection.cs | 37 +-- MongoDB.Entities/DB/DB.File.cs | 12 +- MongoDB.Entities/DB/DB.cs | 6 +- .../DBContext/DBContext.Collection.cs | 33 ++- MongoDB.Entities/DBContext/DBContext.Count.cs | 6 +- .../DBContext/DBContext.Delete.cs | 10 +- .../DBContext/DBContext.Distinct.cs | 2 +- MongoDB.Entities/DBContext/DBContext.File.cs | 13 +- MongoDB.Entities/DBContext/DBContext.Find.cs | 4 +- .../DBContext/DBContext.Fluent.cs | 4 +- .../DBContext/DBContext.GeoNear.cs | 2 +- .../DBContext/DBContext.IMongoDatabase.cs | 234 ++++++++++++++++++ .../DBContext/DBContext.PagedSearch.cs | 4 +- .../DBContext/DBContext.Pipeline.cs | 2 +- .../DBContext/DBContext.Queryable.cs | 2 +- .../DBContext/DBContext.Replace.cs | 2 +- .../DBContext/DBContext.Update.cs | 4 +- MongoDB.Entities/DBContext/DBContext.cs | 128 +++++----- .../MongoContext/MongoContext.IMongoClient.cs | 9 +- MongoDB.Entities/MongoContext/MongoContext.cs | 50 ++++ MongoDB.Entities/MongoDB.Entities.csproj | 2 +- 25 files changed, 477 insertions(+), 180 deletions(-) create mode 100644 MongoDB.Entities/Core/DBContextOptions.cs create mode 100644 MongoDB.Entities/DBContext/DBContext.IMongoDatabase.cs diff --git a/MongoDB.Entities/Core/Cache.cs b/MongoDB.Entities/Core/Cache.cs index d7c293bac..75119667c 100644 --- a/MongoDB.Entities/Core/Cache.cs +++ b/MongoDB.Entities/Core/Cache.cs @@ -14,19 +14,19 @@ internal abstract class Cache { //key: entity type //val: collection name - protected static readonly ConcurrentDictionary typeToCollectionNameMap = new(); + protected readonly ConcurrentDictionary typeToCollectionNameMap = new(); //key: entity type //val: database name without tenant prefix (will be null if not specifically set using DB.DatabaseFor() method) - protected static readonly ConcurrentDictionary typeToDbNameWithoutTenantPrefixMap = new(); + protected readonly ConcurrentDictionary typeToDbNameWithoutTenantPrefixMap = new(); - internal static string CollectionNameFor(Type entityType) + internal string CollectionNameFor(Type entityType) => typeToCollectionNameMap[entityType]; - internal static void MapTypeToDbNameWithoutTenantPrefix(string dbNameWithoutTenantPrefix) where T : IEntity + internal void MapTypeToDbNameWithoutTenantPrefix(string dbNameWithoutTenantPrefix) where T : IEntity => typeToDbNameWithoutTenantPrefixMap[typeof(T)] = dbNameWithoutTenantPrefix; - internal static string GetFullDbName(Type entityType, string tenantPrefix) + internal string GetFullDbName(Type entityType, string tenantPrefix) { string fullDbName = null; @@ -46,22 +46,22 @@ internal static string GetFullDbName(Type entityType, string tenantPrefix) internal class Cache : Cache where T : IEntity { - internal static ConcurrentDictionary> Watchers { get; } = new(); - internal static bool HasCreatedOn { get; } - internal static bool HasModifiedOn { get; } - internal static string ModifiedOnPropName { get; } - internal static PropertyInfo ModifiedByProp { get; } - internal static bool HasIgnoreIfDefaultProps { get; } - internal static string CollectionName { get; } - internal static bool IsFileEntity { get; } + public ConcurrentDictionary> Watchers { get; } = new(); + public bool HasCreatedOn { get; } + public bool HasModifiedOn { get; } + public string ModifiedOnPropName { get; } + public PropertyInfo ModifiedByProp { get; } + public bool HasIgnoreIfDefaultProps { get; } + public string CollectionName { get; } + public bool IsFileEntity { get; } //key: TenantPrefix:CollectionName //val: IMongoCollection - private static readonly ConcurrentDictionary> cache = new(); - private static readonly PropertyInfo[] updatableProps; - private static ProjectionDefinition requiredPropsProjection; + private readonly ConcurrentDictionary> _cache = new(); + private readonly PropertyInfo[] _updatableProps; + private ProjectionDefinition _requiredPropsProjection; - static Cache() + public Cache() { var type = typeof(T); var interfaces = type.GetInterfaces(); @@ -83,20 +83,20 @@ static Cache() ModifiedOnPropName = nameof(IModifiedOn.ModifiedOn); IsFileEntity = type.BaseType == typeof(FileEntity); - updatableProps = type.GetProperties() + _updatableProps = type.GetProperties() .Where(p => p.PropertyType.Name != ManyBase.PropTypeName && !p.IsDefined(typeof(BsonIdAttribute), false) && !p.IsDefined(typeof(BsonIgnoreAttribute), false)) .ToArray(); - HasIgnoreIfDefaultProps = updatableProps.Any(p => + HasIgnoreIfDefaultProps = _updatableProps.Any(p => p.IsDefined(typeof(BsonIgnoreIfDefaultAttribute), false) || p.IsDefined(typeof(BsonIgnoreIfNullAttribute), false)); try { - ModifiedByProp = updatableProps.SingleOrDefault(p => + ModifiedByProp = _updatableProps.SingleOrDefault(p => p.PropertyType == typeof(ModifiedBy) || p.PropertyType.IsSubclassOf(typeof(ModifiedBy))); } @@ -106,32 +106,32 @@ static Cache() } } - internal static IMongoCollection Collection(string tenantPrefix) + public IMongoCollection Collection(string tenantPrefix) { - return cache.GetOrAdd( + return _cache.GetOrAdd( key: $"{tenantPrefix}:{CollectionName}", valueFactory: _ => DB.Database(GetFullDbName(typeof(T), tenantPrefix)).GetCollection(CollectionName)); } - internal static IEnumerable UpdatableProps(T entity) + public IEnumerable UpdatableProps(T entity) { if (HasIgnoreIfDefaultProps) { - return updatableProps.Where(p => + return _updatableProps.Where(p => !(p.IsDefined(typeof(BsonIgnoreIfDefaultAttribute), false) && p.GetValue(entity) == default) && !(p.IsDefined(typeof(BsonIgnoreIfNullAttribute), false) && p.GetValue(entity) == null)); } - return updatableProps; + return _updatableProps; } - internal static ProjectionDefinition CombineWithRequiredProps(ProjectionDefinition userProjection) + public ProjectionDefinition CombineWithRequiredProps(ProjectionDefinition userProjection) { if (userProjection == null) throw new InvalidOperationException("Please use .Project() method before .IncludeRequiredProps()"); - if (requiredPropsProjection is null) + if (_requiredPropsProjection is null) { - requiredPropsProjection = "{_id:1}"; + _requiredPropsProjection = "{_id:1}"; var props = typeof(T) .GetProperties() @@ -146,9 +146,9 @@ internal static ProjectionDefinition CombineWithRequiredProps(); if (attr is null) - requiredPropsProjection = requiredPropsProjection.Include(p.Name); + _requiredPropsProjection = _requiredPropsProjection.Include(p.Name); else - requiredPropsProjection = requiredPropsProjection.Include(attr.ElementName); + _requiredPropsProjection = _requiredPropsProjection.Include(attr.ElementName); } } @@ -158,7 +158,7 @@ internal static ProjectionDefinition CombineWithRequiredProps.Projection.Combine(new[] { - requiredPropsProjection, + _requiredPropsProjection, userProj }); } diff --git a/MongoDB.Entities/Core/DBContextOptions.cs b/MongoDB.Entities/Core/DBContextOptions.cs new file mode 100644 index 000000000..a0571e7ee --- /dev/null +++ b/MongoDB.Entities/Core/DBContextOptions.cs @@ -0,0 +1,17 @@ +#nullable enable + +using MongoDB.Driver; + +namespace MongoDB.Entities +{ + public class DBContextOptions + { + public DBContextOptions(string? tenantId = null) + { + TenantId = tenantId; + } + + public string? TenantId { get; set; } + + } +} diff --git a/MongoDB.Entities/Core/Entity.cs b/MongoDB.Entities/Core/Entity.cs index bc1975b14..a7b7b5073 100644 --- a/MongoDB.Entities/Core/Entity.cs +++ b/MongoDB.Entities/Core/Entity.cs @@ -7,7 +7,7 @@ namespace MongoDB.Entities /// Inherit this class for all entities you want to store in their own collection. /// public abstract class Entity : IEntity - { + { /// /// This property is auto managed. A new ID will be assigned for new entities upon saving. /// diff --git a/MongoDB.Entities/Core/FileEntity.cs b/MongoDB.Entities/Core/FileEntity.cs index 47103c57c..beb2dc732 100644 --- a/MongoDB.Entities/Core/FileEntity.cs +++ b/MongoDB.Entities/Core/FileEntity.cs @@ -43,13 +43,11 @@ public abstract class FileEntity : Entity [IgnoreDefault] public string MD5 { get; set; } - [IgnoreDefault] - public string TenantPrefix { get; set; } /// /// Access the DataStreamer class for uploading and downloading data /// - public DataStreamer Data => streamer ??= new DataStreamer(this, TenantPrefix); + public DataStreamer Data => streamer ??= new DataStreamer(this); } [Collection("[BINARY_CHUNKS]")] @@ -76,7 +74,7 @@ public class DataStreamer private readonly FileEntity parent; private readonly Type parentType; - private readonly IMongoDatabase db; + private readonly DBContext db; private readonly IMongoCollection chunkCollection; private FileChunk doc; private int chunkSize, readCount; @@ -84,11 +82,11 @@ public class DataStreamer private List dataChunk; private MD5 md5; - internal DataStreamer(FileEntity parent, string tenantPrefix) + internal DataStreamer(FileEntity parent, DBContext db) { this.parent = parent; parentType = parent.GetType(); - db = DB.Database(Cache.GetFullDbName(parentType, tenantPrefix)); + this.db = db; chunkCollection = db.GetCollection(DB.CollectionName()); if (indexedDBs.Add(db.DatabaseNamespace.DatabaseName)) diff --git a/MongoDB.Entities/DB/DB.Collection.cs b/MongoDB.Entities/DB/DB.Collection.cs index c26147333..da292a4d4 100644 --- a/MongoDB.Entities/DB/DB.Collection.cs +++ b/MongoDB.Entities/DB/DB.Collection.cs @@ -39,15 +39,9 @@ public static string CollectionName() where T : IEntity /// The type of entity that will be stored in the created collection /// The options to use for collection creation /// An optional cancellation token - /// An optional session if using within a transaction - /// Optional tenant prefix if using multi-tenancy - public static Task CreateCollectionAsync(Action> options, CancellationToken cancellation = default, IClientSessionHandle session = null, string tenantPrefix = null) where T : IEntity + public static Task CreateCollectionAsync(Action> options, CancellationToken cancellation = default) where T : IEntity { - var opts = new CreateCollectionOptions(); - options(opts); - return session == null - ? Cache.Collection(tenantPrefix).Database.CreateCollectionAsync(Cache.CollectionName, opts, cancellation) - : Cache.Collection(tenantPrefix).Database.CreateCollectionAsync(session, Cache.CollectionName, opts, cancellation); + return Context.CreateCollectionAsync(options, cancellation); } /// @@ -55,32 +49,9 @@ public static Task CreateCollectionAsync(Action> o /// TIP: When deleting a collection, all relationships associated with that entity type is also deleted. /// /// The entity type to drop the collection of - /// An optional session if using within a transaction - /// Optional tenant prefix if using multi-tenancy - public static async Task DropCollectionAsync(IClientSessionHandle session = null, string tenantPrefix = null) where T : IEntity + public static async Task DropCollectionAsync() where T : IEntity { - var tasks = new List(); - var db = Database(tenantPrefix); - var collName = CollectionName(); - var options = new ListCollectionNamesOptions - { - Filter = "{$and:[{name:/~/},{name:/" + collName + "/}]}" - }; - - foreach (var cName in await db.ListCollectionNames(options).ToListAsync().ConfigureAwait(false)) - { - tasks.Add( - session == null - ? db.DropCollectionAsync(cName) - : db.DropCollectionAsync(session, cName)); - } - - tasks.Add( - session == null - ? db.DropCollectionAsync(collName) - : db.DropCollectionAsync(session, collName)); - - await Task.WhenAll(tasks).ConfigureAwait(false); + await Context.DropCollectionAsync(); } } } diff --git a/MongoDB.Entities/DB/DB.File.cs b/MongoDB.Entities/DB/DB.File.cs index 778a872cc..89af177f1 100644 --- a/MongoDB.Entities/DB/DB.File.cs +++ b/MongoDB.Entities/DB/DB.File.cs @@ -9,16 +9,10 @@ public static partial class DB /// Returns a DataStreamer object to enable uploading/downloading file data directly by supplying the ID of the file entity /// /// The file entity type - /// The ID of the file entity - /// Optional tenant prefix if using multi-tenancy - public static DataStreamer File(string ID, string tenantPrefix = null) where T : FileEntity, new() + /// The ID of the file entity + public static DataStreamer File(string ID) where T : FileEntity, new() { - if (!ObjectId.TryParse(ID, out _)) - throw new ArgumentException("The ID passed in is not of the correct format!"); - - return new DataStreamer( - new T() { ID = ID, UploadSuccessful = true }, - tenantPrefix); + return Context.File(ID); } } } diff --git a/MongoDB.Entities/DB/DB.cs b/MongoDB.Entities/DB/DB.cs index de631e0cc..499fbcdc4 100644 --- a/MongoDB.Entities/DB/DB.cs +++ b/MongoDB.Entities/DB/DB.cs @@ -33,11 +33,7 @@ static DB() } //TODO: refactor api - public static DBContext Context { get; set; } - - //key: FullDBName(including tenant prefix - ex: TenantX~DBName) - private static readonly ConcurrentDictionary dbs = new(); - private static IMongoDatabase defaultDb; + public static DBContext Context { get; set; } /// /// Initializes a MongoDB connection with the given connection parameters. diff --git a/MongoDB.Entities/DBContext/DBContext.Collection.cs b/MongoDB.Entities/DBContext/DBContext.Collection.cs index 916db93bc..7399448f3 100644 --- a/MongoDB.Entities/DBContext/DBContext.Collection.cs +++ b/MongoDB.Entities/DBContext/DBContext.Collection.cs @@ -1,5 +1,6 @@ using MongoDB.Driver; using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -15,7 +16,12 @@ public partial class DBContext /// An optional cancellation token public Task CreateCollectionAsync(Action> options, CancellationToken cancellation = default) where T : IEntity { - return DB.CreateCollectionAsync(options, cancellation, Session, tenantPrefix); + var opts = new CreateCollectionOptions(); + options(opts); + return Session == null + ? Database.CreateCollectionAsync(Cache.CollectionName, opts, cancellation) + : Database.CreateCollectionAsync(Session, Cache.CollectionName, opts, cancellation); + } /// @@ -23,9 +29,30 @@ public Task CreateCollectionAsync(Action> options, /// TIP: When deleting a collection, all relationships associated with that entity type is also deleted. /// /// The entity type to drop the collection of - public Task DropCollectionAsync() where T : IEntity + public async Task DropCollectionAsync() where T : IEntity { - return DB.DropCollectionAsync(Session, tenantPrefix); + var tasks = new List(); + var db = Database; + var collName = Cache.CollectionName; + var options = new ListCollectionNamesOptions + { + Filter = "{$and:[{name:/~/},{name:/" + collName + "/}]}" + }; + + foreach (var cName in await db.ListCollectionNames(options).ToListAsync().ConfigureAwait(false)) + { + tasks.Add( + Session == null + ? db.DropCollectionAsync(cName) + : db.DropCollectionAsync(Session, cName)); + } + + tasks.Add( + Session == null + ? db.DropCollectionAsync(collName) + : db.DropCollectionAsync(Session, collName)); + + await Task.WhenAll(tasks).ConfigureAwait(false); } } } diff --git a/MongoDB.Entities/DBContext/DBContext.Count.cs b/MongoDB.Entities/DBContext/DBContext.Count.cs index eae348754..ba593a507 100644 --- a/MongoDB.Entities/DBContext/DBContext.Count.cs +++ b/MongoDB.Entities/DBContext/DBContext.Count.cs @@ -30,7 +30,7 @@ public Task CountEstimatedAsync(CancellationToken cancellation = defaul public Task CountAsync(Expression> expression, CancellationToken cancellation = default, CountOptions options = null, bool ignoreGlobalFilters = false) where T : IEntity { return DB.CountAsync( - Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, expression), + Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, expression), Session, cancellation, options, @@ -58,7 +58,7 @@ public Task CountAsync(CancellationToken cancellation = default) where public Task CountAsync(FilterDefinition filter, CancellationToken cancellation = default, CountOptions options = null, bool ignoreGlobalFilters = false) where T : IEntity { return DB.CountAsync( - Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, filter), + Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, filter), Session, cancellation, options, @@ -76,7 +76,7 @@ public Task CountAsync(FilterDefinition filter, CancellationToken ca public Task CountAsync(Func, FilterDefinition> filter, CancellationToken cancellation = default, CountOptions options = null, bool ignoreGlobalFilters = false) where T : IEntity { return DB.CountAsync( - Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, filter(Builders.Filter)), + Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, filter(Builders.Filter)), Session, cancellation, options, diff --git a/MongoDB.Entities/DBContext/DBContext.Delete.cs b/MongoDB.Entities/DBContext/DBContext.Delete.cs index edaa9062a..e5567f1c9 100644 --- a/MongoDB.Entities/DBContext/DBContext.Delete.cs +++ b/MongoDB.Entities/DBContext/DBContext.Delete.cs @@ -20,7 +20,7 @@ public partial class DBContext public Task DeleteAsync(string ID, CancellationToken cancellation = default, bool ignoreGlobalFilters = false) where T : IEntity { return DB.DeleteAsync( - Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, Builders.Filter.Eq(e => e.ID, ID)), + Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, Builders.Filter.Eq(e => e.ID, ID)), Session, cancellation, tenantPrefix: tenantPrefix); @@ -38,7 +38,7 @@ public Task DeleteAsync(string ID, CancellationToken cancellati public Task DeleteAsync(IEnumerable IDs, CancellationToken cancellation = default, bool ignoreGlobalFilters = false) where T : IEntity { return DB.DeleteAsync( - Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, Builders.Filter.In(e => e.ID, IDs)), + Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, Builders.Filter.In(e => e.ID, IDs)), Session, cancellation, tenantPrefix: tenantPrefix); @@ -57,7 +57,7 @@ public Task DeleteAsync(IEnumerable IDs, CancellationTo public Task DeleteAsync(Expression> expression, CancellationToken cancellation = default, Collation collation = null, bool ignoreGlobalFilters = false) where T : IEntity { return DB.DeleteAsync( - Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, Builders.Filter.Where(expression)), + Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, Builders.Filter.Where(expression)), Session, cancellation, collation, @@ -77,7 +77,7 @@ public Task DeleteAsync(Expression> expression, C public Task DeleteAsync(Func, FilterDefinition> filter, CancellationToken cancellation = default, Collation collation = null, bool ignoreGlobalFilters = false) where T : IEntity { return DB.DeleteAsync( - Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, filter(Builders.Filter)), + Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, filter(Builders.Filter)), Session, cancellation, collation, @@ -97,7 +97,7 @@ public Task DeleteAsync(Func, Filter public Task DeleteAsync(FilterDefinition filter, CancellationToken cancellation = default, Collation collation = null, bool ignoreGlobalFilters = false) where T : IEntity { return DB.DeleteAsync( - Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, filter), + Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, filter), Session, cancellation, collation, diff --git a/MongoDB.Entities/DBContext/DBContext.Distinct.cs b/MongoDB.Entities/DBContext/DBContext.Distinct.cs index 6507371f1..c6eaa71a8 100644 --- a/MongoDB.Entities/DBContext/DBContext.Distinct.cs +++ b/MongoDB.Entities/DBContext/DBContext.Distinct.cs @@ -9,7 +9,7 @@ public partial class DBContext /// The type of the property of the entity you'd like to get unique values for public Distinct Distinct() where T : IEntity { - return new Distinct(Session, globalFilters, tenantPrefix); + return new Distinct(Session, _globalFilters, tenantPrefix); } } } diff --git a/MongoDB.Entities/DBContext/DBContext.File.cs b/MongoDB.Entities/DBContext/DBContext.File.cs index 34b3ec23f..956066a22 100644 --- a/MongoDB.Entities/DBContext/DBContext.File.cs +++ b/MongoDB.Entities/DBContext/DBContext.File.cs @@ -1,4 +1,7 @@ -namespace MongoDB.Entities +using MongoDB.Bson; +using System; + +namespace MongoDB.Entities { public partial class DBContext { @@ -9,7 +12,13 @@ public partial class DBContext /// The ID of the file entity public DataStreamer File(string ID) where T : FileEntity, new() { - return DB.File(ID, tenantPrefix); + if (!ObjectId.TryParse(ID, out _)) + throw new ArgumentException("The ID passed in is not of the correct format!"); + + return new DataStreamer( + new T() { ID = ID, UploadSuccessful = true }, + tenantPrefix); + return File(ID); } } } diff --git a/MongoDB.Entities/DBContext/DBContext.Find.cs b/MongoDB.Entities/DBContext/DBContext.Find.cs index a3d7d957b..7a1f9fe35 100644 --- a/MongoDB.Entities/DBContext/DBContext.Find.cs +++ b/MongoDB.Entities/DBContext/DBContext.Find.cs @@ -8,7 +8,7 @@ public partial class DBContext /// The type of entity public Find Find() where T : IEntity { - return new Find(Session, globalFilters, tenantPrefix); + return new Find(Session, _globalFilters, tenantPrefix); } /// @@ -18,7 +18,7 @@ public Find Find() where T : IEntity /// The type of the end result public Find Find() where T : IEntity { - return new Find(Session, globalFilters, tenantPrefix); + return new Find(Session, _globalFilters, tenantPrefix); } } } diff --git a/MongoDB.Entities/DBContext/DBContext.Fluent.cs b/MongoDB.Entities/DBContext/DBContext.Fluent.cs index 9b4f9a86b..fbfda43ae 100644 --- a/MongoDB.Entities/DBContext/DBContext.Fluent.cs +++ b/MongoDB.Entities/DBContext/DBContext.Fluent.cs @@ -12,7 +12,7 @@ public partial class DBContext /// Set to true if you'd like to ignore any global filters for this operation public IAggregateFluent Fluent(AggregateOptions options = null, bool ignoreGlobalFilters = false) where T : IEntity { - var globalFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, Builders.Filter.Empty); + var globalFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, Builders.Filter.Empty); if (globalFilter != Builders.Filter.Empty) { @@ -37,7 +37,7 @@ public IAggregateFluent Fluent(AggregateOptions options = null, bool ignor /// Set to true if you'd like to ignore any global filters for this operation public IAggregateFluent FluentTextSearch(Search searchType, string searchTerm, bool caseSensitive = false, bool diacriticSensitive = false, string language = null, AggregateOptions options = null, bool ignoreGlobalFilters = false) where T : IEntity { - var globalFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, Builders.Filter.Empty); + var globalFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, Builders.Filter.Empty); if (globalFilter != Builders.Filter.Empty) { diff --git a/MongoDB.Entities/DBContext/DBContext.GeoNear.cs b/MongoDB.Entities/DBContext/DBContext.GeoNear.cs index d944fba74..87bed9b59 100644 --- a/MongoDB.Entities/DBContext/DBContext.GeoNear.cs +++ b/MongoDB.Entities/DBContext/DBContext.GeoNear.cs @@ -25,7 +25,7 @@ public partial class DBContext /// Set to true if you'd like to ignore any global filters for this operation public IAggregateFluent GeoNear(Coordinates2D NearCoordinates, Expression> DistanceField, bool Spherical = true, int? MaxDistance = null, int? MinDistance = null, int? Limit = null, BsonDocument Query = null, int? DistanceMultiplier = null, Expression> IncludeLocations = null, string IndexKey = null, AggregateOptions options = null, bool ignoreGlobalFilters = false) where T : IEntity { - var globalFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, Builders.Filter.Empty); + var globalFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, Builders.Filter.Empty); if (globalFilter != Builders.Filter.Empty) { diff --git a/MongoDB.Entities/DBContext/DBContext.IMongoDatabase.cs b/MongoDB.Entities/DBContext/DBContext.IMongoDatabase.cs new file mode 100644 index 000000000..73a2cdbea --- /dev/null +++ b/MongoDB.Entities/DBContext/DBContext.IMongoDatabase.cs @@ -0,0 +1,234 @@ +using MongoDB.Bson.Serialization; +using MongoDB.Driver; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +#nullable enable +namespace MongoDB.Entities +{ + public partial class DBContext + { + IAsyncCursor IMongoDatabase.Aggregate(PipelineDefinition pipeline, AggregateOptions options, CancellationToken cancellationToken) + { + return Database.Aggregate(pipeline, options, cancellationToken); + } + + IAsyncCursor IMongoDatabase.Aggregate(IClientSessionHandle session, PipelineDefinition pipeline, AggregateOptions options, CancellationToken cancellationToken) + { + return Database.Aggregate(session, pipeline, options, cancellationToken); + } + + Task> IMongoDatabase.AggregateAsync(PipelineDefinition pipeline, AggregateOptions options, CancellationToken cancellationToken) + { + return Database.AggregateAsync(pipeline, options, cancellationToken); + } + + Task> IMongoDatabase.AggregateAsync(IClientSessionHandle session, PipelineDefinition pipeline, AggregateOptions options, CancellationToken cancellationToken) + { + return Database.AggregateAsync(session, pipeline, options, cancellationToken); + } + + public void AggregateToCollection(PipelineDefinition pipeline, AggregateOptions? options = null, CancellationToken cancellationToken = default) + { + Database.AggregateToCollection(pipeline, options, cancellationToken); + } + + public void AggregateToCollection(IClientSessionHandle session, PipelineDefinition pipeline, AggregateOptions? options = null, CancellationToken cancellationToken = default) + { + Database.AggregateToCollection(session, pipeline, options, cancellationToken); + } + + public Task AggregateToCollectionAsync(PipelineDefinition pipeline, AggregateOptions? options = null, CancellationToken cancellationToken = default) + { + return Database.AggregateToCollectionAsync(pipeline, options, cancellationToken); + } + + public Task AggregateToCollectionAsync(IClientSessionHandle session, PipelineDefinition pipeline, AggregateOptions? options = null, CancellationToken cancellationToken = default) + { + return Database.AggregateToCollectionAsync(session, pipeline, options, cancellationToken); + } + + public void CreateCollection(string name, CreateCollectionOptions? options = null, CancellationToken cancellationToken = default) + { + Database.CreateCollection(name, options, cancellationToken); + } + + public void CreateCollection(IClientSessionHandle session, string name, CreateCollectionOptions options, CancellationToken cancellationToken) + { + Database.CreateCollection(session, name, options, cancellationToken); + } + + public Task CreateCollectionAsync(string name, CreateCollectionOptions options, CancellationToken cancellationToken) + { + return Database.CreateCollectionAsync(name, options, cancellationToken); + } + + public Task CreateCollectionAsync(IClientSessionHandle session, string name, CreateCollectionOptions options, CancellationToken cancellationToken) + { + return Database.CreateCollectionAsync(session, name, options, cancellationToken); + } + + public void CreateView(string viewName, string viewOn, PipelineDefinition pipeline, CreateViewOptions options, CancellationToken cancellationToken) + { + Database.CreateView(viewName, viewOn, pipeline, options, cancellationToken); + } + + public void CreateView(IClientSessionHandle session, string viewName, string viewOn, PipelineDefinition pipeline, CreateViewOptions options, CancellationToken cancellationToken) + { + Database.CreateView(session, viewName, viewOn, pipeline, options, cancellationToken); + } + + public Task CreateViewAsync(string viewName, string viewOn, PipelineDefinition pipeline, CreateViewOptions options, CancellationToken cancellationToken) + { + return Database.CreateViewAsync(viewName, viewOn, pipeline, options, cancellationToken); + } + + public Task CreateViewAsync(IClientSessionHandle session, string viewName, string viewOn, PipelineDefinition pipeline, CreateViewOptions options, CancellationToken cancellationToken) + { + return Database.CreateViewAsync(session, viewName, viewOn, pipeline, options, cancellationToken); + } + + public void DropCollection(string name, CancellationToken cancellationToken = default) + { + Database.DropCollection(name, cancellationToken); + } + + public void DropCollection(IClientSessionHandle session, string name, CancellationToken cancellationToken = default) + { + Database.DropCollection(session, name, cancellationToken); + } + + public Task DropCollectionAsync(string name, CancellationToken cancellationToken = default) + { + return Database.DropCollectionAsync(name, cancellationToken); + } + + public Task DropCollectionAsync(IClientSessionHandle session, string name, CancellationToken cancellationToken = default) + { + return Database.DropCollectionAsync(session, name, cancellationToken); + } + + public IMongoCollection GetCollection(string name, MongoCollectionSettings? settings = null) + { + return Database.GetCollection(name, settings); + } + + IAsyncCursor IMongoDatabase.ListCollectionNames(ListCollectionNamesOptions options, CancellationToken cancellationToken) + { + return Database.ListCollectionNames(options, cancellationToken); + } + + IAsyncCursor IMongoDatabase.ListCollectionNames(IClientSessionHandle session, ListCollectionNamesOptions options, CancellationToken cancellationToken) + { + return Database.ListCollectionNames(session, options, cancellationToken); + } + + Task> IMongoDatabase.ListCollectionNamesAsync(ListCollectionNamesOptions options, CancellationToken cancellationToken) + { + return Database.ListCollectionNamesAsync(options, cancellationToken); + } + + Task> IMongoDatabase.ListCollectionNamesAsync(IClientSessionHandle session, ListCollectionNamesOptions options, CancellationToken cancellationToken) + { + return Database.ListCollectionNamesAsync(session, options, cancellationToken); + } + + IAsyncCursor IMongoDatabase.ListCollections(ListCollectionsOptions options, CancellationToken cancellationToken) + { + return Database.ListCollections(options, cancellationToken); + } + + IAsyncCursor IMongoDatabase.ListCollections(IClientSessionHandle session, ListCollectionsOptions options, CancellationToken cancellationToken) + { + return Database.ListCollections(session, options, cancellationToken); + } + + Task> IMongoDatabase.ListCollectionsAsync(ListCollectionsOptions options, CancellationToken cancellationToken) + { + return Database.ListCollectionsAsync(options, cancellationToken); + } + + Task> IMongoDatabase.ListCollectionsAsync(IClientSessionHandle session, ListCollectionsOptions options, CancellationToken cancellationToken) + { + return Database.ListCollectionsAsync(session, options, cancellationToken); + } + + void IMongoDatabase.RenameCollection(string oldName, string newName, RenameCollectionOptions options, CancellationToken cancellationToken) + { + Database.RenameCollection(oldName, newName, options, cancellationToken); + } + + void IMongoDatabase.RenameCollection(IClientSessionHandle session, string oldName, string newName, RenameCollectionOptions options, CancellationToken cancellationToken) + { + Database.RenameCollection(session, oldName, newName, options, cancellationToken); + } + + Task IMongoDatabase.RenameCollectionAsync(string oldName, string newName, RenameCollectionOptions options, CancellationToken cancellationToken) + { + return Database.RenameCollectionAsync(oldName, newName, options, cancellationToken); + } + + Task IMongoDatabase.RenameCollectionAsync(IClientSessionHandle session, string oldName, string newName, RenameCollectionOptions options, CancellationToken cancellationToken) + { + return Database.RenameCollectionAsync(session, oldName, newName, options, cancellationToken); + } + + TResult IMongoDatabase.RunCommand(Command command, ReadPreference readPreference, CancellationToken cancellationToken) + { + return Database.RunCommand(command, readPreference, cancellationToken); + } + + TResult IMongoDatabase.RunCommand(IClientSessionHandle session, Command command, ReadPreference readPreference, CancellationToken cancellationToken) + { + return Database.RunCommand(session, command, readPreference, cancellationToken); + } + + Task IMongoDatabase.RunCommandAsync(Command command, ReadPreference readPreference, CancellationToken cancellationToken) + { + return Database.RunCommandAsync(command, readPreference, cancellationToken); + } + + Task IMongoDatabase.RunCommandAsync(IClientSessionHandle session, Command command, ReadPreference readPreference, CancellationToken cancellationToken) + { + return Database.RunCommandAsync(session, command, readPreference, cancellationToken); + } + + IChangeStreamCursor IMongoDatabase.Watch(PipelineDefinition, TResult> pipeline, ChangeStreamOptions options, CancellationToken cancellationToken) + { + return Database.Watch(pipeline, options, cancellationToken); + } + + IChangeStreamCursor IMongoDatabase.Watch(IClientSessionHandle session, PipelineDefinition, TResult> pipeline, ChangeStreamOptions options, CancellationToken cancellationToken) + { + return Database.Watch(session, pipeline, options, cancellationToken); + } + + Task> IMongoDatabase.WatchAsync(PipelineDefinition, TResult> pipeline, ChangeStreamOptions options, CancellationToken cancellationToken) + { + return Database.WatchAsync(pipeline, options, cancellationToken); + } + + Task> IMongoDatabase.WatchAsync(IClientSessionHandle session, PipelineDefinition, TResult> pipeline, ChangeStreamOptions options, CancellationToken cancellationToken) + { + return Database.WatchAsync(session, pipeline, options, cancellationToken); + } + + IMongoDatabase IMongoDatabase.WithReadConcern(ReadConcern readConcern) + { + return Database.WithReadConcern(readConcern); + } + + IMongoDatabase IMongoDatabase.WithReadPreference(ReadPreference readPreference) + { + return Database.WithReadPreference(readPreference); + } + + IMongoDatabase IMongoDatabase.WithWriteConcern(WriteConcern writeConcern) + { + return Database.WithWriteConcern(writeConcern); + } + } +} diff --git a/MongoDB.Entities/DBContext/DBContext.PagedSearch.cs b/MongoDB.Entities/DBContext/DBContext.PagedSearch.cs index ced6c1e30..99818fb0c 100644 --- a/MongoDB.Entities/DBContext/DBContext.PagedSearch.cs +++ b/MongoDB.Entities/DBContext/DBContext.PagedSearch.cs @@ -8,7 +8,7 @@ public partial class DBContext /// Any class that implements IEntity public PagedSearch PagedSearch() where T : IEntity { - return new PagedSearch(Session, globalFilters, tenantPrefix); + return new PagedSearch(Session, _globalFilters, tenantPrefix); } /// @@ -18,7 +18,7 @@ public PagedSearch PagedSearch() where T : IEntity /// The type you'd like to project the results to. public PagedSearch PagedSearch() where T : IEntity { - return new PagedSearch(Session, globalFilters, tenantPrefix); + return new PagedSearch(Session, _globalFilters, tenantPrefix); } } } diff --git a/MongoDB.Entities/DBContext/DBContext.Pipeline.cs b/MongoDB.Entities/DBContext/DBContext.Pipeline.cs index c160b2351..6ea5e01f7 100644 --- a/MongoDB.Entities/DBContext/DBContext.Pipeline.cs +++ b/MongoDB.Entities/DBContext/DBContext.Pipeline.cs @@ -74,7 +74,7 @@ private Template MergeGlobalFilter(Template //WARNING: this has to do the same thing as Logic.MergeGlobalFilter method // if the following logic changes, update the other method also - if (!ignoreGlobalFilters && globalFilters.Count > 0 && globalFilters.TryGetValue(typeof(T), out var gFilter)) + if (!ignoreGlobalFilters && _globalFilters.Count > 0 && _globalFilters.TryGetValue(typeof(T), out var gFilter)) { BsonDocument filter = null; diff --git a/MongoDB.Entities/DBContext/DBContext.Queryable.cs b/MongoDB.Entities/DBContext/DBContext.Queryable.cs index 74a88a6a6..6dda35e32 100644 --- a/MongoDB.Entities/DBContext/DBContext.Queryable.cs +++ b/MongoDB.Entities/DBContext/DBContext.Queryable.cs @@ -13,7 +13,7 @@ public partial class DBContext /// Set to true if you'd like to ignore any global filters for this operation public IMongoQueryable Queryable(AggregateOptions options = null, bool ignoreGlobalFilters = false) where T : IEntity { - var globalFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, Builders.Filter.Empty); + var globalFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, Builders.Filter.Empty); if (globalFilter != Builders.Filter.Empty) { diff --git a/MongoDB.Entities/DBContext/DBContext.Replace.cs b/MongoDB.Entities/DBContext/DBContext.Replace.cs index 9ef9d9511..f8eb9e984 100644 --- a/MongoDB.Entities/DBContext/DBContext.Replace.cs +++ b/MongoDB.Entities/DBContext/DBContext.Replace.cs @@ -10,7 +10,7 @@ public partial class DBContext public Replace Replace() where T : IEntity { ThrowIfModifiedByIsEmpty(); - return new Replace(Session, ModifiedBy, globalFilters, OnBeforeSave(), tenantPrefix); + return new Replace(Session, ModifiedBy, _globalFilters, OnBeforeSave(), tenantPrefix); } } } diff --git a/MongoDB.Entities/DBContext/DBContext.Update.cs b/MongoDB.Entities/DBContext/DBContext.Update.cs index 785d4ed7d..e7ef641d0 100644 --- a/MongoDB.Entities/DBContext/DBContext.Update.cs +++ b/MongoDB.Entities/DBContext/DBContext.Update.cs @@ -8,7 +8,7 @@ public partial class DBContext /// The type of entity public Update Update() where T : IEntity { - var cmd = new Update(Session, globalFilters, OnBeforeUpdate(), tenantPrefix); + var cmd = new Update(Session, _globalFilters, OnBeforeUpdate(), tenantPrefix); if (Cache.ModifiedByProp != null) { ThrowIfModifiedByIsEmpty(); @@ -33,7 +33,7 @@ public UpdateAndGet UpdateAndGet() where T : IEntity /// The type of the end result public UpdateAndGet UpdateAndGet() where T : IEntity { - var cmd = new UpdateAndGet(Session, globalFilters, OnBeforeUpdate(), tenantPrefix); + var cmd = new UpdateAndGet(Session, _globalFilters, OnBeforeUpdate(), tenantPrefix); if (Cache.ModifiedByProp != null) { ThrowIfModifiedByIsEmpty(); diff --git a/MongoDB.Entities/DBContext/DBContext.cs b/MongoDB.Entities/DBContext/DBContext.cs index 84472cecd..1aea7d8b9 100644 --- a/MongoDB.Entities/DBContext/DBContext.cs +++ b/MongoDB.Entities/DBContext/DBContext.cs @@ -4,46 +4,71 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; - +using System.Threading; +using System.Threading.Tasks; +#nullable enable namespace MongoDB.Entities { /// /// This db context class can be used as an alternative entry point instead of the DB static class. /// - public partial class DBContext + public partial class DBContext : IMongoDatabase { /// /// Returns the session object used for transactions /// - public IClientSessionHandle Session { get; protected set; } + public IClientSessionHandle? Session { get; protected set; } - public MongoContext MongoContext { get; } + public MongoContext MongoContext { get; set; } + public IMongoDatabase Database { get; set; } + public DBContextOptions Options { get; set; } /// /// wrapper around so that we don't break the public api /// - public ModifiedBy ModifiedBy + public ModifiedBy? ModifiedBy { get { return MongoContext.ModifiedBy; } + [Obsolete("Use MongoContext.Options.ModifiedBy = value instead")] set { MongoContext.Options.ModifiedBy = value; } } + public IMongoClient Client => MongoContext; + + public DatabaseNamespace DatabaseNamespace => Database.DatabaseNamespace; + public MongoDatabaseSettings Settings => Database.Settings; - protected string tenantPrefix; - private static Type[] allEntitiyTypes; - private Dictionary globalFilters; + private Dictionary _globalFilters = new(); - public DBContext(MongoContext mongoContext) + /// + /// Copy constructor + /// + /// + public DBContext(DBContext other) + { + MongoContext = other.MongoContext; + Database = other.Database; + Options = other.Options; + } + public DBContext(MongoContext mongoContext, IMongoDatabase database, DBContextOptions? options = null) { MongoContext = mongoContext; + Database = database; + Options = options ?? new(); + } + public DBContext(MongoContext mongoContext, string database, MongoDatabaseSettings? settings = null, DBContextOptions? options = null) + { + MongoContext = mongoContext; + Database = mongoContext.GetDatabase(database, settings); + Options = options ?? new(); } /// @@ -57,16 +82,20 @@ public DBContext(MongoContext mongoContext) /// When supplied, all save/update operations performed via this DBContext instance will set the value on entities that has a property of type ModifiedBy. /// You can even inherit from the ModifiedBy class and add your own properties to it. /// Only one ModifiedBy property is allowed on a single entity type. - public DBContext(string database, string host = "127.0.0.1", int port = 27017, ModifiedBy modifiedBy = null) + public DBContext(string database, string host = "127.0.0.1", int port = 27017, ModifiedBy? modifiedBy = null) { - DB.Initialize( - new MongoClientSettings { Server = new MongoServerAddress(host, port) }, - database, - true) - .GetAwaiter() - .GetResult(); - - ModifiedBy = modifiedBy; + MongoContext = new MongoContext( + client: new MongoClient( + new MongoClientSettings + { + Server = new MongoServerAddress(host, port) + }), + options: new() + { + ModifiedBy = modifiedBy + }); + Database = MongoContext.GetDatabase(database); + Options = new(); } /// @@ -79,15 +108,19 @@ public DBContext(string database, string host = "127.0.0.1", int port = 27017, M /// When supplied, all save/update operations performed via this DBContext instance will set the value on entities that has a property of type ModifiedBy. /// You can even inherit from the ModifiedBy class and add your own properties to it. /// Only one ModifiedBy property is allowed on a single entity type. - public DBContext(string database, MongoClientSettings settings, ModifiedBy modifiedBy = null) + public DBContext(string database, MongoClientSettings settings, ModifiedBy? modifiedBy = null) { - DB.Initialize(settings, database, true) - .GetAwaiter() - .GetResult(); - - ModifiedBy = modifiedBy; + MongoContext = new MongoContext( + client: new MongoClient(settings), + options: new() + { + ModifiedBy = modifiedBy + }); + Database = MongoContext.GetDatabase(database); + Options = new(); } + /// /// Instantiates a DBContext instance /// TIP: will throw an error if no connections have been initialized @@ -96,16 +129,17 @@ public DBContext(string database, MongoClientSettings settings, ModifiedBy modif /// When supplied, all save/update operations performed via this DBContext instance will set the value on entities that has a property of type ModifiedBy. /// You can even inherit from the ModifiedBy class and add your own properties to it. /// Only one ModifiedBy property is allowed on a single entity type. - public DBContext(ModifiedBy modifiedBy = null) - => ModifiedBy = modifiedBy; - + [Obsolete("This constructor is obsolete, you can only create a DBContext after knowing the database name")] + public DBContext(ModifiedBy? modifiedBy = null) : this("default", modifiedBy: modifiedBy) + { + } /// /// This event hook will be trigged right before an entity is persisted /// /// Any entity that implements IEntity - protected virtual Action OnBeforeSave() where T : IEntity + protected virtual Action? OnBeforeSave() where T : IEntity { return null; } @@ -114,7 +148,7 @@ protected virtual Action OnBeforeSave() where T : IEntity /// This event hook will be triggered right before an update/replace command is executed /// /// Any entity that implements IEntity - protected virtual Action> OnBeforeUpdate() where T : IEntity + protected virtual Action>? OnBeforeUpdate() where T : IEntity { return null; } @@ -197,9 +231,7 @@ protected void SetGlobalFilterForBaseClass(FuncSet to true if you want to prepend this global filter to your operation filters instead of being appended protected void SetGlobalFilterForBaseClass(FilterDefinition filter, bool prepend = false) where TBase : IEntity { - if (allEntitiyTypes is null) allEntitiyTypes = GetAllEntityTypes(); - - foreach (var entType in allEntitiyTypes.Where(t => t.IsSubclassOf(typeof(TBase)))) + foreach (var entType in MongoContext.AllEntitiyTypes.Where(t => t.IsSubclassOf(typeof(TBase)))) { var bsonDoc = filter.Render( BsonSerializer.SerializerRegistry.GetSerializer(), @@ -221,9 +253,8 @@ protected void SetGlobalFilterForInterface(string jsonString, bool p if (!targetType.IsInterface) throw new ArgumentException("Only interfaces are allowed!", "TInterface"); - if (allEntitiyTypes is null) allEntitiyTypes = GetAllEntityTypes(); - foreach (var entType in allEntitiyTypes.Where(t => targetType.IsAssignableFrom(t))) + foreach (var entType in MongoContext.AllEntitiyTypes.Where(t => targetType.IsAssignableFrom(t))) { AddFilter(entType, (jsonString, prepend)); } @@ -231,33 +262,10 @@ protected void SetGlobalFilterForInterface(string jsonString, bool p - private static Type[] GetAllEntityTypes() - { - var excludes = new[] - { - "Microsoft.", - "System.", - "MongoDB.", - "testhost.", - "netstandard", - "Newtonsoft.", - "mscorlib", - "NuGet." - }; - - return AppDomain.CurrentDomain - .GetAssemblies() - .Where(a => - !a.IsDynamic && - (a.FullName.StartsWith("MongoDB.Entities.Tests") || !excludes.Any(n => a.FullName.StartsWith(n)))) - .SelectMany(a => a.GetTypes()) - .Where(t => typeof(IEntity).IsAssignableFrom(t)) - .ToArray(); - } private void ThrowIfModifiedByIsEmpty() where T : IEntity { - if (Cache.ModifiedByProp != null && ModifiedBy is null) + if (Cache().ModifiedByProp != null && ModifiedBy is null) { throw new InvalidOperationException( $"A value for [{Cache.ModifiedByProp.Name}] must be specified when saving/updating entities of type [{Cache.CollectionName}]"); @@ -266,9 +274,9 @@ private void ThrowIfModifiedByIsEmpty() where T : IEntity private void AddFilter(Type type, (object filterDef, bool prepend) filter) { - if (globalFilters is null) globalFilters = new Dictionary(); + if (_globalFilters is null) _globalFilters = new Dictionary(); - globalFilters[type] = filter; + _globalFilters[type] = filter; } } } diff --git a/MongoDB.Entities/MongoContext/MongoContext.IMongoClient.cs b/MongoDB.Entities/MongoContext/MongoContext.IMongoClient.cs index 879166f26..90abdd0c7 100644 --- a/MongoDB.Entities/MongoContext/MongoContext.IMongoClient.cs +++ b/MongoDB.Entities/MongoContext/MongoContext.IMongoClient.cs @@ -36,7 +36,7 @@ Task IMongoClient.DropDatabaseAsync(IClientSessionHandle session, string name, C return Client.DropDatabaseAsync(session, name, cancellationToken); } - IMongoDatabase IMongoClient.GetDatabase(string name, MongoDatabaseSettings settings) + public IMongoDatabase GetDatabase(string name, MongoDatabaseSettings settings = null) { return Client.GetDatabase(name, settings); } @@ -61,13 +61,6 @@ IAsyncCursor IMongoClient.ListDatabaseNames(IClientSessionHandle session return Client.ListDatabaseNames(session, options, cancellationToken); } - public async Task> AllDatabaseNamesAsync() - { - return await (await - ((IMongoClient)this) - .ListDatabaseNamesAsync().ConfigureAwait(false)) - .ToListAsync().ConfigureAwait(false); - } Task> IMongoClient.ListDatabaseNamesAsync(CancellationToken cancellationToken) { return Client.ListDatabaseNamesAsync(cancellationToken); diff --git a/MongoDB.Entities/MongoContext/MongoContext.cs b/MongoDB.Entities/MongoContext/MongoContext.cs index 7a705074e..a10bd78c2 100644 --- a/MongoDB.Entities/MongoContext/MongoContext.cs +++ b/MongoDB.Entities/MongoContext/MongoContext.cs @@ -1,5 +1,9 @@ using MongoDB.Driver; +using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; #nullable enable namespace MongoDB.Entities { @@ -28,6 +32,52 @@ public MongoContext(IMongoClient client, MongoContextOptions? options = null) /// public ModifiedBy? ModifiedBy => Options.ModifiedBy; + + public async Task> AllDatabaseNamesAsync() + { + return await (await + ((IMongoClient)this) + .ListDatabaseNamesAsync().ConfigureAwait(false)) + .ToListAsync().ConfigureAwait(false); + } + + private Type[]? _allEntitiyTypes; + public Type[] AllEntitiyTypes => _allEntitiyTypes ??= GetAllEntityTypes(); + + private readonly ConcurrentDictionary _cache = new(); + internal Cache Cache() where T : IEntity + { + if (!_cache.TryGetValue(typeof(T), out var c)) + { + c = new Cache(); + } + return (Cache)c; + } + + private static Type[] GetAllEntityTypes() + { + var excludes = new[] + { + "Microsoft.", + "System.", + "MongoDB.", + "testhost.", + "netstandard", + "Newtonsoft.", + "mscorlib", + "NuGet." + }; + + return AppDomain.CurrentDomain + .GetAssemblies() + .Where(a => + !a.IsDynamic && + (a.FullName.StartsWith("MongoDB.Entities.Tests") || !excludes.Any(n => a.FullName.StartsWith(n)))) + .SelectMany(a => a.GetTypes()) + .Where(t => typeof(IEntity).IsAssignableFrom(t)) + .ToArray(); + } + } } diff --git a/MongoDB.Entities/MongoDB.Entities.csproj b/MongoDB.Entities/MongoDB.Entities.csproj index a22fa9a34..c038c070e 100644 --- a/MongoDB.Entities/MongoDB.Entities.csproj +++ b/MongoDB.Entities/MongoDB.Entities.csproj @@ -11,7 +11,7 @@ - upgrade dependencies - netstandard2.0 + netstandard2.1 MongoDB.Entities MongoDB.Entities Đĵ ΝιΓΞΗΛψΚ From e4d479a12e7db395ef6acb2b32e67517a1a1a26d Mon Sep 17 00:00:00 2001 From: Ahmed Fwela Date: Fri, 5 Nov 2021 09:56:14 +0200 Subject: [PATCH 05/26] revert back TargetFramework --- MongoDB.Entities/MongoDB.Entities.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MongoDB.Entities/MongoDB.Entities.csproj b/MongoDB.Entities/MongoDB.Entities.csproj index c038c070e..a22fa9a34 100644 --- a/MongoDB.Entities/MongoDB.Entities.csproj +++ b/MongoDB.Entities/MongoDB.Entities.csproj @@ -11,7 +11,7 @@ - upgrade dependencies - netstandard2.1 + netstandard2.0 MongoDB.Entities MongoDB.Entities Đĵ ΝιΓΞΗΛψΚ From c4d43761efc75e7b80e087b139fb1431f8017d3a Mon Sep 17 00:00:00 2001 From: Ahmed Fwela Date: Fri, 5 Nov 2021 14:28:29 +0200 Subject: [PATCH 06/26] Better find --- MongoDB.Entities/Builders/Find.cs | 303 ++++++++++++++++-------------- 1 file changed, 165 insertions(+), 138 deletions(-) diff --git a/MongoDB.Entities/Builders/Find.cs b/MongoDB.Entities/Builders/Find.cs index 6405f7c39..946972f64 100644 --- a/MongoDB.Entities/Builders/Find.cs +++ b/MongoDB.Entities/Builders/Find.cs @@ -7,89 +7,36 @@ using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; - +#nullable enable namespace MongoDB.Entities { - /// - /// Represents a MongoDB Find command. - /// TIP: Specify your criteria using .Match() .Sort() .Skip() .Take() .Project() .Option() methods and finally call .Execute() - /// Note: For building queries, use the DB.Fluent* interfaces - /// - /// Any class that implements IEntity - public class Find : Find where T : IEntity - { - internal Find( - IClientSessionHandle session, - Dictionary globalFilters, string tenantPrefix) - : base(session, globalFilters, tenantPrefix) { } - } - - /// - /// Represents a MongoDB Find command with the ability to project to a different result type. - /// TIP: Specify your criteria using .Match() .Sort() .Skip() .Take() .Project() .Option() methods and finally call .Execute() - /// - /// Any class that implements IEntity - /// The type you'd like to project the results to. - public class Find where T : IEntity + public class FindBase where T : IEntity where TSelf : FindBase { - private FilterDefinition filter = Builders.Filter.Empty; - private readonly List> sorts = new(); - private readonly FindOptions options = new(); - private readonly IClientSessionHandle session; - private readonly Dictionary globalFilters; - private bool ignoreGlobalFilters; - private readonly string tenantPrefix; + internal FilterDefinition _filter = Builders.Filter.Empty; + internal List> _sorts = new(); + internal FindOptions _options = new(); + internal Dictionary _globalFilters; + internal bool _ignoreGlobalFilters; - internal Find( - IClientSessionHandle session, - Dictionary globalFilters, string tenantPrefix) + internal FindBase(FindBase other) : this(globalFilters: other._globalFilters) { - this.session = session; - this.globalFilters = globalFilters; - this.tenantPrefix = tenantPrefix; + _filter = other._filter; + _options = other._options; + _sorts = other._sorts; + _ignoreGlobalFilters = other._ignoreGlobalFilters; } - - /// - /// Find a single IEntity by ID - /// - /// The unique ID of an IEntity - /// An optional cancellation token - /// A single entity or null if not found - public Task OneAsync(string ID, CancellationToken cancellation = default) - { - Match(ID); - return ExecuteSingleAsync(cancellation); - } - - /// - /// Find entities by supplying a lambda expression - /// - /// x => x.Property == Value - /// An optional cancellation token - /// A list of Entities - public Task> ManyAsync(Expression> expression, CancellationToken cancellation = default) + internal FindBase(Dictionary globalFilters) { - Match(expression); - return ExecuteAsync(cancellation); + _globalFilters = globalFilters; } - /// - /// Find entities by supplying a filter expression - /// - /// f => f.Eq(x => x.Prop, Value) & f.Gt(x => x.Prop, Value) - /// An optional cancellation token - /// A list of Entities - public Task> ManyAsync(Func, FilterDefinition> filter, CancellationToken cancellation = default) - { - Match(filter); - return ExecuteAsync(cancellation); - } + private TSelf This => (TSelf)this; /// /// Specify an IEntity ID as the matching criteria /// /// A unique IEntity ID - public Find MatchID(string ID) + public TSelf MatchID(string ID) { return Match(f => f.Eq(t => t.ID, ID)); } @@ -98,7 +45,7 @@ public Find MatchID(string ID) /// Specify an IEntity ID as the matching criteria /// /// A unique IEntity ID - public Find Match(string ID) + public TSelf Match(string ID) { return Match(f => f.Eq(t => t.ID, ID)); } @@ -107,7 +54,7 @@ public Find Match(string ID) /// Specify the matching criteria with a lambda expression /// /// x => x.Property == Value - public Find Match(Expression> expression) + public TSelf Match(Expression> expression) { return Match(f => f.Where(expression)); } @@ -116,30 +63,30 @@ public Find Match(Expression> expression) /// Specify the matching criteria with a filter expression /// /// f => f.Eq(x => x.Prop, Value) & f.Gt(x => x.Prop, Value) - public Find Match(Func, FilterDefinition> filter) + public TSelf Match(Func, FilterDefinition> filter) { - this.filter &= filter(Builders.Filter); - return this; + _filter &= filter(Builders.Filter); + return This; } /// /// Specify the matching criteria with a filter definition /// /// A filter definition - public Find Match(FilterDefinition filterDefinition) + public TSelf Match(FilterDefinition filterDefinition) { - filter &= filterDefinition; - return this; + _filter &= filterDefinition; + return This; } /// /// Specify the matching criteria with a template /// /// A Template with a find query - public Find Match(Template template) + public TSelf Match(Template template) { - filter &= template.RenderToString(); - return this; + _filter &= template.RenderToString(); + return This; } /// @@ -151,7 +98,7 @@ public Find Match(Template template) /// Case sensitivity of the search (optional) /// Diacritic sensitivity of the search (optional) /// The language for the search (optional) - public Find Match(Search searchType, string searchTerm, bool caseSensitive = false, bool diacriticSensitive = false, string language = null) + public TSelf Match(Search searchType, string searchTerm, bool caseSensitive = false, bool diacriticSensitive = false, string? language = null) { if (searchType == Search.Fuzzy) { @@ -181,7 +128,7 @@ public Find Match(Search searchType, string searchTerm, bool cas /// The search point /// Maximum distance in meters from the search point /// Minimum distance in meters from the search point - public Find Match(Expression> coordinatesProperty, Coordinates2D nearCoordinates, double? maxDistance = null, double? minDistance = null) + public TSelf Match(Expression> coordinatesProperty, Coordinates2D nearCoordinates, double? maxDistance = null, double? minDistance = null) { return Match(f => f.Near(coordinatesProperty, nearCoordinates.ToGeoJsonPoint(), maxDistance, minDistance)); } @@ -190,30 +137,29 @@ public Find Match(Expression> coordinatesPropert /// Specify the matching criteria with a JSON string /// /// { Title : 'The Power Of Now' } - public Find MatchString(string jsonString) + public TSelf MatchString(string jsonString) { - filter &= jsonString; - return this; + _filter &= jsonString; + return This; } /// /// Specify the matching criteria with an aggregation expression (i.e. $expr) /// /// { $gt: ['$Property1', '$Property2'] } - public Find MatchExpression(string expression) + public TSelf MatchExpression(string expression) { - filter &= "{$expr:" + expression + "}"; - return this; + _filter &= "{$expr:" + expression + "}"; + return This; } /// /// Specify the matching criteria with a Template /// /// A Template object - public Find MatchExpression(Template template) + public TSelf MatchExpression(Template template) { - filter &= "{$expr:" + template.RenderToString() + "}"; - return this; + return MatchExpression(template.RenderToString()); } /// @@ -221,13 +167,13 @@ public Find MatchExpression(Template template) /// /// x => x.Prop /// The sort order - public Find Sort(Expression> propertyToSortBy, Order sortOrder) + public TSelf Sort(Expression> propertyToSortBy, Order sortOrder) { return sortOrder switch { Order.Ascending => Sort(s => s.Ascending(propertyToSortBy)), Order.Descending => Sort(s => s.Descending(propertyToSortBy)), - _ => this, + _ => This, }; } @@ -235,7 +181,7 @@ public Find Sort(Expression> propertyToSortBy, O /// Sort the results of a text search by the MetaTextScore /// TIP: Use this method after .Project() if you need to do a projection also /// - public Find SortByTextScore() + public TSelf SortByTextScore() { return SortByTextScore(null); } @@ -245,7 +191,7 @@ public Find SortByTextScore() /// TIP: Use this method after .Project() if you need to do a projection also /// /// x => x.TextScoreProp - public Find SortByTextScore(Expression> scoreProperty) + public TSelf SortByTextScore(Expression>? scoreProperty) { switch (scoreProperty) { @@ -264,37 +210,37 @@ public Find SortByTextScore(Expression> scorePro /// /// s => s.Ascending("Prop1").MetaTextScore("Prop2") /// - public Find Sort(Func, SortDefinition> sortFunction) + public TSelf Sort(Func, SortDefinition> sortFunction) { - sorts.Add(sortFunction(Builders.Sort)); - return this; + _sorts.Add(sortFunction(Builders.Sort)); + return This; } /// /// Specify how many entities to skip /// /// The number to skip - public Find Skip(int skipCount) + public TSelf Skip(int skipCount) { - options.Skip = skipCount; - return this; + _options.Skip = skipCount; + return This; } /// /// Specify how many entities to Take/Limit /// /// The number to limit/take - public Find Limit(int takeCount) + public TSelf Limit(int takeCount) { - options.Limit = takeCount; - return this; + _options.Limit = takeCount; + return This; } /// /// Specify how to project the results using a lambda expression /// /// x => new Test { PropName = x.Prop } - public Find Project(Expression> expression) + public TSelf Project(Expression> expression) { return Project(p => p.Expression(expression)); } @@ -303,22 +249,22 @@ public Find Project(Expression> expression) /// Specify how to project the results using a projection expression /// /// p => p.Include("Prop1").Exclude("Prop2") - public Find Project(Func, ProjectionDefinition> projection) + public TSelf Project(Func, ProjectionDefinition> projection) { - options.Projection = projection(Builders.Projection); - return this; + _options.Projection = projection(Builders.Projection); + return This; } /// /// Specify how to project the results using an exclusion projection expression. /// /// x => new { x.PropToExclude, x.AnotherPropToExclude } - public Find ProjectExcluding(Expression> exclusion) + public TSelf ProjectExcluding(Expression> exclusion) { var props = (exclusion.Body as NewExpression)?.Arguments .Select(a => a.ToString().Split('.')[1]); - if (!props.Any()) + if (props == null || !props.Any()) throw new ArgumentException("Unable to get any properties from the exclusion expression!"); var defs = new List>(props.Count()); @@ -328,41 +274,133 @@ public Find ProjectExcluding(Expression> exclusi defs.Add(Builders.Projection.Exclude(prop)); } - options.Projection = Builders.Projection.Combine(defs); + _options.Projection = Builders.Projection.Combine(defs); - return this; + return This; } /// /// Specify to automatically include all properties marked with [BsonRequired] attribute on the entity in the final projection. /// HINT: this method should only be called after the .Project() method. /// - public Find IncludeRequiredProps() + public TSelf IncludeRequiredProps() { if (typeof(T) != typeof(TProjection)) throw new InvalidOperationException("IncludeRequiredProps() cannot be used when projecting to a different type."); - options.Projection = Cache.CombineWithRequiredProps(options.Projection); - return this; + _options.Projection = Cache.Instance.CombineWithRequiredProps(_options.Projection); + return This; } /// /// Specify an option for this find command (use multiple times if needed) /// /// x => x.OptionName = OptionValue - public Find Option(Action> option) + public TSelf Option(Action> option) { - option(options); - return this; + option(_options); + return This; } /// /// Specify that this operation should ignore any global filters /// - public Find IgnoreGlobalFilters() + public TSelf IgnoreGlobalFilters() { - ignoreGlobalFilters = true; - return this; + _ignoreGlobalFilters = true; + return This; + } + + + private void AddTxtScoreToProjection(string propName) + { + if (_options.Projection == null) _options.Projection = "{}"; + + _options.Projection = + _options.Projection + .Render(BsonSerializer.SerializerRegistry.GetSerializer(), BsonSerializer.SerializerRegistry) + .Document.Add(propName, new BsonDocument { { "$meta", "textScore" } }); + } + + } + + /// + /// Represents a MongoDB Find command. + /// TIP: Specify your criteria using .Match() .Sort() .Skip() .Take() .Project() .Option() methods and finally call .Execute() + /// Note: For building queries, use the DB.Fluent* interfaces + /// + /// Any class that implements IEntity + public class Find : Find where T : IEntity + { + internal Find(Dictionary globalFilters, DBContext context, IMongoCollection collection) + : base(globalFilters, context, collection) { } + } + + + /// + /// Represents a MongoDB Find command with the ability to project to a different result type. + /// TIP: Specify your criteria using .Match() .Sort() .Skip() .Take() .Project() .Option() methods and finally call .Execute() + /// + /// Any class that implements IEntity + /// The type you'd like to project the results to. + public class Find : FindBase> where T : IEntity + { + /// + /// copy constructor + /// + /// + /// + /// + internal Find(FindBase> other, DBContext context, IMongoCollection collection) : base(other) + { + Context = context; + Collection = collection; + } + internal Find(Dictionary globalFilters, DBContext context, IMongoCollection collection) : base(globalFilters) + { + Context = context; + Collection = collection; + } + public DBContext Context { get; private set; } + public IMongoCollection Collection { get; private set; } + + public IClientSessionHandle? Session => Context.Session; + + + /// + /// Find a single IEntity by ID + /// + /// The unique ID of an IEntity + /// An optional cancellation token + /// A single entity or null if not found + public Task OneAsync(string ID, CancellationToken cancellation = default) + { + Match(ID); + return ExecuteSingleAsync(cancellation); + } + + /// + /// Find entities by supplying a lambda expression + /// + /// x => x.Property == Value + /// An optional cancellation token + /// A list of Entities + public Task> ManyAsync(Expression> expression, CancellationToken cancellation = default) + { + Match(expression); + return ExecuteAsync(cancellation); + } + + /// + /// Find entities by supplying a filter expression + /// + /// f => f.Eq(x => x.Prop, Value) & f.Gt(x => x.Prop, Value) + /// An optional cancellation token + /// A list of Entities + public Task> ManyAsync(Func, FilterDefinition> filter, CancellationToken cancellation = default) + { + Match(filter); + return ExecuteAsync(cancellation); } /// @@ -426,27 +464,16 @@ public async Task ExecuteAnyAsync(CancellationToken cancellation = default /// An optional cancellation token public Task> ExecuteCursorAsync(CancellationToken cancellation = default) { - if (sorts.Count > 0) - options.Sort = Builders.Sort.Combine(sorts); + if (_sorts.Count > 0) + _options.Sort = Builders.Sort.Combine(_sorts); - var mergedFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, filter); + var mergedFilter = Logic.MergeWithGlobalFilter(_ignoreGlobalFilters, _globalFilters, _filter); - return session == null - ? DB.Collection(tenantPrefix).FindAsync(mergedFilter, options, cancellation) - : DB.Collection(tenantPrefix).FindAsync(session, mergedFilter, options, cancellation); - } - - private void AddTxtScoreToProjection(string propName) - { - if (options.Projection == null) options.Projection = "{}"; - - options.Projection = - options.Projection - .Render(BsonSerializer.SerializerRegistry.GetSerializer(), BsonSerializer.SerializerRegistry) - .Document.Add(propName, new BsonDocument { { "$meta", "textScore" } }); + return Session == null ? + Collection.FindAsync(mergedFilter, _options, cancellation) : + Collection.FindAsync(Context.Session, mergedFilter, _options, cancellation); } } - public enum Order { Ascending, From 62362a142df0b38026f471d875fffd64d1211048 Mon Sep 17 00:00:00 2001 From: Ahmed Fwela Date: Fri, 5 Nov 2021 16:40:03 +0200 Subject: [PATCH 07/26] WIP missing Update, UpdateAndGet from builders --- MongoDB.Entities/Builders/Distinct.cs | 191 +++-------- MongoDB.Entities/Builders/FilterQueryBase.cs | 169 ++++++++++ MongoDB.Entities/Builders/Find.cs | 280 +++------------- .../Builders/ICollectionRelated.cs | 21 ++ MongoDB.Entities/Builders/Index.cs | 33 +- MongoDB.Entities/Builders/PagedSearch.cs | 316 +++++------------- MongoDB.Entities/Builders/Replace.cs | 221 +++--------- .../Builders/SortFilterQueryBase.cs | 49 +++ MongoDB.Entities/Builders/Update.cs | 279 +++++----------- MongoDB.Entities/Builders/UpdateAndGet.cs | 9 +- MongoDB.Entities/Core/Cache.cs | 8 +- MongoDB.Entities/DBContext/DBContext.cs | 4 +- MongoDB.Entities/Extensions/Entity.cs | 16 - MongoDB.Entities/MongoDB.Entities.csproj | 76 ++--- MongoDB.Entities/Relationships/Many.cs | 2 + 15 files changed, 623 insertions(+), 1051 deletions(-) create mode 100644 MongoDB.Entities/Builders/FilterQueryBase.cs create mode 100644 MongoDB.Entities/Builders/ICollectionRelated.cs create mode 100644 MongoDB.Entities/Builders/SortFilterQueryBase.cs diff --git a/MongoDB.Entities/Builders/Distinct.cs b/MongoDB.Entities/Builders/Distinct.cs index 8fb4e563c..9c134bc79 100644 --- a/MongoDB.Entities/Builders/Distinct.cs +++ b/MongoDB.Entities/Builders/Distinct.cs @@ -7,170 +7,81 @@ namespace MongoDB.Entities { - /// - /// Represents a MongoDB Distinct command where you can get back distinct values for a given property of a given Entity. - /// - /// Any Entity that implements IEntity interface - /// The type of the property of the entity you'd like to get unique values for - public class Distinct where T : IEntity + public class DistinctBase : FilterQueryBase where T : IEntity where TSelf : DistinctBase { - private FieldDefinition field; - private FilterDefinition filter = Builders.Filter.Empty; - private readonly DistinctOptions options = new(); - private readonly IClientSessionHandle session; - private readonly Dictionary globalFilters; - private bool ignoreGlobalFilters; - private readonly string tenantPrefix; - - internal Distinct( - IClientSessionHandle session, - Dictionary globalFilters, string tenantPrefix) - { - this.session = session; - this.globalFilters = globalFilters; - this.tenantPrefix = tenantPrefix; - } - - /// - /// Specify the property you want to get the unique values for (as a string path) - /// - /// ex: "Address.Street" - public Distinct Property(string property) - { - field = property; - return this; - } + internal DistinctOptions _options = new(); + internal FieldDefinition? _field; - /// - /// Specify the property you want to get the unique values for (as a member expression) - /// - /// x => x.Address.Street - public Distinct Property(Expression> property) - { - field = property.FullPath(); - return this; - } - - /// - /// Specify the matching criteria with a filter expression - /// - /// f => f.Eq(x => x.Prop, Value) & f.Gt(x => x.Prop, Value) - public Distinct Match(Func, FilterDefinition> filter) + internal DistinctBase(DistinctBase other) : base(other) { - this.filter &= filter(Builders.Filter); - return this; + _options = other._options; + _field = other._field; } - - /// - /// Specify the matching criteria with a lambda expression - /// - /// x => x.Property == Value - public Distinct Match(Expression> expression) + internal DistinctBase(Dictionary globalFilters) : base(globalFilters) { - return Match(f => f.Where(expression)); + _globalFilters = globalFilters; } - /// - /// Specify the matching criteria with a template - /// - /// A Template with a find query - public Distinct Match(Template template) - { - filter &= template.RenderToString(); - return this; - } - /// - /// Specify a search term to find results from the text index of this particular collection. - /// TIP: Make sure to define a text index with DB.Index<T>() before searching - /// - /// The type of text matching to do - /// The search term - /// Case sensitivity of the search (optional) - /// Diacritic sensitivity of the search (optional) - /// The language for the search (optional) - public Distinct Match(Search searchType, string searchTerm, bool caseSensitive = false, bool diacriticSensitive = false, string language = null) - { - if (searchType == Search.Fuzzy) - { - searchTerm = searchTerm.ToDoubleMetaphoneHash(); - caseSensitive = false; - diacriticSensitive = false; - language = null; - } + private TSelf This => (TSelf)this; - return Match( - f => f.Text( - searchTerm, - new TextSearchOptions - { - CaseSensitive = caseSensitive, - DiacriticSensitive = diacriticSensitive, - Language = language - })); - } /// - /// Specify criteria for matching entities based on GeoSpatial data (longitude & latitude) - /// TIP: Make sure to define a Geo2DSphere index with DB.Index<T>() before searching - /// Note: DB.FluentGeoNear() supports more advanced options + /// Specify an option for this find command (use multiple times if needed) /// - /// The property where 2DCoordinates are stored - /// The search point - /// Maximum distance in meters from the search point - /// Minimum distance in meters from the search point - public Distinct Match(Expression> coordinatesProperty, Coordinates2D nearCoordinates, double? maxDistance = null, double? minDistance = null) + /// x => x.OptionName = OptionValue + public TSelf Option(Action option) { - return Match(f => f.Near(coordinatesProperty, nearCoordinates.ToGeoJsonPoint(), maxDistance, minDistance)); + option(_options); + return This; } /// - /// Specify the matching criteria with a JSON string + /// Specify the property you want to get the unique values for (as a string path) /// - /// { Title : 'The Power Of Now' } - public Distinct MatchString(string jsonString) + /// ex: "Address.Street" + public TSelf Property(string property) { - filter &= jsonString; - return this; + _field = property; + return This; } /// - /// Specify the matching criteria with an aggregation expression (i.e. $expr) + /// Specify the property you want to get the unique values for (as a member expression) /// - /// { $gt: ['$Property1', '$Property2'] } - public Distinct MatchExpression(string expression) + /// x => x.Address.Street + public TSelf Property(Expression> property) { - filter &= "{$expr:" + expression + "}"; - return this; + _field = property.FullPath(); + return This; } + } - /// - /// Specify the matching criteria with a Template - /// - /// A Template object - public Distinct MatchExpression(Template template) - { - filter &= "{$expr:" + template.RenderToString() + "}"; - return this; - } + /// + /// Represents a MongoDB Distinct command where you can get back distinct values for a given property of a given Entity. + /// + /// Any Entity that implements IEntity interface + /// The type of the property of the entity you'd like to get unique values for + public class Distinct : DistinctBase>, ICollectionRelated where T : IEntity + { + public DBContext Context { get; } + public IMongoCollection Collection { get; } - /// - /// Specify an option for this find command (use multiple times if needed) - /// - /// x => x.OptionName = OptionValue - public Distinct Option(Action option) + internal Distinct( + DBContext context, + IMongoCollection collection, + DistinctBase> other) : base(other) { - option(options); - return this; + Context = context; + Collection = collection; } - - /// - /// Specify that this operation should ignore any global filters - /// - public Distinct IgnoreGlobalFilters() + internal Distinct( + DBContext context, + IMongoCollection collection, + Dictionary globalFilters) : base(globalFilters: globalFilters) { - ignoreGlobalFilters = true; - return this; + Context = context; + Collection = collection; } /// @@ -179,14 +90,14 @@ public Distinct IgnoreGlobalFilters() /// An optional cancellation token public Task> ExecuteCursorAsync(CancellationToken cancellation = default) { - if (field == null) + if (_field == null) throw new InvalidOperationException("Please use the .Property() method to specify the field to use for obtaining unique values for!"); - var mergedFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, filter); + var mergedFilter = Logic.MergeWithGlobalFilter(_ignoreGlobalFilters, _globalFilters, _filter); - return session == null - ? DB.Collection(tenantPrefix).DistinctAsync(field, mergedFilter, options, cancellation) - : DB.Collection(tenantPrefix).DistinctAsync(session, field, mergedFilter, options, cancellation); + return Context.Session is IClientSessionHandle session + ? Collection.DistinctAsync(session, _field, mergedFilter, _options, cancellation) + : Collection.DistinctAsync(_field, mergedFilter, _options, cancellation); } /// diff --git a/MongoDB.Entities/Builders/FilterQueryBase.cs b/MongoDB.Entities/Builders/FilterQueryBase.cs new file mode 100644 index 000000000..6b82e93c9 --- /dev/null +++ b/MongoDB.Entities/Builders/FilterQueryBase.cs @@ -0,0 +1,169 @@ +using MongoDB.Driver; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +namespace MongoDB.Entities +{ + public class FilterQueryBase where T : IEntity where TSelf : FilterQueryBase + { + internal FilterDefinition _filter = Builders.Filter.Empty; + internal Dictionary _globalFilters; + internal bool _ignoreGlobalFilters; + + internal FilterQueryBase(FilterQueryBase other) : this(globalFilters: other._globalFilters) + { + _filter = other._filter; + _ignoreGlobalFilters = other._ignoreGlobalFilters; + } + internal FilterQueryBase(Dictionary globalFilters) + { + _globalFilters = globalFilters; + } + + protected FilterDefinition MergedFilter => Logic.MergeWithGlobalFilter(_ignoreGlobalFilters, _globalFilters, _filter); + private TSelf This => (TSelf)this; + + + /// + /// Specify that this operation should ignore any global filters + /// + public TSelf IgnoreGlobalFilters() + { + _ignoreGlobalFilters = true; + return This; + } + + + + /// + /// Specify an IEntity ID as the matching criteria + /// + /// A unique IEntity ID + public TSelf Match(string ID) + { + return Match(f => f.Eq(t => t.ID, ID)); + } + + /// + /// Specify the matching criteria with a lambda expression + /// + /// x => x.Property == Value + public TSelf Match(Expression> expression) + { + return Match(f => f.Where(expression)); + } + + /// + /// Specify the matching criteria with a filter expression + /// + /// f => f.Eq(x => x.Prop, Value) & f.Gt(x => x.Prop, Value) + public TSelf Match(Func, FilterDefinition> filter) + { + _filter &= filter(Builders.Filter); + return This; + } + + /// + /// Specify the matching criteria with a filter definition + /// + /// A filter definition + public TSelf Match(FilterDefinition filterDefinition) + { + _filter &= filterDefinition; + return This; + } + + /// + /// Specify the matching criteria with a template + /// + /// A Template with a find query + public TSelf Match(Template template) + { + _filter &= template.RenderToString(); + return This; + } + + /// + /// Specify a search term to find results from the text index of this particular collection. + /// TIP: Make sure to define a text index with DB.Index<T>() before searching + /// + /// The type of text matching to do + /// The search term + /// Case sensitivity of the search (optional) + /// Diacritic sensitivity of the search (optional) + /// The language for the search (optional) + public TSelf Match(Search searchType, string searchTerm, bool caseSensitive = false, bool diacriticSensitive = false, string? language = null) + { + if (searchType == Search.Fuzzy) + { + searchTerm = searchTerm.ToDoubleMetaphoneHash(); + caseSensitive = false; + diacriticSensitive = false; + language = null; + } + + return Match( + f => f.Text( + searchTerm, + new TextSearchOptions + { + CaseSensitive = caseSensitive, + DiacriticSensitive = diacriticSensitive, + Language = language + })); + } + + /// + /// Specify criteria for matching entities based on GeoSpatial data (longitude & latitude) + /// TIP: Make sure to define a Geo2DSphere index with DB.Index<T>() before searching + /// Note: DB.FluentGeoNear() supports more advanced options + /// + /// The property where 2DCoordinates are stored + /// The search point + /// Maximum distance in meters from the search point + /// Minimum distance in meters from the search point + public TSelf Match(Expression> coordinatesProperty, Coordinates2D nearCoordinates, double? maxDistance = null, double? minDistance = null) + { + return Match(f => f.Near(coordinatesProperty, nearCoordinates.ToGeoJsonPoint(), maxDistance, minDistance)); + } + + /// + /// Specify the matching criteria with an aggregation expression (i.e. $expr) + /// + /// { $gt: ['$Property1', '$Property2'] } + public TSelf MatchExpression(string expression) + { + _filter &= "{$expr:" + expression + "}"; + return This; + } + + /// + /// Specify the matching criteria with a Template + /// + /// A Template object + public TSelf MatchExpression(Template template) + { + return MatchExpression(template.RenderToString()); + } + + /// + /// Specify an IEntity ID as the matching criteria + /// + /// A unique IEntity ID + public TSelf MatchID(string ID) + { + return Match(f => f.Eq(t => t.ID, ID)); + } + + /// + /// Specify the matching criteria with a JSON string + /// + /// { Title : 'The Power Of Now' } + public TSelf MatchString(string jsonString) + { + _filter &= jsonString; + return This; + } + } +} diff --git a/MongoDB.Entities/Builders/Find.cs b/MongoDB.Entities/Builders/Find.cs index 946972f64..13ccc3faf 100644 --- a/MongoDB.Entities/Builders/Find.cs +++ b/MongoDB.Entities/Builders/Find.cs @@ -10,212 +10,21 @@ #nullable enable namespace MongoDB.Entities { - public class FindBase where T : IEntity where TSelf : FindBase + public class FindBase : SortFilterQueryBase where T : IEntity where TSelf : FindBase { - internal FilterDefinition _filter = Builders.Filter.Empty; - internal List> _sorts = new(); internal FindOptions _options = new(); - internal Dictionary _globalFilters; - internal bool _ignoreGlobalFilters; - internal FindBase(FindBase other) : this(globalFilters: other._globalFilters) + internal FindBase(FindBase other) : base(other) { - _filter = other._filter; _options = other._options; - _sorts = other._sorts; - _ignoreGlobalFilters = other._ignoreGlobalFilters; } - internal FindBase(Dictionary globalFilters) + internal FindBase(Dictionary globalFilters) : base(globalFilters: globalFilters) { _globalFilters = globalFilters; } private TSelf This => (TSelf)this; - /// - /// Specify an IEntity ID as the matching criteria - /// - /// A unique IEntity ID - public TSelf MatchID(string ID) - { - return Match(f => f.Eq(t => t.ID, ID)); - } - - /// - /// Specify an IEntity ID as the matching criteria - /// - /// A unique IEntity ID - public TSelf Match(string ID) - { - return Match(f => f.Eq(t => t.ID, ID)); - } - - /// - /// Specify the matching criteria with a lambda expression - /// - /// x => x.Property == Value - public TSelf Match(Expression> expression) - { - return Match(f => f.Where(expression)); - } - - /// - /// Specify the matching criteria with a filter expression - /// - /// f => f.Eq(x => x.Prop, Value) & f.Gt(x => x.Prop, Value) - public TSelf Match(Func, FilterDefinition> filter) - { - _filter &= filter(Builders.Filter); - return This; - } - - /// - /// Specify the matching criteria with a filter definition - /// - /// A filter definition - public TSelf Match(FilterDefinition filterDefinition) - { - _filter &= filterDefinition; - return This; - } - - /// - /// Specify the matching criteria with a template - /// - /// A Template with a find query - public TSelf Match(Template template) - { - _filter &= template.RenderToString(); - return This; - } - - /// - /// Specify a search term to find results from the text index of this particular collection. - /// TIP: Make sure to define a text index with DB.Index<T>() before searching - /// - /// The type of text matching to do - /// The search term - /// Case sensitivity of the search (optional) - /// Diacritic sensitivity of the search (optional) - /// The language for the search (optional) - public TSelf Match(Search searchType, string searchTerm, bool caseSensitive = false, bool diacriticSensitive = false, string? language = null) - { - if (searchType == Search.Fuzzy) - { - searchTerm = searchTerm.ToDoubleMetaphoneHash(); - caseSensitive = false; - diacriticSensitive = false; - language = null; - } - - return Match( - f => f.Text( - searchTerm, - new TextSearchOptions - { - CaseSensitive = caseSensitive, - DiacriticSensitive = diacriticSensitive, - Language = language - })); - } - - /// - /// Specify criteria for matching entities based on GeoSpatial data (longitude & latitude) - /// TIP: Make sure to define a Geo2DSphere index with DB.Index<T>() before searching - /// Note: DB.FluentGeoNear() supports more advanced options - /// - /// The property where 2DCoordinates are stored - /// The search point - /// Maximum distance in meters from the search point - /// Minimum distance in meters from the search point - public TSelf Match(Expression> coordinatesProperty, Coordinates2D nearCoordinates, double? maxDistance = null, double? minDistance = null) - { - return Match(f => f.Near(coordinatesProperty, nearCoordinates.ToGeoJsonPoint(), maxDistance, minDistance)); - } - - /// - /// Specify the matching criteria with a JSON string - /// - /// { Title : 'The Power Of Now' } - public TSelf MatchString(string jsonString) - { - _filter &= jsonString; - return This; - } - - /// - /// Specify the matching criteria with an aggregation expression (i.e. $expr) - /// - /// { $gt: ['$Property1', '$Property2'] } - public TSelf MatchExpression(string expression) - { - _filter &= "{$expr:" + expression + "}"; - return This; - } - - /// - /// Specify the matching criteria with a Template - /// - /// A Template object - public TSelf MatchExpression(Template template) - { - return MatchExpression(template.RenderToString()); - } - - /// - /// Specify which property and order to use for sorting (use multiple times if needed) - /// - /// x => x.Prop - /// The sort order - public TSelf Sort(Expression> propertyToSortBy, Order sortOrder) - { - return sortOrder switch - { - Order.Ascending => Sort(s => s.Ascending(propertyToSortBy)), - Order.Descending => Sort(s => s.Descending(propertyToSortBy)), - _ => This, - }; - } - - /// - /// Sort the results of a text search by the MetaTextScore - /// TIP: Use this method after .Project() if you need to do a projection also - /// - public TSelf SortByTextScore() - { - return SortByTextScore(null); - } - - /// - /// Sort the results of a text search by the MetaTextScore and get back the score as well - /// TIP: Use this method after .Project() if you need to do a projection also - /// - /// x => x.TextScoreProp - public TSelf SortByTextScore(Expression>? scoreProperty) - { - switch (scoreProperty) - { - case null: - AddTxtScoreToProjection("_Text_Match_Score_"); - return Sort(s => s.MetaTextScore("_Text_Match_Score_")); - - default: - AddTxtScoreToProjection(Prop.Path(scoreProperty)); - return Sort(s => s.MetaTextScore(Prop.Path(scoreProperty))); - } - } - - /// - /// Specify how to sort using a sort expression - /// - /// s => s.Ascending("Prop1").MetaTextScore("Prop2") - /// - public TSelf Sort(Func, SortDefinition> sortFunction) - { - _sorts.Add(sortFunction(Builders.Sort)); - return This; - } - /// /// Specify how many entities to skip /// @@ -255,6 +64,19 @@ public TSelf Project(Func, ProjectionDefinition + /// Specify to automatically include all properties marked with [BsonRequired] attribute on the entity in the final projection. + /// HINT: this method should only be called after the .Project() method. + /// + public TSelf IncludeRequiredProps() + { + if (typeof(T) != typeof(TProjection)) + throw new InvalidOperationException("IncludeRequiredProps() cannot be used when projecting to a different type."); + + _options.Projection = Cache.Instance.CombineWithRequiredProps(_options.Projection); + return This; + } + /// /// Specify how to project the results using an exclusion projection expression. /// @@ -279,19 +101,6 @@ public TSelf ProjectExcluding(Expression> exclusion) return This; } - /// - /// Specify to automatically include all properties marked with [BsonRequired] attribute on the entity in the final projection. - /// HINT: this method should only be called after the .Project() method. - /// - public TSelf IncludeRequiredProps() - { - if (typeof(T) != typeof(TProjection)) - throw new InvalidOperationException("IncludeRequiredProps() cannot be used when projecting to a different type."); - - _options.Projection = Cache.Instance.CombineWithRequiredProps(_options.Projection); - return This; - } - /// /// Specify an option for this find command (use multiple times if needed) /// @@ -302,16 +111,6 @@ public TSelf Option(Action> option) return This; } - /// - /// Specify that this operation should ignore any global filters - /// - public TSelf IgnoreGlobalFilters() - { - _ignoreGlobalFilters = true; - return This; - } - - private void AddTxtScoreToProjection(string propName) { if (_options.Projection == null) _options.Projection = "{}"; @@ -322,6 +121,33 @@ private void AddTxtScoreToProjection(string propName) .Document.Add(propName, new BsonDocument { { "$meta", "textScore" } }); } + /// + /// Sort the results of a text search by the MetaTextScore + /// TIP: Use this method after .Project() if you need to do a projection also + /// + public TSelf SortByTextScore() + { + return SortByTextScore(null); + } + + /// + /// Sort the results of a text search by the MetaTextScore and get back the score as well + /// TIP: Use this method after .Project() if you need to do a projection also + /// + /// x => x.TextScoreProp + public TSelf SortByTextScore(Expression>? scoreProperty) + { + switch (scoreProperty) + { + case null: + AddTxtScoreToProjection("_Text_Match_Score_"); + return Sort(s => s.MetaTextScore("_Text_Match_Score_")); + + default: + AddTxtScoreToProjection(Prop.Path(scoreProperty)); + return Sort(s => s.MetaTextScore(Prop.Path(scoreProperty))); + } + } } /// @@ -332,8 +158,11 @@ private void AddTxtScoreToProjection(string propName) /// Any class that implements IEntity public class Find : Find where T : IEntity { - internal Find(Dictionary globalFilters, DBContext context, IMongoCollection collection) - : base(globalFilters, context, collection) { } + internal Find(DBContext context, IMongoCollection collection, Dictionary globalFilters) + : base(context, collection, globalFilters) { } + + internal Find(DBContext context, IMongoCollection collection, FindBase> baseQuery) + : base(context, collection, baseQuery) { } } @@ -343,7 +172,7 @@ internal Find(Dictionary globalFilters, /// /// Any class that implements IEntity /// The type you'd like to project the results to. - public class Find : FindBase> where T : IEntity + public class Find : FindBase>, ICollectionRelated where T : IEntity { /// /// copy constructor @@ -351,20 +180,20 @@ public class Find : FindBase /// /// - internal Find(FindBase> other, DBContext context, IMongoCollection collection) : base(other) + internal Find(DBContext context, IMongoCollection collection, FindBase> other) : base(other) { Context = context; Collection = collection; } - internal Find(Dictionary globalFilters, DBContext context, IMongoCollection collection) : base(globalFilters) + internal Find(DBContext context, IMongoCollection collection, Dictionary globalFilters) : base(globalFilters) { Context = context; Collection = collection; } + public DBContext Context { get; private set; } public IMongoCollection Collection { get; private set; } - public IClientSessionHandle? Session => Context.Session; /// @@ -469,11 +298,12 @@ public Task> ExecuteCursorAsync(CancellationToken canc var mergedFilter = Logic.MergeWithGlobalFilter(_ignoreGlobalFilters, _globalFilters, _filter); - return Session == null ? + return this.Session() is not IClientSessionHandle session ? Collection.FindAsync(mergedFilter, _options, cancellation) : - Collection.FindAsync(Context.Session, mergedFilter, _options, cancellation); + Collection.FindAsync(session, mergedFilter, _options, cancellation); } } + public enum Order { Ascending, diff --git a/MongoDB.Entities/Builders/ICollectionRelated.cs b/MongoDB.Entities/Builders/ICollectionRelated.cs new file mode 100644 index 000000000..5ca08ca05 --- /dev/null +++ b/MongoDB.Entities/Builders/ICollectionRelated.cs @@ -0,0 +1,21 @@ +using MongoDB.Driver; +using System; +using System.Collections.Generic; +using System.Text; +#nullable enable +namespace MongoDB.Entities +{ + internal interface ICollectionRelated where T : IEntity + { + public DBContext Context { get; } + public IMongoCollection Collection { get; } + } + internal static class ICollectionRelatedExt + { + public static IClientSessionHandle? Session(this ICollectionRelated collectionRelated) where T : IEntity + { + return collectionRelated.Context.Session; + } + + } +} diff --git a/MongoDB.Entities/Builders/Index.cs b/MongoDB.Entities/Builders/Index.cs index fab18e129..3ed2209b6 100644 --- a/MongoDB.Entities/Builders/Index.cs +++ b/MongoDB.Entities/Builders/Index.cs @@ -12,15 +12,18 @@ namespace MongoDB.Entities /// TIP: Define the keys first with .Key() method and finally call the .Create() method. /// /// Any class that implements IEntity - public class Index where T : IEntity + public class Index : ICollectionRelated where T : IEntity { internal List> Keys { get; set; } = new List>(); - private readonly CreateIndexOptions options = new() { Background = true }; - private readonly string tenantPrefix; + public DBContext Context { get; } + public IMongoCollection Collection { get; } - internal Index(string tenantPrefix) + private readonly CreateIndexOptions _options = new() { Background = true }; + + internal Index(DBContext context, IMongoCollection collection) { - this.tenantPrefix = tenantPrefix; + Context = context; + Collection = collection; } /// @@ -74,17 +77,17 @@ public async Task CreateAsync(CancellationToken cancellation = default) propNames.Add(key.PropertyName + keyType); } - if (string.IsNullOrEmpty(options.Name)) + if (string.IsNullOrEmpty(_options.Name)) { if (isTextIndex) - options.Name = "[TEXT]"; + _options.Name = "[TEXT]"; else - options.Name = string.Join(" | ", propNames); + _options.Name = string.Join(" | ", propNames); } var model = new CreateIndexModel( Builders.IndexKeys.Combine(keyDefs), - options); + _options); try { @@ -92,11 +95,11 @@ public async Task CreateAsync(CancellationToken cancellation = default) } catch (MongoCommandException x) when (x.Code == 85 || x.Code == 86) { - await DropAsync(options.Name, cancellation).ConfigureAwait(false); + await DropAsync(_options.Name, cancellation).ConfigureAwait(false); await CreateAsync(model, cancellation).ConfigureAwait(false); } - return options.Name; + return _options.Name; } /// @@ -106,7 +109,7 @@ public async Task CreateAsync(CancellationToken cancellation = default) /// x => x.OptionName = OptionValue public Index Option(Action> option) { - option(options); + option(_options); return this; } @@ -129,7 +132,7 @@ public Index Key(Expression> propertyToIndex, KeyType type) /// An optional cancellation token public async Task DropAsync(string name, CancellationToken cancellation = default) { - await DB.Collection(tenantPrefix).Indexes.DropOneAsync(name, cancellation).ConfigureAwait(false); + await Collection.Indexes.DropOneAsync(name, cancellation).ConfigureAwait(false); } /// @@ -138,12 +141,12 @@ public async Task DropAsync(string name, CancellationToken cancellation = defaul /// An optional cancellation token public async Task DropAllAsync(CancellationToken cancellation = default) { - await DB.Collection(tenantPrefix).Indexes.DropAllAsync(cancellation).ConfigureAwait(false); + await Collection.Indexes.DropAllAsync(cancellation).ConfigureAwait(false); } private Task CreateAsync(CreateIndexModel model, CancellationToken cancellation = default) { - return DB.Collection(tenantPrefix).Indexes.CreateOneAsync(model, cancellationToken: cancellation); + return Collection.Indexes.CreateOneAsync(model, cancellationToken: cancellation); } } diff --git a/MongoDB.Entities/Builders/PagedSearch.cs b/MongoDB.Entities/Builders/PagedSearch.cs index cb41fbfde..140a050e4 100644 --- a/MongoDB.Entities/Builders/PagedSearch.cs +++ b/MongoDB.Entities/Builders/PagedSearch.cs @@ -10,50 +10,21 @@ namespace MongoDB.Entities { - /// - /// Represents an aggregation query that retrieves results with easy paging support. - /// - /// Any class that implements IEntity - public class PagedSearch : PagedSearch where T : IEntity - { - internal PagedSearch( - IClientSessionHandle session, - Dictionary globalFilters, - string tenantPrefix) - : base(session, globalFilters, tenantPrefix) { } - } - - /// - /// Represents an aggregation query that retrieves results with easy paging support. - /// - /// Any class that implements IEntity - /// The type you'd like to project the results to. - public class PagedSearch where T : IEntity + public class PagedSearchBase : SortFilterQueryBase where TSelf : PagedSearchBase where T : IEntity { - private IAggregateFluent fluentPipeline; - private FilterDefinition filter = Builders.Filter.Empty; - private readonly List> sorts = new(); - private readonly AggregateOptions options = new(); - private PipelineStageDefinition projectionStage; - private readonly IClientSessionHandle session; - private readonly Dictionary globalFilters; - private bool ignoreGlobalFilters; - private int pageNumber = 1, pageSize = 100; - private readonly string tenantPrefix; - - internal PagedSearch( - IClientSessionHandle session, - Dictionary globalFilters, - string tenantPrefix) + internal PagedSearchBase(PagedSearchBase other) : base(other) { - var type = typeof(TProjection); - if (type.IsPrimitive || type.IsValueType || (type == typeof(string))) - throw new NotSupportedException("Projecting to primitive types is not supported!"); + } - this.session = session; - this.globalFilters = globalFilters; - this.tenantPrefix = tenantPrefix; + internal PagedSearchBase(Dictionary globalFilters) : base(globalFilters) + { } + private TSelf This => (TSelf)this; + + internal IAggregateFluent? _fluentPipeline; + internal AggregateOptions _options = new(); + internal PipelineStageDefinition? _projectionStage; + internal int _pageNumber = 1, _pageSize = 100; /// /// Begins the paged search aggregation pipeline with the provided fluent pipeline. @@ -61,150 +32,21 @@ internal PagedSearch( /// /// The type of the input pipeline /// The input IAggregateFluent pipeline - public PagedSearch WithFluent(TFluent fluentPipeline) where TFluent : IAggregateFluent - { - this.fluentPipeline = fluentPipeline; - return this; - } - - /// - /// Specify the matching criteria with a lambda expression - /// - /// x => x.Property == Value - public PagedSearch Match(Expression> expression) - { - return Match(f => f.Where(expression)); - } - - /// - /// Specify the matching criteria with a filter expression - /// - /// f => f.Eq(x => x.Prop, Value) & f.Gt(x => x.Prop, Value) - public PagedSearch Match(Func, FilterDefinition> filter) - { - this.filter &= filter(Builders.Filter); - return this; - } - - /// - /// Specify the matching criteria with a filter definition - /// - /// A filter definition - public PagedSearch Match(FilterDefinition filterDefinition) - { - filter &= filterDefinition; - return this; - } - - /// - /// Specify the matching criteria with a template - /// - /// A Template with a find query - public PagedSearch Match(Template template) - { - filter &= template.RenderToString(); - return this; - } - - /// - /// Specify a search term to find results from the text index of this particular collection. - /// TIP: Make sure to define a text index with DB.Index<T>() before searching - /// - /// The type of text matching to do - /// The search term - /// Case sensitivity of the search (optional) - /// Diacritic sensitivity of the search (optional) - /// The language for the search (optional) - public PagedSearch Match(Search searchType, string searchTerm, bool caseSensitive = false, bool diacriticSensitive = false, string language = null) - { - if (searchType == Search.Fuzzy) - { - searchTerm = searchTerm.ToDoubleMetaphoneHash(); - caseSensitive = false; - diacriticSensitive = false; - language = null; - } - - return Match( - f => f.Text( - searchTerm, - new TextSearchOptions - { - CaseSensitive = caseSensitive, - DiacriticSensitive = diacriticSensitive, - Language = language - })); - } - - /// - /// Specify criteria for matching entities based on GeoSpatial data (longitude & latitude) - /// TIP: Make sure to define a Geo2DSphere index with DB.Index<T>() before searching - /// Note: DB.FluentGeoNear() supports more advanced options - /// - /// The property where 2DCoordinates are stored - /// The search point - /// Maximum distance in meters from the search point - /// Minimum distance in meters from the search point - public PagedSearch Match(Expression> coordinatesProperty, Coordinates2D nearCoordinates, double? maxDistance = null, double? minDistance = null) + public TSelf WithFluent(TFluent fluentPipeline) where TFluent : IAggregateFluent { - return Match(f => f.Near(coordinatesProperty, nearCoordinates.ToGeoJsonPoint(), maxDistance, minDistance)); + this._fluentPipeline = fluentPipeline; + return This; } - /// - /// Specify the matching criteria with a JSON string - /// - /// { Title : 'The Power Of Now' } - public PagedSearch MatchString(string jsonString) - { - filter &= jsonString; - return this; - } - /// - /// Specify the matching criteria with an aggregation expression (i.e. $expr) - /// - /// { $gt: ['$Property1', '$Property2'] } - public PagedSearch MatchExpression(string expression) - { - filter &= "{$expr:" + expression + "}"; - return this; - } - /// - /// Specify the matching criteria with a Template - /// - /// A Template object - public PagedSearch MatchExpression(Template template) - { - filter &= "{$expr:" + template.RenderToString() + "}"; - return this; - } - /// - /// Specify which property and order to use for sorting (use multiple times if needed) - /// - /// x => x.Prop - /// The sort order - public PagedSearch Sort(Expression> propertyToSortBy, Order sortOrder) - { - switch (sortOrder) - { - case Order.Ascending: - return Sort(s => s.Ascending(propertyToSortBy)); - - case Order.Descending: - return Sort(s => s.Descending(propertyToSortBy)); - - default: - return this; - } - } /// /// Sort the results of a text search by the MetaTextScore /// TIP: Use this method after .Project() if you need to do a projection also /// - public PagedSearch SortByTextScore() + public TSelf SortByTextScore() { return SortByTextScore(null); } @@ -214,7 +56,7 @@ public PagedSearch SortByTextScore() /// TIP: Use this method after .Project() if you need to do a projection also /// /// x => x.TextScoreProp - public PagedSearch SortByTextScore(Expression> scoreProperty) + public TSelf SortByTextScore(Expression>? scoreProperty) { switch (scoreProperty) { @@ -230,81 +72,73 @@ public PagedSearch SortByTextScore(Expression> s private void AddTxtScoreToProjection(string fieldName) { - if (projectionStage == null) + if (_projectionStage == null) { - projectionStage = $"{{ $set : {{ {fieldName} : {{ $meta : 'textScore' }} }} }}"; + _projectionStage = $"{{ $set : {{ {fieldName} : {{ $meta : 'textScore' }} }} }}"; return; } - var renderedStage = projectionStage.Render( + var renderedStage = _projectionStage.Render( BsonSerializer.SerializerRegistry.GetSerializer(), BsonSerializer.SerializerRegistry); renderedStage.Document["$project"][fieldName] = new BsonDocument { { "$meta", "textScore" } }; - projectionStage = renderedStage.Document; + _projectionStage = renderedStage.Document; } - /// - /// Specify how to sort using a sort expression - /// - /// s => s.Ascending("Prop1").MetaTextScore("Prop2") - public PagedSearch Sort(Func, SortDefinition> sortFunction) - { - sorts.Add(sortFunction(Builders.Sort)); - return this; - } + /// /// Specify the page number to get /// /// The page number - public PagedSearch PageNumber(int pageNumber) + public TSelf PageNumber(int pageNumber) { - this.pageNumber = pageNumber; - return this; + this._pageNumber = pageNumber; + return This; } /// /// Specify the number of items per page /// /// The size of a page - public PagedSearch PageSize(int pageSize) + public TSelf PageSize(int pageSize) { - this.pageSize = pageSize; - return this; + this._pageSize = pageSize; + return This; } /// /// Specify how to project the results using a lambda expression /// /// x => new Test { PropName = x.Prop } - public PagedSearch Project(Expression> expression) + public TSelf Project(Expression> expression) { - projectionStage = PipelineStageDefinitionBuilder.Project(expression); - return this; + _projectionStage = PipelineStageDefinitionBuilder.Project(expression); + return This; } /// /// Specify how to project the results using a projection expression /// /// p => p.Include("Prop1").Exclude("Prop2") - public PagedSearch Project(Func, ProjectionDefinition> projection) + public TSelf Project(Func, ProjectionDefinition> projection) { - projectionStage = PipelineStageDefinitionBuilder.Project(projection(Builders.Projection)); - return this; + _projectionStage = PipelineStageDefinitionBuilder.Project(projection(Builders.Projection)); + return This; } /// /// Specify how to project the results using an exclusion projection expression. /// /// x => new { x.PropToExclude, x.AnotherPropToExclude } - public PagedSearch ProjectExcluding(Expression> exclusion) + public TSelf ProjectExcluding(Expression> exclusion) { var props = (exclusion.Body as NewExpression)?.Arguments .Select(a => a.ToString().Split('.')[1]); - if (!props.Any()) + if (props == null || !props.Any()) throw new ArgumentException("Unable to get any properties from the exclusion expression!"); var defs = new List>(props.Count()); @@ -314,51 +148,79 @@ public PagedSearch ProjectExcluding(Expression> defs.Add(Builders.Projection.Exclude(prop)); } - projectionStage = PipelineStageDefinitionBuilder.Project(Builders.Projection.Combine(defs)); + _projectionStage = PipelineStageDefinitionBuilder.Project(Builders.Projection.Combine(defs)); - return this; + return This; } /// /// Specify an option for this find command (use multiple times if needed) /// /// x => x.OptionName = OptionValue - public PagedSearch Option(Action option) + public TSelf Option(Action option) { - option(options); - return this; + option(_options); + return This; } - /// - /// Specify that this operation should ignore any global filters - /// - public PagedSearch IgnoreGlobalFilters() + + } + + /// + /// Represents an aggregation query that retrieves results with easy paging support. + /// + /// Any class that implements IEntity + public class PagedSearch : PagedSearch where T : IEntity + { + internal PagedSearch( + DBContext context, IMongoCollection collection, + Dictionary globalFilters) + : base(context, collection, globalFilters) { } + } + + /// + /// Represents an aggregation query that retrieves results with easy paging support. + /// + /// Any class that implements IEntity + /// The type you'd like to project the results to. + public class PagedSearch : PagedSearchBase> where T : IEntity + { + + public DBContext Context { get; set; } + public IMongoCollection Collection { get; set; } + + internal PagedSearch(DBContext context, IMongoCollection collection, Dictionary globalFilters) : base(globalFilters) { - ignoreGlobalFilters = true; - return this; + var type = typeof(TProjection); + if (type.IsPrimitive || type.IsValueType || (type == typeof(string))) + throw new NotSupportedException("Projecting to primitive types is not supported!"); + Context = context; + Collection = collection; } + + /// /// Run the aggregation search command in MongoDB server and get a page of results and total + page count /// /// An optional cancellation token public async Task<(IReadOnlyList Results, long TotalCount, int PageCount)> ExecuteAsync(CancellationToken cancellation = default) { - if (filter != Builders.Filter.Empty && fluentPipeline != null) + if (_filter != Builders.Filter.Empty && _fluentPipeline != null) throw new InvalidOperationException(".Match() and .WithFluent() cannot be used together!"); var pipelineStages = new List(4); - if (sorts.Count == 0) + if (_sorts.Count == 0) throw new InvalidOperationException("Paging without sorting is a sin!"); else - pipelineStages.Add(PipelineStageDefinitionBuilder.Sort(Builders.Sort.Combine(sorts))); + pipelineStages.Add(PipelineStageDefinitionBuilder.Sort(Builders.Sort.Combine(_sorts))); - pipelineStages.Add(PipelineStageDefinitionBuilder.Skip((pageNumber - 1) * pageSize)); - pipelineStages.Add(PipelineStageDefinitionBuilder.Limit(pageSize)); + pipelineStages.Add(PipelineStageDefinitionBuilder.Skip((_pageNumber - 1) * _pageSize)); + pipelineStages.Add(PipelineStageDefinitionBuilder.Limit(_pageSize)); - if (projectionStage != null) - pipelineStages.Add(projectionStage); + if (_projectionStage != null) + pipelineStages.Add(_projectionStage); var resultsFacet = AggregateFacet.Create("_results", pipelineStages); @@ -370,18 +232,18 @@ public PagedSearch IgnoreGlobalFilters() AggregateFacetResults facetResult; - if (fluentPipeline == null) //.Match() used + if (_fluentPipeline == null) //.Match() used { - var filterDef = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, filter); + var filterDef = Logic.MergeWithGlobalFilter(_ignoreGlobalFilters, _globalFilters, _filter); facetResult = - session == null - ? await DB.Collection(tenantPrefix).Aggregate(options).Match(filterDef).Facet(countFacet, resultsFacet).SingleAsync(cancellation).ConfigureAwait(false) - : await DB.Collection(tenantPrefix).Aggregate(session, options).Match(filterDef).Facet(countFacet, resultsFacet).SingleAsync(cancellation).ConfigureAwait(false); + Context.Session is not IClientSessionHandle session + ? await Collection.Aggregate(_options).Match(filterDef).Facet(countFacet, resultsFacet).SingleAsync(cancellation).ConfigureAwait(false) + : await Collection.Aggregate(session, _options).Match(filterDef).Facet(countFacet, resultsFacet).SingleAsync(cancellation).ConfigureAwait(false); } else //.WithFluent() used { - facetResult = await fluentPipeline + facetResult = await _fluentPipeline .Facet(countFacet, resultsFacet) .SingleAsync(cancellation) .ConfigureAwait(false); @@ -394,9 +256,9 @@ public PagedSearch IgnoreGlobalFilters() ) ?? 0; int pageCount = - matchCount > 0 && matchCount <= pageSize + matchCount > 0 && matchCount <= _pageSize ? 1 - : (int)Math.Ceiling((double)matchCount / pageSize); + : (int)Math.Ceiling((double)matchCount / _pageSize); var results = facetResult.Facets .First(x => x.Name == "_results") diff --git a/MongoDB.Entities/Builders/Replace.cs b/MongoDB.Entities/Builders/Replace.cs index b05408e34..fe25a2044 100644 --- a/MongoDB.Entities/Builders/Replace.cs +++ b/MongoDB.Entities/Builders/Replace.cs @@ -14,154 +14,30 @@ namespace MongoDB.Entities /// TIP: Specify a filter first with the .Match(). Then set entity with .WithEntity() and finally call .Execute() to run the command. /// /// Any class that implements IEntity - public class Replace where T : IEntity + public class Replace : FilterQueryBase>, ICollectionRelated where T : IEntity { - private FilterDefinition filter = Builders.Filter.Empty; - private ReplaceOptions options = new(); - private readonly IClientSessionHandle session; - private readonly List> models = new(); - private readonly ModifiedBy modifiedBy; - private readonly Dictionary globalFilters; - private readonly Action onSaveAction; - private bool ignoreGlobalFilters; - private readonly string tenantPrefix; - private T entity; + private ReplaceOptions _options = new(); + private readonly List> _models = new(); + private readonly ModifiedBy? _modifiedBy; + private readonly Action _onSaveAction; + private T? _entity; + + public DBContext Context { get; } + public IMongoCollection Collection { get; } internal Replace( - IClientSessionHandle session, + DBContext context, + IMongoCollection collection, ModifiedBy modifiedBy, Dictionary globalFilters, - Action onSaveAction, - string tenantPrefix) - { - this.session = session; - this.modifiedBy = modifiedBy; - this.globalFilters = globalFilters; - this.onSaveAction = onSaveAction; - this.tenantPrefix = tenantPrefix; - } - - /// - /// Specify an IEntity ID as the matching criteria - /// - /// A unique IEntity ID - public Replace MatchID(string ID) - { - return Match(f => f.Eq(t => t.ID, ID)); - } - - /// - /// Specify the matching criteria with a lambda expression - /// - /// x => x.Property == Value - public Replace Match(Expression> expression) - { - return Match(f => f.Where(expression)); - } - - /// - /// Specify the matching criteria with a filter expression - /// - /// f => f.Eq(x => x.Prop, Value) & f.Gt(x => x.Prop, Value) - public Replace Match(Func, FilterDefinition> filter) - { - this.filter &= filter(Builders.Filter); - return this; - } - - /// - /// Specify the matching criteria with a filter definition - /// - /// A filter definition - public Replace Match(FilterDefinition filterDefinition) - { - filter &= filterDefinition; - return this; - } - - /// - /// Specify the matching criteria with a template - /// - /// A Template with a find query - public Replace Match(Template template) - { - filter &= template.RenderToString(); - return this; - } - - /// - /// Specify a search term to find results from the text index of this particular collection. - /// TIP: Make sure to define a text index with DB.Index<T>() before searching - /// - /// The type of text matching to do - /// The search term - /// Case sensitivity of the search (optional) - /// Diacritic sensitivity of the search (optional) - /// The language for the search (optional) - public Replace Match(Search searchType, string searchTerm, bool caseSensitive = false, bool diacriticSensitive = false, string language = null) + Action onSaveAction) : base(globalFilters) { - if (searchType == Search.Fuzzy) - { - searchTerm = searchTerm.ToDoubleMetaphoneHash(); - caseSensitive = false; - diacriticSensitive = false; - language = null; - } - - return Match( - f => f.Text( - searchTerm, - new TextSearchOptions - { - CaseSensitive = caseSensitive, - DiacriticSensitive = diacriticSensitive, - Language = language - })); + Context = context; + Collection = collection; + this._modifiedBy = modifiedBy; + this._onSaveAction = onSaveAction; } - /// - /// Specify criteria for matching entities based on GeoSpatial data (longitude & latitude) - /// TIP: Make sure to define a Geo2DSphere index with DB.Index<T>() before searching - /// Note: DB.FluentGeoNear() supports more advanced options - /// - /// The property where 2DCoordinates are stored - /// The search point - /// Maximum distance in meters from the search point - /// Minimum distance in meters from the search point - public Replace Match(Expression> coordinatesProperty, Coordinates2D nearCoordinates, double? maxDistance = null, double? minDistance = null) - { - return Match(f => f.Near(coordinatesProperty, nearCoordinates.ToGeoJsonPoint(), maxDistance, minDistance)); - } - - /// - /// Specify the matching criteria with a JSON string - /// - /// { Title : 'The Power Of Now' } - public Replace MatchString(string jsonString) - { - filter &= jsonString; - return this; - } - - /// - /// Specify the matching criteria with an aggregation expression (i.e. $expr) - /// - /// { $gt: ['$Property1', '$Property2'] } - public Replace MatchExpression(string expression) - { - filter &= "{$expr:" + expression + "}"; - return this; - } - - /// - /// Specify the matching criteria with a Template - /// - /// A Template object - public Replace MatchExpression(Template template) - { - filter &= "{$expr:" + template.RenderToString() + "}"; - return this; - } /// /// Supply the entity to replace the first matched document with @@ -173,12 +49,9 @@ public Replace WithEntity(T entity) if (string.IsNullOrEmpty(entity.ID)) throw new InvalidOperationException("Cannot replace an entity with an empty ID value!"); - onSaveAction?.Invoke(entity); - - this.entity = entity; - - this.entity.SetTenantPrefixOnFileEntity(tenantPrefix); + _onSaveAction?.Invoke(entity); + this._entity = entity; return this; } @@ -189,38 +62,30 @@ public Replace WithEntity(T entity) /// x => x.OptionName = OptionValue public Replace Option(Action option) { - option(options); + option(_options); return this; } - /// - /// Specify that this operation should ignore any global filters - /// - public Replace IgnoreGlobalFilters() - { - ignoreGlobalFilters = true; - return this; - } /// /// Queue up a replace command for bulk execution later. /// public Replace AddToQueue() { - var mergedFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, filter); + var mergedFilter = Logic.MergeWithGlobalFilter(_ignoreGlobalFilters, _globalFilters, _filter); if (mergedFilter == Builders.Filter.Empty) throw new ArgumentException("Please use Match() method first!"); - if (entity == null) throw new ArgumentException("Please use WithEntity() method first!"); + if (_entity == null) throw new ArgumentException("Please use WithEntity() method first!"); SetModOnAndByValues(); - models.Add(new ReplaceOneModel(mergedFilter, entity) + _models.Add(new ReplaceOneModel(mergedFilter, _entity) { - Collation = options.Collation, - Hint = options.Hint, - IsUpsert = options.IsUpsert + Collation = _options.Collation, + Hint = _options.Hint, + IsUpsert = _options.IsUpsert }); - filter = Builders.Filter.Empty; - entity = default; - options = new ReplaceOptions(); + _filter = Builders.Filter.Empty; + _entity = default; + _options = new ReplaceOptions(); return this; } @@ -230,15 +95,15 @@ public Replace AddToQueue() /// An optional cancellation token public async Task ExecuteAsync(CancellationToken cancellation = default) { - if (models.Count > 0) + if (_models.Count > 0) { var bulkWriteResult = await ( - session == null - ? DB.Collection(tenantPrefix).BulkWriteAsync(models, null, cancellation) - : DB.Collection(tenantPrefix).BulkWriteAsync(session, models, null, cancellation) + this.Session() is not IClientSessionHandle session + ? Collection.BulkWriteAsync(_models, null, cancellation) + : Collection.BulkWriteAsync(session, _models, null, cancellation) ).ConfigureAwait(false); - models.Clear(); + _models.Clear(); if (!bulkWriteResult.IsAcknowledged) return ReplaceOneResult.Unacknowledged.Instance; @@ -247,25 +112,25 @@ public async Task ExecuteAsync(CancellationToken cancellation } else { - var mergedFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, filter); + var mergedFilter = MergedFilter; if (mergedFilter == Builders.Filter.Empty) throw new ArgumentException("Please use Match() method first!"); - if (entity == null) throw new ArgumentException("Please use WithEntity() method first!"); + if (_entity == null) throw new ArgumentException("Please use WithEntity() method first!"); SetModOnAndByValues(); - return session == null - ? await DB.Collection(tenantPrefix).ReplaceOneAsync(mergedFilter, entity, options, cancellation).ConfigureAwait(false) - : await DB.Collection(tenantPrefix).ReplaceOneAsync(session, mergedFilter, entity, options, cancellation).ConfigureAwait(false); + return this.Session() is not IClientSessionHandle session + ? await Collection.ReplaceOneAsync(mergedFilter, _entity, _options, cancellation).ConfigureAwait(false) + : await Collection.ReplaceOneAsync(session, mergedFilter, _entity, _options, cancellation).ConfigureAwait(false); } } private void SetModOnAndByValues() { - if (Cache.HasModifiedOn) ((IModifiedOn)entity).ModifiedOn = DateTime.UtcNow; - if (Cache.ModifiedByProp != null && modifiedBy != null) + if (Cache.Instance.HasModifiedOn && _entity is IModifiedOn _entityModifiedOn) _entityModifiedOn.ModifiedOn = DateTime.UtcNow; + if (Cache.Instance.ModifiedByProp != null && _modifiedBy != null) { - Cache.ModifiedByProp.SetValue( - entity, - BsonSerializer.Deserialize(modifiedBy.ToBson(), Cache.ModifiedByProp.PropertyType)); + Cache.Instance.ModifiedByProp.SetValue( + _entity, + BsonSerializer.Deserialize(_modifiedBy.ToBson(), Cache.Instance.ModifiedByProp.PropertyType)); } } } diff --git a/MongoDB.Entities/Builders/SortFilterQueryBase.cs b/MongoDB.Entities/Builders/SortFilterQueryBase.cs new file mode 100644 index 000000000..f127f8f0a --- /dev/null +++ b/MongoDB.Entities/Builders/SortFilterQueryBase.cs @@ -0,0 +1,49 @@ +using MongoDB.Driver; +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +#nullable enable +namespace MongoDB.Entities +{ + public class SortFilterQueryBase : FilterQueryBase where T : IEntity where TSelf : SortFilterQueryBase + { + internal List> _sorts = new(); + private TSelf This => (TSelf)this; + + internal SortFilterQueryBase(SortFilterQueryBase other) : base(other) + { + _sorts = other._sorts; + } + internal SortFilterQueryBase(Dictionary globalFilters) : base(globalFilters: globalFilters) + { + _globalFilters = globalFilters; + } + + + /// + /// Specify which property and order to use for sorting (use multiple times if needed) + /// + /// x => x.Prop + /// The sort order + public TSelf Sort(Expression> propertyToSortBy, Order sortOrder) + { + return sortOrder switch + { + Order.Ascending => Sort(s => s.Ascending(propertyToSortBy)), + Order.Descending => Sort(s => s.Descending(propertyToSortBy)), + _ => This, + }; + } + + /// + /// Specify how to sort using a sort expression + /// + /// s => s.Ascending("Prop1").MetaTextScore("Prop2") + /// + public TSelf Sort(Func, SortDefinition> sortFunction) + { + _sorts.Add(sortFunction(Builders.Sort)); + return This; + } + } +} diff --git a/MongoDB.Entities/Builders/Update.cs b/MongoDB.Entities/Builders/Update.cs index bfeba3164..86bad7a9e 100644 --- a/MongoDB.Entities/Builders/Update.cs +++ b/MongoDB.Entities/Builders/Update.cs @@ -9,13 +9,21 @@ namespace MongoDB.Entities { - public abstract class UpdateBase where T : IEntity + public class UpdateBase : FilterQueryBase where T : IEntity where TSelf : UpdateBase { //note: this base class exists for facilating the OnBeforeUpdate custom hook of DBContext class // there's no other purpose for this. protected readonly List> defs = new(); + internal UpdateBase(FilterQueryBase other) : base(other) + { + } + + internal UpdateBase(Dictionary globalFilters) : base(globalFilters) + { + } + /// /// Specify the property and it's value to modify (use multiple times if needed) /// @@ -53,15 +61,15 @@ public void AddModification(Template template) AddModification(template.RenderToString()); } - protected void SetTenantDbOnFileEntities(string tenantPrefix) - { - if (Cache.IsFileEntity) - { - defs.Add(Builders.Update.Set( - nameof(FileEntity.TenantPrefix), - Cache.Collection(tenantPrefix).Database.DatabaseNamespace.DatabaseName)); - } - } + //protected void SetTenantDbOnFileEntities(string tenantPrefix) + //{ + // if (Cache.Instance.IsFileEntity) + // { + // defs.Add(Builders.Update.Set( + // nameof(FileEntity.TenantPrefix), + // Cache.Instance.Collection(tenantPrefix).Database.DatabaseNamespace.DatabaseName)); + // } + //} } /// @@ -69,151 +77,29 @@ protected void SetTenantDbOnFileEntities(string tenantPrefix) /// TIP: Specify a filter first with the .Match(). Then set property values with .Modify() and finally call .Execute() to run the command. /// /// Any class that implements IEntity - public class Update : UpdateBase where T : IEntity + public class Update : UpdateBase>, ICollectionRelated where T : IEntity { - private readonly List> stages = new(); - private FilterDefinition filter = Builders.Filter.Empty; - private UpdateOptions options = new(); - private readonly IClientSessionHandle session; - private readonly List> models = new(); - private readonly Dictionary globalFilters; - private readonly Action> onUpdateAction; - private bool ignoreGlobalFilters; - private readonly string tenantPrefix; + private readonly List> _stages = new(); + private UpdateOptions _options = new(); + private readonly List> _models = new(); + private readonly Action> _onUpdateAction; + + public DBContext Context { get; } + public IMongoCollection Collection { get; } internal Update( - IClientSessionHandle session, + DBContext context, + IMongoCollection collection, Dictionary globalFilters, - Action> onUpdateAction, - string tenantPrefix) - { - this.session = session; - this.globalFilters = globalFilters; - this.onUpdateAction = onUpdateAction; - this.tenantPrefix = tenantPrefix; - } - - /// - /// Specify an IEntity ID as the matching criteria - /// - /// A unique IEntity ID - public Update MatchID(string ID) - { - return Match(f => f.Eq(t => t.ID, ID)); - } - - /// - /// Specify the matching criteria with a lambda expression - /// - /// x => x.Property == Value - public Update Match(Expression> expression) + Action> onUpdateAction) : + base(globalFilters) { - return Match(f => f.Where(expression)); + Context = context; + Collection = collection; + _onUpdateAction = onUpdateAction; } - /// - /// Specify the matching criteria with a filter expression - /// - /// f => f.Eq(x => x.Prop, Value) & f.Gt(x => x.Prop, Value) - public Update Match(Func, FilterDefinition> filter) - { - this.filter &= filter(Builders.Filter); - return this; - } - /// - /// Specify the matching criteria with a filter definition - /// - /// A filter definition - public Update Match(FilterDefinition filterDefinition) - { - filter &= filterDefinition; - return this; - } - - /// - /// Specify the matching criteria with a template - /// - /// A Template with a find query - public Update Match(Template template) - { - filter &= template.RenderToString(); - return this; - } - - /// - /// Specify a search term to find results from the text index of this particular collection. - /// TIP: Make sure to define a text index with DB.Index<T>() before searching - /// - /// The type of text matching to do - /// The search term - /// Case sensitivity of the search (optional) - /// Diacritic sensitivity of the search (optional) - /// The language for the search (optional) - public Update Match(Search searchType, string searchTerm, bool caseSensitive = false, bool diacriticSensitive = false, string language = null) - { - if (searchType == Search.Fuzzy) - { - searchTerm = searchTerm.ToDoubleMetaphoneHash(); - caseSensitive = false; - diacriticSensitive = false; - language = null; - } - - return Match( - f => f.Text( - searchTerm, - new TextSearchOptions - { - CaseSensitive = caseSensitive, - DiacriticSensitive = diacriticSensitive, - Language = language - })); - } - - /// - /// Specify criteria for matching entities based on GeoSpatial data (longitude & latitude) - /// TIP: Make sure to define a Geo2DSphere index with DB.Index<T>() before searching - /// Note: DB.FluentGeoNear() supports more advanced options - /// - /// The property where 2DCoordinates are stored - /// The search point - /// Maximum distance in meters from the search point - /// Minimum distance in meters from the search point - public Update Match(Expression> coordinatesProperty, Coordinates2D nearCoordinates, double? maxDistance = null, double? minDistance = null) - { - return Match(f => f.Near(coordinatesProperty, nearCoordinates.ToGeoJsonPoint(), maxDistance, minDistance)); - } - - /// - /// Specify the matching criteria with a JSON string - /// - /// { Title : 'The Power Of Now' } - public Update MatchString(string jsonString) - { - filter &= jsonString; - return this; - } - - /// - /// Specify the matching criteria with an aggregation expression (i.e. $expr) - /// - /// { $gt: ['$Property1', '$Property2'] } - public Update MatchExpression(string expression) - { - filter &= "{$expr:" + expression + "}"; - return this; - } - - /// - /// Specify the matching criteria with a Template - /// - /// A Template object - public Update MatchExpression(Template template) - { - filter &= "{$expr:" + template.RenderToString() + "}"; - return this; - } /// /// Specify the property and it's value to modify (use multiple times if needed) @@ -263,7 +149,7 @@ public Update Modify(Template template) /// The entity instance to read the property values from public Update ModifyWith(T entity) { - if (Cache.HasModifiedOn) ((IModifiedOn)entity).ModifiedOn = DateTime.UtcNow; + if (Cache.Instance.HasModifiedOn) ((IModifiedOn)entity).ModifiedOn = DateTime.UtcNow; defs.AddRange(Logic.BuildUpdateDefs(entity)); return this; } @@ -275,7 +161,7 @@ public Update ModifyWith(T entity) /// The entity instance to read the corresponding values from public Update ModifyOnly(Expression> members, T entity) { - if (Cache.HasModifiedOn) ((IModifiedOn)entity).ModifiedOn = DateTime.UtcNow; + if (Cache.Instance.HasModifiedOn) ((IModifiedOn)entity).ModifiedOn = DateTime.UtcNow; defs.AddRange(Logic.BuildUpdateDefs(entity, members)); return this; } @@ -287,7 +173,7 @@ public Update ModifyOnly(Expression> members, T entity) /// The entity instance to read the corresponding values from public Update ModifyExcept(Expression> members, T entity) { - if (Cache.HasModifiedOn) ((IModifiedOn)entity).ModifiedOn = DateTime.UtcNow; + if (Cache.Instance.HasModifiedOn) ((IModifiedOn)entity).ModifiedOn = DateTime.UtcNow; defs.AddRange(Logic.BuildUpdateDefs(entity, members, excludeMode: true)); return this; } @@ -301,7 +187,7 @@ public Update WithPipeline(Template template) { foreach (var stage in template.ToStages()) { - stages.Add(stage); + _stages.Add(stage); } return this; @@ -314,7 +200,7 @@ public Update WithPipeline(Template template) /// { $set: { FullName: { $concat: ['$Name', ' ', '$Surname'] } } } public Update WithPipelineStage(string stage) { - stages.Add(stage); + _stages.Add(stage); return this; } @@ -336,10 +222,10 @@ public Update WithArrayFilter(string filter) { ArrayFilterDefinition def = filter; - options.ArrayFilters = - options.ArrayFilters == null + _options.ArrayFilters = + _options.ArrayFilters == null ? new[] { def } - : options.ArrayFilters.Concat(new[] { def }); + : _options.ArrayFilters.Concat(new[] { def }); return this; } @@ -362,10 +248,10 @@ public Update WithArrayFilters(Template template) { var defs = template.ToArrayFilters(); - options.ArrayFilters = - options.ArrayFilters == null + _options.ArrayFilters = + _options.ArrayFilters == null ? defs - : options.ArrayFilters.Concat(defs); + : _options.ArrayFilters.Concat(defs); return this; } @@ -377,40 +263,32 @@ public Update WithArrayFilters(Template template) /// x => x.OptionName = OptionValue public Update Option(Action option) { - option(options); + option(_options); return this; } - /// - /// Specify that this operation should ignore any global filters - /// - public Update IgnoreGlobalFilters() - { - ignoreGlobalFilters = true; - return this; - } + /// /// Queue up an update command for bulk execution later. /// public Update AddToQueue() { - var mergedFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, filter); + var mergedFilter = MergedFilter; if (mergedFilter == Builders.Filter.Empty) throw new ArgumentException("Please use Match() method first!"); if (defs.Count == 0) throw new ArgumentException("Please use Modify() method first!"); - if (Cache.HasModifiedOn) Modify(b => b.CurrentDate(Cache.ModifiedOnPropName)); - SetTenantDbOnFileEntities(tenantPrefix); - onUpdateAction?.Invoke(this); - models.Add(new UpdateManyModel(mergedFilter, Builders.Update.Combine(defs)) + if (Cache.Instance.HasModifiedOn) Modify(b => b.CurrentDate(Cache.Instance.ModifiedOnPropName)); + _onUpdateAction?.Invoke(this); + _models.Add(new UpdateManyModel(mergedFilter, Builders.Update.Combine(defs)) { - ArrayFilters = options.ArrayFilters, - Collation = options.Collation, - Hint = options.Hint, - IsUpsert = options.IsUpsert + ArrayFilters = _options.ArrayFilters, + Collation = _options.Collation, + Hint = _options.Hint, + IsUpsert = _options.IsUpsert }); - filter = Builders.Filter.Empty; + _filter = Builders.Filter.Empty; defs.Clear(); - options = new UpdateOptions(); + _options = new UpdateOptions(); return this; } @@ -420,15 +298,15 @@ public Update AddToQueue() /// An optional cancellation token public async Task ExecuteAsync(CancellationToken cancellation = default) { - if (models.Count > 0) + if (_models.Count > 0) { var bulkWriteResult = await ( - session == null - ? DB.Collection(tenantPrefix).BulkWriteAsync(models, null, cancellation) - : DB.Collection(tenantPrefix).BulkWriteAsync(session, models, null, cancellation) + this.Session() is not IClientSessionHandle session + ? Collection.BulkWriteAsync(_models, null, cancellation) + : Collection.BulkWriteAsync(session, _models, null, cancellation) ).ConfigureAwait(false); - models.Clear(); + _models.Clear(); if (!bulkWriteResult.IsAcknowledged) return UpdateResult.Unacknowledged.Instance; @@ -437,14 +315,14 @@ public async Task ExecuteAsync(CancellationToken cancellation = de } else { - var mergedFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, filter); + var mergedFilter = MergedFilter; if (mergedFilter == Builders.Filter.Empty) throw new ArgumentException("Please use Match() method first!"); if (defs.Count == 0) throw new ArgumentException("Please use a Modify() method first!"); - if (stages.Count > 0) throw new ArgumentException("Regular updates and Pipeline updates cannot be used together!"); - if (ShouldSetModDate()) Modify(b => b.CurrentDate(Cache.ModifiedOnPropName)); - SetTenantDbOnFileEntities(tenantPrefix); - onUpdateAction?.Invoke(this); - return await UpdateAsync(mergedFilter, Builders.Update.Combine(defs), options, session, cancellation).ConfigureAwait(false); + if (_stages.Count > 0) throw new ArgumentException("Regular updates and Pipeline updates cannot be used together!"); + if (ShouldSetModDate()) Modify(b => b.CurrentDate(Cache.Instance.ModifiedOnPropName)); + + _onUpdateAction?.Invoke(this); + return await UpdateAsync(mergedFilter, Builders.Update.Combine(defs), _options, this.Session(), cancellation).ConfigureAwait(false); } } @@ -454,18 +332,17 @@ public async Task ExecuteAsync(CancellationToken cancellation = de /// An optional cancellation token public Task ExecutePipelineAsync(CancellationToken cancellation = default) { - var mergedFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, filter); + var mergedFilter = MergedFilter; if (mergedFilter == Builders.Filter.Empty) throw new ArgumentException("Please use Match() method first!"); - if (stages.Count == 0) throw new ArgumentException("Please use WithPipelineStage() method first!"); + if (_stages.Count == 0) throw new ArgumentException("Please use WithPipelineStage() method first!"); if (defs.Count > 0) throw new ArgumentException("Pipeline updates cannot be used together with regular updates!"); - if (ShouldSetModDate()) WithPipelineStage($"{{ $set: {{ '{Cache.ModifiedOnPropName}': new Date() }} }}"); - SetTenantDbOnFileEntities(tenantPrefix); + if (ShouldSetModDate()) WithPipelineStage($"{{ $set: {{ '{Cache.Instance.ModifiedOnPropName}': new Date() }} }}"); return UpdateAsync( mergedFilter, - Builders.Update.Pipeline(stages.ToArray()), - options, - session, + Builders.Update.Pipeline(_stages.ToArray()), + _options, + this.Session(), cancellation); } @@ -474,18 +351,18 @@ private bool ShouldSetModDate() //only set mod date by library if user hasn't done anything with the ModifiedOn property return - Cache.HasModifiedOn && + Cache.Instance.HasModifiedOn && !defs.Any(d => d .Render(BsonSerializer.SerializerRegistry.GetSerializer(), BsonSerializer.SerializerRegistry) .ToString() - .Contains($"\"{Cache.ModifiedOnPropName}\"")); + .Contains($"\"{Cache.Instance.ModifiedOnPropName}\"")); } - private Task UpdateAsync(FilterDefinition filter, UpdateDefinition definition, UpdateOptions options, IClientSessionHandle session = null, CancellationToken cancellation = default) + private Task UpdateAsync(FilterDefinition filter, UpdateDefinition definition, UpdateOptions options, IClientSessionHandle? session = null, CancellationToken cancellation = default) { return session == null - ? DB.Collection(tenantPrefix).UpdateManyAsync(filter, definition, options, cancellation) - : DB.Collection(tenantPrefix).UpdateManyAsync(session, filter, definition, options, cancellation); + ? Collection.UpdateManyAsync(filter, definition, options, cancellation) + : Collection.UpdateManyAsync(session, filter, definition, options, cancellation); } } } diff --git a/MongoDB.Entities/Builders/UpdateAndGet.cs b/MongoDB.Entities/Builders/UpdateAndGet.cs index fc42f67c4..3e3a0f814 100644 --- a/MongoDB.Entities/Builders/UpdateAndGet.cs +++ b/MongoDB.Entities/Builders/UpdateAndGet.cs @@ -19,7 +19,7 @@ public class UpdateAndGet : UpdateAndGet where T : IEntity internal UpdateAndGet( IClientSessionHandle session, Dictionary globalFilters, - Action> onUpdateAction, + Action> onUpdateAction, string tenantPrefix) : base(session, globalFilters, onUpdateAction, tenantPrefix) { } } @@ -30,16 +30,11 @@ internal UpdateAndGet( /// /// Any class that implements IEntity /// The type to project to - public class UpdateAndGet : UpdateBase where T : IEntity + public class UpdateAndGet : UpdateBase> where T : IEntity { private readonly List> stages = new(); - private FilterDefinition filter = Builders.Filter.Empty; private protected readonly FindOneAndUpdateOptions options = new() { ReturnDocument = ReturnDocument.After }; - private readonly IClientSessionHandle session; - private readonly Dictionary globalFilters; private readonly Action> onUpdateAction; - private bool ignoreGlobalFilters; - private readonly string tenantPrefix; internal UpdateAndGet( IClientSessionHandle session, diff --git a/MongoDB.Entities/Core/Cache.cs b/MongoDB.Entities/Core/Cache.cs index 75119667c..77c9d1084 100644 --- a/MongoDB.Entities/Core/Cache.cs +++ b/MongoDB.Entities/Core/Cache.cs @@ -7,7 +7,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; - +#nullable enable namespace MongoDB.Entities { internal abstract class Cache @@ -46,6 +46,9 @@ internal string GetFullDbName(Type entityType, string tenantPrefix) internal class Cache : Cache where T : IEntity { + private static Cache? _instance; + public static Cache Instance => _instance ??= new(); + public ConcurrentDictionary> Watchers { get; } = new(); public bool HasCreatedOn { get; } public bool HasModifiedOn { get; } @@ -59,10 +62,11 @@ internal class Cache : Cache where T : IEntity //val: IMongoCollection private readonly ConcurrentDictionary> _cache = new(); private readonly PropertyInfo[] _updatableProps; - private ProjectionDefinition _requiredPropsProjection; + private ProjectionDefinition? _requiredPropsProjection; public Cache() { + if (_instance == null) _instance = this; var type = typeof(T); var interfaces = type.GetInterfaces(); diff --git a/MongoDB.Entities/DBContext/DBContext.cs b/MongoDB.Entities/DBContext/DBContext.cs index 1aea7d8b9..058410df4 100644 --- a/MongoDB.Entities/DBContext/DBContext.cs +++ b/MongoDB.Entities/DBContext/DBContext.cs @@ -265,10 +265,10 @@ protected void SetGlobalFilterForInterface(string jsonString, bool p private void ThrowIfModifiedByIsEmpty() where T : IEntity { - if (Cache().ModifiedByProp != null && ModifiedBy is null) + if (Cache.Instance.ModifiedByProp != null && ModifiedBy is null) { throw new InvalidOperationException( - $"A value for [{Cache.ModifiedByProp.Name}] must be specified when saving/updating entities of type [{Cache.CollectionName}]"); + $"A value for [{Cache.Instance.ModifiedByProp.Name}] must be specified when saving/updating entities of type [{Cache.Instance.CollectionName}]"); } } diff --git a/MongoDB.Entities/Extensions/Entity.cs b/MongoDB.Entities/Extensions/Entity.cs index bfb3dca39..6717ea969 100644 --- a/MongoDB.Entities/Extensions/Entity.cs +++ b/MongoDB.Entities/Extensions/Entity.cs @@ -170,21 +170,5 @@ public static Task NextSequentialNumberAsync(this T _, CancellationTok { return DB.NextSequentialNumberAsync(cancellation, tenantPrefix); } - - internal static void SetTenantPrefixOnFileEntity(this T entity, string tenantPrefix) where T : IEntity - { - if (entity is FileEntity e) - { - e.TenantPrefix = tenantPrefix; - } - } - - internal static void SetTenantDbOnFileEntities(this IEnumerable entities, string tenantPrefix) where T : IEntity - { - foreach (var entity in entities) - { - SetTenantPrefixOnFileEntity(entity, tenantPrefix); - } - } } } diff --git a/MongoDB.Entities/MongoDB.Entities.csproj b/MongoDB.Entities/MongoDB.Entities.csproj index a22fa9a34..a6002511a 100644 --- a/MongoDB.Entities/MongoDB.Entities.csproj +++ b/MongoDB.Entities/MongoDB.Entities.csproj @@ -1,47 +1,47 @@  - - - 21.0.0-alpha3 + - - - add support for db per tenant multi-tenancy - - remove previously deprecated NameAttribute - - remove ability to change default database during runtime - - upgrade dependencies - + 21.0.0-alpha3 + enable + + - add support for db per tenant multi-tenancy + - remove previously deprecated NameAttribute + - remove ability to change default database during runtime + - upgrade dependencies + - netstandard2.0 - MongoDB.Entities - MongoDB.Entities - Đĵ ΝιΓΞΗΛψΚ - A data access library for MongoDB with an elegant api, LINQ support and built-in entity relationship management. - https://mongodb-entities.com - Đĵ ΝιΓΞΗΛψΚ - MongoDB.Entities - MongoDB.Entities - https://github.com/dj-nitehawk/MongoDB.Entities.git - git - MIT - icon.png - README.md - mongodb mongodb-orm mongodb-repo mongodb-repository entities nosql orm linq netcore repository aspnetcore netcore2 netcore3 dotnetstandard database persistance dal repo - true - 9.0 - + netstandard2.0 + MongoDB.Entities + MongoDB.Entities + Đĵ ΝιΓΞΗΛψΚ + A data access library for MongoDB with an elegant api, LINQ support and built-in entity relationship management. + https://mongodb-entities.com + Đĵ ΝιΓΞΗΛψΚ + MongoDB.Entities + MongoDB.Entities + https://github.com/dj-nitehawk/MongoDB.Entities.git + git + MIT + icon.png + README.md + mongodb mongodb-orm mongodb-repo mongodb-repository entities nosql orm linq netcore repository aspnetcore netcore2 netcore3 dotnetstandard database persistance dal repo + true + 9.0 + - - CS1591,RCS1158 - + + CS1591,RCS1158 + - - - - + + + + - - - - + + + + diff --git a/MongoDB.Entities/Relationships/Many.cs b/MongoDB.Entities/Relationships/Many.cs index e3d5f9fa0..4b5f4a2da 100644 --- a/MongoDB.Entities/Relationships/Many.cs +++ b/MongoDB.Entities/Relationships/Many.cs @@ -38,6 +38,8 @@ public sealed partial class Many : ManyBase where TChild : IEntity /// public IMongoCollection JoinCollection { get; private set; } + public string TargetCollectionName { get; set; } + /// /// Get the number of children for a relationship /// From 0ccb4e6b2426dac59b36879200920b914af39067 Mon Sep 17 00:00:00 2001 From: Ahmed Fwela Date: Fri, 5 Nov 2021 16:49:39 +0200 Subject: [PATCH 08/26] WIP update --- MongoDB.Entities/Builders/Update.cs | 107 +++++++++++++++------------- 1 file changed, 58 insertions(+), 49 deletions(-) diff --git a/MongoDB.Entities/Builders/Update.cs b/MongoDB.Entities/Builders/Update.cs index 86bad7a9e..304429d8a 100644 --- a/MongoDB.Entities/Builders/Update.cs +++ b/MongoDB.Entities/Builders/Update.cs @@ -14,14 +14,19 @@ public class UpdateBase : FilterQueryBase where T : IEntity //note: this base class exists for facilating the OnBeforeUpdate custom hook of DBContext class // there's no other purpose for this. - protected readonly List> defs = new(); + protected readonly List> defs; + protected readonly Action? onUpdateAction; - internal UpdateBase(FilterQueryBase other) : base(other) + internal UpdateBase(UpdateBase other) : base(other) { + onUpdateAction = other.onUpdateAction; + defs = other.defs; } - - internal UpdateBase(Dictionary globalFilters) : base(globalFilters) + private TSelf This => (TSelf)this; + internal UpdateBase(Dictionary globalFilters, Action? onUpdateAction, List>? defs) : base(globalFilters) { + this.onUpdateAction = onUpdateAction; + this.defs = defs ?? new(); } /// @@ -70,35 +75,6 @@ public void AddModification(Template template) // Cache.Instance.Collection(tenantPrefix).Database.DatabaseNamespace.DatabaseName)); // } //} - } - - /// - /// Represents an update command - /// TIP: Specify a filter first with the .Match(). Then set property values with .Modify() and finally call .Execute() to run the command. - /// - /// Any class that implements IEntity - public class Update : UpdateBase>, ICollectionRelated where T : IEntity - { - private readonly List> _stages = new(); - private UpdateOptions _options = new(); - private readonly List> _models = new(); - private readonly Action> _onUpdateAction; - - public DBContext Context { get; } - public IMongoCollection Collection { get; } - - internal Update( - DBContext context, - IMongoCollection collection, - Dictionary globalFilters, - Action> onUpdateAction) : - base(globalFilters) - { - Context = context; - Collection = collection; - _onUpdateAction = onUpdateAction; - } - /// @@ -106,10 +82,10 @@ internal Update( /// /// x => x.Property /// The value to set on the property - public Update Modify(Expression> property, TProp value) + public TSelf Modify(Expression> property, TProp value) { AddModification(property, value); - return this; + return This; } /// @@ -117,41 +93,41 @@ public Update Modify(Expression> property, TProp value) /// /// b => b.Inc(x => x.PropName, Value) /// - public Update Modify(Func, UpdateDefinition> operation) + public TSelf Modify(Func, UpdateDefinition> operation) { AddModification(operation); - return this; + return This; } /// /// Specify an update (json string) to modify the Entities (use multiple times if needed) /// /// { $set: { 'RootProp.$[x].SubProp' : 321 } } - public Update Modify(string update) + public TSelf Modify(string update) { AddModification(update); - return this; + return This; } /// /// Specify an update with a Template to modify the Entities (use multiple times if needed) /// /// A Template with a single update - public Update Modify(Template template) + public TSelf Modify(Template template) { AddModification(template.RenderToString()); - return this; + return This; } /// /// Modify ALL properties with the values from the supplied entity instance. /// /// The entity instance to read the property values from - public Update ModifyWith(T entity) + public TSelf ModifyWith(T entity) { if (Cache.Instance.HasModifiedOn) ((IModifiedOn)entity).ModifiedOn = DateTime.UtcNow; defs.AddRange(Logic.BuildUpdateDefs(entity)); - return this; + return This; } /// @@ -159,11 +135,11 @@ public Update ModifyWith(T entity) /// /// A new expression with the properties to include. Ex: x => new { x.PropOne, x.PropTwo } /// The entity instance to read the corresponding values from - public Update ModifyOnly(Expression> members, T entity) + public TSelf ModifyOnly(Expression> members, T entity) { if (Cache.Instance.HasModifiedOn) ((IModifiedOn)entity).ModifiedOn = DateTime.UtcNow; defs.AddRange(Logic.BuildUpdateDefs(entity, members)); - return this; + return This; } /// @@ -171,13 +147,46 @@ public Update ModifyOnly(Expression> members, T entity) /// /// Supply a new expression with the properties to exclude. Ex: x => new { x.Prop1, x.Prop2 } /// The entity instance to read the corresponding values from - public Update ModifyExcept(Expression> members, T entity) + public TSelf ModifyExcept(Expression> members, T entity) { if (Cache.Instance.HasModifiedOn) ((IModifiedOn)entity).ModifiedOn = DateTime.UtcNow; defs.AddRange(Logic.BuildUpdateDefs(entity, members, excludeMode: true)); - return this; + return This; + } + } + + /// + /// Represents an update command + /// TIP: Specify a filter first with the .Match(). Then set property values with .Modify() and finally call .Execute() to run the command. + /// + /// Any class that implements IEntity + public class Update : UpdateBase>, ICollectionRelated where T : IEntity + { + private readonly List> _stages = new(); + private UpdateOptions _options = new(); + private readonly List> _models = new(); + + internal Update(DBContext context, IMongoCollection collection, UpdateBase> other) : base(other) + { + Context = context; + Collection = collection; + } + + internal Update(DBContext context, IMongoCollection collection, Dictionary globalFilters, Action>? onUpdateAction, List>? defs) : base(globalFilters, onUpdateAction, defs) + { + Context = context; + Collection = collection; } + public DBContext Context { get; } + public IMongoCollection Collection { get; } + + + + + + + /// /// Specify an update pipeline with multiple stages using a Template to modify the Entities. /// NOTE: pipeline updates and regular updates cannot be used together. @@ -278,7 +287,7 @@ public Update AddToQueue() if (mergedFilter == Builders.Filter.Empty) throw new ArgumentException("Please use Match() method first!"); if (defs.Count == 0) throw new ArgumentException("Please use Modify() method first!"); if (Cache.Instance.HasModifiedOn) Modify(b => b.CurrentDate(Cache.Instance.ModifiedOnPropName)); - _onUpdateAction?.Invoke(this); + onUpdateAction?.Invoke(this); _models.Add(new UpdateManyModel(mergedFilter, Builders.Update.Combine(defs)) { ArrayFilters = _options.ArrayFilters, @@ -321,7 +330,7 @@ public async Task ExecuteAsync(CancellationToken cancellation = de if (_stages.Count > 0) throw new ArgumentException("Regular updates and Pipeline updates cannot be used together!"); if (ShouldSetModDate()) Modify(b => b.CurrentDate(Cache.Instance.ModifiedOnPropName)); - _onUpdateAction?.Invoke(this); + onUpdateAction?.Invoke(this); return await UpdateAsync(mergedFilter, Builders.Update.Combine(defs), _options, this.Session(), cancellation).ConfigureAwait(false); } } From da7e4d984c4fa56c6a5994677bcc509e24a2f364 Mon Sep 17 00:00:00 2001 From: Ahmed Fwela Date: Fri, 5 Nov 2021 17:16:01 +0200 Subject: [PATCH 09/26] WIP * Major refactoring * No more repeating code in builders * Multi-tenancy is per database now instead of per collection, which should be cleaner --- MongoDB.Entities/Builders/Update.cs | 4 +- MongoDB.Entities/Builders/UpdateAndGet.cs | 289 +++--------------- MongoDB.Entities/Core/Cache.cs | 1 - MongoDB.Entities/Core/DBContextOptions.cs | 7 +- MongoDB.Entities/Core/MongoContextOptions.cs | 4 +- .../DBContext/DBContext.Collection.cs | 6 +- MongoDB.Entities/DBContext/DBContext.Find.cs | 4 +- .../DBContext/DBContext.Update.cs | 12 +- MongoDB.Entities/DBContext/DBContext.cs | 30 +- MongoDB.Entities/Extensions/Entity.cs | 2 +- .../MongoContext/MongoContext.IMongoClient.cs | 2 +- MongoDB.Entities/MongoContext/MongoContext.cs | 9 - MongoDB.Entities/Relationships/One.cs | 87 +++--- 13 files changed, 130 insertions(+), 327 deletions(-) diff --git a/MongoDB.Entities/Builders/Update.cs b/MongoDB.Entities/Builders/Update.cs index 304429d8a..7fadaf296 100644 --- a/MongoDB.Entities/Builders/Update.cs +++ b/MongoDB.Entities/Builders/Update.cs @@ -23,7 +23,7 @@ internal UpdateBase(UpdateBase other) : base(other) defs = other.defs; } private TSelf This => (TSelf)this; - internal UpdateBase(Dictionary globalFilters, Action? onUpdateAction, List>? defs) : base(globalFilters) + internal UpdateBase(Dictionary globalFilters, Action? onUpdateAction = null, List>? defs = null) : base(globalFilters) { this.onUpdateAction = onUpdateAction; this.defs = defs ?? new(); @@ -172,7 +172,7 @@ internal Update(DBContext context, IMongoCollection collection, UpdateBase collection, Dictionary globalFilters, Action>? onUpdateAction, List>? defs) : base(globalFilters, onUpdateAction, defs) + internal Update(DBContext context, IMongoCollection collection, Dictionary globalFilters, Action>? onUpdateAction, List>? defs = null) : base(globalFilters, onUpdateAction, defs) { Context = context; Collection = collection; diff --git a/MongoDB.Entities/Builders/UpdateAndGet.cs b/MongoDB.Entities/Builders/UpdateAndGet.cs index 3e3a0f814..54364c986 100644 --- a/MongoDB.Entities/Builders/UpdateAndGet.cs +++ b/MongoDB.Entities/Builders/UpdateAndGet.cs @@ -16,12 +16,13 @@ namespace MongoDB.Entities /// Any class that implements IEntity public class UpdateAndGet : UpdateAndGet where T : IEntity { - internal UpdateAndGet( - IClientSessionHandle session, - Dictionary globalFilters, - Action> onUpdateAction, - string tenantPrefix) - : base(session, globalFilters, onUpdateAction, tenantPrefix) { } + internal UpdateAndGet(DBContext context, IMongoCollection collection, UpdateBase> other) : base(context, collection, other) + { + } + + internal UpdateAndGet(DBContext context, IMongoCollection collection, Dictionary globalFilters, Action>? onUpdateAction, List>? defs) : base(context, collection, globalFilters, onUpdateAction, defs) + { + } } /// @@ -30,221 +31,27 @@ internal UpdateAndGet( /// /// Any class that implements IEntity /// The type to project to - public class UpdateAndGet : UpdateBase> where T : IEntity + public class UpdateAndGet : UpdateBase>, ICollectionRelated where T : IEntity { - private readonly List> stages = new(); - private protected readonly FindOneAndUpdateOptions options = new() { ReturnDocument = ReturnDocument.After }; - private readonly Action> onUpdateAction; - - internal UpdateAndGet( - IClientSessionHandle session, - Dictionary globalFilters, - Action> onUpdateAction, - string tenantPrefix) - { - this.session = session; - this.globalFilters = globalFilters; - this.onUpdateAction = onUpdateAction; - this.tenantPrefix = tenantPrefix; - } - - /// - /// Specify an IEntity ID as the matching criteria - /// - /// A unique IEntity ID - public UpdateAndGet MatchID(string ID) - { - return Match(f => f.Eq(t => t.ID, ID)); - } - - /// - /// Specify the matching criteria with a lambda expression - /// - /// x => x.Property == Value - public UpdateAndGet Match(Expression> expression) - { - return Match(f => f.Where(expression)); - } - - /// - /// Specify the matching criteria with a filter expression - /// - /// f => f.Eq(x => x.Prop, Value) & f.Gt(x => x.Prop, Value) - public UpdateAndGet Match(Func, FilterDefinition> filter) - { - this.filter &= filter(Builders.Filter); - return this; - } - - /// - /// Specify the matching criteria with a filter definition - /// - /// A filter definition - public UpdateAndGet Match(FilterDefinition filterDefinition) - { - filter &= filterDefinition; - return this; - } + private readonly List> _stages = new(); + private protected readonly FindOneAndUpdateOptions _options = new() { ReturnDocument = ReturnDocument.After }; - /// - /// Specify the matching criteria with a template - /// - /// A Template with a find query - public UpdateAndGet Match(Template template) - { - filter &= template.RenderToString(); - return this; - } + public DBContext Context { get; } + public IMongoCollection Collection { get; } - /// - /// Specify a search term to find results from the text index of this particular collection. - /// TIP: Make sure to define a text index with DB.Index<T>() before searching - /// - /// The type of text matching to do - /// The search term - /// Case sensitivity of the search (optional) - /// Diacritic sensitivity of the search (optional) - /// The language for the search (optional) - public UpdateAndGet Match(Search searchType, string searchTerm, bool caseSensitive = false, bool diacriticSensitive = false, string language = null) + internal UpdateAndGet(DBContext context, IMongoCollection collection, UpdateBase> other) : base(other) { - if (searchType == Search.Fuzzy) - { - searchTerm = searchTerm.ToDoubleMetaphoneHash(); - caseSensitive = false; - diacriticSensitive = false; - language = null; - } - - return Match( - f => f.Text( - searchTerm, - new TextSearchOptions - { - CaseSensitive = caseSensitive, - DiacriticSensitive = diacriticSensitive, - Language = language - })); + Context = context; + Collection = collection; } - /// - /// Specify criteria for matching entities based on GeoSpatial data (longitude & latitude) - /// TIP: Make sure to define a Geo2DSphere index with DB.Index<T>() before searching - /// Note: DB.FluentGeoNear() supports more advanced options - /// - /// The property where 2DCoordinates are stored - /// The search point - /// Maximum distance in meters from the search point - /// Minimum distance in meters from the search point - public UpdateAndGet Match(Expression> coordinatesProperty, Coordinates2D nearCoordinates, double? maxDistance = null, double? minDistance = null) + internal UpdateAndGet(DBContext context, IMongoCollection collection, Dictionary globalFilters, Action>? onUpdateAction = null, List>? defs = null) : base(globalFilters, onUpdateAction, defs) { - return Match(f => f.Near(coordinatesProperty, nearCoordinates.ToGeoJsonPoint(), maxDistance, minDistance)); + Context = context; + Collection = collection; } - /// - /// Specify the matching criteria with a JSON string - /// - /// { Title : 'The Power Of Now' } - public UpdateAndGet MatchString(string jsonString) - { - filter &= jsonString; - return this; - } - /// - /// Specify the matching criteria with an aggregation expression (i.e. $expr) - /// - /// { $gt: ['$Property1', '$Property2'] } - public UpdateAndGet MatchExpression(string expression) - { - filter &= "{$expr:" + expression + "}"; - return this; - } - - /// - /// Specify the matching criteria with a Template - /// - /// A Template object - public UpdateAndGet MatchExpression(Template template) - { - filter &= "{$expr:" + template.RenderToString() + "}"; - return this; - } - - /// - /// Specify the property and it's value to modify (use multiple times if needed) - /// - /// x => x.Property - /// The value to set on the property - public UpdateAndGet Modify(Expression> property, TProp value) - { - AddModification(property, value); - return this; - } - - /// - /// Specify the update definition builder operation to modify the Entities (use multiple times if needed) - /// - /// b => b.Inc(x => x.PropName, Value) - public UpdateAndGet Modify(Func, UpdateDefinition> operation) - { - AddModification(operation); - return this; - } - - /// - /// Specify an update (json string) to modify the Entities (use multiple times if needed) - /// - /// { $set: { 'RootProp.$[x].SubProp' : 321 } } - public UpdateAndGet Modify(string update) - { - AddModification(update); - return this; - } - - /// - /// Specify an update with a Template to modify the Entities (use multiple times if needed) - /// - /// A Template with a single update - public UpdateAndGet Modify(Template template) - { - AddModification(template.RenderToString()); - return this; - } - - /// - /// Modify ALL properties with the values from the supplied entity instance. - /// - /// The entity instance to read the property values from - public UpdateAndGet ModifyWith(T entity) - { - if (Cache.HasModifiedOn) ((IModifiedOn)entity).ModifiedOn = DateTime.UtcNow; - defs.AddRange(Logic.BuildUpdateDefs(entity)); - return this; - } - - /// - /// Modify ONLY the specified properties with the values from a given entity instance. - /// - /// A new expression with the properties to include. Ex: x => new { x.PropOne, x.PropTwo } - /// The entity instance to read the corresponding values from - public UpdateAndGet ModifyOnly(Expression> members, T entity) - { - if (Cache.HasModifiedOn) ((IModifiedOn)entity).ModifiedOn = DateTime.UtcNow; - defs.AddRange(Logic.BuildUpdateDefs(entity, members)); - return this; - } - - /// - /// Modify all EXCEPT the specified properties with the values from a given entity instance. - /// - /// Supply a new expression with the properties to exclude. Ex: x => new { x.Prop1, x.Prop2 } - /// The entity instance to read the corresponding values from - public UpdateAndGet ModifyExcept(Expression> members, T entity) - { - if (Cache.HasModifiedOn) ((IModifiedOn)entity).ModifiedOn = DateTime.UtcNow; - defs.AddRange(Logic.BuildUpdateDefs(entity, members, excludeMode: true)); - return this; - } /// /// Specify an update pipeline with multiple stages using a Template to modify the Entities. @@ -255,7 +62,7 @@ public UpdateAndGet WithPipeline(Template template) { foreach (var stage in template.ToStages()) { - stages.Add(stage); + _stages.Add(stage); } return this; @@ -268,7 +75,7 @@ public UpdateAndGet WithPipeline(Template template) /// { $set: { FullName: { $concat: ['$Name', ' ', '$Surname'] } } } public UpdateAndGet WithPipelineStage(string stage) { - stages.Add(stage); + _stages.Add(stage); return this; } @@ -290,10 +97,10 @@ public UpdateAndGet WithArrayFilter(string filter) { ArrayFilterDefinition def = filter; - options.ArrayFilters = - options.ArrayFilters == null + _options.ArrayFilters = + _options.ArrayFilters == null ? new[] { def } - : options.ArrayFilters.Concat(new[] { def }); + : _options.ArrayFilters.Concat(new[] { def }); return this; } @@ -316,10 +123,10 @@ public UpdateAndGet WithArrayFilters(Template template) { var defs = template.ToArrayFilters(); - options.ArrayFilters = - options.ArrayFilters == null + _options.ArrayFilters = + _options.ArrayFilters == null ? defs - : options.ArrayFilters.Concat(defs); + : _options.ArrayFilters.Concat(defs); return this; } @@ -331,7 +138,7 @@ public UpdateAndGet WithArrayFilters(Template template) /// x => x.OptionName = OptionValue public UpdateAndGet Option(Action> option) { - option(options); + option(_options); return this; } @@ -350,7 +157,7 @@ public UpdateAndGet Project(Expression> exp /// p => p.Include("Prop1").Exclude("Prop2") public UpdateAndGet Project(Func, ProjectionDefinition> projection) { - options.Projection = projection(Builders.Projection); + _options.Projection = projection(Builders.Projection); return this; } @@ -363,16 +170,7 @@ public UpdateAndGet IncludeRequiredProps() if (typeof(T) != typeof(TProjection)) throw new InvalidOperationException("IncludeRequiredProps() cannot be used when projecting to a different type."); - options.Projection = Cache.CombineWithRequiredProps(options.Projection); - return this; - } - - /// - /// Specify that this operation should ignore any global filters - /// - public UpdateAndGet IgnoreGlobalFilters() - { - ignoreGlobalFilters = true; + _options.Projection = Cache.Instance.CombineWithRequiredProps(_options.Projection); return this; } @@ -382,14 +180,13 @@ public UpdateAndGet IgnoreGlobalFilters() /// An optional cancellation token public async Task ExecuteAsync(CancellationToken cancellation = default) { - var mergedFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, filter); + var mergedFilter = MergedFilter; if (mergedFilter == Builders.Filter.Empty) throw new ArgumentException("Please use Match() method first!"); if (defs.Count == 0) throw new ArgumentException("Please use Modify() method first!"); - if (stages.Count > 0) throw new ArgumentException("Regular updates and Pipeline updates cannot be used together!"); - if (ShouldSetModDate()) Modify(b => b.CurrentDate(Cache.ModifiedOnPropName)); - SetTenantDbOnFileEntities(tenantPrefix); + if (_stages.Count > 0) throw new ArgumentException("Regular updates and Pipeline updates cannot be used together!"); + if (ShouldSetModDate()) Modify(b => b.CurrentDate(Cache.Instance.ModifiedOnPropName)); onUpdateAction?.Invoke(this); - return await UpdateAndGetAsync(mergedFilter, Builders.Update.Combine(defs), options, session, cancellation).ConfigureAwait(false); + return await UpdateAndGetAsync(mergedFilter, Builders.Update.Combine(defs), _options, this.Session(), cancellation).ConfigureAwait(false); } /// @@ -398,14 +195,14 @@ public async Task ExecuteAsync(CancellationToken cancellation = def /// An optional cancellation token public Task ExecutePipelineAsync(CancellationToken cancellation = default) { - var mergedFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, filter); + var mergedFilter = MergedFilter; if (mergedFilter == Builders.Filter.Empty) throw new ArgumentException("Please use Match() method first!"); - if (stages.Count == 0) throw new ArgumentException("Please use WithPipelineStage() method first!"); + if (_stages.Count == 0) throw new ArgumentException("Please use WithPipelineStage() method first!"); if (defs.Count > 0) throw new ArgumentException("Pipeline updates cannot be used together with regular updates!"); - if (ShouldSetModDate()) WithPipelineStage($"{{ $set: {{ '{Cache.ModifiedOnPropName}': new Date() }} }}"); - SetTenantDbOnFileEntities(tenantPrefix); + if (ShouldSetModDate()) WithPipelineStage($"{{ $set: {{ '{Cache.Instance.ModifiedOnPropName}': new Date() }} }}"); + - return UpdateAndGetAsync(mergedFilter, Builders.Update.Pipeline(stages.ToArray()), options, session, cancellation); + return UpdateAndGetAsync(mergedFilter, Builders.Update.Pipeline(_stages.ToArray()), _options, this.Session(), cancellation); } private bool ShouldSetModDate() @@ -413,18 +210,18 @@ private bool ShouldSetModDate() //only set mod date by library if user hasn't done anything with the ModifiedOn property return - Cache.HasModifiedOn && + Cache.Instance.HasModifiedOn && !defs.Any(d => d .Render(BsonSerializer.SerializerRegistry.GetSerializer(), BsonSerializer.SerializerRegistry) .ToString() - .Contains($"\"{Cache.ModifiedOnPropName}\"")); + .Contains($"\"{Cache.Instance.ModifiedOnPropName}\"")); } - private Task UpdateAndGetAsync(FilterDefinition filter, UpdateDefinition definition, FindOneAndUpdateOptions options, IClientSessionHandle session = null, CancellationToken cancellation = default) + private Task UpdateAndGetAsync(FilterDefinition filter, UpdateDefinition definition, FindOneAndUpdateOptions options, IClientSessionHandle? session = null, CancellationToken cancellation = default) { return session == null - ? DB.Collection(tenantPrefix).FindOneAndUpdateAsync(filter, definition, options, cancellation) - : DB.Collection(tenantPrefix).FindOneAndUpdateAsync(session, filter, definition, options, cancellation); + ? Collection.FindOneAndUpdateAsync(filter, definition, options, cancellation) + : Collection.FindOneAndUpdateAsync(session, filter, definition, options, cancellation); } } } diff --git a/MongoDB.Entities/Core/Cache.cs b/MongoDB.Entities/Core/Cache.cs index 77c9d1084..704b71802 100644 --- a/MongoDB.Entities/Core/Cache.cs +++ b/MongoDB.Entities/Core/Cache.cs @@ -66,7 +66,6 @@ internal class Cache : Cache where T : IEntity public Cache() { - if (_instance == null) _instance = this; var type = typeof(T); var interfaces = type.GetInterfaces(); diff --git a/MongoDB.Entities/Core/DBContextOptions.cs b/MongoDB.Entities/Core/DBContextOptions.cs index a0571e7ee..db734ce92 100644 --- a/MongoDB.Entities/Core/DBContextOptions.cs +++ b/MongoDB.Entities/Core/DBContextOptions.cs @@ -6,12 +6,7 @@ namespace MongoDB.Entities { public class DBContextOptions { - public DBContextOptions(string? tenantId = null) - { - TenantId = tenantId; - } - - public string? TenantId { get; set; } + } } diff --git a/MongoDB.Entities/Core/MongoContextOptions.cs b/MongoDB.Entities/Core/MongoContextOptions.cs index 8403c13eb..503267109 100644 --- a/MongoDB.Entities/Core/MongoContextOptions.cs +++ b/MongoDB.Entities/Core/MongoContextOptions.cs @@ -9,14 +9,16 @@ namespace MongoDB.Entities { public class MongoContextOptions { - public MongoContextOptions(ModifiedBy? modifiedBy = null) + public MongoContextOptions(ModifiedBy? modifiedBy = null, string? tenantId = null) { ModifiedBy = modifiedBy; + TenantId = tenantId; } /// /// The value of this property will be automatically set on entities when saving/updating if the entity has a ModifiedBy property /// public ModifiedBy? ModifiedBy { get; set; } + public string? TenantId { get; set; } } } diff --git a/MongoDB.Entities/DBContext/DBContext.Collection.cs b/MongoDB.Entities/DBContext/DBContext.Collection.cs index 7399448f3..93a106202 100644 --- a/MongoDB.Entities/DBContext/DBContext.Collection.cs +++ b/MongoDB.Entities/DBContext/DBContext.Collection.cs @@ -19,8 +19,8 @@ public Task CreateCollectionAsync(Action> options, var opts = new CreateCollectionOptions(); options(opts); return Session == null - ? Database.CreateCollectionAsync(Cache.CollectionName, opts, cancellation) - : Database.CreateCollectionAsync(Session, Cache.CollectionName, opts, cancellation); + ? Database.CreateCollectionAsync(Cache().CollectionName, opts, cancellation) + : Database.CreateCollectionAsync(Session, Cache().CollectionName, opts, cancellation); } @@ -33,7 +33,7 @@ public async Task DropCollectionAsync() where T : IEntity { var tasks = new List(); var db = Database; - var collName = Cache.CollectionName; + var collName = Cache().CollectionName; var options = new ListCollectionNamesOptions { Filter = "{$and:[{name:/~/},{name:/" + collName + "/}]}" diff --git a/MongoDB.Entities/DBContext/DBContext.Find.cs b/MongoDB.Entities/DBContext/DBContext.Find.cs index 7a1f9fe35..250ac7086 100644 --- a/MongoDB.Entities/DBContext/DBContext.Find.cs +++ b/MongoDB.Entities/DBContext/DBContext.Find.cs @@ -8,7 +8,7 @@ public partial class DBContext /// The type of entity public Find Find() where T : IEntity { - return new Find(Session, _globalFilters, tenantPrefix); + return new Find(this, CollectionFor(), _globalFilters); } /// @@ -18,7 +18,7 @@ public Find Find() where T : IEntity /// The type of the end result public Find Find() where T : IEntity { - return new Find(Session, _globalFilters, tenantPrefix); + return new Find(this, CollectionFor(), _globalFilters); } } } diff --git a/MongoDB.Entities/DBContext/DBContext.Update.cs b/MongoDB.Entities/DBContext/DBContext.Update.cs index e7ef641d0..928b0fe87 100644 --- a/MongoDB.Entities/DBContext/DBContext.Update.cs +++ b/MongoDB.Entities/DBContext/DBContext.Update.cs @@ -8,11 +8,11 @@ public partial class DBContext /// The type of entity public Update Update() where T : IEntity { - var cmd = new Update(Session, _globalFilters, OnBeforeUpdate(), tenantPrefix); - if (Cache.ModifiedByProp != null) + var cmd = new Update(this, CollectionFor(), _globalFilters, OnBeforeUpdate>()); + if (Cache().ModifiedByProp != null) { ThrowIfModifiedByIsEmpty(); - cmd.Modify(b => b.Set(Cache.ModifiedByProp.Name, ModifiedBy)); + cmd.Modify(b => b.Set(Cache().ModifiedByProp.Name, ModifiedBy)); } return cmd; } @@ -33,11 +33,11 @@ public UpdateAndGet UpdateAndGet() where T : IEntity /// The type of the end result public UpdateAndGet UpdateAndGet() where T : IEntity { - var cmd = new UpdateAndGet(Session, _globalFilters, OnBeforeUpdate(), tenantPrefix); - if (Cache.ModifiedByProp != null) + var cmd = new UpdateAndGet(this, CollectionFor(), _globalFilters, OnBeforeUpdate>()); + if (Cache().ModifiedByProp != null) { ThrowIfModifiedByIsEmpty(); - cmd.Modify(b => b.Set(Cache.ModifiedByProp.Name, ModifiedBy)); + cmd.Modify(b => b.Set(Cache().ModifiedByProp.Name, ModifiedBy)); } return cmd; } diff --git a/MongoDB.Entities/DBContext/DBContext.cs b/MongoDB.Entities/DBContext/DBContext.cs index 058410df4..2e9646370 100644 --- a/MongoDB.Entities/DBContext/DBContext.cs +++ b/MongoDB.Entities/DBContext/DBContext.cs @@ -1,6 +1,7 @@ using MongoDB.Bson.Serialization; using MongoDB.Driver; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; @@ -67,8 +68,8 @@ public DBContext(MongoContext mongoContext, IMongoDatabase database, DBContextOp public DBContext(MongoContext mongoContext, string database, MongoDatabaseSettings? settings = null, DBContextOptions? options = null) { MongoContext = mongoContext; - Database = mongoContext.GetDatabase(database, settings); Options = options ?? new(); + Database = mongoContext.GetDatabase((mongoContext.Options.TenantId ?? "") + database, settings); } /// @@ -94,8 +95,8 @@ public DBContext(string database, string host = "127.0.0.1", int port = 27017, M { ModifiedBy = modifiedBy }); - Database = MongoContext.GetDatabase(database); Options = new(); + Database = MongoContext.GetDatabase((MongoContext.Options.TenantId ?? "") + database); } /// @@ -116,8 +117,8 @@ public DBContext(string database, MongoClientSettings settings, ModifiedBy? modi { ModifiedBy = modifiedBy }); - Database = MongoContext.GetDatabase(database); Options = new(); + Database = MongoContext.GetDatabase((MongoContext.Options.TenantId ?? "") + database); } @@ -148,7 +149,8 @@ public DBContext(ModifiedBy? modifiedBy = null) : this("default", modifiedBy: mo /// This event hook will be triggered right before an update/replace command is executed /// /// Any entity that implements IEntity - protected virtual Action>? OnBeforeUpdate() where T : IEntity + /// Any entity that implements IEntity + protected virtual Action>? OnBeforeUpdate() where T : IEntity where TSelf : UpdateBase { return null; } @@ -265,10 +267,10 @@ protected void SetGlobalFilterForInterface(string jsonString, bool p private void ThrowIfModifiedByIsEmpty() where T : IEntity { - if (Cache.Instance.ModifiedByProp != null && ModifiedBy is null) + if (Cache().ModifiedByProp != null && ModifiedBy is null) { throw new InvalidOperationException( - $"A value for [{Cache.Instance.ModifiedByProp.Name}] must be specified when saving/updating entities of type [{Cache.Instance.CollectionName}]"); + $"A value for [{Cache().ModifiedByProp.Name}] must be specified when saving/updating entities of type [{Cache().CollectionName}]"); } } @@ -278,5 +280,21 @@ private void AddFilter(Type type, (object filterDef, bool prepend) filter) _globalFilters[type] = filter; } + + + private readonly ConcurrentDictionary _cache = new(); + internal Cache Cache() where T : IEntity + { + if (!_cache.TryGetValue(typeof(T), out var c)) + { + c = new Cache(); + } + return (Cache)c; + } + + public IMongoCollection CollectionFor() where T : IEntity + { + return Database.GetCollection(Cache().CollectionName); + } } } diff --git a/MongoDB.Entities/Extensions/Entity.cs b/MongoDB.Entities/Extensions/Entity.cs index 6717ea969..80e0bf4a6 100644 --- a/MongoDB.Entities/Extensions/Entity.cs +++ b/MongoDB.Entities/Extensions/Entity.cs @@ -76,7 +76,7 @@ public static string FullPath(this Expression> expression) /// /// An IQueryable collection of sibling Entities. /// - public static IMongoQueryable Queryable(this T _, AggregateOptions options = null, string tenantPrefix = null) where T : IEntity + public static IMongoQueryable Queryable(this T _, AggregateOptions? options = null, string tenantPrefix = null) where T : IEntity { return DB.Queryable(options, tenantPrefix: tenantPrefix); } diff --git a/MongoDB.Entities/MongoContext/MongoContext.IMongoClient.cs b/MongoDB.Entities/MongoContext/MongoContext.IMongoClient.cs index 90abdd0c7..fd475a6ec 100644 --- a/MongoDB.Entities/MongoContext/MongoContext.IMongoClient.cs +++ b/MongoDB.Entities/MongoContext/MongoContext.IMongoClient.cs @@ -36,7 +36,7 @@ Task IMongoClient.DropDatabaseAsync(IClientSessionHandle session, string name, C return Client.DropDatabaseAsync(session, name, cancellationToken); } - public IMongoDatabase GetDatabase(string name, MongoDatabaseSettings settings = null) + public IMongoDatabase GetDatabase(string name, MongoDatabaseSettings? settings = null) { return Client.GetDatabase(name, settings); } diff --git a/MongoDB.Entities/MongoContext/MongoContext.cs b/MongoDB.Entities/MongoContext/MongoContext.cs index a10bd78c2..3e0253446 100644 --- a/MongoDB.Entities/MongoContext/MongoContext.cs +++ b/MongoDB.Entities/MongoContext/MongoContext.cs @@ -44,15 +44,6 @@ public async Task> AllDatabaseNamesAsync() private Type[]? _allEntitiyTypes; public Type[] AllEntitiyTypes => _allEntitiyTypes ??= GetAllEntityTypes(); - private readonly ConcurrentDictionary _cache = new(); - internal Cache Cache() where T : IEntity - { - if (!_cache.TryGetValue(typeof(T), out var c)) - { - c = new Cache(); - } - return (Cache)c; - } private static Type[] GetAllEntityTypes() { diff --git a/MongoDB.Entities/Relationships/One.cs b/MongoDB.Entities/Relationships/One.cs index aae1a4845..e6d2f6220 100644 --- a/MongoDB.Entities/Relationships/One.cs +++ b/MongoDB.Entities/Relationships/One.cs @@ -50,50 +50,51 @@ public static implicit operator One(T entity) return new One(entity); } - /// - /// Fetches the actual entity this reference represents from the database. - /// - /// An optional session - /// An optional cancellation token - /// Optional tenant prefix if using multi-tenancy - /// A Task containing the actual entity - public Task ToEntityAsync(IClientSessionHandle session = null, CancellationToken cancellation = default, string tenantPrefix = null) - { - return new Find(session, null, tenantPrefix).OneAsync(ID, cancellation); - } + //TODO: revamp One/Many api + ///// + ///// Fetches the actual entity this reference represents from the database. + ///// + ///// An optional session + ///// An optional cancellation token + ///// Optional tenant prefix if using multi-tenancy + ///// A Task containing the actual entity + //public Task ToEntityAsync(IClientSessionHandle session = null, CancellationToken cancellation = default, string tenantPrefix = null) + //{ + // return new Find(session, null, tenantPrefix).OneAsync(ID, cancellation); + //} - /// - /// Fetches the actual entity this reference represents from the database with a projection. - /// - /// x => new Test { PropName = x.Prop } - /// An optional session if using within a transaction - /// An optional cancellation token - /// Optional tenant prefix if using multi-tenancy - /// A Task containing the actual projected entity - public async Task ToEntityAsync(Expression> projection, IClientSessionHandle session = null, CancellationToken cancellation = default, string tenantPrefix = null) - { - return (await new Find(session, null, tenantPrefix) - .Match(ID) - .Project(projection) - .ExecuteAsync(cancellation).ConfigureAwait(false)) - .SingleOrDefault(); - } + ///// + ///// Fetches the actual entity this reference represents from the database with a projection. + ///// + ///// x => new Test { PropName = x.Prop } + ///// An optional session if using within a transaction + ///// An optional cancellation token + ///// Optional tenant prefix if using multi-tenancy + ///// A Task containing the actual projected entity + //public async Task ToEntityAsync(Expression> projection, IClientSessionHandle session = null, CancellationToken cancellation = default, string tenantPrefix = null) + //{ + // return (await new Find(session, null, tenantPrefix) + // .Match(ID) + // .Project(projection) + // .ExecuteAsync(cancellation).ConfigureAwait(false)) + // .SingleOrDefault(); + //} - /// - /// Fetches the actual entity this reference represents from the database with a projection. - /// - /// p=> p.Include("Prop1").Exclude("Prop2") - /// An optional session if using within a transaction - /// An optional cancellation token - /// Optional tenant prefix if using multi-tenancy - /// A Task containing the actual projected entity - public async Task ToEntityAsync(Func, ProjectionDefinition> projection, IClientSessionHandle session = null, CancellationToken cancellation = default, string tenantPrefix = null) - { - return (await new Find(session, null, tenantPrefix) - .Match(ID) - .Project(projection) - .ExecuteAsync(cancellation).ConfigureAwait(false)) - .SingleOrDefault(); - } + ///// + ///// Fetches the actual entity this reference represents from the database with a projection. + ///// + ///// p=> p.Include("Prop1").Exclude("Prop2") + ///// An optional session if using within a transaction + ///// An optional cancellation token + ///// Optional tenant prefix if using multi-tenancy + ///// A Task containing the actual projected entity + //public async Task ToEntityAsync(Func, ProjectionDefinition> projection, IClientSessionHandle session = null, CancellationToken cancellation = default, string tenantPrefix = null) + //{ + // return (await new Find(session, null, tenantPrefix) + // .Match(ID) + // .Project(projection) + // .ExecuteAsync(cancellation).ConfigureAwait(false)) + // .SingleOrDefault(); + //} } } From 2e15d5904af4b592f80b28ea74d19e01597460cd Mon Sep 17 00:00:00 2001 From: Ahmed Fwela Date: Sat, 6 Nov 2021 02:10:54 +0200 Subject: [PATCH 10/26] WIP --- MongoDB.Entities/Builders/Update.cs | 15 +------- MongoDB.Entities/Core/Logic.cs | 10 +++--- MongoDB.Entities/Core/MongoContextOptions.cs | 4 +-- MongoDB.Entities/DB/DB.Count.cs | 35 ++++++------------ MongoDB.Entities/DB/DB.Delete.cs | 2 +- MongoDB.Entities/DB/DB.Queryable.cs | 8 ++--- .../DBContext/DBContext.Collection.cs | 1 - MongoDB.Entities/DBContext/DBContext.Count.cs | 36 +++++++------------ .../DBContext/DBContext.Delete.cs | 3 +- .../DBContext/DBContext.Queryable.cs | 11 +++--- MongoDB.Entities/DBContext/DBContext.cs | 13 ++++--- 11 files changed, 47 insertions(+), 91 deletions(-) diff --git a/MongoDB.Entities/Builders/Update.cs b/MongoDB.Entities/Builders/Update.cs index 7fadaf296..6498f1910 100644 --- a/MongoDB.Entities/Builders/Update.cs +++ b/MongoDB.Entities/Builders/Update.cs @@ -11,9 +11,6 @@ namespace MongoDB.Entities { public class UpdateBase : FilterQueryBase where T : IEntity where TSelf : UpdateBase { - //note: this base class exists for facilating the OnBeforeUpdate custom hook of DBContext class - // there's no other purpose for this. - protected readonly List> defs; protected readonly Action? onUpdateAction; @@ -65,17 +62,7 @@ public void AddModification(Template template) { AddModification(template.RenderToString()); } - - //protected void SetTenantDbOnFileEntities(string tenantPrefix) - //{ - // if (Cache.Instance.IsFileEntity) - // { - // defs.Add(Builders.Update.Set( - // nameof(FileEntity.TenantPrefix), - // Cache.Instance.Collection(tenantPrefix).Database.DatabaseNamespace.DatabaseName)); - // } - //} - + /// /// Specify the property and it's value to modify (use multiple times if needed) diff --git a/MongoDB.Entities/Core/Logic.cs b/MongoDB.Entities/Core/Logic.cs index eb5747e56..16c6ba9e5 100644 --- a/MongoDB.Entities/Core/Logic.cs +++ b/MongoDB.Entities/Core/Logic.cs @@ -9,17 +9,17 @@ namespace MongoDB.Entities { internal static class Logic { - internal static IEnumerable> BuildUpdateDefs(T entity) where T : IEntity + internal static IEnumerable> BuildUpdateDefs(T entity, DBContext? context = null) where T : IEntity { if (entity == null) throw new ArgumentException("The supplied entity cannot be null!"); - var props = Cache.UpdatableProps(entity); + var props = (context?.Cache() ?? Cache.Instance).UpdatableProps(entity); return props.Select(p => Builders.Update.Set(p.Name, p.GetValue(entity))); } - internal static IEnumerable> BuildUpdateDefs(T entity, Expression> members, bool excludeMode = false) where T : IEntity + internal static IEnumerable> BuildUpdateDefs(T entity, Expression> members, bool excludeMode = false, DBContext? context = null) where T : IEntity { var propNames = (members?.Body as NewExpression)?.Arguments .Select(a => a.ToString().Split('.')[1]); @@ -27,7 +27,7 @@ internal static IEnumerable> BuildUpdateDefs(T entity, Ex if (!propNames.Any()) throw new ArgumentException("Unable to get any properties from the members expression!"); - var props = Cache.UpdatableProps(entity); + var props = (context?.Cache() ?? Cache.Instance).UpdatableProps(entity); if (excludeMode) props = props.Where(p => !propNames.Contains(p.Name)); @@ -37,7 +37,7 @@ internal static IEnumerable> BuildUpdateDefs(T entity, Ex return props.Select(p => Builders.Update.Set(p.Name, p.GetValue(entity))); } - internal static FilterDefinition MergeWithGlobalFilter(bool ignoreGlobalFilters, Dictionary globalFilters, FilterDefinition filter) where T : IEntity + internal static FilterDefinition MergeWithGlobalFilter(bool ignoreGlobalFilters, Dictionary? globalFilters, FilterDefinition filter) where T : IEntity { //WARNING: this has to do the same thing as DBContext.Pipeline.MergeWithGlobalFilter method // if the following logic changes, update the other method also diff --git a/MongoDB.Entities/Core/MongoContextOptions.cs b/MongoDB.Entities/Core/MongoContextOptions.cs index 503267109..8403c13eb 100644 --- a/MongoDB.Entities/Core/MongoContextOptions.cs +++ b/MongoDB.Entities/Core/MongoContextOptions.cs @@ -9,16 +9,14 @@ namespace MongoDB.Entities { public class MongoContextOptions { - public MongoContextOptions(ModifiedBy? modifiedBy = null, string? tenantId = null) + public MongoContextOptions(ModifiedBy? modifiedBy = null) { ModifiedBy = modifiedBy; - TenantId = tenantId; } /// /// The value of this property will be automatically set on entities when saving/updating if the entity has a ModifiedBy property /// public ModifiedBy? ModifiedBy { get; set; } - public string? TenantId { get; set; } } } diff --git a/MongoDB.Entities/DB/DB.Count.cs b/MongoDB.Entities/DB/DB.Count.cs index 367fed665..dd923c63d 100644 --- a/MongoDB.Entities/DB/DB.Count.cs +++ b/MongoDB.Entities/DB/DB.Count.cs @@ -15,9 +15,9 @@ public static partial class DB /// The entity type to get the count for /// An optional cancellation token /// Optional tenant prefix if using multi-tenancy - public static Task CountEstimatedAsync(CancellationToken cancellation = default, string tenantPrefix = null) where T : IEntity + public static Task CountEstimatedAsync(CancellationToken cancellation = default) where T : IEntity { - return Collection(tenantPrefix).EstimatedDocumentCountAsync(null, cancellation); + return Context.CountEstimatedAsync(cancellation); } /// @@ -25,16 +25,11 @@ public static Task CountEstimatedAsync(CancellationToken cancellation = /// /// The entity type to get the count for /// A lambda expression for getting the count for a subset of the data - /// An optional session if using within a transaction /// An optional cancellation token /// An optional CountOptions object - /// Optional tenant prefix if using multi-tenancy - public static Task CountAsync(Expression> expression, IClientSessionHandle session = null, CancellationToken cancellation = default, CountOptions options = null, string tenantPrefix = null) where T : IEntity + public static Task CountAsync(Expression> expression, CancellationToken cancellation = default, CountOptions? options = null) where T : IEntity { - return - session == null - ? Collection(tenantPrefix).CountDocumentsAsync(expression, options, cancellation) - : Collection(tenantPrefix).CountDocumentsAsync(session, expression, options, cancellation); + return Context.CountAsync(expression, cancellation, options); } /// @@ -42,16 +37,11 @@ public static Task CountAsync(Expression> expression, ICl /// /// The entity type to get the count for /// A filter definition - /// An optional session if using within a transaction /// An optional cancellation token /// An optional CountOptions object - /// Optional tenant prefix if using multi-tenancy - public static Task CountAsync(FilterDefinition filter, IClientSessionHandle session = null, CancellationToken cancellation = default, CountOptions options = null, string tenantPrefix = null) where T : IEntity + public static Task CountAsync(FilterDefinition filter, CancellationToken cancellation = default, CountOptions? options = null) where T : IEntity { - return - session == null - ? Collection(tenantPrefix).CountDocumentsAsync(filter, options, cancellation) - : Collection(tenantPrefix).CountDocumentsAsync(session, filter, options, cancellation); + return Context.CountAsync(filter, cancellation, options); } /// @@ -63,24 +53,19 @@ public static Task CountAsync(FilterDefinition filter, IClientSessio /// An optional cancellation token /// An optional CountOptions object /// Optional tenant prefix if using multi-tenancy - public static Task CountAsync(Func, FilterDefinition> filter, IClientSessionHandle session = null, CancellationToken cancellation = default, CountOptions options = null, string tenantPrefix = null) where T : IEntity + public static Task CountAsync(Func, FilterDefinition> filter, CancellationToken cancellation = default, CountOptions? options = null) where T : IEntity { - return - session == null - ? Collection(tenantPrefix).CountDocumentsAsync(filter(Builders.Filter), options, cancellation) - : Collection(tenantPrefix).CountDocumentsAsync(session, filter(Builders.Filter), options, cancellation); + return Context.CountAsync(filter, cancellation, options); } /// /// Gets an accurate count of how many total entities are in the collection for a given entity type /// /// The entity type to get the count for - /// An optional session if using within a transaction /// An optional cancellation token - /// Optional tenant prefix if using multi-tenancy - public static Task CountAsync(IClientSessionHandle session = null, CancellationToken cancellation = default, string tenantPrefix = null) where T : IEntity + public static Task CountAsync(CancellationToken cancellation = default) where T : IEntity { - return CountAsync(_ => true, session, cancellation, tenantPrefix: tenantPrefix); + return Context.CountAsync(cancellation); } } } diff --git a/MongoDB.Entities/DB/DB.Delete.cs b/MongoDB.Entities/DB/DB.Delete.cs index 1f1c51373..2171fc0e4 100644 --- a/MongoDB.Entities/DB/DB.Delete.cs +++ b/MongoDB.Entities/DB/DB.Delete.cs @@ -152,7 +152,7 @@ public static Task DeleteAsync(Func, /// An optional cancellation token /// An optional collation object /// Optional tenant prefix if using multi-tenancy - public static async Task DeleteAsync(FilterDefinition filter, IClientSessionHandle session = null, CancellationToken cancellation = default, Collation collation = null, string tenantPrefix = null) where T : IEntity + public static async Task DeleteAsync(FilterDefinition filter, IClientSessionHandle? session = null, CancellationToken cancellation = default, Collation? collation = null) where T : IEntity { ThrowIfCancellationNotSupported(session, cancellation); diff --git a/MongoDB.Entities/DB/DB.Queryable.cs b/MongoDB.Entities/DB/DB.Queryable.cs index 13e2cfca5..f75126adf 100644 --- a/MongoDB.Entities/DB/DB.Queryable.cs +++ b/MongoDB.Entities/DB/DB.Queryable.cs @@ -9,14 +9,10 @@ public static partial class DB /// Exposes the MongoDB collection for the given IEntity as an IQueryable in order to facilitate LINQ queries. /// /// The aggregate options - /// An optional session if used within a transaction - /// Optional tenant prefix if using multi-tenancy /// Any class that implements IEntity - public static IMongoQueryable Queryable(AggregateOptions options = null, IClientSessionHandle session = null, string tenantPrefix = null) where T : IEntity + public static IMongoQueryable Queryable(AggregateOptions? options = null) where T : IEntity { - return session == null - ? Collection(tenantPrefix).AsQueryable(options) - : Collection(tenantPrefix).AsQueryable(session, options); + return Context.Queryable(options); } } } diff --git a/MongoDB.Entities/DBContext/DBContext.Collection.cs b/MongoDB.Entities/DBContext/DBContext.Collection.cs index 93a106202..e0d632daf 100644 --- a/MongoDB.Entities/DBContext/DBContext.Collection.cs +++ b/MongoDB.Entities/DBContext/DBContext.Collection.cs @@ -21,7 +21,6 @@ public Task CreateCollectionAsync(Action> options, return Session == null ? Database.CreateCollectionAsync(Cache().CollectionName, opts, cancellation) : Database.CreateCollectionAsync(Session, Cache().CollectionName, opts, cancellation); - } /// diff --git a/MongoDB.Entities/DBContext/DBContext.Count.cs b/MongoDB.Entities/DBContext/DBContext.Count.cs index ba593a507..e1be5fcbf 100644 --- a/MongoDB.Entities/DBContext/DBContext.Count.cs +++ b/MongoDB.Entities/DBContext/DBContext.Count.cs @@ -16,7 +16,7 @@ public partial class DBContext /// An optional cancellation token public Task CountEstimatedAsync(CancellationToken cancellation = default) where T : IEntity { - return DB.CountEstimatedAsync(cancellation, tenantPrefix); + return CollectionFor().EstimatedDocumentCountAsync(cancellationToken: cancellation); } /// @@ -27,14 +27,9 @@ public Task CountEstimatedAsync(CancellationToken cancellation = defaul /// An optional cancellation token /// An optional CountOptions object /// Set to true if you'd like to ignore any global filters for this operation - public Task CountAsync(Expression> expression, CancellationToken cancellation = default, CountOptions options = null, bool ignoreGlobalFilters = false) where T : IEntity + public Task CountAsync(Expression> expression, CancellationToken cancellation = default, CountOptions? options = null, bool ignoreGlobalFilters = false) where T : IEntity { - return DB.CountAsync( - Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, expression), - Session, - cancellation, - options, - tenantPrefix); + return CountAsync((FilterDefinition)expression, cancellation, options, ignoreGlobalFilters); } /// @@ -44,7 +39,8 @@ public Task CountAsync(Expression> expression, Cancellati /// An optional cancellation token public Task CountAsync(CancellationToken cancellation = default) where T : IEntity { - return DB.CountAsync(Session, cancellation, tenantPrefix); + return CountAsync(_ => true, cancellation); + } /// @@ -55,14 +51,13 @@ public Task CountAsync(CancellationToken cancellation = default) where /// An optional cancellation token /// An optional CountOptions object /// Set to true if you'd like to ignore any global filters for this operation - public Task CountAsync(FilterDefinition filter, CancellationToken cancellation = default, CountOptions options = null, bool ignoreGlobalFilters = false) where T : IEntity + public Task CountAsync(FilterDefinition filter, CancellationToken cancellation = default, CountOptions? options = null, bool ignoreGlobalFilters = false) where T : IEntity { - return DB.CountAsync( - Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, filter), - Session, - cancellation, - options, - tenantPrefix); + filter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, filter); + return + Session == null + ? CollectionFor().CountDocumentsAsync(filter, options, cancellation) + : CollectionFor().CountDocumentsAsync(Session, filter, options, cancellation); } /// @@ -73,14 +68,9 @@ public Task CountAsync(FilterDefinition filter, CancellationToken ca /// An optional cancellation token /// An optional CountOptions object /// Set to true if you'd like to ignore any global filters for this operation - public Task CountAsync(Func, FilterDefinition> filter, CancellationToken cancellation = default, CountOptions options = null, bool ignoreGlobalFilters = false) where T : IEntity + public Task CountAsync(Func, FilterDefinition> filter, CancellationToken cancellation = default, CountOptions? options = null, bool ignoreGlobalFilters = false) where T : IEntity { - return DB.CountAsync( - Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, filter(Builders.Filter)), - Session, - cancellation, - options, - tenantPrefix); + return CountAsync(filter(Builders.Filter), cancellation, options, ignoreGlobalFilters); } } } diff --git a/MongoDB.Entities/DBContext/DBContext.Delete.cs b/MongoDB.Entities/DBContext/DBContext.Delete.cs index e5567f1c9..cb46b46dd 100644 --- a/MongoDB.Entities/DBContext/DBContext.Delete.cs +++ b/MongoDB.Entities/DBContext/DBContext.Delete.cs @@ -22,8 +22,7 @@ public Task DeleteAsync(string ID, CancellationToken cancellati return DB.DeleteAsync( Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, Builders.Filter.Eq(e => e.ID, ID)), Session, - cancellation, - tenantPrefix: tenantPrefix); + cancellation); } /// diff --git a/MongoDB.Entities/DBContext/DBContext.Queryable.cs b/MongoDB.Entities/DBContext/DBContext.Queryable.cs index 6dda35e32..608c0627a 100644 --- a/MongoDB.Entities/DBContext/DBContext.Queryable.cs +++ b/MongoDB.Entities/DBContext/DBContext.Queryable.cs @@ -11,17 +11,20 @@ public partial class DBContext /// The aggregate options /// The type of entity /// Set to true if you'd like to ignore any global filters for this operation - public IMongoQueryable Queryable(AggregateOptions options = null, bool ignoreGlobalFilters = false) where T : IEntity + public IMongoQueryable Queryable(AggregateOptions? options = null, bool ignoreGlobalFilters = false) where T : IEntity { var globalFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, Builders.Filter.Empty); + var q = Session == null + ? CollectionFor().AsQueryable(options) + : CollectionFor().AsQueryable(Session, options); + if (globalFilter != Builders.Filter.Empty) { - return DB.Queryable(options, Session, tenantPrefix) - .Where(_ => globalFilter.Inject()); + q = q.Where(_ => globalFilter.Inject()); } - return DB.Queryable(options, Session, tenantPrefix); + return q; } } } diff --git a/MongoDB.Entities/DBContext/DBContext.cs b/MongoDB.Entities/DBContext/DBContext.cs index 2e9646370..39057a4c0 100644 --- a/MongoDB.Entities/DBContext/DBContext.cs +++ b/MongoDB.Entities/DBContext/DBContext.cs @@ -47,7 +47,8 @@ public ModifiedBy? ModifiedBy public MongoDatabaseSettings Settings => Database.Settings; - private Dictionary _globalFilters = new(); + private Dictionary? _globalFilters; + private Dictionary GlobalFilters => _globalFilters ??= new(); /// /// Copy constructor @@ -69,7 +70,7 @@ public DBContext(MongoContext mongoContext, string database, MongoDatabaseSettin { MongoContext = mongoContext; Options = options ?? new(); - Database = mongoContext.GetDatabase((mongoContext.Options.TenantId ?? "") + database, settings); + Database = mongoContext.GetDatabase(database, settings); } /// @@ -96,7 +97,7 @@ public DBContext(string database, string host = "127.0.0.1", int port = 27017, M ModifiedBy = modifiedBy }); Options = new(); - Database = MongoContext.GetDatabase((MongoContext.Options.TenantId ?? "") + database); + Database = MongoContext.GetDatabase(database); } /// @@ -118,7 +119,7 @@ public DBContext(string database, MongoClientSettings settings, ModifiedBy? modi ModifiedBy = modifiedBy }); Options = new(); - Database = MongoContext.GetDatabase((MongoContext.Options.TenantId ?? "") + database); + Database = MongoContext.GetDatabase(database); } @@ -276,9 +277,7 @@ private void ThrowIfModifiedByIsEmpty() where T : IEntity private void AddFilter(Type type, (object filterDef, bool prepend) filter) { - if (_globalFilters is null) _globalFilters = new Dictionary(); - - _globalFilters[type] = filter; + GlobalFilters[type] = filter; } From 389d86f8ab55d24d2a45cfb0af9dd21535401aee Mon Sep 17 00:00:00 2001 From: Ahmed Fwela Date: Sat, 6 Nov 2021 02:11:29 +0200 Subject: [PATCH 11/26] Rename --- MongoDB.Entities/DBContext/DBContext.cs | 32 +++++++++---------- .../MongoContext/MongoContext.IMongoClient.cs | 2 +- MongoDB.Entities/MongoContext/MongoContext.cs | 4 +-- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/MongoDB.Entities/DBContext/DBContext.cs b/MongoDB.Entities/DBContext/DBContext.cs index 39057a4c0..3c4409cb7 100644 --- a/MongoDB.Entities/DBContext/DBContext.cs +++ b/MongoDB.Entities/DBContext/DBContext.cs @@ -21,27 +21,27 @@ public partial class DBContext : IMongoDatabase public IClientSessionHandle? Session { get; protected set; } - public MongoContext MongoContext { get; set; } + public MongoServerContext MongoServerContext { get; set; } public IMongoDatabase Database { get; set; } public DBContextOptions Options { get; set; } /// - /// wrapper around so that we don't break the public api + /// wrapper around so that we don't break the public api /// public ModifiedBy? ModifiedBy { get { - return MongoContext.ModifiedBy; + return MongoServerContext.ModifiedBy; } [Obsolete("Use MongoContext.Options.ModifiedBy = value instead")] set { - MongoContext.Options.ModifiedBy = value; + MongoServerContext.Options.ModifiedBy = value; } } - public IMongoClient Client => MongoContext; + public IMongoClient Client => MongoServerContext; public DatabaseNamespace DatabaseNamespace => Database.DatabaseNamespace; @@ -56,19 +56,19 @@ public ModifiedBy? ModifiedBy /// public DBContext(DBContext other) { - MongoContext = other.MongoContext; + MongoServerContext = other.MongoServerContext; Database = other.Database; Options = other.Options; } - public DBContext(MongoContext mongoContext, IMongoDatabase database, DBContextOptions? options = null) + public DBContext(MongoServerContext mongoContext, IMongoDatabase database, DBContextOptions? options = null) { - MongoContext = mongoContext; + MongoServerContext = mongoContext; Database = database; Options = options ?? new(); } - public DBContext(MongoContext mongoContext, string database, MongoDatabaseSettings? settings = null, DBContextOptions? options = null) + public DBContext(MongoServerContext mongoContext, string database, MongoDatabaseSettings? settings = null, DBContextOptions? options = null) { - MongoContext = mongoContext; + MongoServerContext = mongoContext; Options = options ?? new(); Database = mongoContext.GetDatabase(database, settings); } @@ -86,7 +86,7 @@ public DBContext(MongoContext mongoContext, string database, MongoDatabaseSettin /// Only one ModifiedBy property is allowed on a single entity type. public DBContext(string database, string host = "127.0.0.1", int port = 27017, ModifiedBy? modifiedBy = null) { - MongoContext = new MongoContext( + MongoServerContext = new MongoServerContext( client: new MongoClient( new MongoClientSettings { @@ -97,7 +97,7 @@ public DBContext(string database, string host = "127.0.0.1", int port = 27017, M ModifiedBy = modifiedBy }); Options = new(); - Database = MongoContext.GetDatabase(database); + Database = MongoServerContext.GetDatabase(database); } /// @@ -112,14 +112,14 @@ public DBContext(string database, string host = "127.0.0.1", int port = 27017, M /// Only one ModifiedBy property is allowed on a single entity type. public DBContext(string database, MongoClientSettings settings, ModifiedBy? modifiedBy = null) { - MongoContext = new MongoContext( + MongoServerContext = new MongoServerContext( client: new MongoClient(settings), options: new() { ModifiedBy = modifiedBy }); Options = new(); - Database = MongoContext.GetDatabase(database); + Database = MongoServerContext.GetDatabase(database); } @@ -234,7 +234,7 @@ protected void SetGlobalFilterForBaseClass(FuncSet to true if you want to prepend this global filter to your operation filters instead of being appended protected void SetGlobalFilterForBaseClass(FilterDefinition filter, bool prepend = false) where TBase : IEntity { - foreach (var entType in MongoContext.AllEntitiyTypes.Where(t => t.IsSubclassOf(typeof(TBase)))) + foreach (var entType in MongoServerContext.AllEntitiyTypes.Where(t => t.IsSubclassOf(typeof(TBase)))) { var bsonDoc = filter.Render( BsonSerializer.SerializerRegistry.GetSerializer(), @@ -257,7 +257,7 @@ protected void SetGlobalFilterForInterface(string jsonString, bool p if (!targetType.IsInterface) throw new ArgumentException("Only interfaces are allowed!", "TInterface"); - foreach (var entType in MongoContext.AllEntitiyTypes.Where(t => targetType.IsAssignableFrom(t))) + foreach (var entType in MongoServerContext.AllEntitiyTypes.Where(t => targetType.IsAssignableFrom(t))) { AddFilter(entType, (jsonString, prepend)); } diff --git a/MongoDB.Entities/MongoContext/MongoContext.IMongoClient.cs b/MongoDB.Entities/MongoContext/MongoContext.IMongoClient.cs index fd475a6ec..a3cbe7bfd 100644 --- a/MongoDB.Entities/MongoContext/MongoContext.IMongoClient.cs +++ b/MongoDB.Entities/MongoContext/MongoContext.IMongoClient.cs @@ -10,7 +10,7 @@ namespace MongoDB.Entities { //Make these interface implmentation explicit, so we can fine-tune the api return result - public partial class MongoContext + public partial class MongoServerContext { public ICluster Cluster => Client.Cluster; diff --git a/MongoDB.Entities/MongoContext/MongoContext.cs b/MongoDB.Entities/MongoContext/MongoContext.cs index 3e0253446..6833ff3e6 100644 --- a/MongoDB.Entities/MongoContext/MongoContext.cs +++ b/MongoDB.Entities/MongoContext/MongoContext.cs @@ -10,14 +10,14 @@ namespace MongoDB.Entities /// /// MongoContext is a wrapper around an /// - public partial class MongoContext : IMongoClient + public partial class MongoServerContext : IMongoClient { /// /// Creates a new context /// /// The backing client, usually a /// The options to configure the context - public MongoContext(IMongoClient client, MongoContextOptions? options = null) + public MongoServerContext(IMongoClient client, MongoContextOptions? options = null) { Client = client; Options = options ?? new(); From 2d93a7d5cda13536260c37079ab026497189e1fe Mon Sep 17 00:00:00 2001 From: Ahmed Fwela Date: Tue, 9 Nov 2021 18:39:11 +0200 Subject: [PATCH 12/26] fixed Delete --- MongoDB.Entities/DB/DB.Delete.cs | 133 ++---------------- .../DBContext/DBContext.Delete.cs | 123 ++++++++++++---- 2 files changed, 103 insertions(+), 153 deletions(-) diff --git a/MongoDB.Entities/DB/DB.Delete.cs b/MongoDB.Entities/DB/DB.Delete.cs index 2171fc0e4..98b085483 100644 --- a/MongoDB.Entities/DB/DB.Delete.cs +++ b/MongoDB.Entities/DB/DB.Delete.cs @@ -10,57 +10,6 @@ namespace MongoDB.Entities { public static partial class DB { - private static readonly int deleteBatchSize = 100000; - - private static async Task DeleteCascadingAsync(string tenantPrefix, - IEnumerable IDs, - IClientSessionHandle session = null, - CancellationToken cancellation = default) where T : IEntity - { - // note: cancellation should not be enabled outside of transactions because multiple collections are involved - // and premature cancellation could cause data inconsistencies. - // i.e. don't pass the cancellation token to delete methods below that don't take a session. - // also make consumers call ThrowIfCancellationNotSupported() before calling this method. - - var db = Database(tenantPrefix); - var options = new ListCollectionNamesOptions - { - Filter = "{$and:[{name:/~/},{name:/" + CollectionName() + "/}]}" - }; - - var tasks = new List(); - - // note: db.listCollections() mongo command does not support transactions. - // so don't add session support here. - var collNamesCursor = await db.ListCollectionNamesAsync(options, cancellation).ConfigureAwait(false); - - foreach (var cName in await collNamesCursor.ToListAsync(cancellation).ConfigureAwait(false)) - { - tasks.Add( - session == null - ? db.GetCollection(cName).DeleteManyAsync(r => IDs.Contains(r.ChildID) || IDs.Contains(r.ParentID)) - : db.GetCollection(cName).DeleteManyAsync(session, r => IDs.Contains(r.ChildID) || IDs.Contains(r.ParentID), null, cancellation)); - } - - var delResTask = - session == null - ? Collection(tenantPrefix).DeleteManyAsync(x => IDs.Contains(x.ID)) - : Collection(tenantPrefix).DeleteManyAsync(session, x => IDs.Contains(x.ID), null, cancellation); - - tasks.Add(delResTask); - - if (typeof(T).BaseType == typeof(FileEntity)) - { - tasks.Add( - session == null - ? db.GetCollection(CollectionName()).DeleteManyAsync(x => IDs.Contains(x.FileID)) - : db.GetCollection(CollectionName()).DeleteManyAsync(session, x => IDs.Contains(x.FileID), null, cancellation)); - } - - await Task.WhenAll(tasks).ConfigureAwait(false); - - return await delResTask.ConfigureAwait(false); - } /// /// Deletes a single entity from MongoDB. @@ -68,13 +17,10 @@ private static async Task DeleteCascadingAsync(string tenantPre /// /// Any class that implements IEntity /// The Id of the entity to delete - /// An optional session if using within a transaction /// An optional cancellation token - /// Optional tenant prefix if using multi-tenancy - public static Task DeleteAsync(string ID, IClientSessionHandle session = null, CancellationToken cancellation = default, string tenantPrefix = null) where T : IEntity + public static Task DeleteAsync(string ID, CancellationToken cancellation = default) where T : IEntity { - ThrowIfCancellationNotSupported(session, cancellation); - return DeleteCascadingAsync(tenantPrefix, new[] { ID }, session, cancellation); + return Context.DeleteAsync(ID, cancellation); } /// @@ -84,29 +30,10 @@ public static Task DeleteAsync(string ID, IClientSessionHandle /// /// Any class that implements IEntity /// An IEnumerable of entity IDs - /// An optional session if using within a transaction /// An optional cancellation token - /// Optional tenant prefix if using multi-tenancy - public static async Task DeleteAsync(IEnumerable IDs, IClientSessionHandle session = null, CancellationToken cancellation = default, string tenantPrefix = null) where T : IEntity + public static Task DeleteAsync(IEnumerable IDs, CancellationToken cancellation = default) where T : IEntity { - ThrowIfCancellationNotSupported(session, cancellation); - - if (IDs.Count() <= deleteBatchSize) - return await DeleteCascadingAsync(tenantPrefix, IDs, session, cancellation).ConfigureAwait(false); - - long deletedCount = 0; - DeleteResult res = null; - - foreach (var batch in IDs.ToBatches(deleteBatchSize)) - { - res = await DeleteCascadingAsync(tenantPrefix, batch, session, cancellation).ConfigureAwait(false); - deletedCount += res.DeletedCount; - } - - if (res?.IsAcknowledged == false) - return DeleteResult.Unacknowledged.Instance; - - return new DeleteResult.Acknowledged(deletedCount); + return Context.DeleteAsync(IDs, cancellation); } /// @@ -116,13 +43,11 @@ public static async Task DeleteAsync(IEnumerable IDs, I /// /// Any class that implements IEntity /// A lambda expression for matching entities to delete. - /// An optional session if using within a transaction /// An optional cancellation token /// An optional collation object - /// Optional tenant prefix if using multi-tenancy - public static Task DeleteAsync(Expression> expression, IClientSessionHandle session = null, CancellationToken cancellation = default, Collation collation = null, string tenantPrefix = null) where T : IEntity + public static Task DeleteAsync(Expression> expression, CancellationToken cancellation = default, Collation? collation = null) where T : IEntity { - return DeleteAsync(Builders.Filter.Where(expression), session, cancellation, collation, tenantPrefix); + return Context.DeleteAsync(expression, collation: collation, cancellation: cancellation); } /// @@ -132,13 +57,11 @@ public static Task DeleteAsync(Expression> expres /// /// Any class that implements IEntity /// f => f.Eq(x => x.Prop, Value) & f.Gt(x => x.Prop, Value) - /// An optional session if using within a transaction /// An optional cancellation token /// An optional collation object - /// Optional tenant prefix if using multi-tenancy - public static Task DeleteAsync(Func, FilterDefinition> filter, IClientSessionHandle session = null, CancellationToken cancellation = default, Collation collation = null, string tenantPrefix = null) where T : IEntity + public static Task DeleteAsync(Func, FilterDefinition> filter, CancellationToken cancellation = default, Collation? collation = null) where T : IEntity { - return DeleteAsync(filter(Builders.Filter), session, cancellation, collation, tenantPrefix); + return Context.DeleteAsync(filter, collation: collation, cancellation: cancellation); } /// @@ -148,47 +71,11 @@ public static Task DeleteAsync(Func, /// /// Any class that implements IEntity /// A filter definition for matching entities to delete. - /// An optional session if using within a transaction /// An optional cancellation token /// An optional collation object - /// Optional tenant prefix if using multi-tenancy - public static async Task DeleteAsync(FilterDefinition filter, IClientSessionHandle? session = null, CancellationToken cancellation = default, Collation? collation = null) where T : IEntity - { - ThrowIfCancellationNotSupported(session, cancellation); - - var cursor = await new Find(session, null, tenantPrefix) - .Match(_ => filter) - .Project(e => e.ID) - .Option(o => o.BatchSize = deleteBatchSize) - .Option(o => o.Collation = collation) - .ExecuteCursorAsync(cancellation) - .ConfigureAwait(false); - - long deletedCount = 0; - DeleteResult res = null; - - using (cursor) - { - while (await cursor.MoveNextAsync(cancellation).ConfigureAwait(false)) - { - if (cursor.Current.Any()) - { - res = await DeleteCascadingAsync(tenantPrefix, cursor.Current, session, cancellation).ConfigureAwait(false); - deletedCount += res.DeletedCount; - } - } - } - - if (res?.IsAcknowledged == false) - return DeleteResult.Unacknowledged.Instance; - - return new DeleteResult.Acknowledged(deletedCount); - } - - private static void ThrowIfCancellationNotSupported(IClientSessionHandle session = null, CancellationToken cancellation = default) + public static Task DeleteAsync(FilterDefinition filter, CancellationToken cancellation = default, Collation? collation = null) where T : IEntity { - if (cancellation != default && session == null) - throw new NotSupportedException("Cancellation is only supported within transactions for delete operations!"); + return Context.DeleteAsync(filter, collation: collation, cancellation: cancellation); } } } diff --git a/MongoDB.Entities/DBContext/DBContext.Delete.cs b/MongoDB.Entities/DBContext/DBContext.Delete.cs index cb46b46dd..e615a5831 100644 --- a/MongoDB.Entities/DBContext/DBContext.Delete.cs +++ b/MongoDB.Entities/DBContext/DBContext.Delete.cs @@ -1,6 +1,7 @@ using MongoDB.Driver; using System; using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; @@ -9,6 +10,61 @@ namespace MongoDB.Entities { public partial class DBContext { + private static readonly int _deleteBatchSize = 100000; + private void ThrowIfCancellationNotSupported(CancellationToken cancellation = default) + { + if (cancellation != default && Session is null) + throw new NotSupportedException("Cancellation is only supported within transactions for delete operations!"); + } + + private async Task DeleteCascadingAsync(IEnumerable IDs, CancellationToken cancellation = default) where T : IEntity + { + // note: cancellation should not be enabled outside of transactions because multiple collections are involved + // and premature cancellation could cause data inconsistencies. + // i.e. don't pass the cancellation token to delete methods below that don't take a session. + // also make consumers call ThrowIfCancellationNotSupported() before calling this method. + + var db = Database; + var options = new ListCollectionNamesOptions + { + Filter = "{$and:[{name:/~/},{name:/" + Cache().CollectionName + "/}]}" + }; + + var tasks = new List(); + + // note: db.listCollections() mongo command does not support transactions. + // so don't add session support here. + var collNamesCursor = await db.ListCollectionNamesAsync(options, cancellation).ConfigureAwait(false); + + foreach (var cName in await collNamesCursor.ToListAsync(cancellation).ConfigureAwait(false)) + { + tasks.Add( + Session is null + ? db.GetCollection(cName).DeleteManyAsync(r => IDs.Contains(r.ChildID) || IDs.Contains(r.ParentID)) + : db.GetCollection(cName).DeleteManyAsync(Session, r => IDs.Contains(r.ChildID) || IDs.Contains(r.ParentID), null, cancellation)); + } + + var delResTask = + Session == null + ? CollectionFor().DeleteManyAsync(x => IDs.Contains(x.ID)) + : CollectionFor().DeleteManyAsync(Session, x => IDs.Contains(x.ID), null, cancellation); + + tasks.Add(delResTask); + + if (typeof(T).BaseType == typeof(FileEntity)) + { + tasks.Add( + Session is null + ? db.GetCollection(Cache().CollectionName).DeleteManyAsync(x => IDs.Contains(x.FileID)) + : db.GetCollection(Cache().CollectionName).DeleteManyAsync(Session, x => IDs.Contains(x.FileID), null, cancellation)); + } + + await Task.WhenAll(tasks).ConfigureAwait(false); + + return await delResTask.ConfigureAwait(false); + } + + /// /// Deletes a single entity from MongoDB /// HINT: If this entity is referenced by one-to-many/many-to-many relationships, those references are also deleted. @@ -19,10 +75,7 @@ public partial class DBContext /// Set to true if you'd like to ignore any global filters for this operation public Task DeleteAsync(string ID, CancellationToken cancellation = default, bool ignoreGlobalFilters = false) where T : IEntity { - return DB.DeleteAsync( - Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, Builders.Filter.Eq(e => e.ID, ID)), - Session, - cancellation); + return DeleteAsync(Builders.Filter.Eq(e => e.ID, ID), cancellation, ignoreGlobalFilters: ignoreGlobalFilters); } /// @@ -36,11 +89,7 @@ public Task DeleteAsync(string ID, CancellationToken cancellati /// Set to true if you'd like to ignore any global filters for this operation public Task DeleteAsync(IEnumerable IDs, CancellationToken cancellation = default, bool ignoreGlobalFilters = false) where T : IEntity { - return DB.DeleteAsync( - Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, Builders.Filter.In(e => e.ID, IDs)), - Session, - cancellation, - tenantPrefix: tenantPrefix); + return DeleteAsync(Builders.Filter.In(e => e.ID, IDs), cancellation, ignoreGlobalFilters: ignoreGlobalFilters); } /// @@ -53,14 +102,9 @@ public Task DeleteAsync(IEnumerable IDs, CancellationTo /// An optional cancellation token /// An optional collation object /// Set to true if you'd like to ignore any global filters for this operation - public Task DeleteAsync(Expression> expression, CancellationToken cancellation = default, Collation collation = null, bool ignoreGlobalFilters = false) where T : IEntity + public Task DeleteAsync(Expression> expression, CancellationToken cancellation = default, Collation? collation = null, bool ignoreGlobalFilters = false) where T : IEntity { - return DB.DeleteAsync( - Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, Builders.Filter.Where(expression)), - Session, - cancellation, - collation, - tenantPrefix); + return DeleteAsync(Builders.Filter.Where(expression), cancellation, collation, ignoreGlobalFilters); } /// @@ -73,14 +117,9 @@ public Task DeleteAsync(Expression> expression, C /// An optional cancellation token /// An optional collation object /// Set to true if you'd like to ignore any global filters for this operation - public Task DeleteAsync(Func, FilterDefinition> filter, CancellationToken cancellation = default, Collation collation = null, bool ignoreGlobalFilters = false) where T : IEntity + public Task DeleteAsync(Func, FilterDefinition> filter, CancellationToken cancellation = default, Collation? collation = null, bool ignoreGlobalFilters = false) where T : IEntity { - return DB.DeleteAsync( - Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, filter(Builders.Filter)), - Session, - cancellation, - collation, - tenantPrefix); + return DeleteAsync(filter(Builders.Filter), cancellation, collation, ignoreGlobalFilters); } /// @@ -93,14 +132,38 @@ public Task DeleteAsync(Func, Filter /// An optional cancellation token /// An optional collation object /// Set to true if you'd like to ignore any global filters for this operation - public Task DeleteAsync(FilterDefinition filter, CancellationToken cancellation = default, Collation collation = null, bool ignoreGlobalFilters = false) where T : IEntity + public async Task DeleteAsync(FilterDefinition filter, CancellationToken cancellation = default, Collation? collation = null, bool ignoreGlobalFilters = false) where T : IEntity { - return DB.DeleteAsync( - Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, filter), - Session, - cancellation, - collation, - tenantPrefix); + ThrowIfCancellationNotSupported(cancellation); + + var filterDef = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, filter); + var cursor = await new Find(this, CollectionFor(), GlobalFilters) + .Match(filter) + .Project(e => e.ID) + .Option(o => o.BatchSize = _deleteBatchSize) + .Option(o => o.Collation = collation) + .ExecuteCursorAsync(cancellation) + .ConfigureAwait(false); + + long deletedCount = 0; + DeleteResult? res = null; + + using (cursor) + { + while (await cursor.MoveNextAsync(cancellation).ConfigureAwait(false)) + { + if (cursor.Current.Any()) + { + res = await DeleteCascadingAsync(cursor.Current, cancellation).ConfigureAwait(false); + deletedCount += res.DeletedCount; + } + } + } + + if (res?.IsAcknowledged == false) + return DeleteResult.Unacknowledged.Instance; + + return new DeleteResult.Acknowledged(deletedCount); } } } From 936633a1165c72027684da580ab8e7822aba3b41 Mon Sep 17 00:00:00 2001 From: Ahmed Fwela Date: Tue, 9 Nov 2021 19:00:53 +0200 Subject: [PATCH 13/26] reworked builders --- MongoDB.Entities/Builders/Distinct.cs | 3 +- MongoDB.Entities/Builders/FilterQueryBase.cs | 2 +- MongoDB.Entities/Builders/Find.cs | 6 +-- MongoDB.Entities/Builders/PagedSearch.cs | 7 ++-- MongoDB.Entities/Builders/Replace.cs | 18 ++++----- .../Builders/SortFilterQueryBase.cs | 3 +- MongoDB.Entities/Builders/Update.cs | 37 +++++++++++-------- MongoDB.Entities/Builders/UpdateAndGet.cs | 26 +++++++------ .../DBContext/DBContext.Delete.cs | 2 +- .../DBContext/DBContext.Distinct.cs | 2 +- MongoDB.Entities/DBContext/DBContext.cs | 2 +- 11 files changed, 56 insertions(+), 52 deletions(-) diff --git a/MongoDB.Entities/Builders/Distinct.cs b/MongoDB.Entities/Builders/Distinct.cs index 9c134bc79..9f4f63577 100644 --- a/MongoDB.Entities/Builders/Distinct.cs +++ b/MongoDB.Entities/Builders/Distinct.cs @@ -77,8 +77,7 @@ internal Distinct( } internal Distinct( DBContext context, - IMongoCollection collection, - Dictionary globalFilters) : base(globalFilters: globalFilters) + IMongoCollection collection) : base(globalFilters: context.GlobalFilters) { Context = context; Collection = collection; diff --git a/MongoDB.Entities/Builders/FilterQueryBase.cs b/MongoDB.Entities/Builders/FilterQueryBase.cs index 6b82e93c9..ece5a1c99 100644 --- a/MongoDB.Entities/Builders/FilterQueryBase.cs +++ b/MongoDB.Entities/Builders/FilterQueryBase.cs @@ -5,7 +5,7 @@ using System.Linq.Expressions; namespace MongoDB.Entities { - public class FilterQueryBase where T : IEntity where TSelf : FilterQueryBase + public abstract class FilterQueryBase where T : IEntity where TSelf : FilterQueryBase { internal FilterDefinition _filter = Builders.Filter.Empty; internal Dictionary _globalFilters; diff --git a/MongoDB.Entities/Builders/Find.cs b/MongoDB.Entities/Builders/Find.cs index 13ccc3faf..77a45df25 100644 --- a/MongoDB.Entities/Builders/Find.cs +++ b/MongoDB.Entities/Builders/Find.cs @@ -158,8 +158,8 @@ public TSelf SortByTextScore(Expression>? scoreProperty) /// Any class that implements IEntity public class Find : Find where T : IEntity { - internal Find(DBContext context, IMongoCollection collection, Dictionary globalFilters) - : base(context, collection, globalFilters) { } + internal Find(DBContext context, IMongoCollection collection) + : base(context, collection) { } internal Find(DBContext context, IMongoCollection collection, FindBase> baseQuery) : base(context, collection, baseQuery) { } @@ -185,7 +185,7 @@ internal Find(DBContext context, IMongoCollection collection, FindBase collection, Dictionary globalFilters) : base(globalFilters) + internal Find(DBContext context, IMongoCollection collection) : base(context.GlobalFilters) { Context = context; Collection = collection; diff --git a/MongoDB.Entities/Builders/PagedSearch.cs b/MongoDB.Entities/Builders/PagedSearch.cs index 140a050e4..760d4b24e 100644 --- a/MongoDB.Entities/Builders/PagedSearch.cs +++ b/MongoDB.Entities/Builders/PagedSearch.cs @@ -173,9 +173,8 @@ public TSelf Option(Action option) public class PagedSearch : PagedSearch where T : IEntity { internal PagedSearch( - DBContext context, IMongoCollection collection, - Dictionary globalFilters) - : base(context, collection, globalFilters) { } + DBContext context, IMongoCollection collection) + : base(context, collection) { } } /// @@ -189,7 +188,7 @@ public class PagedSearch : PagedSearchBase Collection { get; set; } - internal PagedSearch(DBContext context, IMongoCollection collection, Dictionary globalFilters) : base(globalFilters) + internal PagedSearch(DBContext context, IMongoCollection collection) : base(context.GlobalFilters) { var type = typeof(TProjection); if (type.IsPrimitive || type.IsValueType || (type == typeof(string))) diff --git a/MongoDB.Entities/Builders/Replace.cs b/MongoDB.Entities/Builders/Replace.cs index fe25a2044..29004d5c7 100644 --- a/MongoDB.Entities/Builders/Replace.cs +++ b/MongoDB.Entities/Builders/Replace.cs @@ -29,13 +29,12 @@ internal Replace( DBContext context, IMongoCollection collection, ModifiedBy modifiedBy, - Dictionary globalFilters, - Action onSaveAction) : base(globalFilters) + Action onSaveAction) : base(context.GlobalFilters) { Context = context; Collection = collection; - this._modifiedBy = modifiedBy; - this._onSaveAction = onSaveAction; + _modifiedBy = modifiedBy; + _onSaveAction = onSaveAction; } @@ -51,7 +50,7 @@ public Replace WithEntity(T entity) _onSaveAction?.Invoke(entity); - this._entity = entity; + _entity = entity; return this; } @@ -125,12 +124,13 @@ public async Task ExecuteAsync(CancellationToken cancellation private void SetModOnAndByValues() { - if (Cache.Instance.HasModifiedOn && _entity is IModifiedOn _entityModifiedOn) _entityModifiedOn.ModifiedOn = DateTime.UtcNow; - if (Cache.Instance.ModifiedByProp != null && _modifiedBy != null) + var cache = Context.Cache(); + if (cache.HasModifiedOn && _entity is IModifiedOn _entityModifiedOn) _entityModifiedOn.ModifiedOn = DateTime.UtcNow; + if (cache.ModifiedByProp != null && _modifiedBy != null) { - Cache.Instance.ModifiedByProp.SetValue( + cache.ModifiedByProp.SetValue( _entity, - BsonSerializer.Deserialize(_modifiedBy.ToBson(), Cache.Instance.ModifiedByProp.PropertyType)); + BsonSerializer.Deserialize(_modifiedBy.ToBson(), cache.ModifiedByProp.PropertyType)); } } } diff --git a/MongoDB.Entities/Builders/SortFilterQueryBase.cs b/MongoDB.Entities/Builders/SortFilterQueryBase.cs index f127f8f0a..08eb7d9ab 100644 --- a/MongoDB.Entities/Builders/SortFilterQueryBase.cs +++ b/MongoDB.Entities/Builders/SortFilterQueryBase.cs @@ -5,7 +5,7 @@ #nullable enable namespace MongoDB.Entities { - public class SortFilterQueryBase : FilterQueryBase where T : IEntity where TSelf : SortFilterQueryBase + public abstract class SortFilterQueryBase : FilterQueryBase where T : IEntity where TSelf : SortFilterQueryBase { internal List> _sorts = new(); private TSelf This => (TSelf)this; @@ -16,7 +16,6 @@ internal SortFilterQueryBase(SortFilterQueryBase other) : base(other) } internal SortFilterQueryBase(Dictionary globalFilters) : base(globalFilters: globalFilters) { - _globalFilters = globalFilters; } diff --git a/MongoDB.Entities/Builders/Update.cs b/MongoDB.Entities/Builders/Update.cs index 6498f1910..35c43fdfd 100644 --- a/MongoDB.Entities/Builders/Update.cs +++ b/MongoDB.Entities/Builders/Update.cs @@ -9,11 +9,13 @@ namespace MongoDB.Entities { - public class UpdateBase : FilterQueryBase where T : IEntity where TSelf : UpdateBase + public abstract class UpdateBase : FilterQueryBase where T : IEntity where TSelf : UpdateBase { protected readonly List> defs; protected readonly Action? onUpdateAction; + internal abstract Cache Cache(); + internal UpdateBase(UpdateBase other) : base(other) { onUpdateAction = other.onUpdateAction; @@ -62,7 +64,7 @@ public void AddModification(Template template) { AddModification(template.RenderToString()); } - + /// /// Specify the property and it's value to modify (use multiple times if needed) @@ -112,7 +114,8 @@ public TSelf Modify(Template template) /// The entity instance to read the property values from public TSelf ModifyWith(T entity) { - if (Cache.Instance.HasModifiedOn) ((IModifiedOn)entity).ModifiedOn = DateTime.UtcNow; + + if (Cache().HasModifiedOn) ((IModifiedOn)entity).ModifiedOn = DateTime.UtcNow; defs.AddRange(Logic.BuildUpdateDefs(entity)); return This; } @@ -124,7 +127,7 @@ public TSelf ModifyWith(T entity) /// The entity instance to read the corresponding values from public TSelf ModifyOnly(Expression> members, T entity) { - if (Cache.Instance.HasModifiedOn) ((IModifiedOn)entity).ModifiedOn = DateTime.UtcNow; + if (Cache().HasModifiedOn) ((IModifiedOn)entity).ModifiedOn = DateTime.UtcNow; defs.AddRange(Logic.BuildUpdateDefs(entity, members)); return This; } @@ -136,7 +139,7 @@ public TSelf ModifyOnly(Expression> members, T entity) /// The entity instance to read the corresponding values from public TSelf ModifyExcept(Expression> members, T entity) { - if (Cache.Instance.HasModifiedOn) ((IModifiedOn)entity).ModifiedOn = DateTime.UtcNow; + if (Cache().HasModifiedOn) ((IModifiedOn)entity).ModifiedOn = DateTime.UtcNow; defs.AddRange(Logic.BuildUpdateDefs(entity, members, excludeMode: true)); return This; } @@ -159,7 +162,7 @@ internal Update(DBContext context, IMongoCollection collection, UpdateBase collection, Dictionary globalFilters, Action>? onUpdateAction, List>? defs = null) : base(globalFilters, onUpdateAction, defs) + internal Update(DBContext context, IMongoCollection collection, Action>? onUpdateAction, List>? defs = null) : base(context.GlobalFilters, onUpdateAction, defs) { Context = context; Collection = collection; @@ -169,6 +172,8 @@ internal Update(DBContext context, IMongoCollection collection, Dictionary Collection { get; } + private Cache? _cache; + internal override Cache Cache() => _cache ??= Context.Cache(); @@ -273,7 +278,7 @@ public Update AddToQueue() var mergedFilter = MergedFilter; if (mergedFilter == Builders.Filter.Empty) throw new ArgumentException("Please use Match() method first!"); if (defs.Count == 0) throw new ArgumentException("Please use Modify() method first!"); - if (Cache.Instance.HasModifiedOn) Modify(b => b.CurrentDate(Cache.Instance.ModifiedOnPropName)); + if (Cache().HasModifiedOn) Modify(b => b.CurrentDate(Cache().ModifiedOnPropName)); onUpdateAction?.Invoke(this); _models.Add(new UpdateManyModel(mergedFilter, Builders.Update.Combine(defs)) { @@ -315,10 +320,10 @@ public async Task ExecuteAsync(CancellationToken cancellation = de if (mergedFilter == Builders.Filter.Empty) throw new ArgumentException("Please use Match() method first!"); if (defs.Count == 0) throw new ArgumentException("Please use a Modify() method first!"); if (_stages.Count > 0) throw new ArgumentException("Regular updates and Pipeline updates cannot be used together!"); - if (ShouldSetModDate()) Modify(b => b.CurrentDate(Cache.Instance.ModifiedOnPropName)); + if (ShouldSetModDate()) Modify(b => b.CurrentDate(Cache().ModifiedOnPropName)); onUpdateAction?.Invoke(this); - return await UpdateAsync(mergedFilter, Builders.Update.Combine(defs), _options, this.Session(), cancellation).ConfigureAwait(false); + return await UpdateAsync(mergedFilter, Builders.Update.Combine(defs), _options, cancellation).ConfigureAwait(false); } } @@ -332,13 +337,12 @@ public Task ExecutePipelineAsync(CancellationToken cancellation = if (mergedFilter == Builders.Filter.Empty) throw new ArgumentException("Please use Match() method first!"); if (_stages.Count == 0) throw new ArgumentException("Please use WithPipelineStage() method first!"); if (defs.Count > 0) throw new ArgumentException("Pipeline updates cannot be used together with regular updates!"); - if (ShouldSetModDate()) WithPipelineStage($"{{ $set: {{ '{Cache.Instance.ModifiedOnPropName}': new Date() }} }}"); + if (ShouldSetModDate()) WithPipelineStage($"{{ $set: {{ '{Cache().ModifiedOnPropName}': new Date() }} }}"); return UpdateAsync( mergedFilter, Builders.Update.Pipeline(_stages.ToArray()), _options, - this.Session(), cancellation); } @@ -347,18 +351,19 @@ private bool ShouldSetModDate() //only set mod date by library if user hasn't done anything with the ModifiedOn property return - Cache.Instance.HasModifiedOn && + Cache().HasModifiedOn && !defs.Any(d => d .Render(BsonSerializer.SerializerRegistry.GetSerializer(), BsonSerializer.SerializerRegistry) .ToString() - .Contains($"\"{Cache.Instance.ModifiedOnPropName}\"")); + .Contains($"\"{Cache().ModifiedOnPropName}\"")); } - private Task UpdateAsync(FilterDefinition filter, UpdateDefinition definition, UpdateOptions options, IClientSessionHandle? session = null, CancellationToken cancellation = default) + private Task UpdateAsync(FilterDefinition filter, UpdateDefinition definition, UpdateOptions options, CancellationToken cancellation = default) { - return session == null + return Context.Session is null ? Collection.UpdateManyAsync(filter, definition, options, cancellation) - : Collection.UpdateManyAsync(session, filter, definition, options, cancellation); + : Collection.UpdateManyAsync(Context.Session, filter, definition, options, cancellation); } + } } diff --git a/MongoDB.Entities/Builders/UpdateAndGet.cs b/MongoDB.Entities/Builders/UpdateAndGet.cs index 54364c986..5f32d46d2 100644 --- a/MongoDB.Entities/Builders/UpdateAndGet.cs +++ b/MongoDB.Entities/Builders/UpdateAndGet.cs @@ -20,7 +20,7 @@ internal UpdateAndGet(DBContext context, IMongoCollection collection, UpdateB { } - internal UpdateAndGet(DBContext context, IMongoCollection collection, Dictionary globalFilters, Action>? onUpdateAction, List>? defs) : base(context, collection, globalFilters, onUpdateAction, defs) + internal UpdateAndGet(DBContext context, IMongoCollection collection, Action>? onUpdateAction, List>? defs) : base(context, collection, onUpdateAction, defs) { } } @@ -45,13 +45,14 @@ internal UpdateAndGet(DBContext context, IMongoCollection collection, UpdateB Collection = collection; } - internal UpdateAndGet(DBContext context, IMongoCollection collection, Dictionary globalFilters, Action>? onUpdateAction = null, List>? defs = null) : base(globalFilters, onUpdateAction, defs) + internal UpdateAndGet(DBContext context, IMongoCollection collection, Action>? onUpdateAction = null, List>? defs = null) : base(context.GlobalFilters, onUpdateAction, defs) { Context = context; Collection = collection; } - + private Cache? _cache; + internal override Cache Cache() => _cache ??= Context.Cache(); /// /// Specify an update pipeline with multiple stages using a Template to modify the Entities. @@ -170,7 +171,7 @@ public UpdateAndGet IncludeRequiredProps() if (typeof(T) != typeof(TProjection)) throw new InvalidOperationException("IncludeRequiredProps() cannot be used when projecting to a different type."); - _options.Projection = Cache.Instance.CombineWithRequiredProps(_options.Projection); + _options.Projection = Cache().CombineWithRequiredProps(_options.Projection); return this; } @@ -184,9 +185,9 @@ public async Task ExecuteAsync(CancellationToken cancellation = def if (mergedFilter == Builders.Filter.Empty) throw new ArgumentException("Please use Match() method first!"); if (defs.Count == 0) throw new ArgumentException("Please use Modify() method first!"); if (_stages.Count > 0) throw new ArgumentException("Regular updates and Pipeline updates cannot be used together!"); - if (ShouldSetModDate()) Modify(b => b.CurrentDate(Cache.Instance.ModifiedOnPropName)); + if (ShouldSetModDate()) Modify(b => b.CurrentDate(Cache().ModifiedOnPropName)); onUpdateAction?.Invoke(this); - return await UpdateAndGetAsync(mergedFilter, Builders.Update.Combine(defs), _options, this.Session(), cancellation).ConfigureAwait(false); + return await UpdateAndGetAsync(mergedFilter, Builders.Update.Combine(defs), _options, cancellation).ConfigureAwait(false); } /// @@ -199,10 +200,10 @@ public Task ExecutePipelineAsync(CancellationToken cancellation = d if (mergedFilter == Builders.Filter.Empty) throw new ArgumentException("Please use Match() method first!"); if (_stages.Count == 0) throw new ArgumentException("Please use WithPipelineStage() method first!"); if (defs.Count > 0) throw new ArgumentException("Pipeline updates cannot be used together with regular updates!"); - if (ShouldSetModDate()) WithPipelineStage($"{{ $set: {{ '{Cache.Instance.ModifiedOnPropName}': new Date() }} }}"); + if (ShouldSetModDate()) WithPipelineStage($"{{ $set: {{ '{Cache().ModifiedOnPropName}': new Date() }} }}"); - return UpdateAndGetAsync(mergedFilter, Builders.Update.Pipeline(_stages.ToArray()), _options, this.Session(), cancellation); + return UpdateAndGetAsync(mergedFilter, Builders.Update.Pipeline(_stages.ToArray()), _options, cancellation); } private bool ShouldSetModDate() @@ -210,18 +211,19 @@ private bool ShouldSetModDate() //only set mod date by library if user hasn't done anything with the ModifiedOn property return - Cache.Instance.HasModifiedOn && + Cache().HasModifiedOn && !defs.Any(d => d .Render(BsonSerializer.SerializerRegistry.GetSerializer(), BsonSerializer.SerializerRegistry) .ToString() - .Contains($"\"{Cache.Instance.ModifiedOnPropName}\"")); + .Contains($"\"{Cache().ModifiedOnPropName}\"")); } - private Task UpdateAndGetAsync(FilterDefinition filter, UpdateDefinition definition, FindOneAndUpdateOptions options, IClientSessionHandle? session = null, CancellationToken cancellation = default) + private Task UpdateAndGetAsync(FilterDefinition filter, UpdateDefinition definition, FindOneAndUpdateOptions options, CancellationToken cancellation = default) { - return session == null + return Context.Session is not IClientSessionHandle session ? Collection.FindOneAndUpdateAsync(filter, definition, options, cancellation) : Collection.FindOneAndUpdateAsync(session, filter, definition, options, cancellation); } + } } diff --git a/MongoDB.Entities/DBContext/DBContext.Delete.cs b/MongoDB.Entities/DBContext/DBContext.Delete.cs index e615a5831..b8800ebf6 100644 --- a/MongoDB.Entities/DBContext/DBContext.Delete.cs +++ b/MongoDB.Entities/DBContext/DBContext.Delete.cs @@ -137,7 +137,7 @@ public async Task DeleteAsync(FilterDefinition filter, Cance ThrowIfCancellationNotSupported(cancellation); var filterDef = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, filter); - var cursor = await new Find(this, CollectionFor(), GlobalFilters) + var cursor = await new Find(this, CollectionFor()) .Match(filter) .Project(e => e.ID) .Option(o => o.BatchSize = _deleteBatchSize) diff --git a/MongoDB.Entities/DBContext/DBContext.Distinct.cs b/MongoDB.Entities/DBContext/DBContext.Distinct.cs index c6eaa71a8..3beaa8437 100644 --- a/MongoDB.Entities/DBContext/DBContext.Distinct.cs +++ b/MongoDB.Entities/DBContext/DBContext.Distinct.cs @@ -9,7 +9,7 @@ public partial class DBContext /// The type of the property of the entity you'd like to get unique values for public Distinct Distinct() where T : IEntity { - return new Distinct(Session, _globalFilters, tenantPrefix); + return new Distinct(this, CollectionFor()); } } } diff --git a/MongoDB.Entities/DBContext/DBContext.cs b/MongoDB.Entities/DBContext/DBContext.cs index 3c4409cb7..f5b2e08c2 100644 --- a/MongoDB.Entities/DBContext/DBContext.cs +++ b/MongoDB.Entities/DBContext/DBContext.cs @@ -48,7 +48,7 @@ public ModifiedBy? ModifiedBy public MongoDatabaseSettings Settings => Database.Settings; private Dictionary? _globalFilters; - private Dictionary GlobalFilters => _globalFilters ??= new(); + internal Dictionary GlobalFilters => _globalFilters ??= new(); /// /// Copy constructor From 9595d38373696dbd54bc3bbf2f33252df240cd3b Mon Sep 17 00:00:00 2001 From: Ahmed Fwela Date: Tue, 9 Nov 2021 21:16:01 +0200 Subject: [PATCH 14/26] WIP insert --- MongoDB.Entities/Core/FileEntity.cs | 201 +++++++++--------- MongoDB.Entities/Core/GeoNear.cs | 16 +- MongoDB.Entities/DB/DB.Fluent.cs | 33 +-- MongoDB.Entities/DB/DB.GeoNear.cs | 19 +- MongoDB.Entities/DBContext/DBContext.File.cs | 7 +- MongoDB.Entities/DBContext/DBContext.Find.cs | 4 +- .../DBContext/DBContext.Fluent.cs | 43 +++- .../DBContext/DBContext.GeoNear.cs | 23 +- MongoDB.Entities/DBContext/DBContext.Index.cs | 2 +- .../DBContext/DBContext.Insert.cs | 6 +- .../DBContext/DBContext.Pipeline.cs | 8 +- MongoDB.Entities/DBContext/DBContext.Save.cs | 33 ++- MongoDB.Entities/DBContext/DBContext.cs | 9 +- 13 files changed, 190 insertions(+), 214 deletions(-) diff --git a/MongoDB.Entities/Core/FileEntity.cs b/MongoDB.Entities/Core/FileEntity.cs index beb2dc732..6d6add2de 100644 --- a/MongoDB.Entities/Core/FileEntity.cs +++ b/MongoDB.Entities/Core/FileEntity.cs @@ -17,8 +17,6 @@ namespace MongoDB.Entities /// public abstract class FileEntity : Entity { - private DataStreamer streamer; - /// /// The total amount of data in bytes that has been uploaded so far /// @@ -41,25 +39,19 @@ public abstract class FileEntity : Entity /// If this value is set, the uploaded data will be hashed and matched against this value. If the hash is not equal, an exception will be thrown by the UploadAsync() method. /// [IgnoreDefault] - public string MD5 { get; set; } - - - /// - /// Access the DataStreamer class for uploading and downloading data - /// - public DataStreamer Data => streamer ??= new DataStreamer(this); + public string? MD5 { get; set; } } [Collection("[BINARY_CHUNKS]")] internal class FileChunk : IEntity { [BsonId, ObjectId] - public string ID { get; set; } + public string ID { get; set; } = null!; [AsObjectId] - public string FileID { get; set; } + public string FileID { get; set; } = null!; - public byte[] Data { get; set; } + public byte[] Data { get; set; } = Array.Empty(); public string GenerateNewID() => ObjectId.GenerateNewId().ToString(); @@ -68,30 +60,28 @@ public string GenerateNewID() /// /// Provides the interface for uploading and downloading data chunks for file entities. /// - public class DataStreamer + public class DataStreamer where T : FileEntity { - private static readonly HashSet indexedDBs = new(); - - private readonly FileEntity parent; - private readonly Type parentType; - private readonly DBContext db; - private readonly IMongoCollection chunkCollection; - private FileChunk doc; - private int chunkSize, readCount; - private byte[] buffer; - private List dataChunk; - private MD5 md5; - - internal DataStreamer(FileEntity parent, DBContext db) + private static readonly HashSet _indexedDBs = new(); + + private readonly T _parent; + private readonly DBContext _db; + private readonly IMongoCollection _chunkCollection; + private FileChunk? _doc; + private int _chunkSize, _readCount; + private byte[]? _buffer; + private List? _dataChunk; + private MD5? _md5; + + internal DataStreamer(T parent, DBContext db) { - this.parent = parent; - parentType = parent.GetType(); - this.db = db; - chunkCollection = db.GetCollection(DB.CollectionName()); + _parent = parent; + _db = db; + _chunkCollection = db.CollectionFor(); - if (indexedDBs.Add(db.DatabaseNamespace.DatabaseName)) + if (_indexedDBs.Add(db.DatabaseNamespace.DatabaseName)) { - _ = chunkCollection.Indexes.CreateOneAsync( + _ = _chunkCollection.Indexes.CreateOneAsync( new CreateIndexModel( Builders.IndexKeys.Ascending(c => c.FileID), new CreateIndexOptions { Background = true, Name = $"{nameof(FileChunk.FileID)}(Asc)" })); @@ -104,10 +94,9 @@ internal DataStreamer(FileEntity parent, DBContext db) /// The output stream to write the data /// The maximum number of seconds allowed for the operation to complete /// - /// - public Task DownloadWithTimeoutAsync(Stream stream, int timeOutSeconds, int batchSize = 1, IClientSessionHandle session = null) + public Task DownloadWithTimeoutAsync(Stream stream, int timeOutSeconds, int batchSize = 1) { - return DownloadAsync(stream, batchSize, new CancellationTokenSource(timeOutSeconds * 1000).Token, session); + return DownloadAsync(stream, batchSize, new CancellationTokenSource(timeOutSeconds * 1000).Token); } /// @@ -116,14 +105,13 @@ public Task DownloadWithTimeoutAsync(Stream stream, int timeOutSeconds, int batc /// The output stream to write the data /// The number of chunks you want returned at once /// An optional cancellation token. - /// An optional session if using within a transaction - public async Task DownloadAsync(Stream stream, int batchSize = 1, CancellationToken cancellation = default, IClientSessionHandle session = null) + public async Task DownloadAsync(Stream stream, int batchSize = 1, CancellationToken cancellation = default) { - parent.ThrowIfUnsaved(); - if (!parent.UploadSuccessful) throw new InvalidOperationException("Data for this file hasn't been uploaded successfully (yet)!"); + _parent.ThrowIfUnsaved(); + if (!_parent.UploadSuccessful) throw new InvalidOperationException("Data for this file hasn't been uploaded successfully (yet)!"); if (!stream.CanWrite) throw new NotSupportedException("The supplied stream is not writable!"); - var filter = Builders.Filter.Eq(c => c.FileID, parent.ID); + var filter = Builders.Filter.Eq(c => c.FileID, _parent.ID); var options = new FindOptions { BatchSize = batchSize, @@ -132,9 +120,9 @@ public async Task DownloadAsync(Stream stream, int batchSize = 1, CancellationTo }; var findTask = - session == null - ? chunkCollection.FindAsync(filter, options, cancellation) - : chunkCollection.FindAsync(session, filter, options, cancellation); + _db.Session is not IClientSessionHandle session + ? _chunkCollection.FindAsync(filter, options, cancellation) + : _chunkCollection.FindAsync(session, filter, options, cancellation); using var cursor = await findTask.ConfigureAwait(false); var hasChunks = false; @@ -148,7 +136,7 @@ public async Task DownloadAsync(Stream stream, int batchSize = 1, CancellationTo } } - if (!hasChunks) throw new InvalidOperationException($"No data was found for file entity with ID: {parent.ID}"); + if (!hasChunks) throw new InvalidOperationException($"No data was found for file entity with ID: {_parent.ID}"); } /// @@ -157,10 +145,9 @@ public async Task DownloadAsync(Stream stream, int batchSize = 1, CancellationTo /// The input stream to read the data from /// The maximum number of seconds allowed for the operation to complete /// The 'average' size of one chunk in KiloBytes - /// An optional session if using within a transaction - public Task UploadWithTimeoutAsync(Stream stream, int timeOutSeconds, int chunkSizeKB = 256, IClientSessionHandle session = null) + public Task UploadWithTimeoutAsync(Stream stream, int timeOutSeconds, int chunkSizeKB = 256) { - return UploadAsync(stream, chunkSizeKB, new CancellationTokenSource(timeOutSeconds * 1000).Token, session); + return UploadAsync(stream, chunkSizeKB, new CancellationTokenSource(timeOutSeconds * 1000).Token); } /// @@ -170,42 +157,41 @@ public Task UploadWithTimeoutAsync(Stream stream, int timeOutSeconds, int chunkS /// The input stream to read the data from /// The 'average' size of one chunk in KiloBytes /// An optional cancellation token. - /// An optional session if using within a transaction - public async Task UploadAsync(Stream stream, int chunkSizeKB = 256, CancellationToken cancellation = default, IClientSessionHandle session = null) + public async Task UploadAsync(Stream stream, int chunkSizeKB = 256, CancellationToken cancellation = default) { - parent.ThrowIfUnsaved(); + _parent.ThrowIfUnsaved(); if (chunkSizeKB < 128 || chunkSizeKB > 4096) throw new ArgumentException("Please specify a chunk size from 128KB to 4096KB"); if (!stream.CanRead) throw new NotSupportedException("The supplied stream is not readable!"); - await CleanUpAsync(session).ConfigureAwait(false); + await CleanUpAsync().ConfigureAwait(false); - doc = new FileChunk { FileID = parent.ID }; - chunkSize = chunkSizeKB * 1024; - dataChunk = new List(chunkSize); - buffer = new byte[64 * 1024]; // 64kb read buffer - readCount = 0; + _doc = new FileChunk { FileID = _parent.ID }; + _chunkSize = chunkSizeKB * 1024; + _dataChunk = new List(_chunkSize); + _buffer = new byte[64 * 1024]; // 64kb read buffer + _readCount = 0; - if (!string.IsNullOrEmpty(parent.MD5)) - md5 = MD5.Create(); + if (!string.IsNullOrEmpty(_parent.MD5)) + _md5 = MD5.Create(); try { if (stream.CanSeek && stream.Position > 0) stream.Position = 0; - while ((readCount = await stream.ReadAsync(buffer, 0, buffer.Length, cancellation).ConfigureAwait(false)) > 0) + while ((_readCount = await stream.ReadAsync(_buffer, 0, _buffer.Length, cancellation).ConfigureAwait(false)) > 0) { - md5?.TransformBlock(buffer, 0, readCount, null, 0); - await FlushToDBAsync(session, isLastChunk: false, cancellation).ConfigureAwait(false); + _md5?.TransformBlock(_buffer, 0, _readCount, null, 0); + await FlushToDBAsync(isLastChunk: false, cancellation).ConfigureAwait(false); } - if (parent.FileSize > 0) + if (_parent.FileSize > 0) { - md5?.TransformFinalBlock(buffer, 0, readCount); - if (md5 != null && !BitConverter.ToString(md5.Hash).Replace("-", "").Equals(parent.MD5, StringComparison.OrdinalIgnoreCase)) + _md5?.TransformFinalBlock(_buffer, 0, _readCount); + if (_md5 != null && !BitConverter.ToString(_md5.Hash).Replace("-", "").Equals(_parent.MD5, StringComparison.OrdinalIgnoreCase)) { throw new InvalidDataException("MD5 of uploaded data doesn't match with file entity MD5."); } - await FlushToDBAsync(session, isLastChunk: true, cancellation).ConfigureAwait(false); - parent.UploadSuccessful = true; + await FlushToDBAsync(isLastChunk: true, cancellation).ConfigureAwait(false); + _parent.UploadSuccessful = true; } else { @@ -214,77 +200,80 @@ public async Task UploadAsync(Stream stream, int chunkSizeKB = 256, Cancellation } catch (Exception) { - await CleanUpAsync(session).ConfigureAwait(false); + await CleanUpAsync().ConfigureAwait(false); throw; } finally { - await UpdateMetaDataAsync(session).ConfigureAwait(false); - doc = null; - buffer = null; - dataChunk = null; - md5?.Dispose(); - md5 = null; + await UpdateMetaDataAsync().ConfigureAwait(false); + _doc = null; + _buffer = null; + _dataChunk = null; + _md5?.Dispose(); + _md5 = null; } } /// /// Deletes only the binary chunks stored in the database for this file entity. /// - /// An optional session if using within a transaction /// An optional cancellation token. - public Task DeleteBinaryChunks(IClientSessionHandle session = null, CancellationToken cancellation = default) + public Task DeleteBinaryChunks(CancellationToken cancellation = default) { - parent.ThrowIfUnsaved(); + _parent.ThrowIfUnsaved(); - if (cancellation != default && session == null) + if (cancellation != default && _db.Session == null) throw new NotSupportedException("Cancellation is only supported within transactions for deleting binary chunks!"); - return CleanUpAsync(session, cancellation); + return CleanUpAsync(cancellation); } - private Task CleanUpAsync(IClientSessionHandle session, CancellationToken cancellation = default) + private Task CleanUpAsync(CancellationToken cancellation = default) { - parent.FileSize = 0; - parent.ChunkCount = 0; - parent.UploadSuccessful = false; - return session == null - ? chunkCollection.DeleteManyAsync(c => c.FileID == parent.ID, cancellation) - : chunkCollection.DeleteManyAsync(session, c => c.FileID == parent.ID, null, cancellation); + _parent.FileSize = 0; + _parent.ChunkCount = 0; + _parent.UploadSuccessful = false; + return _db.Session is not IClientSessionHandle session + ? _chunkCollection.DeleteManyAsync(c => c.FileID == _parent.ID, cancellation) + : _chunkCollection.DeleteManyAsync(session, c => c.FileID == _parent.ID, null, cancellation); } - private Task FlushToDBAsync(IClientSessionHandle session, bool isLastChunk = false, CancellationToken cancellation = default) + private Task FlushToDBAsync(bool isLastChunk = false, CancellationToken cancellation = default) { if (!isLastChunk) { - dataChunk.AddRange(new ArraySegment(buffer, 0, readCount)); - parent.FileSize += readCount; + _dataChunk?.AddRange(new ArraySegment(_buffer, 0, _readCount)); + _parent.FileSize += _readCount; } - - if (dataChunk.Count >= chunkSize || isLastChunk) + if (_doc is null) + { + return Task.CompletedTask; + } + if (_dataChunk is not null && (_dataChunk.Count >= _chunkSize || isLastChunk)) { - doc.ID = doc.GenerateNewID(); - doc.Data = dataChunk.ToArray(); - dataChunk.Clear(); - parent.ChunkCount++; - return session == null - ? chunkCollection.InsertOneAsync(doc, null, cancellation) - : chunkCollection.InsertOneAsync(session, doc, null, cancellation); + + _doc.ID = _doc.GenerateNewID(); + _doc.Data = _dataChunk.ToArray(); + _dataChunk.Clear(); + _parent.ChunkCount++; + return _db.Session is not IClientSessionHandle session + ? _chunkCollection.InsertOneAsync(_doc, null, cancellation) + : _chunkCollection.InsertOneAsync(session, _doc, null, cancellation); } return Task.CompletedTask; } - private Task UpdateMetaDataAsync(IClientSessionHandle session) + private Task UpdateMetaDataAsync() { - var collection = db.GetCollection(Cache.CollectionNameFor(parentType)); - var filter = Builders.Filter.Eq(e => e.ID, parent.ID); - var update = Builders.Update - .Set(e => e.FileSize, parent.FileSize) - .Set(e => e.ChunkCount, parent.ChunkCount) - .Set(e => e.UploadSuccessful, parent.UploadSuccessful); - - return session == null + var collection = _db.CollectionFor(); + var filter = Builders.Filter.Eq(e => e.ID, _parent.ID); + var update = Builders.Update + .Set(e => e.FileSize, _parent.FileSize) + .Set(e => e.ChunkCount, _parent.ChunkCount) + .Set(e => e.UploadSuccessful, _parent.UploadSuccessful); + + return _db.Session is not IClientSessionHandle session ? collection.UpdateOneAsync(filter, update) : collection.UpdateOneAsync(session, filter, update); } diff --git a/MongoDB.Entities/Core/GeoNear.cs b/MongoDB.Entities/Core/GeoNear.cs index 20e37b383..441567be3 100644 --- a/MongoDB.Entities/Core/GeoNear.cs +++ b/MongoDB.Entities/Core/GeoNear.cs @@ -50,24 +50,24 @@ public class GeoNear where T : IEntity { #pragma warning disable IDE1006 public Coordinates2D near { get; set; } - public string distanceField { get; set; } + public string? distanceField { get; set; } public bool spherical { get; set; } [BsonIgnoreIfNull] public int? limit { get; set; } [BsonIgnoreIfNull] public double? maxDistance { get; set; } - [BsonIgnoreIfNull] public BsonDocument query { get; set; } + [BsonIgnoreIfNull] public BsonDocument? query { get; set; } [BsonIgnoreIfNull] public double? distanceMultiplier { get; set; } - [BsonIgnoreIfNull] public string includeLocs { get; set; } + [BsonIgnoreIfNull] public string? includeLocs { get; set; } [BsonIgnoreIfNull] public double? minDistance { get; set; } - [BsonIgnoreIfNull] public string key { get; set; } + [BsonIgnoreIfNull] public string? key { get; set; } #pragma warning restore IDE1006 - internal IAggregateFluent ToFluent(string tenantPrefix, AggregateOptions options = null, IClientSessionHandle session = null) + internal IAggregateFluent ToFluent(DBContext context, AggregateOptions? options = null) { var stage = new BsonDocument { { "$geoNear", this.ToBsonDocument() } }; - return session == null - ? DB.Collection(tenantPrefix).Aggregate(options).AppendStage(stage) - : DB.Collection(tenantPrefix).Aggregate(session, options).AppendStage(stage); + return context.Session == null + ? context.CollectionFor().Aggregate(options).AppendStage(stage) + : context.CollectionFor().Aggregate(context.Session, options).AppendStage(stage); } } } diff --git a/MongoDB.Entities/DB/DB.Fluent.cs b/MongoDB.Entities/DB/DB.Fluent.cs index 423afcfdd..1cd48f3ad 100644 --- a/MongoDB.Entities/DB/DB.Fluent.cs +++ b/MongoDB.Entities/DB/DB.Fluent.cs @@ -9,13 +9,9 @@ public static partial class DB /// /// Any class that implements IEntity /// The options for the aggregation. This is not required. - /// An optional session if using within a transaction - /// Optional tenant prefix if using multi-tenancy - public static IAggregateFluent Fluent(AggregateOptions options = null, IClientSessionHandle session = null, string tenantPrefix = null) where T : IEntity + public static IAggregateFluent Fluent(AggregateOptions? options = null) where T : IEntity { - return session == null - ? Collection(tenantPrefix).Aggregate(options) - : Collection(tenantPrefix).Aggregate(session, options); + return Context.Fluent(options); } /// @@ -28,30 +24,9 @@ public static IAggregateFluent Fluent(AggregateOptions options = null, ICl /// Diacritic sensitivity of the search (optional) /// The language for the search (optional) /// Options for finding documents (not required) - /// Optional tenant prefix if using multi-tenancy - /// An optional session if using within a transaction - public static IAggregateFluent FluentTextSearch(Search searchType, string searchTerm, bool caseSensitive = false, bool diacriticSensitive = false, string language = null, AggregateOptions options = null, IClientSessionHandle session = null, string tenantPrefix = null) where T : IEntity + public static IAggregateFluent FluentTextSearch(Search searchType, string searchTerm, bool caseSensitive = false, bool diacriticSensitive = false, string? language = null, AggregateOptions? options = null) where T : IEntity { - if (searchType == Search.Fuzzy) - { - searchTerm = searchTerm.ToDoubleMetaphoneHash(); - caseSensitive = false; - diacriticSensitive = false; - language = null; - } - - var filter = Builders.Filter.Text( - searchTerm, - new TextSearchOptions - { - CaseSensitive = caseSensitive, - DiacriticSensitive = diacriticSensitive, - Language = language - }); - - return session == null - ? Collection(tenantPrefix).Aggregate(options).Match(filter) - : Collection(tenantPrefix).Aggregate(session, options).Match(filter); + return Context.FluentTextSearch(searchType: searchType, searchTerm: searchTerm, caseSensitive: caseSensitive, diacriticSensitive: diacriticSensitive, language: language, options: options); } } } diff --git a/MongoDB.Entities/DB/DB.GeoNear.cs b/MongoDB.Entities/DB/DB.GeoNear.cs index a8e52e7c2..7f51fd763 100644 --- a/MongoDB.Entities/DB/DB.GeoNear.cs +++ b/MongoDB.Entities/DB/DB.GeoNear.cs @@ -21,24 +21,9 @@ public static partial class DB /// Specify the output field to store the point used to calculate the distance /// /// The options for the aggregation. This is not required. - /// An optional session if using within a transaction - /// Optional tenant prefix if using multi-tenancy - public static IAggregateFluent FluentGeoNear(Coordinates2D NearCoordinates, Expression> DistanceField, bool Spherical = true, double? MaxDistance = null, double? MinDistance = null, int? Limit = null, BsonDocument Query = null, double? DistanceMultiplier = null, Expression> IncludeLocations = null, string IndexKey = null, AggregateOptions options = null, IClientSessionHandle session = null, string tenantPrefix = null) where T : IEntity + public static IAggregateFluent FluentGeoNear(Coordinates2D NearCoordinates, Expression>? DistanceField, bool Spherical = true, double? MaxDistance = null, double? MinDistance = null, int? Limit = null, BsonDocument? Query = null, double? DistanceMultiplier = null, Expression>? IncludeLocations = null, string? IndexKey = null, AggregateOptions? options = null) where T : IEntity { - return new GeoNear - { - near = NearCoordinates, - distanceField = DistanceField?.FullPath(), - spherical = Spherical, - maxDistance = MaxDistance, - minDistance = MinDistance, - query = Query, - distanceMultiplier = DistanceMultiplier, - limit = Limit, - includeLocs = IncludeLocations?.FullPath(), - key = IndexKey, - } - .ToFluent(tenantPrefix, options, session); + return Context.GeoNear(NearCoordinates: NearCoordinates, DistanceField: DistanceField, Spherical: Spherical, MaxDistance: MaxDistance, MinDistance: MinDistance, Limit: Limit, Query: Query, DistanceMultiplier: DistanceMultiplier, IncludeLocations: IncludeLocations, IndexKey: IndexKey, options: options); } } } diff --git a/MongoDB.Entities/DBContext/DBContext.File.cs b/MongoDB.Entities/DBContext/DBContext.File.cs index 956066a22..8826cc358 100644 --- a/MongoDB.Entities/DBContext/DBContext.File.cs +++ b/MongoDB.Entities/DBContext/DBContext.File.cs @@ -10,15 +10,12 @@ public partial class DBContext /// /// The file entity type /// The ID of the file entity - public DataStreamer File(string ID) where T : FileEntity, new() + public DataStreamer File(string ID) where T : FileEntity, new() { if (!ObjectId.TryParse(ID, out _)) throw new ArgumentException("The ID passed in is not of the correct format!"); - return new DataStreamer( - new T() { ID = ID, UploadSuccessful = true }, - tenantPrefix); - return File(ID); + return new DataStreamer(new T() { ID = ID, UploadSuccessful = true }, this); } } } diff --git a/MongoDB.Entities/DBContext/DBContext.Find.cs b/MongoDB.Entities/DBContext/DBContext.Find.cs index 250ac7086..194f6cda9 100644 --- a/MongoDB.Entities/DBContext/DBContext.Find.cs +++ b/MongoDB.Entities/DBContext/DBContext.Find.cs @@ -8,7 +8,7 @@ public partial class DBContext /// The type of entity public Find Find() where T : IEntity { - return new Find(this, CollectionFor(), _globalFilters); + return new Find(this, CollectionFor()); } /// @@ -18,7 +18,7 @@ public Find Find() where T : IEntity /// The type of the end result public Find Find() where T : IEntity { - return new Find(this, CollectionFor(), _globalFilters); + return new Find(this, CollectionFor()); } } } diff --git a/MongoDB.Entities/DBContext/DBContext.Fluent.cs b/MongoDB.Entities/DBContext/DBContext.Fluent.cs index fbfda43ae..649036c52 100644 --- a/MongoDB.Entities/DBContext/DBContext.Fluent.cs +++ b/MongoDB.Entities/DBContext/DBContext.Fluent.cs @@ -10,18 +10,19 @@ public partial class DBContext /// The type of entity /// The options for the aggregation. This is not required. /// Set to true if you'd like to ignore any global filters for this operation - public IAggregateFluent Fluent(AggregateOptions options = null, bool ignoreGlobalFilters = false) where T : IEntity + public IAggregateFluent Fluent(AggregateOptions? options = null, bool ignoreGlobalFilters = false) where T : IEntity { var globalFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, Builders.Filter.Empty); + var aggregate = Session is not IClientSessionHandle session + ? CollectionFor().Aggregate(options) + : CollectionFor().Aggregate(session, options); + if (globalFilter != Builders.Filter.Empty) { - return DB - .Fluent(options, Session, tenantPrefix) - .Match(globalFilter); + aggregate = aggregate.Match(globalFilter); } - - return DB.Fluent(options, Session, tenantPrefix); + return aggregate; } /// @@ -35,18 +36,38 @@ public IAggregateFluent Fluent(AggregateOptions options = null, bool ignor /// The language for the search (optional) /// Options for finding documents (not required) /// Set to true if you'd like to ignore any global filters for this operation - public IAggregateFluent FluentTextSearch(Search searchType, string searchTerm, bool caseSensitive = false, bool diacriticSensitive = false, string language = null, AggregateOptions options = null, bool ignoreGlobalFilters = false) where T : IEntity + public IAggregateFluent FluentTextSearch(Search searchType, string searchTerm, bool caseSensitive = false, bool diacriticSensitive = false, string? language = null, AggregateOptions? options = null, bool ignoreGlobalFilters = false) where T : IEntity { var globalFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, Builders.Filter.Empty); + if (searchType == Search.Fuzzy) + { + searchTerm = searchTerm.ToDoubleMetaphoneHash(); + caseSensitive = false; + diacriticSensitive = false; + language = null; + } + + var filter = Builders.Filter.Text( + searchTerm, + new TextSearchOptions + { + CaseSensitive = caseSensitive, + DiacriticSensitive = diacriticSensitive, + Language = language + }); + + var aggregate = Session is not IClientSessionHandle session + ? CollectionFor().Aggregate(options).Match(filter) + : CollectionFor().Aggregate(session, options).Match(filter); + + if (globalFilter != Builders.Filter.Empty) { - return DB - .FluentTextSearch(searchType, searchTerm, caseSensitive, diacriticSensitive, language, options, Session, tenantPrefix) - .Match(globalFilter); + aggregate = aggregate.Match(globalFilter); } - return DB.FluentTextSearch(searchType, searchTerm, caseSensitive, diacriticSensitive, language, options, Session, tenantPrefix); + return aggregate; } } } diff --git a/MongoDB.Entities/DBContext/DBContext.GeoNear.cs b/MongoDB.Entities/DBContext/DBContext.GeoNear.cs index 87bed9b59..8e9238d6f 100644 --- a/MongoDB.Entities/DBContext/DBContext.GeoNear.cs +++ b/MongoDB.Entities/DBContext/DBContext.GeoNear.cs @@ -23,18 +23,31 @@ public partial class DBContext /// The options for the aggregation. This is not required. /// The type of entity /// Set to true if you'd like to ignore any global filters for this operation - public IAggregateFluent GeoNear(Coordinates2D NearCoordinates, Expression> DistanceField, bool Spherical = true, int? MaxDistance = null, int? MinDistance = null, int? Limit = null, BsonDocument Query = null, int? DistanceMultiplier = null, Expression> IncludeLocations = null, string IndexKey = null, AggregateOptions options = null, bool ignoreGlobalFilters = false) where T : IEntity + public IAggregateFluent GeoNear(Coordinates2D NearCoordinates, Expression>? DistanceField, bool Spherical = true, double? MaxDistance = null, double? MinDistance = null, int? Limit = null, BsonDocument? Query = null, double? DistanceMultiplier = null, Expression>? IncludeLocations = null, string? IndexKey = null, AggregateOptions? options = null, bool ignoreGlobalFilters = false) where T : IEntity { var globalFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, Builders.Filter.Empty); + var fluent = new GeoNear + { + near = NearCoordinates, + distanceField = DistanceField?.FullPath(), + spherical = Spherical, + maxDistance = MaxDistance, + minDistance = MinDistance, + query = Query, + distanceMultiplier = DistanceMultiplier, + limit = Limit, + includeLocs = IncludeLocations?.FullPath(), + key = IndexKey, + } + .ToFluent(this, options); + if (globalFilter != Builders.Filter.Empty) { - return DB - .FluentGeoNear(NearCoordinates, DistanceField, Spherical, MaxDistance, MinDistance, Limit, Query, DistanceMultiplier, IncludeLocations, IndexKey, options, Session, tenantPrefix) - .Match(globalFilter); + fluent = fluent.Match(globalFilter); } - return DB.FluentGeoNear(NearCoordinates, DistanceField, Spherical, MaxDistance, MinDistance, Limit, Query, DistanceMultiplier, IncludeLocations, IndexKey, options, Session, tenantPrefix); + return fluent; } } } diff --git a/MongoDB.Entities/DBContext/DBContext.Index.cs b/MongoDB.Entities/DBContext/DBContext.Index.cs index c7b1eff3b..9898d7cdd 100644 --- a/MongoDB.Entities/DBContext/DBContext.Index.cs +++ b/MongoDB.Entities/DBContext/DBContext.Index.cs @@ -9,7 +9,7 @@ public partial class DBContext /// Any class that implements IEntity public Index Index() where T : IEntity { - return new Index(tenantPrefix); + return new Index(this, CollectionFor()); } } } diff --git a/MongoDB.Entities/DBContext/DBContext.Insert.cs b/MongoDB.Entities/DBContext/DBContext.Insert.cs index b40985b8f..da90c4884 100644 --- a/MongoDB.Entities/DBContext/DBContext.Insert.cs +++ b/MongoDB.Entities/DBContext/DBContext.Insert.cs @@ -17,9 +17,8 @@ public partial class DBContext public Task InsertAsync(T entity, CancellationToken cancellation = default) where T : IEntity { SetModifiedBySingle(entity); - entity.SetTenantPrefixOnFileEntity(tenantPrefix); OnBeforeSave()?.Invoke(entity); - return DB.InsertAsync(entity, Session, cancellation, tenantPrefix); + return DB.InsertAsync(entity, Session, cancellation); } /// @@ -32,9 +31,8 @@ public Task InsertAsync(T entity, CancellationToken cancellation = default) w public Task> InsertAsync(IEnumerable entities, CancellationToken cancellation = default) where T : IEntity { SetModifiedByMultiple(entities); - entities.SetTenantDbOnFileEntities(tenantPrefix); foreach (var ent in entities) OnBeforeSave()?.Invoke(ent); - return DB.InsertAsync(entities, Session, cancellation, tenantPrefix); + return DB.InsertAsync(entities, Session, cancellation); } } } diff --git a/MongoDB.Entities/DBContext/DBContext.Pipeline.cs b/MongoDB.Entities/DBContext/DBContext.Pipeline.cs index 6ea5e01f7..927d9eeab 100644 --- a/MongoDB.Entities/DBContext/DBContext.Pipeline.cs +++ b/MongoDB.Entities/DBContext/DBContext.Pipeline.cs @@ -21,7 +21,7 @@ public partial class DBContext /// Set to true if you'd like to ignore any global filters for this operation public Task> PipelineCursorAsync(Template template, AggregateOptions options = null, CancellationToken cancellation = default, bool ignoreGlobalFilters = false) where T : IEntity { - return DB.PipelineCursorAsync(MergeGlobalFilter(template, ignoreGlobalFilters), options, Session, cancellation, tenantPrefix); + return DB.PipelineCursorAsync(MergeGlobalFilter(template, ignoreGlobalFilters), options, Session, cancellation); } /// @@ -36,7 +36,7 @@ public Task> PipelineCursorAsync(TemplateSet to true if you'd like to ignore any global filters for this operation public Task> PipelineAsync(Template template, AggregateOptions options = null, CancellationToken cancellation = default, bool ignoreGlobalFilters = false) where T : IEntity { - return DB.PipelineAsync(MergeGlobalFilter(template, ignoreGlobalFilters), options, Session, cancellation, tenantPrefix); + return DB.PipelineAsync(MergeGlobalFilter(template, ignoreGlobalFilters), options, Session, cancellation); } /// @@ -51,7 +51,7 @@ public Task> PipelineAsync(Template templa /// Set to true if you'd like to ignore any global filters for this operation public Task PipelineSingleAsync(Template template, AggregateOptions options = null, CancellationToken cancellation = default, bool ignoreGlobalFilters = false) where T : IEntity { - return DB.PipelineSingleAsync(MergeGlobalFilter(template, ignoreGlobalFilters), options, Session, cancellation, tenantPrefix); + return DB.PipelineSingleAsync(MergeGlobalFilter(template, ignoreGlobalFilters), options, Session, cancellation); } /// @@ -66,7 +66,7 @@ public Task PipelineSingleAsync(Template templa /// Set to true if you'd like to ignore any global filters for this operation public Task PipelineFirstAsync(Template template, AggregateOptions options = null, CancellationToken cancellation = default, bool ignoreGlobalFilters = false) where T : IEntity { - return DB.PipelineFirstAsync(MergeGlobalFilter(template, ignoreGlobalFilters), options, Session, cancellation, tenantPrefix); + return DB.PipelineFirstAsync(MergeGlobalFilter(template, ignoreGlobalFilters), options, Session, cancellation); } private Template MergeGlobalFilter(Template template, bool ignoreGlobalFilters) where T : IEntity diff --git a/MongoDB.Entities/DBContext/DBContext.Save.cs b/MongoDB.Entities/DBContext/DBContext.Save.cs index b0f8b8a0a..c79ebd972 100644 --- a/MongoDB.Entities/DBContext/DBContext.Save.cs +++ b/MongoDB.Entities/DBContext/DBContext.Save.cs @@ -21,9 +21,8 @@ public partial class DBContext public Task SaveAsync(T entity, CancellationToken cancellation = default) where T : IEntity { SetModifiedBySingle(entity); - entity.SetTenantPrefixOnFileEntity(tenantPrefix); OnBeforeSave()?.Invoke(entity); - return DB.SaveAsync(entity, Session, cancellation, tenantPrefix); + return DB.SaveAsync(entity, Session, cancellation); } /// @@ -36,9 +35,8 @@ public Task SaveAsync(T entity, CancellationToken cancellation = default) whe public Task> SaveAsync(IEnumerable entities, CancellationToken cancellation = default) where T : IEntity { SetModifiedByMultiple(entities); - entities.SetTenantDbOnFileEntities(tenantPrefix); foreach (var ent in entities) OnBeforeSave()?.Invoke(ent); - return DB.SaveAsync(entities, Session, cancellation, tenantPrefix); + return DB.SaveAsync(entities, Session, cancellation); } /// @@ -54,9 +52,8 @@ public Task> SaveAsync(IEnumerable entities, Cancellati public Task SaveOnlyAsync(T entity, Expression> members, CancellationToken cancellation = default) where T : IEntity { SetModifiedBySingle(entity); - entity.SetTenantPrefixOnFileEntity(tenantPrefix); OnBeforeSave()?.Invoke(entity); - return DB.SaveOnlyAsync(entity, members, Session, cancellation, tenantPrefix); + return DB.SaveOnlyAsync(entity, members, Session, cancellation); } /// @@ -72,9 +69,8 @@ public Task SaveOnlyAsync(T entity, Expression> public Task> SaveOnlyAsync(IEnumerable entities, Expression> members, CancellationToken cancellation = default) where T : IEntity { SetModifiedByMultiple(entities); - entities.SetTenantDbOnFileEntities(tenantPrefix); foreach (var ent in entities) OnBeforeSave()?.Invoke(ent); - return DB.SaveOnlyAsync(entities, members, Session, cancellation, tenantPrefix); + return DB.SaveOnlyAsync(entities, members, Session, cancellation); } /// @@ -90,9 +86,8 @@ public Task> SaveOnlyAsync(IEnumerable entities, Expres public Task SaveExceptAsync(T entity, Expression> members, CancellationToken cancellation = default) where T : IEntity { SetModifiedBySingle(entity); - entity.SetTenantPrefixOnFileEntity(tenantPrefix); OnBeforeSave()?.Invoke(entity); - return DB.SaveExceptAsync(entity, members, Session, cancellation, tenantPrefix); + return DB.SaveExceptAsync(entity, members, Session, cancellation); } /// @@ -108,9 +103,8 @@ public Task SaveExceptAsync(T entity, Expression> SaveExceptAsync(IEnumerable entities, Expression> members, CancellationToken cancellation = default) where T : IEntity { SetModifiedByMultiple(entities); - entities.SetTenantDbOnFileEntities(tenantPrefix); foreach (var ent in entities) OnBeforeSave()?.Invoke(ent); - return DB.SaveExceptAsync(entities, members, Session, cancellation, tenantPrefix); + return DB.SaveExceptAsync(entities, members, Session, cancellation); } /// @@ -123,32 +117,33 @@ public Task> SaveExceptAsync(IEnumerable entities, Expr public Task SavePreservingAsync(T entity, CancellationToken cancellation = default) where T : IEntity { SetModifiedBySingle(entity); - entity.SetTenantPrefixOnFileEntity(tenantPrefix); OnBeforeSave()?.Invoke(entity); - return DB.SavePreservingAsync(entity, Session, cancellation, tenantPrefix); + return DB.SavePreservingAsync(entity, Session, cancellation); } private void SetModifiedBySingle(T entity) where T : IEntity { ThrowIfModifiedByIsEmpty(); - Cache.ModifiedByProp?.SetValue( + var cache = Cache(); + cache.ModifiedByProp?.SetValue( entity, - BsonSerializer.Deserialize(ModifiedBy.ToBson(), Cache.ModifiedByProp.PropertyType)); + BsonSerializer.Deserialize(ModifiedBy.ToBson(), cache.ModifiedByProp.PropertyType)); //note: we can't use an IModifiedBy interface because the above line needs a concrete type // to be able to correctly deserialize a user supplied derived/sub class of ModifiedOn. } private void SetModifiedByMultiple(IEnumerable entities) where T : IEntity { - if (Cache.ModifiedByProp is null) + var cache = Cache(); + if (Cache().ModifiedByProp is null) return; ThrowIfModifiedByIsEmpty(); - var val = BsonSerializer.Deserialize(ModifiedBy.ToBson(), Cache.ModifiedByProp.PropertyType); + var val = BsonSerializer.Deserialize(ModifiedBy.ToBson(), cache.ModifiedByProp.PropertyType); foreach (var e in entities) - Cache.ModifiedByProp.SetValue(e, val); + cache.ModifiedByProp.SetValue(e, val); } } } diff --git a/MongoDB.Entities/DBContext/DBContext.cs b/MongoDB.Entities/DBContext/DBContext.cs index f5b2e08c2..4f06b90d9 100644 --- a/MongoDB.Entities/DBContext/DBContext.cs +++ b/MongoDB.Entities/DBContext/DBContext.cs @@ -290,10 +290,13 @@ internal Cache Cache() where T : IEntity } return (Cache)c; } - - public IMongoCollection CollectionFor() where T : IEntity + public virtual string CollectionNameFor() where T : IEntity + { + return Cache().CollectionName; + } + public virtual IMongoCollection CollectionFor() where T : IEntity { - return Database.GetCollection(Cache().CollectionName); + return Database.GetCollection(CollectionNameFor()); } } } From 0141761c5a5844917ad3d683040fb2600a4ea3d4 Mon Sep 17 00:00:00 2001 From: Ahmed Fwela Date: Wed, 10 Nov 2021 13:19:52 +0200 Subject: [PATCH 15/26] DBContext is done --- MongoDB.Entities/Builders/Find.cs | 9 +- MongoDB.Entities/Builders/Replace.cs | 7 +- MongoDB.Entities/Builders/Update.cs | 15 +- MongoDB.Entities/Builders/UpdateAndGet.cs | 4 +- .../Core/{Cache.cs => EntityCache.cs} | 239 ++++---- MongoDB.Entities/Core/FileEntity.cs | 12 +- MongoDB.Entities/Core/GeoNear.cs | 8 +- MongoDB.Entities/Core/Logic.cs | 6 +- MongoDB.Entities/Core/Prop.cs | 2 +- MongoDB.Entities/Core/SequenceCounter.cs | 2 +- MongoDB.Entities/Core/Watcher.cs | 174 +++--- MongoDB.Entities/DB/DB.Collection.cs | 11 +- MongoDB.Entities/DB/DB.Count.cs | 3 - MongoDB.Entities/DB/DB.Distinct.cs | 7 +- MongoDB.Entities/DB/DB.File.cs | 2 +- MongoDB.Entities/DB/DB.Find.cs | 12 +- MongoDB.Entities/DB/DB.Index.cs | 9 +- MongoDB.Entities/DB/DB.Insert.cs | 23 +- MongoDB.Entities/DB/DB.Pipeline.cs | 45 +- MongoDB.Entities/DB/DB.Save.cs | 154 +----- MongoDB.Entities/DB/DB.Sequence.cs | 15 +- MongoDB.Entities/DB/DB.Watcher.cs | 12 +- MongoDB.Entities/DB/DB.cs | 140 ++--- .../DBCollection.IMongoCollection.cs | 487 +++++++++++++++++ MongoDB.Entities/DBCollection/DBCollection.cs | 24 + .../DBContext/DBContext.Collection.cs | 82 +-- MongoDB.Entities/DBContext/DBContext.Count.cs | 131 ++--- .../DBContext/DBContext.Delete.cs | 272 ++++----- .../DBContext/DBContext.Distinct.cs | 23 +- MongoDB.Entities/DBContext/DBContext.File.cs | 28 +- MongoDB.Entities/DBContext/DBContext.Find.cs | 39 +- .../DBContext/DBContext.Fluent.cs | 16 +- .../DBContext/DBContext.GeoNear.cs | 6 +- .../DBContext/DBContext.IMongoDatabase.cs | 10 +- MongoDB.Entities/DBContext/DBContext.Index.cs | 8 +- .../DBContext/DBContext.Insert.cs | 72 ++- .../DBContext/DBContext.PagedSearch.cs | 12 +- .../DBContext/DBContext.Pipeline.cs | 52 +- .../DBContext/DBContext.Queryable.cs | 10 +- .../DBContext/DBContext.Replace.cs | 8 +- MongoDB.Entities/DBContext/DBContext.Save.cs | 137 ++++- .../DBContext/DBContext.Sequence.cs | 13 +- .../DBContext/DBContext.Transaction.cs | 38 +- .../DBContext/DBContext.Update.cs | 25 +- .../DBContext/DBContext.Watcher.cs | 17 +- MongoDB.Entities/DBContext/DBContext.cs | 514 ++++++++++-------- MongoDB.Entities/DBContext/TenantContext.cs | 69 --- .../MongoContext/MongoContext.IMongoClient.cs | 5 +- MongoDB.Entities/MongoContext/MongoContext.cs | 13 +- MongoDB.Entities/MongoDB.Entities.csproj | 2 +- MongoDB.Entities/Relationships/JoinRecord.cs | 4 +- MongoDB.Entities/Relationships/Many.Remove.cs | 2 +- 52 files changed, 1761 insertions(+), 1269 deletions(-) rename MongoDB.Entities/Core/{Cache.cs => EntityCache.cs} (57%) create mode 100644 MongoDB.Entities/DBCollection/DBCollection.IMongoCollection.cs create mode 100644 MongoDB.Entities/DBCollection/DBCollection.cs delete mode 100644 MongoDB.Entities/DBContext/TenantContext.cs diff --git a/MongoDB.Entities/Builders/Find.cs b/MongoDB.Entities/Builders/Find.cs index 77a45df25..cd51a3335 100644 --- a/MongoDB.Entities/Builders/Find.cs +++ b/MongoDB.Entities/Builders/Find.cs @@ -10,7 +10,7 @@ #nullable enable namespace MongoDB.Entities { - public class FindBase : SortFilterQueryBase where T : IEntity where TSelf : FindBase + public abstract class FindBase : SortFilterQueryBase where T : IEntity where TSelf : FindBase { internal FindOptions _options = new(); @@ -22,7 +22,7 @@ internal FindBase(Dictionary globalFilte { _globalFilters = globalFilters; } - + public abstract DBContext Context { get; } private TSelf This => (TSelf)this; /// @@ -73,7 +73,7 @@ public TSelf IncludeRequiredProps() if (typeof(T) != typeof(TProjection)) throw new InvalidOperationException("IncludeRequiredProps() cannot be used when projecting to a different type."); - _options.Projection = Cache.Instance.CombineWithRequiredProps(_options.Projection); + _options.Projection = Context.Cache().CombineWithRequiredProps(_options.Projection); return This; } @@ -174,6 +174,7 @@ internal Find(DBContext context, IMongoCollection collection, FindBaseThe type you'd like to project the results to. public class Find : FindBase>, ICollectionRelated where T : IEntity { + /// /// copy constructor /// @@ -191,7 +192,7 @@ internal Find(DBContext context, IMongoCollection collection) : base(context. Collection = collection; } - public DBContext Context { get; private set; } + public override DBContext Context { get; } public IMongoCollection Collection { get; private set; } diff --git a/MongoDB.Entities/Builders/Replace.cs b/MongoDB.Entities/Builders/Replace.cs index 29004d5c7..1f02ea2d5 100644 --- a/MongoDB.Entities/Builders/Replace.cs +++ b/MongoDB.Entities/Builders/Replace.cs @@ -19,7 +19,7 @@ public class Replace : FilterQueryBase>, ICollectionRelated private ReplaceOptions _options = new(); private readonly List> _models = new(); private readonly ModifiedBy? _modifiedBy; - private readonly Action _onSaveAction; + private readonly Action? _onSaveAction; private T? _entity; public DBContext Context { get; } @@ -28,12 +28,11 @@ public class Replace : FilterQueryBase>, ICollectionRelated internal Replace( DBContext context, IMongoCollection collection, - ModifiedBy modifiedBy, - Action onSaveAction) : base(context.GlobalFilters) + Action? onSaveAction) : base(context.GlobalFilters) { Context = context; Collection = collection; - _modifiedBy = modifiedBy; + _modifiedBy = context.ModifiedBy; _onSaveAction = onSaveAction; } diff --git a/MongoDB.Entities/Builders/Update.cs b/MongoDB.Entities/Builders/Update.cs index 35c43fdfd..0f9426fbb 100644 --- a/MongoDB.Entities/Builders/Update.cs +++ b/MongoDB.Entities/Builders/Update.cs @@ -14,7 +14,9 @@ public abstract class UpdateBase : FilterQueryBase where T : protected readonly List> defs; protected readonly Action? onUpdateAction; - internal abstract Cache Cache(); + public abstract DBContext Context { get; } + private EntityCache? _cache; + internal EntityCache Cache() => _cache ??= Context.Cache(); internal UpdateBase(UpdateBase other) : base(other) { @@ -116,7 +118,7 @@ public TSelf ModifyWith(T entity) { if (Cache().HasModifiedOn) ((IModifiedOn)entity).ModifiedOn = DateTime.UtcNow; - defs.AddRange(Logic.BuildUpdateDefs(entity)); + defs.AddRange(Logic.BuildUpdateDefs(entity, Context)); return This; } @@ -168,17 +170,10 @@ internal Update(DBContext context, IMongoCollection collection, Action Collection { get; } - private Cache? _cache; - internal override Cache Cache() => _cache ??= Context.Cache(); - - - - - /// /// Specify an update pipeline with multiple stages using a Template to modify the Entities. /// NOTE: pipeline updates and regular updates cannot be used together. diff --git a/MongoDB.Entities/Builders/UpdateAndGet.cs b/MongoDB.Entities/Builders/UpdateAndGet.cs index 5f32d46d2..26ec002e6 100644 --- a/MongoDB.Entities/Builders/UpdateAndGet.cs +++ b/MongoDB.Entities/Builders/UpdateAndGet.cs @@ -51,8 +51,8 @@ internal UpdateAndGet(DBContext context, IMongoCollection collection, Action< Collection = collection; } - private Cache? _cache; - internal override Cache Cache() => _cache ??= Context.Cache(); + private EntityCache? _cache; + internal override EntityCache Cache() => _cache ??= Context.Cache(); /// /// Specify an update pipeline with multiple stages using a Template to modify the Entities. diff --git a/MongoDB.Entities/Core/Cache.cs b/MongoDB.Entities/Core/EntityCache.cs similarity index 57% rename from MongoDB.Entities/Core/Cache.cs rename to MongoDB.Entities/Core/EntityCache.cs index 704b71802..0fc9c30a3 100644 --- a/MongoDB.Entities/Core/Cache.cs +++ b/MongoDB.Entities/Core/EntityCache.cs @@ -1,169 +1,122 @@ -using MongoDB.Bson.Serialization; -using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Attributes; using MongoDB.Driver; -using MongoDB.Driver.Linq; +using MongoDB.Driver.Linq; using System; using System.Collections.Concurrent; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Reflection; #nullable enable namespace MongoDB.Entities { - internal abstract class Cache - { - //key: entity type - //val: collection name - protected readonly ConcurrentDictionary typeToCollectionNameMap = new(); - - //key: entity type - //val: database name without tenant prefix (will be null if not specifically set using DB.DatabaseFor() method) - protected readonly ConcurrentDictionary typeToDbNameWithoutTenantPrefixMap = new(); - - internal string CollectionNameFor(Type entityType) - => typeToCollectionNameMap[entityType]; - - internal void MapTypeToDbNameWithoutTenantPrefix(string dbNameWithoutTenantPrefix) where T : IEntity - => typeToDbNameWithoutTenantPrefixMap[typeof(T)] = dbNameWithoutTenantPrefix; - - internal string GetFullDbName(Type entityType, string tenantPrefix) - { - string fullDbName = null; - - string dbNameWithoutTenantPrefix = typeToDbNameWithoutTenantPrefixMap[entityType]; - - if (!string.IsNullOrEmpty(dbNameWithoutTenantPrefix)) - { - if (!string.IsNullOrEmpty(tenantPrefix)) - fullDbName = $"{tenantPrefix}~{dbNameWithoutTenantPrefix}"; - else - fullDbName = dbNameWithoutTenantPrefix; - } - - return fullDbName; - } + internal abstract class Cache + { + protected PropertyInfo[] _updatableProps = null!; + + public bool HasCreatedOn { get; protected set; } + public bool HasModifiedOn { get; protected set; } + public string ModifiedOnPropName { get; } = nameof(IModifiedOn.ModifiedOn); + public PropertyInfo? ModifiedByProp { get; protected set; } + public bool HasIgnoreIfDefaultProps { get; protected set; } + public string CollectionName { get; protected set; } = null!; + public bool IsFileEntity { get; protected set; } } - internal class Cache : Cache where T : IEntity + internal class EntityCache : Cache where T : IEntity { - private static Cache? _instance; - public static Cache Instance => _instance ??= new(); - + + public ConcurrentDictionary> Watchers { get; } = new(); - public bool HasCreatedOn { get; } - public bool HasModifiedOn { get; } - public string ModifiedOnPropName { get; } - public PropertyInfo ModifiedByProp { get; } - public bool HasIgnoreIfDefaultProps { get; } - public string CollectionName { get; } - public bool IsFileEntity { get; } - - //key: TenantPrefix:CollectionName - //val: IMongoCollection - private readonly ConcurrentDictionary> _cache = new(); - private readonly PropertyInfo[] _updatableProps; + private ProjectionDefinition? _requiredPropsProjection; - public Cache() + public EntityCache() { - var type = typeof(T); - var interfaces = type.GetInterfaces(); - - var collAttrb = type.GetCustomAttribute(false); - - CollectionName = collAttrb != null ? collAttrb.Name : type.Name; - - if (string.IsNullOrWhiteSpace(CollectionName) || CollectionName.Contains("~")) - throw new ArgumentException($"{CollectionName} is an illegal name for a collection!"); - - typeToCollectionNameMap[type] = CollectionName; - - if (!typeToDbNameWithoutTenantPrefixMap.ContainsKey(type)) - typeToDbNameWithoutTenantPrefixMap[type] = null; - + var type = typeof(T); + var interfaces = type.GetInterfaces(); + + var collAttrb = type.GetCustomAttribute(false); + + CollectionName = collAttrb != null ? collAttrb.Name : type.Name; + + if (string.IsNullOrWhiteSpace(CollectionName) || CollectionName.Contains("~")) + throw new ArgumentException($"{CollectionName} is an illegal name for a collection!"); + + HasCreatedOn = interfaces.Any(i => i == typeof(ICreatedOn)); HasModifiedOn = interfaces.Any(i => i == typeof(IModifiedOn)); - ModifiedOnPropName = nameof(IModifiedOn.ModifiedOn); - IsFileEntity = type.BaseType == typeof(FileEntity); + IsFileEntity = typeof(FileEntity).IsAssignableFrom(type.BaseType); - _updatableProps = type.GetProperties() - .Where(p => - p.PropertyType.Name != ManyBase.PropTypeName && + _updatableProps = type.GetProperties() + .Where(p => + p.PropertyType.Name != ManyBase.PropTypeName && !p.IsDefined(typeof(BsonIdAttribute), false) && !p.IsDefined(typeof(BsonIgnoreAttribute), false)) - .ToArray(); - - HasIgnoreIfDefaultProps = _updatableProps.Any(p => - p.IsDefined(typeof(BsonIgnoreIfDefaultAttribute), false) || - p.IsDefined(typeof(BsonIgnoreIfNullAttribute), false)); - - try - { - ModifiedByProp = _updatableProps.SingleOrDefault(p => - p.PropertyType == typeof(ModifiedBy) || - p.PropertyType.IsSubclassOf(typeof(ModifiedBy))); - } - catch (InvalidOperationException) - { - throw new InvalidOperationException("Multiple [ModifiedBy] properties are not allowed on entities!"); + .ToArray(); + + HasIgnoreIfDefaultProps = _updatableProps.Any(p => + p.IsDefined(typeof(BsonIgnoreIfDefaultAttribute), false) || + p.IsDefined(typeof(BsonIgnoreIfNullAttribute), false)); + + try + { + ModifiedByProp = _updatableProps.SingleOrDefault(p => typeof(ModifiedBy).IsAssignableFrom(p.PropertyType)); + } + catch (InvalidOperationException) + { + throw new InvalidOperationException("Multiple [ModifiedBy] properties are not allowed on entities!"); } - } - - public IMongoCollection Collection(string tenantPrefix) - { - return _cache.GetOrAdd( - key: $"{tenantPrefix}:{CollectionName}", - valueFactory: _ => DB.Database(GetFullDbName(typeof(T), tenantPrefix)).GetCollection(CollectionName)); - } - - public IEnumerable UpdatableProps(T entity) - { - if (HasIgnoreIfDefaultProps) - { - return _updatableProps.Where(p => - !(p.IsDefined(typeof(BsonIgnoreIfDefaultAttribute), false) && p.GetValue(entity) == default) && - !(p.IsDefined(typeof(BsonIgnoreIfNullAttribute), false) && p.GetValue(entity) == null)); - } - return _updatableProps; } - public ProjectionDefinition CombineWithRequiredProps(ProjectionDefinition userProjection) - { - if (userProjection == null) - throw new InvalidOperationException("Please use .Project() method before .IncludeRequiredProps()"); - - if (_requiredPropsProjection is null) - { - _requiredPropsProjection = "{_id:1}"; - - var props = typeof(T) - .GetProperties() - .Where(p => p.IsDefined(typeof(BsonRequiredAttribute), false)); - - if (!props.Any()) - throw new InvalidOperationException("Unable to find any entity properties marked with [BsonRequired] attribute!"); - - FieldAttribute attr; - foreach (var p in props) - { - attr = p.GetCustomAttribute(); - - if (attr is null) - _requiredPropsProjection = _requiredPropsProjection.Include(p.Name); - else - _requiredPropsProjection = _requiredPropsProjection.Include(attr.ElementName); - } - } - - ProjectionDefinition userProj = userProjection.Render( - BsonSerializer.LookupSerializer(), - BsonSerializer.SerializerRegistry).Document; - - return Builders.Projection.Combine(new[] - { - _requiredPropsProjection, - userProj - }); + public IEnumerable UpdatableProps(T entity) + { + if (HasIgnoreIfDefaultProps) + { + return _updatableProps.Where(p => + !(p.IsDefined(typeof(BsonIgnoreIfDefaultAttribute), false) && p.GetValue(entity) == default) && + !(p.IsDefined(typeof(BsonIgnoreIfNullAttribute), false) && p.GetValue(entity) == null)); + } + return _updatableProps; + } + + public ProjectionDefinition CombineWithRequiredProps(ProjectionDefinition userProjection) + { + if (userProjection == null) + throw new InvalidOperationException("Please use .Project() method before .IncludeRequiredProps()"); + + if (_requiredPropsProjection is null) + { + _requiredPropsProjection = "{_id:1}"; + + var props = typeof(T) + .GetProperties() + .Where(p => p.IsDefined(typeof(BsonRequiredAttribute), false)); + + if (!props.Any()) + throw new InvalidOperationException("Unable to find any entity properties marked with [BsonRequired] attribute!"); + + FieldAttribute attr; + foreach (var p in props) + { + attr = p.GetCustomAttribute(); + + if (attr is null) + _requiredPropsProjection = _requiredPropsProjection.Include(p.Name); + else + _requiredPropsProjection = _requiredPropsProjection.Include(attr.ElementName); + } + } + + ProjectionDefinition userProj = userProjection.Render( + BsonSerializer.LookupSerializer(), + BsonSerializer.SerializerRegistry).Document; + + return Builders.Projection.Combine(new[] + { + _requiredPropsProjection, + userProj + }); } } } \ No newline at end of file diff --git a/MongoDB.Entities/Core/FileEntity.cs b/MongoDB.Entities/Core/FileEntity.cs index 6d6add2de..97a2349c2 100644 --- a/MongoDB.Entities/Core/FileEntity.cs +++ b/MongoDB.Entities/Core/FileEntity.cs @@ -66,6 +66,7 @@ public class DataStreamer where T : FileEntity private readonly T _parent; private readonly DBContext _db; + private readonly IMongoCollection _collection; private readonly IMongoCollection _chunkCollection; private FileChunk? _doc; private int _chunkSize, _readCount; @@ -73,12 +74,12 @@ public class DataStreamer where T : FileEntity private List? _dataChunk; private MD5? _md5; - internal DataStreamer(T parent, DBContext db) + internal DataStreamer(T parent, DBContext db, IMongoCollection collection) { _parent = parent; _db = db; - _chunkCollection = db.CollectionFor(); - + _chunkCollection = db.Collection(); + _collection = collection; if (_indexedDBs.Add(db.DatabaseNamespace.DatabaseName)) { _ = _chunkCollection.Indexes.CreateOneAsync( @@ -266,7 +267,6 @@ private Task FlushToDBAsync(bool isLastChunk = false, CancellationToken cancella private Task UpdateMetaDataAsync() { - var collection = _db.CollectionFor(); var filter = Builders.Filter.Eq(e => e.ID, _parent.ID); var update = Builders.Update .Set(e => e.FileSize, _parent.FileSize) @@ -274,8 +274,8 @@ private Task UpdateMetaDataAsync() .Set(e => e.UploadSuccessful, _parent.UploadSuccessful); return _db.Session is not IClientSessionHandle session - ? collection.UpdateOneAsync(filter, update) - : collection.UpdateOneAsync(session, filter, update); + ? _collection.UpdateOneAsync(filter, update) + : _collection.UpdateOneAsync(session, filter, update); } } } diff --git a/MongoDB.Entities/Core/GeoNear.cs b/MongoDB.Entities/Core/GeoNear.cs index 441567be3..cddf561d2 100644 --- a/MongoDB.Entities/Core/GeoNear.cs +++ b/MongoDB.Entities/Core/GeoNear.cs @@ -49,7 +49,7 @@ public static GeoJsonPoint GeoJsonPoint(double l public class GeoNear where T : IEntity { #pragma warning disable IDE1006 - public Coordinates2D near { get; set; } + public Coordinates2D near { get; set; } = null!; public string? distanceField { get; set; } public bool spherical { get; set; } [BsonIgnoreIfNull] public int? limit { get; set; } @@ -61,13 +61,13 @@ public class GeoNear where T : IEntity [BsonIgnoreIfNull] public string? key { get; set; } #pragma warning restore IDE1006 - internal IAggregateFluent ToFluent(DBContext context, AggregateOptions? options = null) + internal IAggregateFluent ToFluent(DBContext context, AggregateOptions? options = null, string? collectionName = null, IMongoCollection? collection = null) { var stage = new BsonDocument { { "$geoNear", this.ToBsonDocument() } }; return context.Session == null - ? context.CollectionFor().Aggregate(options).AppendStage(stage) - : context.CollectionFor().Aggregate(context.Session, options).AppendStage(stage); + ? context.Collection(collectionName, collection).Aggregate(options).AppendStage(stage) + : context.Collection(collectionName, collection).Aggregate(context.Session, options).AppendStage(stage); } } } diff --git a/MongoDB.Entities/Core/Logic.cs b/MongoDB.Entities/Core/Logic.cs index 16c6ba9e5..abb12cd7d 100644 --- a/MongoDB.Entities/Core/Logic.cs +++ b/MongoDB.Entities/Core/Logic.cs @@ -14,7 +14,7 @@ internal static IEnumerable> BuildUpdateDefs(T entity, DB if (entity == null) throw new ArgumentException("The supplied entity cannot be null!"); - var props = (context?.Cache() ?? Cache.Instance).UpdatableProps(entity); + var props = (context?.Cache() ?? EntityCache.Instance).UpdatableProps(entity); return props.Select(p => Builders.Update.Set(p.Name, p.GetValue(entity))); } @@ -27,7 +27,7 @@ internal static IEnumerable> BuildUpdateDefs(T entity, Ex if (!propNames.Any()) throw new ArgumentException("Unable to get any properties from the members expression!"); - var props = (context?.Cache() ?? Cache.Instance).UpdatableProps(entity); + var props = (context?.Cache() ?? EntityCache.Instance).UpdatableProps(entity); if (excludeMode) props = props.Where(p => !propNames.Contains(p.Name)); @@ -42,7 +42,7 @@ internal static FilterDefinition MergeWithGlobalFilter(bool ignoreGlobalFi //WARNING: this has to do the same thing as DBContext.Pipeline.MergeWithGlobalFilter method // if the following logic changes, update the other method also - if (!ignoreGlobalFilters && globalFilters?.Count > 0 && globalFilters.TryGetValue(typeof(T), out var gFilter)) + if (!ignoreGlobalFilters && globalFilters is not null && globalFilters.Count > 0 && globalFilters.TryGetValue(typeof(T), out var gFilter)) { switch (gFilter.filterDef) { diff --git a/MongoDB.Entities/Core/Prop.cs b/MongoDB.Entities/Core/Prop.cs index fcf8a551b..81a92d6a1 100644 --- a/MongoDB.Entities/Core/Prop.cs +++ b/MongoDB.Entities/Core/Prop.cs @@ -62,7 +62,7 @@ internal static string GetPath(string expString) /// The type of the entity to get the collection name of public static string Collection() where T : IEntity { - return Cache.CollectionName; + return EntityCache.CollectionName; } /// diff --git a/MongoDB.Entities/Core/SequenceCounter.cs b/MongoDB.Entities/Core/SequenceCounter.cs index e2f10dbb3..1331b3e47 100644 --- a/MongoDB.Entities/Core/SequenceCounter.cs +++ b/MongoDB.Entities/Core/SequenceCounter.cs @@ -7,7 +7,7 @@ namespace MongoDB.Entities internal class SequenceCounter : IEntity { [BsonId] - public string ID { get; set; } + public string ID { get; set; } = null!; [BsonRepresentation(BsonType.Int64)] public ulong Count { get; set; } diff --git a/MongoDB.Entities/Core/Watcher.cs b/MongoDB.Entities/Core/Watcher.cs index 7ffff7dff..42397fccf 100644 --- a/MongoDB.Entities/Core/Watcher.cs +++ b/MongoDB.Entities/Core/Watcher.cs @@ -29,32 +29,32 @@ public class Watcher where T : IEntity /// /// This event is fired when the desired types of events have occured. Will have a list of 'entities' that was received as input. /// - public event Action> OnChanges; + public event Action>? OnChanges; /// /// This event is fired when the desired types of events have occured. Will have a list of 'entities' that was received as input. /// - public event AsyncEventHandler> OnChangesAsync; + public event AsyncEventHandler>? OnChangesAsync; /// /// This event is fired when the desired types of events have occured. Will have a list of 'ChangeStreamDocuments' that was received as input. /// - public event Action>> OnChangesCSD; + public event Action>>? OnChangesCSD; /// /// This event is fired when the desired types of events have occured. Will have a list of 'ChangeStreamDocuments' that was received as input. /// - public event AsyncEventHandler>> OnChangesCSDAsync; + public event AsyncEventHandler>>? OnChangesCSDAsync; /// /// This event is fired when an exception is thrown in the change-stream. /// - public event Action OnError; + public event Action? OnError; /// /// This event is fired when the internal cursor get closed due to an 'invalidate' event or cancellation is requested via the cancellation token. /// - public event Action OnStop; + public event Action? OnStop; /// /// The name of this watcher instance @@ -70,24 +70,23 @@ public class Watcher where T : IEntity /// Returns true if watching can be restarted if it was stopped due to an error or invalidate event. /// Will always return false after cancellation is requested via the cancellation token. /// - public bool CanRestart { get => !cancelToken.IsCancellationRequested; } + public bool CanRestart { get => !_cancelToken.IsCancellationRequested; } /// /// The last resume token received from mongodb server. Can be used to resume watching with .StartWithToken() method. /// - public BsonDocument ResumeToken => options?.StartAfter; + public BsonDocument? ResumeToken => _options?.StartAfter; - private PipelineDefinition, ChangeStreamDocument> pipeline; - private ChangeStreamOptions options; - private bool resume; - private CancellationToken cancelToken; + private PipelineDefinition, ChangeStreamDocument>? _pipeline; + private ChangeStreamOptions? _options; + private bool _resume; + private CancellationToken _cancelToken; private bool _initialized; - private readonly string tenantPrefix; - - internal Watcher(string name, string tenantPrefix) + private readonly IMongoCollection _collection; + internal Watcher(string name, DBContext context, IMongoCollection collection) { Name = name; - this.tenantPrefix = tenantPrefix; + _collection = collection; } /// @@ -101,7 +100,7 @@ internal Watcher(string name, string tenantPrefix) /// A cancellation token for ending the watching/change stream public void Start( EventType eventTypes, - Expression, bool>> filter = null, + Expression, bool>>? filter = null, int batchSize = 25, bool onlyGetIDs = false, bool autoResume = true, @@ -119,8 +118,8 @@ public void Start( /// A cancellation token for ending the watching/change stream public void Start( EventType eventTypes, - Expression> projection, - Expression, bool>> filter = null, + Expression>? projection, + Expression, bool>>? filter = null, int batchSize = 25, bool autoResume = true, CancellationToken cancellation = default) @@ -174,7 +173,7 @@ public void Start( public void StartWithToken( BsonDocument resumeToken, EventType eventTypes, - Expression, bool>> filter = null, + Expression, bool>>? filter = null, int batchSize = 25, bool onlyGetIDs = false, CancellationToken cancellation = default) @@ -193,7 +192,7 @@ public void StartWithToken( BsonDocument resumeToken, EventType eventTypes, Expression> projection, - Expression, bool>> filter = null, + Expression, bool>>? filter = null, int batchSize = 25, CancellationToken cancellation = default) => Init(resumeToken, eventTypes, filter, projection, batchSize, false, true, cancellation); @@ -235,10 +234,10 @@ public void StartWithToken( => Init(resumeToken, eventTypes, filter(Builders>.Filter), projection, batchSize, false, true, cancellation); private void Init( - BsonDocument resumeToken, + BsonDocument? resumeToken, EventType eventTypes, FilterDefinition> filter, - Expression> projection, + Expression>? projection, int batchSize, bool onlyGetIDs, bool autoResume, @@ -247,8 +246,8 @@ private void Init( if (_initialized) throw new InvalidOperationException("This watcher has already been initialized!"); - resume = autoResume; - cancelToken = cancellation; + _resume = autoResume; + _cancelToken = cancellation; var ops = new List(3) { ChangeStreamOperationType.Invalidate }; @@ -303,9 +302,9 @@ private void Init( if (projection != null) stages.Add(PipelineStageDefinitionBuilder.Project(BuildProjection(projection))); - pipeline = stages; + _pipeline = stages; - options = new ChangeStreamOptions + _options = new ChangeStreamOptions { StartAfter = resumeToken, BatchSize = batchSize, @@ -349,7 +348,7 @@ private static ProjectionDefinition, ChangeStreamDocumen /// If the watcher stopped due to an error or invalidate event, you can try to restart the watching again with this method. /// /// An optional resume token to restart watching with - public void ReStart(BsonDocument resumeToken = null) + public void ReStart(BsonDocument? resumeToken = null) { if (!CanRestart) { @@ -362,11 +361,11 @@ public void ReStart(BsonDocument resumeToken = null) if (!_initialized) throw new InvalidOperationException("This watcher was never started. Please use .Start() first!"); - if (cancelToken.IsCancellationRequested) + if (_cancelToken.IsCancellationRequested) throw new InvalidOperationException("This watcher cannot be restarted as it has been aborted/cancelled!"); - if (resumeToken != null) - options.StartAfter = resumeToken; + if (resumeToken != null && _options is not null) + _options.StartAfter = resumeToken; StartWatching(); } @@ -379,81 +378,80 @@ private void StartWatching() // continuations will be run on differnt threadpool threads upon re-entry. // i.e. long running thread creation is useless/wasteful for async delegates. - _ = IterateCursorAsync(); + _ = IterateCursorAsync(); async Task IterateCursorAsync() { try { - using (var cursor = await DB.Collection(tenantPrefix).WatchAsync(pipeline, options, cancelToken).ConfigureAwait(false)) + + using var cursor = await _collection.WatchAsync(_pipeline, _options, _cancelToken).ConfigureAwait(false); + while (!_cancelToken.IsCancellationRequested && await cursor.MoveNextAsync(_cancelToken).ConfigureAwait(false)) { - while (!cancelToken.IsCancellationRequested && await cursor.MoveNextAsync(cancelToken).ConfigureAwait(false)) + if (cursor.Current.Any()) { - if (cursor.Current.Any()) + if (_resume && _options is not null) + _options.StartAfter = cursor.Current.Last().ResumeToken; + + if (OnChangesAsync != null) { - if (resume) - options.StartAfter = cursor.Current.Last().ResumeToken; - - if (OnChangesAsync != null) - { - await OnChangesAsync.InvokeAllAsync( - cursor.Current - .Where(d => d.OperationType != ChangeStreamOperationType.Invalidate) - .Select(d => d.FullDocument) - ).ConfigureAwait(false); - } - - OnChanges?.Invoke( - cursor.Current - .Where(d => d.OperationType != ChangeStreamOperationType.Invalidate) - .Select(d => d.FullDocument)); - - if (OnChangesCSDAsync != null) - await OnChangesCSDAsync.InvokeAllAsync(cursor.Current).ConfigureAwait(false); - - OnChangesCSD?.Invoke(cursor.Current); + await OnChangesAsync.InvokeAllAsync( + cursor.Current + .Where(d => d.OperationType != ChangeStreamOperationType.Invalidate) + .Select(d => d.FullDocument) + ).ConfigureAwait(false); } + + OnChanges?.Invoke( + cursor.Current + .Where(d => d.OperationType != ChangeStreamOperationType.Invalidate) + .Select(d => d.FullDocument)); + + if (OnChangesCSDAsync != null) + await OnChangesCSDAsync.InvokeAllAsync(cursor.Current).ConfigureAwait(false); + + OnChangesCSD?.Invoke(cursor.Current); } + } - OnStop?.Invoke(); + OnStop?.Invoke(); - if (cancelToken.IsCancellationRequested) + if (_cancelToken.IsCancellationRequested) + { + if (OnChangesAsync != null) { - if (OnChangesAsync != null) - { - foreach (var h in OnChangesAsync.GetHandlers()) - OnChangesAsync -= h; - } + foreach (var h in OnChangesAsync.GetHandlers()) + OnChangesAsync -= h; + } - if (OnChangesCSDAsync != null) - { - foreach (var h in OnChangesCSDAsync.GetHandlers()) - OnChangesCSDAsync -= h; - } + if (OnChangesCSDAsync != null) + { + foreach (var h in OnChangesCSDAsync.GetHandlers()) + OnChangesCSDAsync -= h; + } - if (OnChanges != null) - { - foreach (Action> a in OnChanges.GetInvocationList()) - OnChanges -= a; - } + if (OnChanges != null) + { + foreach (Action> a in OnChanges.GetInvocationList()) + OnChanges -= a; + } - if (OnChangesCSD != null) - { - foreach (Action>> a in OnChangesCSD.GetInvocationList()) - OnChangesCSD -= a; - } + if (OnChangesCSD != null) + { + foreach (Action>> a in OnChangesCSD.GetInvocationList()) + OnChangesCSD -= a; + } - if (OnError != null) - { - foreach (Action a in OnError.GetInvocationList()) - OnError -= a; - } + if (OnError != null) + { + foreach (Action a in OnError.GetInvocationList()) + OnError -= a; + } - if (OnStop != null) - { - foreach (Action a in OnStop.GetInvocationList()) - OnStop -= a; - } + if (OnStop != null) + { + foreach (Action a in OnStop.GetInvocationList()) + OnStop -= a; } } } diff --git a/MongoDB.Entities/DB/DB.Collection.cs b/MongoDB.Entities/DB/DB.Collection.cs index da292a4d4..6272f4958 100644 --- a/MongoDB.Entities/DB/DB.Collection.cs +++ b/MongoDB.Entities/DB/DB.Collection.cs @@ -10,18 +10,17 @@ public static partial class DB { internal static IMongoCollection GetRefCollection(string name) where T : IEntity { - //no support for multi-tenancy :-( - return Database(null).GetCollection(name); + return Context.GetCollection(name); } /// /// Gets the IMongoCollection for a given IEntity type. - /// TIP: Try never to use this unless really necessary. /// /// Any class that implements IEntity - public static IMongoCollection Collection(string tenantPrefix = null) where T : IEntity + /// Optionally use a specific collection name + public static IMongoCollection Collection(string? collectionName = null) where T : IEntity { - return Cache.Collection(tenantPrefix); + return Context.Collection(collectionName); } /// @@ -30,7 +29,7 @@ public static IMongoCollection Collection(string tenantPrefix = null) wher /// The type of entity to get the collection name for public static string CollectionName() where T : IEntity { - return Cache.CollectionName; + return Context.CollectionName(); } /// diff --git a/MongoDB.Entities/DB/DB.Count.cs b/MongoDB.Entities/DB/DB.Count.cs index dd923c63d..56aa6c5fb 100644 --- a/MongoDB.Entities/DB/DB.Count.cs +++ b/MongoDB.Entities/DB/DB.Count.cs @@ -14,7 +14,6 @@ public static partial class DB /// /// The entity type to get the count for /// An optional cancellation token - /// Optional tenant prefix if using multi-tenancy public static Task CountEstimatedAsync(CancellationToken cancellation = default) where T : IEntity { return Context.CountEstimatedAsync(cancellation); @@ -49,10 +48,8 @@ public static Task CountAsync(FilterDefinition filter, CancellationT /// /// The entity type to get the count for /// f => f.Eq(x => x.Prop, Value) & f.Gt(x => x.Prop, Value) - /// An optional session if using within a transaction /// An optional cancellation token /// An optional CountOptions object - /// Optional tenant prefix if using multi-tenancy public static Task CountAsync(Func, FilterDefinition> filter, CancellationToken cancellation = default, CountOptions? options = null) where T : IEntity { return Context.CountAsync(filter, cancellation, options); diff --git a/MongoDB.Entities/DB/DB.Distinct.cs b/MongoDB.Entities/DB/DB.Distinct.cs index 973355a7f..b5170053f 100644 --- a/MongoDB.Entities/DB/DB.Distinct.cs +++ b/MongoDB.Entities/DB/DB.Distinct.cs @@ -9,9 +9,8 @@ public static partial class DB /// /// Any Entity that implements IEntity interface /// The type of the property of the entity you'd like to get unique values for - /// An optional session if using within a transaction - /// Optional tenant prefix if using multi-tenancy - public static Distinct Distinct(IClientSessionHandle session = null, string tenantPrefix = null) where T : IEntity - => new(session, null, tenantPrefix); + /// Specifiy to override the collection name + public static Distinct Distinct(string? collectionName = null) where T : IEntity + => new(Context, Collection(collectionName)); } } diff --git a/MongoDB.Entities/DB/DB.File.cs b/MongoDB.Entities/DB/DB.File.cs index 89af177f1..0aef3d0b9 100644 --- a/MongoDB.Entities/DB/DB.File.cs +++ b/MongoDB.Entities/DB/DB.File.cs @@ -10,7 +10,7 @@ public static partial class DB /// /// The file entity type /// The ID of the file entity - public static DataStreamer File(string ID) where T : FileEntity, new() + public static DataStreamer File(string ID) where T : FileEntity, new() { return Context.File(ID); } diff --git a/MongoDB.Entities/DB/DB.Find.cs b/MongoDB.Entities/DB/DB.Find.cs index 1860cd108..02fda51c7 100644 --- a/MongoDB.Entities/DB/DB.Find.cs +++ b/MongoDB.Entities/DB/DB.Find.cs @@ -9,10 +9,8 @@ public static partial class DB /// TIP: Specify your criteria using .Match() .Sort() .Skip() .Take() .Project() .Option() methods and finally call .Execute() /// /// Any class that implements IEntity - /// An optional session if using within a transaction - /// Optional tenant prefix if using multi-tenancy - public static Find Find(IClientSessionHandle session = null, string tenantPrefix = null) where T : IEntity - => new(session, null, tenantPrefix); + public static Find Find(string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + => new(Context, collection ?? Collection(collectionName)); /// /// Represents a MongoDB Find command @@ -20,9 +18,7 @@ public static Find Find(IClientSessionHandle session = null, string tenant /// /// Any class that implements IEntity /// The type that is returned by projection - /// An optional session if using within a transaction - /// Optional tenant prefix if using multi-tenancy - public static Find Find(IClientSessionHandle session = null, string tenantPrefix = null) where T : IEntity - => new(session, null, tenantPrefix); + public static Find Find(string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + => new(Context, collection ?? Collection(collectionName)); } } diff --git a/MongoDB.Entities/DB/DB.Index.cs b/MongoDB.Entities/DB/DB.Index.cs index 1a441866f..1598df601 100644 --- a/MongoDB.Entities/DB/DB.Index.cs +++ b/MongoDB.Entities/DB/DB.Index.cs @@ -1,4 +1,6 @@ -namespace MongoDB.Entities +using MongoDB.Driver; + +namespace MongoDB.Entities { public static partial class DB { @@ -7,10 +9,9 @@ public static partial class DB /// TIP: Define the keys first with .Key() method and finally call the .Create() method. /// /// Any class that implements IEntity - /// Optional tenant prefix if using multi-tenancy - public static Index Index(string tenantPrefix = null) where T : IEntity + public static Index Index(string? collectionName = null, IMongoCollection? collection = null) where T : IEntity { - return new Index(tenantPrefix); + return new Index(Context, collection ?? Collection(collectionName)); } } } diff --git a/MongoDB.Entities/DB/DB.Insert.cs b/MongoDB.Entities/DB/DB.Insert.cs index 083465294..024fbf410 100644 --- a/MongoDB.Entities/DB/DB.Insert.cs +++ b/MongoDB.Entities/DB/DB.Insert.cs @@ -14,15 +14,10 @@ public static partial class DB /// /// Any class that implements IEntity /// The instance to persist - /// An optional session if using within a transaction /// And optional cancellation token - /// Optional tenant prefix if using multi-tenancy - public static Task InsertAsync(T entity, IClientSessionHandle session = null, CancellationToken cancellation = default, string tenantPrefix = null) where T : IEntity + public static Task InsertAsync(T entity, CancellationToken cancellation = default) where T : IEntity { - PrepAndCheckIfInsert(entity); - return session == null - ? Collection(tenantPrefix).InsertOneAsync(entity, null, cancellation) - : Collection(tenantPrefix).InsertOneAsync(session, entity, null, cancellation); + return Context.InsertAsync(entity, cancellation); } /// @@ -33,19 +28,9 @@ public static Task InsertAsync(T entity, IClientSessionHandle session = null, /// An optional session if using within a transaction /// And optional cancellation token /// Optional tenant prefix if using multi-tenancy - public static Task> InsertAsync(IEnumerable entities, IClientSessionHandle session = null, CancellationToken cancellation = default, string tenantPrefix = null) where T : IEntity + public static Task> InsertAsync(IEnumerable entities, CancellationToken cancellation = default) where T : IEntity { - var models = new List>(entities.Count()); - - foreach (var ent in entities) - { - PrepAndCheckIfInsert(ent); - models.Add(new InsertOneModel(ent)); - } - - return session == null - ? Collection(tenantPrefix).BulkWriteAsync(models, unOrdBlkOpts, cancellation) - : Collection(tenantPrefix).BulkWriteAsync(session, models, unOrdBlkOpts, cancellation); + return Context.InsertAsync(entities, cancellation); } } } diff --git a/MongoDB.Entities/DB/DB.Pipeline.cs b/MongoDB.Entities/DB/DB.Pipeline.cs index 28786e875..cf6d94fab 100644 --- a/MongoDB.Entities/DB/DB.Pipeline.cs +++ b/MongoDB.Entities/DB/DB.Pipeline.cs @@ -18,11 +18,9 @@ public static partial class DB /// An optional session if using within a transaction /// An optional cancellation token /// Optional tenant prefix if using multi-tenancy - public static Task> PipelineCursorAsync(Template template, AggregateOptions options = null, IClientSessionHandle session = null, CancellationToken cancellation = default, string tenantPrefix = null) where T : IEntity + public static Task> PipelineCursorAsync(Template template, AggregateOptions? options = null, CancellationToken cancellation = default) where T : IEntity { - return session == null - ? Collection(tenantPrefix).AggregateAsync(template.ToPipeline(), options, cancellation) - : Collection(tenantPrefix).AggregateAsync(session, template.ToPipeline(), options, cancellation); + return Context.PipelineCursorAsync(template, options, cancellation); } /// @@ -32,20 +30,10 @@ public static Task> PipelineCursorAsync(Templa /// The type of the resulting objects /// A 'Template' object with tags replaced /// The options for the aggregation. This is not required. - /// An optional session if using within a transaction /// An optional cancellation token - /// Optional tenant prefix if using multi-tenancy - public static async Task> PipelineAsync(Template template, AggregateOptions options = null, IClientSessionHandle session = null, CancellationToken cancellation = default, string tenantPrefix = null) where T : IEntity + public static Task> PipelineAsync(Template template, AggregateOptions? options = null, CancellationToken cancellation = default) where T : IEntity { - var list = new List(); - using (var cursor = await PipelineCursorAsync(template, options, session, cancellation, tenantPrefix).ConfigureAwait(false)) - { - while (await cursor.MoveNextAsync(cancellation).ConfigureAwait(false)) - { - list.AddRange(cursor.Current); - } - } - return list; + return Context.PipelineAsync(template, options, cancellation); } /// @@ -56,19 +44,10 @@ public static async Task> PipelineAsync(TemplateThe type of the resulting object /// A 'Template' object with tags replaced /// The options for the aggregation. This is not required. - /// An optional session if using within a transaction /// An optional cancellation token - /// Optional tenant prefix if using multi-tenancy - public static async Task PipelineSingleAsync(Template template, AggregateOptions options = null, IClientSessionHandle session = null, CancellationToken cancellation = default, string tenantPrefix = null) where T : IEntity + public static Task PipelineSingleAsync(Template template, AggregateOptions? options = null, CancellationToken cancellation = default) where T : IEntity { - AggregateOptions opts = options ?? new AggregateOptions(); - opts.BatchSize = 2; - - using (var cursor = await PipelineCursorAsync(template, opts, session, cancellation, tenantPrefix).ConfigureAwait(false)) - { - await cursor.MoveNextAsync(cancellation).ConfigureAwait(false); - return cursor.Current.SingleOrDefault(); - } + return Context.PipelineSingleAsync(template, options, cancellation); } /// @@ -78,19 +57,11 @@ public static async Task PipelineSingleAsync(TemplateThe type of the resulting object /// A 'Template' object with tags replaced /// The options for the aggregation. This is not required. - /// An optional session if using within a transaction /// An optional cancellation token - /// Optional tenant prefix if using multi-tenancy - public static async Task PipelineFirstAsync(Template template, AggregateOptions options = null, IClientSessionHandle session = null, CancellationToken cancellation = default, string tenantPrefix = null) where T : IEntity + public static Task PipelineFirstAsync(Template template, AggregateOptions? options = null, CancellationToken cancellation = default) where T : IEntity { - AggregateOptions opts = options ?? new AggregateOptions(); - opts.BatchSize = 1; + return Context.PipelineFirstAsync(template, options, cancellation); - using (var cursor = await PipelineCursorAsync(template, opts, session, cancellation, tenantPrefix).ConfigureAwait(false)) - { - await cursor.MoveNextAsync(cancellation).ConfigureAwait(false); - return cursor.Current.SingleOrDefault(); - } } } } diff --git a/MongoDB.Entities/DB/DB.Save.cs b/MongoDB.Entities/DB/DB.Save.cs index 68dbe0919..f5a562ec7 100644 --- a/MongoDB.Entities/DB/DB.Save.cs +++ b/MongoDB.Entities/DB/DB.Save.cs @@ -10,8 +10,7 @@ namespace MongoDB.Entities { public static partial class DB { - private static readonly BulkWriteOptions unOrdBlkOpts = new() { IsOrdered = false }; - private static readonly UpdateOptions updateOptions = new() { IsUpsert = true }; + /// /// Saves a complete entity replacing an existing entity or creating a new one if it does not exist. @@ -19,21 +18,10 @@ public static partial class DB /// /// Any class that implements IEntity /// The instance to persist - /// An optional session if using within a transaction /// And optional cancellation token - /// Optional tenant prefix if using multi-tenancy - public static Task SaveAsync(T entity, IClientSessionHandle session = null, CancellationToken cancellation = default, string tenantPrefix = null) where T : IEntity + public static Task SaveAsync(T entity, CancellationToken cancellation = default) where T : IEntity { - if (PrepAndCheckIfInsert(entity)) - { - return session == null - ? Collection(tenantPrefix).InsertOneAsync(entity, null, cancellation) - : Collection(tenantPrefix).InsertOneAsync(session, entity, null, cancellation); - } - - return session == null - ? Collection(tenantPrefix).ReplaceOneAsync(x => x.ID == entity.ID, entity, new ReplaceOptions { IsUpsert = true }, cancellation) - : Collection(tenantPrefix).ReplaceOneAsync(session, x => x.ID == entity.ID, entity, new ReplaceOptions { IsUpsert = true }, cancellation); + return Context.SaveAsync(entity, cancellation); } /// @@ -42,30 +30,10 @@ public static Task SaveAsync(T entity, IClientSessionHandle session = null, C /// /// Any class that implements IEntity /// The entities to persist - /// An optional session if using within a transaction /// And optional cancellation token - /// Optional tenant prefix if using multi-tenancy - public static Task> SaveAsync(IEnumerable entities, IClientSessionHandle session = null, CancellationToken cancellation = default, string tenantPrefix = null) where T : IEntity + public static Task> SaveAsync(IEnumerable entities, CancellationToken cancellation = default) where T : IEntity { - var models = new List>(entities.Count()); - - foreach (var ent in entities) - { - if (PrepAndCheckIfInsert(ent)) - { - models.Add(new InsertOneModel(ent)); - } - else - { - models.Add(new ReplaceOneModel( - filter: Builders.Filter.Eq(e => e.ID, ent.ID), - replacement: ent) - { IsUpsert = true }); - } - } - return session == null - ? Collection(tenantPrefix).BulkWriteAsync(models, unOrdBlkOpts, cancellation) - : Collection(tenantPrefix).BulkWriteAsync(session, models, unOrdBlkOpts, cancellation); + return Context.SaveAsync(entities, cancellation); } /// @@ -77,12 +45,11 @@ public static Task> SaveAsync(IEnumerable entities, ICl /// Any class that implements IEntity /// The entity to save /// x => new { x.PropOne, x.PropTwo } - /// An optional session if using within a transaction /// An optional cancellation token - /// Optional tenant prefix if using multi-tenancy - public static Task SaveOnlyAsync(T entity, Expression> members, IClientSessionHandle session = null, CancellationToken cancellation = default, string tenantPrefix = null) where T : IEntity + public static Task SaveOnlyAsync(T entity, Expression> members, CancellationToken cancellation = default) where T : IEntity { - return SavePartial(entity, members, tenantPrefix, session, cancellation); + return Context.SaveOnlyAsync(entity, members, cancellation); + } /// @@ -94,12 +61,10 @@ public static Task SaveOnlyAsync(T entity, ExpressionAny class that implements IEntity /// The batch of entities to save /// x => new { x.PropOne, x.PropTwo } - /// An optional session if using within a transaction /// An optional cancellation token - /// Optional tenant prefix if using multi-tenancy - public static Task> SaveOnlyAsync(IEnumerable entities, Expression> members, IClientSessionHandle session = null, CancellationToken cancellation = default, string tenantPrefix = null) where T : IEntity + public static Task> SaveOnlyAsync(IEnumerable entities, Expression> members, CancellationToken cancellation = default) where T : IEntity { - return SavePartial(entities, members, tenantPrefix, session, cancellation); + return Context.SaveOnlyAsync(entities, members, cancellation); } /// @@ -111,12 +76,10 @@ public static Task> SaveOnlyAsync(IEnumerable entities, /// Any class that implements IEntity /// The entity to save /// x => new { x.PropOne, x.PropTwo } - /// An optional session if using within a transaction /// An optional cancellation token - /// Optional tenant prefix if using multi-tenancy - public static Task SaveExceptAsync(T entity, Expression> members, IClientSessionHandle session = null, CancellationToken cancellation = default, string tenantPrefix = null) where T : IEntity + public static Task SaveExceptAsync(T entity, Expression> members, CancellationToken cancellation = default) where T : IEntity { - return SavePartial(entity, members, tenantPrefix, session, cancellation, true); + return Context.SaveExceptAsync(entity, members, cancellation); } /// @@ -128,12 +91,10 @@ public static Task SaveExceptAsync(T entity, ExpressionAny class that implements IEntity /// The batch of entities to save /// x => new { x.PropOne, x.PropTwo } - /// An optional session if using within a transaction /// An optional cancellation token - /// Optional tenant prefix if using multi-tenancy - public static Task> SaveExceptAsync(IEnumerable entities, Expression> members, IClientSessionHandle session = null, CancellationToken cancellation = default, string tenantPrefix = null) where T : IEntity + public static Task> SaveExceptAsync(IEnumerable entities, Expression> members, CancellationToken cancellation = default) where T : IEntity { - return SavePartial(entities, members, tenantPrefix, session, cancellation, true); + return Context.SaveExceptAsync(entities, members, cancellation); } /// @@ -142,95 +103,12 @@ public static Task> SaveExceptAsync(IEnumerable entitie /// /// Any class that implements IEntity /// The entity to save - /// An optional session if using within a transaction /// An optional cancellation token - /// Optional tenant prefix if using multi-tenancy - public static Task SavePreservingAsync(T entity, IClientSessionHandle session = null, CancellationToken cancellation = default, string tenantPrefix = null) where T : IEntity + public static Task SavePreservingAsync(T entity, CancellationToken cancellation = default) where T : IEntity { - entity.ThrowIfUnsaved(); - - var propsToUpdate = Cache.UpdatableProps(entity); - - IEnumerable propsToPreserve = new string[0]; - - var dontProps = propsToUpdate.Where(p => p.IsDefined(typeof(DontPreserveAttribute), false)).Select(p => p.Name); - var presProps = propsToUpdate.Where(p => p.IsDefined(typeof(PreserveAttribute), false)).Select(p => p.Name); - - if (dontProps.Any() && presProps.Any()) - throw new NotSupportedException("[Preseve] and [DontPreserve] attributes cannot be used together on the same entity!"); - - if (dontProps.Any()) - propsToPreserve = propsToUpdate.Where(p => !dontProps.Contains(p.Name)).Select(p => p.Name); - - if (presProps.Any()) - propsToPreserve = propsToUpdate.Where(p => presProps.Contains(p.Name)).Select(p => p.Name); - - if (!propsToPreserve.Any()) - throw new ArgumentException("No properties are being preserved. Please use .SaveAsync() method instead!"); - - propsToUpdate = propsToUpdate.Where(p => !propsToPreserve.Contains(p.Name)); - - var propsToUpdateCount = propsToUpdate.Count(); - - if (propsToUpdateCount == 0) - throw new ArgumentException("At least one property must be not preserved!"); - - var defs = new List>(propsToUpdateCount); - - foreach (var p in propsToUpdate) - { - if (p.Name == Cache.ModifiedOnPropName) - defs.Add(Builders.Update.CurrentDate(Cache.ModifiedOnPropName)); - else - defs.Add(Builders.Update.Set(p.Name, p.GetValue(entity))); - } - - return - session == null - ? Collection(tenantPrefix).UpdateOneAsync(e => e.ID == entity.ID, Builders.Update.Combine(defs), updateOptions, cancellation) - : Collection(tenantPrefix).UpdateOneAsync(session, e => e.ID == entity.ID, Builders.Update.Combine(defs), updateOptions, cancellation); + return Context.SavePreservingAsync(entity, cancellation); } - private static Task SavePartial(T entity, Expression> members, string tenantPrefix, IClientSessionHandle session, CancellationToken cancellation, bool excludeMode = false) where T : IEntity - { - PrepAndCheckIfInsert(entity); //just prep. we don't care about inserts here - return - session == null - ? Collection(tenantPrefix).UpdateOneAsync(e => e.ID == entity.ID, Builders.Update.Combine(Logic.BuildUpdateDefs(entity, members, excludeMode)), updateOptions, cancellation) - : Collection(tenantPrefix).UpdateOneAsync(session, e => e.ID == entity.ID, Builders.Update.Combine(Logic.BuildUpdateDefs(entity, members, excludeMode)), updateOptions, cancellation); - } - - private static Task> SavePartial(IEnumerable entities, Expression> members, string tenantPrefix, IClientSessionHandle session, CancellationToken cancellation, bool excludeMode = false) where T : IEntity - { - var models = new List>(entities.Count()); - foreach (var ent in entities) - { - PrepAndCheckIfInsert(ent); //just prep. we don't care about inserts here - models.Add( - new UpdateOneModel( - filter: Builders.Filter.Eq(e => e.ID, ent.ID), - update: Builders.Update.Combine(Logic.BuildUpdateDefs(ent, members, excludeMode))) - { IsUpsert = true }); - } - - return session == null - ? Collection(tenantPrefix).BulkWriteAsync(models, unOrdBlkOpts, cancellation) - : Collection(tenantPrefix).BulkWriteAsync(session, models, unOrdBlkOpts, cancellation); - } - - private static bool PrepAndCheckIfInsert(T entity) where T : IEntity - { - if (string.IsNullOrEmpty(entity.ID)) - { - entity.ID = entity.GenerateNewID(); - if (Cache.HasCreatedOn) ((ICreatedOn)entity).CreatedOn = DateTime.UtcNow; - if (Cache.HasModifiedOn) ((IModifiedOn)entity).ModifiedOn = DateTime.UtcNow; - return true; - } - - if (Cache.HasModifiedOn) ((IModifiedOn)entity).ModifiedOn = DateTime.UtcNow; - return false; - } } } diff --git a/MongoDB.Entities/DB/DB.Sequence.cs b/MongoDB.Entities/DB/DB.Sequence.cs index 739a0c14e..05bd537bb 100644 --- a/MongoDB.Entities/DB/DB.Sequence.cs +++ b/MongoDB.Entities/DB/DB.Sequence.cs @@ -12,10 +12,9 @@ public static partial class DB /// /// The type of entity to get the next sequential number for /// An optional cancellation token - /// Optional tenant prefix if using multi-tenancy - public static Task NextSequentialNumberAsync(CancellationToken cancellation = default, string tenantPrefix = null) where T : IEntity + public static Task NextSequentialNumberAsync(CancellationToken cancellation = default) where T : IEntity { - return NextSequentialNumberAsync(CollectionName(), cancellation, tenantPrefix); + return Context.NextSequentialNumberAsync(cancellation); } /// @@ -23,15 +22,9 @@ public static Task NextSequentialNumberAsync(CancellationToken cancell /// /// The name of the sequence to get the next number for /// An optional cancellation token - /// Optional tenant prefix if using multi-tenancy - public static Task NextSequentialNumberAsync(string sequenceName, CancellationToken cancellation = default, string tenantPrefix = null) + public static Task NextSequentialNumberAsync(string sequenceName, CancellationToken cancellation = default) { - return new UpdateAndGet(null, null, null, tenantPrefix) - .Match(s => s.ID == sequenceName) - .Modify(b => b.Inc(s => s.Count, 1ul)) - .Option(o => o.IsUpsert = true) - .Project(s => s.Count) - .ExecuteAsync(cancellation); + return Context.NextSequentialNumberAsync(sequenceName, cancellation); } } } diff --git a/MongoDB.Entities/DB/DB.Watcher.cs b/MongoDB.Entities/DB/DB.Watcher.cs index 6758c6540..0776c751a 100644 --- a/MongoDB.Entities/DB/DB.Watcher.cs +++ b/MongoDB.Entities/DB/DB.Watcher.cs @@ -11,21 +11,15 @@ public static partial class DB /// /// The entity type to get a watcher for /// A unique name for the watcher of this entity type. Names can be duplicate among different entity types. - /// Optional tenant prefix if using multi-tenancy - public static Watcher Watcher(string name, string tenantPrefix = null) where T : IEntity + public static Watcher Watcher(string name) where T : IEntity { - if (Cache.Watchers.TryGetValue(name.ToLower().Trim(), out Watcher watcher)) - return watcher; - - watcher = new Watcher(name.ToLower().Trim(), tenantPrefix); - Cache.Watchers.TryAdd(name, watcher); - return watcher; + return Context.Watcher(name); } /// /// Returns all the watchers for a given entity type /// /// The entity type to get the watcher of - public static IEnumerable> Watchers() where T : IEntity => Cache.Watchers.Values; + public static IEnumerable> Watchers() where T : IEntity => Context.Cache().Watchers.Values; } } diff --git a/MongoDB.Entities/DB/DB.cs b/MongoDB.Entities/DB/DB.cs index 499fbcdc4..fc6b72146 100644 --- a/MongoDB.Entities/DB/DB.cs +++ b/MongoDB.Entities/DB/DB.cs @@ -13,27 +13,30 @@ namespace MongoDB.Entities /// /// The main entrypoint for all data access methods of the library /// + /// Please consider using DBContext directly, this will be made obsolete in the future public static partial class DB - { + { + private static DBContext? _context; + static DB() - { - BsonSerializer.RegisterSerializer(new DateSerializer()); - BsonSerializer.RegisterSerializer(new FuzzyStringSerializer()); - BsonSerializer.RegisterSerializer(typeof(decimal), new DecimalSerializer(BsonType.Decimal128)); - BsonSerializer.RegisterSerializer(typeof(decimal?), new NullableSerializer(new DecimalSerializer(BsonType.Decimal128))); - - ConventionRegistry.Register( - "DefaultConventions", - new ConventionPack - { - new IgnoreExtraElementsConvention(true), - new IgnoreManyPropsConvention() - }, - _ => true); + { + DBContext.InitStatic(); } - //TODO: refactor api - public static DBContext Context { get; set; } + /// + /// Checks if InitAsync was called successfully + /// + public static bool IsInitialized => _context is not null; + + /// + /// Contains the latest initialized context + /// + /// Throws an exception if InitAsync wasn't called, check + public static DBContext Context + { + get => _context ?? throw new ArgumentException("The database hasn't been initialized yet! please call [InitAsync] first"); + set => _context = value; + } /// /// Initializes a MongoDB connection with the given connection parameters. @@ -42,48 +45,62 @@ static DB() /// /// Name of the database /// Address of the MongoDB server - /// Port number of the server - public static Task InitAsync(string database, string host = "127.0.0.1", int port = 27017) + /// Port number of the server + /// Whether to skip pinging the host on initialization, in which case the task completes instantly + public static Task InitAsync(string database, string host = "127.0.0.1", int port = 27017, bool skipNetworkPing = false) { return Initialize( - new MongoClientSettings { Server = new MongoServerAddress(host, port) }, database); - } - + new MongoClientSettings { Server = new MongoServerAddress(host, port) }, database, skipNetworkPing); + } + /// /// Initializes a MongoDB connection with the given connection parameters. /// WARNING: will throw an error if server is not reachable! /// You can call this method as many times as you want (such as in serverless functions) with the same parameters and the connections won't get duplicated. /// /// Name of the database - /// A MongoClientSettings object - public static Task InitAsync(string database, MongoClientSettings settings) + /// A MongoClientSettings object + /// Whether to skip pinging the host on initialization, in which case the task completes instantly + public static Task InitAsync(string database, MongoClientSettings settings, bool skipNetworkPing = false) { - return Initialize(settings, database); - } - + return Initialize(settings, database, skipNetworkPing); + } + + /// + /// Initializes a MongoDB connection with the given DBContext parameters. + /// WARNING: will throw an error if server is not reachable! + /// + /// The database context + /// Whether to skip pinging the host on initialization, in which case the task completes instantly + public static async Task InitAsync(DBContext context, bool skipNetworkPing = false) + { + if (skipNetworkPing || await context.PingNetwork()) + { + _context = context; + } + else + { + _context = null; + } + } + + internal static async Task Initialize(MongoClientSettings settings, string dbName, bool skipNetworkPing = false) { if (string.IsNullOrEmpty(dbName)) - throw new ArgumentNullException(nameof(dbName), "Database name cannot be empty!"); - - if (dbs.ContainsKey(dbName)) - return; - - try - { - var db = new MongoClient(settings).GetDatabase(dbName); + throw new ArgumentNullException(nameof(dbName), "Database name cannot be empty!"); - if (dbs.Count == 0) - defaultDb = db; - - if (dbs.TryAdd(dbName, db) && !skipNetworkPing) - await db.RunCommandAsync((Command)"{ping:1}").ConfigureAwait(false); - } - catch (Exception) - { - dbs.TryRemove(dbName, out _); - throw; - } + if (dbName == _context?.DatabaseNamespace.DatabaseName) return; + + var newCtx = new DBContext(dbName, settings); + if (skipNetworkPing || await newCtx.PingNetwork()) + { + _context = newCtx; + } + else + { + _context = null; + } } /// @@ -114,19 +131,19 @@ public static async Task> AllDatabaseNamesAsync(MongoClientS /// /// Any class that implements IEntity /// The name of the database + [Obsolete("This method does nothing", error: true)] public static void DatabaseFor(string databaseName) where T : IEntity { - Cache.MapTypeToDbNameWithoutTenantPrefix(databaseName); } /// /// Gets the IMongoDatabase for the given entity type /// /// The type of entity - /// Optional tenant prefix if using multi-tenancy - public static IMongoDatabase Database(string tenantPrefix = null) where T : IEntity + [Obsolete("This method returns the current Context", error: true)] + public static IMongoDatabase Database() where T : IEntity { - return Cache.Collection(tenantPrefix).Database; + return Context; } /// @@ -134,32 +151,19 @@ public static IMongoDatabase Database(string tenantPrefix = null) where T : I /// You can also get the default database by passing 'default' or 'null' for the name parameter. /// /// The name of the database to retrieve - public static IMongoDatabase Database(string name) + public static IMongoDatabase Database(string? name) { - IMongoDatabase db = null; - - if (dbs.Count > 0) - { - if (string.IsNullOrEmpty(name)) - db = defaultDb; - else - dbs.TryGetValue(name, out db); - } - - if (db == null) - throw new InvalidOperationException($"Database connection is not initialized for [{(string.IsNullOrEmpty(name) ? "Default" : name)}]"); - - return db; + return name == null ? Context : Context.MongoServerContext.GetDatabase(name); } /// /// Gets the name of the database a given entity type is attached to. Returns name of default database if not specifically attached. /// /// Any class that implements IEntity - /// Optional tenant prefix if using multi-tenancy - public static string DatabaseName(string tenantPrefix = null) where T : IEntity + [Obsolete("This method returns the current DatabaseName in the Context")] + public static string DatabaseName() where T : IEntity { - return Database(tenantPrefix).DatabaseNamespace.DatabaseName; + return Context.DatabaseNamespace.DatabaseName; } /// diff --git a/MongoDB.Entities/DBCollection/DBCollection.IMongoCollection.cs b/MongoDB.Entities/DBCollection/DBCollection.IMongoCollection.cs new file mode 100644 index 000000000..017a4af7b --- /dev/null +++ b/MongoDB.Entities/DBCollection/DBCollection.IMongoCollection.cs @@ -0,0 +1,487 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Driver; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace MongoDB.Entities; + +public partial class DBCollection +{ + public CollectionNamespace CollectionNamespace => Collection.CollectionNamespace; + + public IMongoDatabase Database => Collection.Database; + + public IBsonSerializer DocumentSerializer => Collection.DocumentSerializer; + + public IMongoIndexManager Indexes => Collection.Indexes; + + public MongoCollectionSettings Settings => Collection.Settings; + + public IAsyncCursor Aggregate(PipelineDefinition pipeline, AggregateOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.Aggregate(pipeline, options, cancellationToken); + } + + public IAsyncCursor Aggregate(IClientSessionHandle session, PipelineDefinition pipeline, AggregateOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.Aggregate(session, pipeline, options, cancellationToken); + } + + public Task> AggregateAsync(PipelineDefinition pipeline, AggregateOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.AggregateAsync(pipeline, options, cancellationToken); + } + + public Task> AggregateAsync(IClientSessionHandle session, PipelineDefinition pipeline, AggregateOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.AggregateAsync(session, pipeline, options, cancellationToken); + } + + public void AggregateToCollection(PipelineDefinition pipeline, AggregateOptions? options = null, CancellationToken cancellationToken = default) + { + Collection.AggregateToCollection(pipeline, options, cancellationToken); + } + + public void AggregateToCollection(IClientSessionHandle session, PipelineDefinition pipeline, AggregateOptions? options = null, CancellationToken cancellationToken = default) + { + Collection.AggregateToCollection(session, pipeline, options, cancellationToken); + } + + public Task AggregateToCollectionAsync(PipelineDefinition pipeline, AggregateOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.AggregateToCollectionAsync(pipeline, options, cancellationToken); + } + + public Task AggregateToCollectionAsync(IClientSessionHandle session, PipelineDefinition pipeline, AggregateOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.AggregateToCollectionAsync(session, pipeline, options, cancellationToken); + } + + public BulkWriteResult BulkWrite(IEnumerable> requests, BulkWriteOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.BulkWrite(requests, options, cancellationToken); + } + + public BulkWriteResult BulkWrite(IClientSessionHandle session, IEnumerable> requests, BulkWriteOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.BulkWrite(session, requests, options, cancellationToken); + } + + public Task> BulkWriteAsync(IEnumerable> requests, BulkWriteOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.BulkWriteAsync(requests, options, cancellationToken); + } + + public Task> BulkWriteAsync(IClientSessionHandle session, IEnumerable> requests, BulkWriteOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.BulkWriteAsync(session, requests, options, cancellationToken); + } + + [Obsolete("Use CountDocuments or EstimatedDocumentCount instead.")] + long IMongoCollection.Count(FilterDefinition filter, CountOptions options, CancellationToken cancellationToken) + { + return Collection.Count(filter, options, cancellationToken); + } + + [Obsolete("Use CountDocuments or EstimatedDocumentCount instead.")] + long IMongoCollection.Count(IClientSessionHandle session, FilterDefinition filter, CountOptions options, CancellationToken cancellationToken) + { + return Collection.Count(session, filter, options, cancellationToken); + } + + [Obsolete("Use CountDocumentsAsync or EstimatedDocumentCountAsync instead.")] + Task IMongoCollection.CountAsync(FilterDefinition filter, CountOptions? options, CancellationToken cancellationToken) + { + return Collection.CountAsync(filter, options, cancellationToken); + } + + [Obsolete("Use CountDocumentsAsync or EstimatedDocumentCountAsync instead.")] + Task IMongoCollection.CountAsync(IClientSessionHandle session, FilterDefinition filter, CountOptions? options, CancellationToken cancellationToken) + { + return Collection.CountAsync(session, filter, options, cancellationToken); + } + + public long CountDocuments(FilterDefinition filter, CountOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.CountDocuments(filter, options, cancellationToken); + } + + public long CountDocuments(IClientSessionHandle session, FilterDefinition filter, CountOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.CountDocuments(session, filter, options, cancellationToken); + } + + public Task CountDocumentsAsync(FilterDefinition filter, CountOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.CountDocumentsAsync(filter, options, cancellationToken); + } + + public Task CountDocumentsAsync(IClientSessionHandle session, FilterDefinition filter, CountOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.CountDocumentsAsync(session, filter, options, cancellationToken); + } + + public DeleteResult DeleteMany(FilterDefinition filter, CancellationToken cancellationToken = default) + { + return Collection.DeleteMany(filter, cancellationToken); + } + + public DeleteResult DeleteMany(FilterDefinition filter, DeleteOptions options, CancellationToken cancellationToken = default) + { + return Collection.DeleteMany(filter, options, cancellationToken); + } + + public DeleteResult DeleteMany(IClientSessionHandle session, FilterDefinition filter, DeleteOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.DeleteMany(session, filter, options, cancellationToken); + } + + public Task DeleteManyAsync(FilterDefinition filter, CancellationToken cancellationToken = default) + { + return Collection.DeleteManyAsync(filter, cancellationToken); + } + + public Task DeleteManyAsync(FilterDefinition filter, DeleteOptions options, CancellationToken cancellationToken = default) + { + return Collection.DeleteManyAsync(filter, options, cancellationToken); + } + + public Task DeleteManyAsync(IClientSessionHandle session, FilterDefinition filter, DeleteOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.DeleteManyAsync(session, filter, options, cancellationToken); + } + + public DeleteResult DeleteOne(FilterDefinition filter, CancellationToken cancellationToken = default) + { + return Collection.DeleteOne(filter, cancellationToken); + } + + public DeleteResult DeleteOne(FilterDefinition filter, DeleteOptions options, CancellationToken cancellationToken = default) + { + return Collection.DeleteOne(filter, options, cancellationToken); + } + + public DeleteResult DeleteOne(IClientSessionHandle session, FilterDefinition filter, DeleteOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.DeleteOne(session, filter, options, cancellationToken); + } + + public Task DeleteOneAsync(FilterDefinition filter, CancellationToken cancellationToken = default) + { + return Collection.DeleteOneAsync(filter, cancellationToken); + } + + public Task DeleteOneAsync(FilterDefinition filter, DeleteOptions options, CancellationToken cancellationToken = default) + { + return Collection.DeleteOneAsync(filter, options, cancellationToken); + } + + public Task DeleteOneAsync(IClientSessionHandle session, FilterDefinition filter, DeleteOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.DeleteOneAsync(session, filter, options, cancellationToken); + } + + public IAsyncCursor Distinct(FieldDefinition field, FilterDefinition filter, DistinctOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.Distinct(field, filter, options, cancellationToken); + } + + public IAsyncCursor Distinct(IClientSessionHandle session, FieldDefinition field, FilterDefinition filter, DistinctOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.Distinct(session, field, filter, options, cancellationToken); + } + + public Task> DistinctAsync(FieldDefinition field, FilterDefinition filter, DistinctOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.DistinctAsync(field, filter, options, cancellationToken); + } + + public Task> DistinctAsync(IClientSessionHandle session, FieldDefinition field, FilterDefinition filter, DistinctOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.DistinctAsync(session, field, filter, options, cancellationToken); + } + + public long EstimatedDocumentCount(EstimatedDocumentCountOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.EstimatedDocumentCount(options, cancellationToken); + } + + public Task EstimatedDocumentCountAsync(EstimatedDocumentCountOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.EstimatedDocumentCountAsync(options, cancellationToken); + } + + public Task> FindAsync(FilterDefinition filter, FindOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.FindAsync(filter, options, cancellationToken); + } + + public Task> FindAsync(IClientSessionHandle session, FilterDefinition filter, FindOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.FindAsync(session, filter, options, cancellationToken); + } + + public TProjection FindOneAndDelete(FilterDefinition filter, FindOneAndDeleteOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.FindOneAndDelete(filter, options, cancellationToken); + } + + public TProjection FindOneAndDelete(IClientSessionHandle session, FilterDefinition filter, FindOneAndDeleteOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.FindOneAndDelete(session, filter, options, cancellationToken); + } + + public Task FindOneAndDeleteAsync(FilterDefinition filter, FindOneAndDeleteOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.FindOneAndDeleteAsync(filter, options, cancellationToken); + } + + public Task FindOneAndDeleteAsync(IClientSessionHandle session, FilterDefinition filter, FindOneAndDeleteOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.FindOneAndDeleteAsync(session, filter, options, cancellationToken); + } + + public TProjection FindOneAndReplace(FilterDefinition filter, T replacement, FindOneAndReplaceOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.FindOneAndReplace(filter, replacement, options, cancellationToken); + } + + public TProjection FindOneAndReplace(IClientSessionHandle session, FilterDefinition filter, T replacement, FindOneAndReplaceOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.FindOneAndReplace(session, filter, replacement, options, cancellationToken); + } + + public Task FindOneAndReplaceAsync(FilterDefinition filter, T replacement, FindOneAndReplaceOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.FindOneAndReplaceAsync(filter, replacement, options, cancellationToken); + } + + public Task FindOneAndReplaceAsync(IClientSessionHandle session, FilterDefinition filter, T replacement, FindOneAndReplaceOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.FindOneAndReplaceAsync(session, filter, replacement, options, cancellationToken); + } + + public TProjection FindOneAndUpdate(FilterDefinition filter, UpdateDefinition update, FindOneAndUpdateOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.FindOneAndUpdate(filter, update, options, cancellationToken); + } + + public TProjection FindOneAndUpdate(IClientSessionHandle session, FilterDefinition filter, UpdateDefinition update, FindOneAndUpdateOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.FindOneAndUpdate(session, filter, update, options, cancellationToken); + } + + public Task FindOneAndUpdateAsync(FilterDefinition filter, UpdateDefinition update, FindOneAndUpdateOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.FindOneAndUpdateAsync(filter, update, options, cancellationToken); + } + + public Task FindOneAndUpdateAsync(IClientSessionHandle session, FilterDefinition filter, UpdateDefinition update, FindOneAndUpdateOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.FindOneAndUpdateAsync(session, filter, update, options, cancellationToken); + } + + public IAsyncCursor FindSync(FilterDefinition filter, FindOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.FindSync(filter, options, cancellationToken); + } + + public IAsyncCursor FindSync(IClientSessionHandle session, FilterDefinition filter, FindOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.FindSync(session, filter, options, cancellationToken); + } + + public void InsertMany(IEnumerable documents, InsertManyOptions? options = null, CancellationToken cancellationToken = default) + { + Collection.InsertMany(documents, options, cancellationToken); + } + + public void InsertMany(IClientSessionHandle session, IEnumerable documents, InsertManyOptions? options = null, CancellationToken cancellationToken = default) + { + Collection.InsertMany(session, documents, options, cancellationToken); + } + + public Task InsertManyAsync(IEnumerable documents, InsertManyOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.InsertManyAsync(documents, options, cancellationToken); + } + + public Task InsertManyAsync(IClientSessionHandle session, IEnumerable documents, InsertManyOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.InsertManyAsync(session, documents, options, cancellationToken); + } + + public void InsertOne(T document, InsertOneOptions? options = null, CancellationToken cancellationToken = default) + { + Collection.InsertOne(document, options, cancellationToken); + } + + public void InsertOne(IClientSessionHandle session, T document, InsertOneOptions? options = null, CancellationToken cancellationToken = default) + { + Collection.InsertOne(session, document, options, cancellationToken); + } + + [Obsolete("Use the new overload of InsertOneAsync with an InsertOneOptions parameter instead.")] + Task IMongoCollection.InsertOneAsync(T document, CancellationToken _cancellationToken) + { + return Collection.InsertOneAsync(document, _cancellationToken); + } + + public Task InsertOneAsync(T document, InsertOneOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.InsertOneAsync(document, options, cancellationToken); + } + + public Task InsertOneAsync(IClientSessionHandle session, T document, InsertOneOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.InsertOneAsync(session, document, options, cancellationToken); + } + + public IAsyncCursor MapReduce(BsonJavaScript map, BsonJavaScript reduce, MapReduceOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.MapReduce(map, reduce, options, cancellationToken); + } + + public IAsyncCursor MapReduce(IClientSessionHandle session, BsonJavaScript map, BsonJavaScript reduce, MapReduceOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.MapReduce(session, map, reduce, options, cancellationToken); + } + + public Task> MapReduceAsync(BsonJavaScript map, BsonJavaScript reduce, MapReduceOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.MapReduceAsync(map, reduce, options, cancellationToken); + } + + public Task> MapReduceAsync(IClientSessionHandle session, BsonJavaScript map, BsonJavaScript reduce, MapReduceOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.MapReduceAsync(session, map, reduce, options, cancellationToken); + } + + public IFilteredMongoCollection OfType() where TDerivedDocument : T + { + return Collection.OfType(); + } + + public ReplaceOneResult ReplaceOne(FilterDefinition filter, T replacement, ReplaceOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.ReplaceOne(filter, replacement, options, cancellationToken); + } + + [Obsolete("Use the overload that takes a ReplaceOptions instead of an UpdateOptions.")] + ReplaceOneResult IMongoCollection.ReplaceOne(FilterDefinition filter, T replacement, UpdateOptions options, CancellationToken cancellationToken) + { + return Collection.ReplaceOne(filter, replacement, options, cancellationToken); + } + + public ReplaceOneResult ReplaceOne(IClientSessionHandle session, FilterDefinition filter, T replacement, ReplaceOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.ReplaceOne(session, filter, replacement, options, cancellationToken); + } + + [Obsolete("Use the overload that takes a ReplaceOptions instead of an UpdateOptions.")] + ReplaceOneResult IMongoCollection.ReplaceOne(IClientSessionHandle session, FilterDefinition filter, T replacement, UpdateOptions options, CancellationToken cancellationToken) + { + return Collection.ReplaceOne(session, filter, replacement, options, cancellationToken); + } + + public Task ReplaceOneAsync(FilterDefinition filter, T replacement, ReplaceOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.ReplaceOneAsync(filter, replacement, options, cancellationToken); + } + + [Obsolete("Use the overload that takes a ReplaceOptions instead of an UpdateOptions.")] + Task IMongoCollection.ReplaceOneAsync(FilterDefinition filter, T replacement, UpdateOptions options, CancellationToken cancellationToken) + { + return Collection.ReplaceOneAsync(filter, replacement, options, cancellationToken); + } + + public Task ReplaceOneAsync(IClientSessionHandle session, FilterDefinition filter, T replacement, ReplaceOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.ReplaceOneAsync(session, filter, replacement, options, cancellationToken); + } + + [Obsolete("Use the overload that takes a ReplaceOptions instead of an UpdateOptions.")] + Task IMongoCollection.ReplaceOneAsync(IClientSessionHandle session, FilterDefinition filter, T replacement, UpdateOptions options, CancellationToken cancellationToken) + { + return Collection.ReplaceOneAsync(session, filter, replacement, options, cancellationToken); + } + + public UpdateResult UpdateMany(FilterDefinition filter, UpdateDefinition update, UpdateOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.UpdateMany(filter, update, options, cancellationToken); + } + + public UpdateResult UpdateMany(IClientSessionHandle session, FilterDefinition filter, UpdateDefinition update, UpdateOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.UpdateMany(session, filter, update, options, cancellationToken); + } + + public Task UpdateManyAsync(FilterDefinition filter, UpdateDefinition update, UpdateOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.UpdateManyAsync(filter, update, options, cancellationToken); + } + + public Task UpdateManyAsync(IClientSessionHandle session, FilterDefinition filter, UpdateDefinition update, UpdateOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.UpdateManyAsync(session, filter, update, options, cancellationToken); + } + + public UpdateResult UpdateOne(FilterDefinition filter, UpdateDefinition update, UpdateOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.UpdateOne(filter, update, options, cancellationToken); + } + + public UpdateResult UpdateOne(IClientSessionHandle session, FilterDefinition filter, UpdateDefinition update, UpdateOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.UpdateOne(session, filter, update, options, cancellationToken); + } + + public Task UpdateOneAsync(FilterDefinition filter, UpdateDefinition update, UpdateOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.UpdateOneAsync(filter, update, options, cancellationToken); + } + + public Task UpdateOneAsync(IClientSessionHandle session, FilterDefinition filter, UpdateDefinition update, UpdateOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.UpdateOneAsync(session, filter, update, options, cancellationToken); + } + + public IChangeStreamCursor Watch(PipelineDefinition, TResult> pipeline, ChangeStreamOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.Watch(pipeline, options, cancellationToken); + } + + public IChangeStreamCursor Watch(IClientSessionHandle session, PipelineDefinition, TResult> pipeline, ChangeStreamOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.Watch(session, pipeline, options, cancellationToken); + } + + public Task> WatchAsync(PipelineDefinition, TResult> pipeline, ChangeStreamOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.WatchAsync(pipeline, options, cancellationToken); + } + + public Task> WatchAsync(IClientSessionHandle session, PipelineDefinition, TResult> pipeline, ChangeStreamOptions? options = null, CancellationToken cancellationToken = default) + { + return Collection.WatchAsync(session, pipeline, options, cancellationToken); + } + + public IMongoCollection WithReadConcern(ReadConcern readConcern) + { + return Collection.WithReadConcern(readConcern); + } + + public IMongoCollection WithReadPreference(ReadPreference readPreference) + { + return Collection.WithReadPreference(readPreference); + } + + public IMongoCollection WithWriteConcern(WriteConcern writeConcern) + { + return Collection.WithWriteConcern(writeConcern); + } +} diff --git a/MongoDB.Entities/DBCollection/DBCollection.cs b/MongoDB.Entities/DBCollection/DBCollection.cs new file mode 100644 index 000000000..dd416312a --- /dev/null +++ b/MongoDB.Entities/DBCollection/DBCollection.cs @@ -0,0 +1,24 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Driver; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace MongoDB.Entities; + +/// +/// +/// +/// +public partial class DBCollection : IMongoCollection +{ + public IMongoCollection Collection { get; } + + public DBCollection(IMongoCollection collection) + { + Collection = collection; + } +} diff --git a/MongoDB.Entities/DBContext/DBContext.Collection.cs b/MongoDB.Entities/DBContext/DBContext.Collection.cs index e0d632daf..ee05df23f 100644 --- a/MongoDB.Entities/DBContext/DBContext.Collection.cs +++ b/MongoDB.Entities/DBContext/DBContext.Collection.cs @@ -4,54 +4,54 @@ using System.Threading; using System.Threading.Tasks; -namespace MongoDB.Entities +namespace MongoDB.Entities; + +public partial class DBContext { - public partial class DBContext + /// + /// Creates a collection for an Entity type explicitly using the given options + /// + /// The type of entity that will be stored in the created collection + /// The options to use for collection creation + /// An optional cancellation token + /// + public Task CreateCollectionAsync(Action> options, CancellationToken cancellation = default, string? collectionName = null) where T : IEntity { - /// - /// Creates a collection for an Entity type explicitly using the given options - /// - /// The type of entity that will be stored in the created collection - /// The options to use for collection creation - /// An optional cancellation token - public Task CreateCollectionAsync(Action> options, CancellationToken cancellation = default) where T : IEntity - { - var opts = new CreateCollectionOptions(); - options(opts); - return Session == null - ? Database.CreateCollectionAsync(Cache().CollectionName, opts, cancellation) - : Database.CreateCollectionAsync(Session, Cache().CollectionName, opts, cancellation); - } + var opts = new CreateCollectionOptions(); + options(opts); + return Session == null + ? Database.CreateCollectionAsync(collectionName ?? CollectionName(), opts, cancellation) + : Database.CreateCollectionAsync(Session, collectionName ?? CollectionName(), opts, cancellation); + } - /// - /// Deletes the collection of a given entity type as well as the join collections for that entity. - /// TIP: When deleting a collection, all relationships associated with that entity type is also deleted. - /// - /// The entity type to drop the collection of - public async Task DropCollectionAsync() where T : IEntity + /// + /// Deletes the collection of a given entity type as well as the join collections for that entity. + /// TIP: When deleting a collection, all relationships associated with that entity type is also deleted. + /// + /// The entity type to drop the collection of + public async Task DropCollectionAsync(string? collectionName = null) where T : IEntity + { + var tasks = new List(); + var db = Database; + var collName = collectionName ?? CollectionName(); + var options = new ListCollectionNamesOptions { - var tasks = new List(); - var db = Database; - var collName = Cache().CollectionName; - var options = new ListCollectionNamesOptions - { - Filter = "{$and:[{name:/~/},{name:/" + collName + "/}]}" - }; - - foreach (var cName in await db.ListCollectionNames(options).ToListAsync().ConfigureAwait(false)) - { - tasks.Add( - Session == null - ? db.DropCollectionAsync(cName) - : db.DropCollectionAsync(Session, cName)); - } + Filter = "{$and:[{name:/~/},{name:/" + collName + "/}]}" + }; + foreach (var cName in await db.ListCollectionNames(options).ToListAsync().ConfigureAwait(false)) + { tasks.Add( Session == null - ? db.DropCollectionAsync(collName) - : db.DropCollectionAsync(Session, collName)); - - await Task.WhenAll(tasks).ConfigureAwait(false); + ? db.DropCollectionAsync(cName) + : db.DropCollectionAsync(Session, cName)); } + + tasks.Add( + Session == null + ? db.DropCollectionAsync(collName) + : db.DropCollectionAsync(Session, collName)); + + await Task.WhenAll(tasks).ConfigureAwait(false); } } diff --git a/MongoDB.Entities/DBContext/DBContext.Count.cs b/MongoDB.Entities/DBContext/DBContext.Count.cs index e1be5fcbf..76dc3d07f 100644 --- a/MongoDB.Entities/DBContext/DBContext.Count.cs +++ b/MongoDB.Entities/DBContext/DBContext.Count.cs @@ -4,73 +4,82 @@ using System.Threading; using System.Threading.Tasks; -namespace MongoDB.Entities +namespace MongoDB.Entities; + +public partial class DBContext { - public partial class DBContext + /// + /// Gets a fast estimation of how many documents are in the collection using metadata. + /// HINT: The estimation may not be exactly accurate. + /// + /// The entity type to get the count for + /// To override the default collection name + /// To override the default collection + /// An optional cancellation token + public Task CountEstimatedAsync(CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity { - /// - /// Gets a fast estimation of how many documents are in the collection using metadata. - /// HINT: The estimation may not be exactly accurate. - /// - /// The entity type to get the count for - /// An optional cancellation token - public Task CountEstimatedAsync(CancellationToken cancellation = default) where T : IEntity - { - return CollectionFor().EstimatedDocumentCountAsync(cancellationToken: cancellation); - } + return Collection(collectionName, collection).EstimatedDocumentCountAsync(cancellationToken: cancellation); + } - /// - /// Gets an accurate count of how many entities are matched for a given expression/filter - /// - /// The entity type to get the count for - /// A lambda expression for getting the count for a subset of the data - /// An optional cancellation token - /// An optional CountOptions object - /// Set to true if you'd like to ignore any global filters for this operation - public Task CountAsync(Expression> expression, CancellationToken cancellation = default, CountOptions? options = null, bool ignoreGlobalFilters = false) where T : IEntity - { - return CountAsync((FilterDefinition)expression, cancellation, options, ignoreGlobalFilters); - } + /// + /// Gets an accurate count of how many entities are matched for a given expression/filter + /// + /// The entity type to get the count for + /// A lambda expression for getting the count for a subset of the data + /// An optional cancellation token + /// An optional CountOptions object + /// Set to true if you'd like to ignore any global filters for this operation + /// + /// + public Task CountAsync(Expression> expression, CancellationToken cancellation = default, CountOptions? options = null, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + { + return CountAsync((FilterDefinition)expression, cancellation, options, ignoreGlobalFilters, collection: collection, collectionName: collectionName); + } - /// - /// Gets an accurate count of how many total entities are in the collection for a given entity type - /// - /// The entity type to get the count for - /// An optional cancellation token - public Task CountAsync(CancellationToken cancellation = default) where T : IEntity - { - return CountAsync(_ => true, cancellation); + /// + /// Gets an accurate count of how many total entities are in the collection for a given entity type + /// + /// The entity type to get the count for + /// An optional cancellation token + /// + /// + public Task CountAsync(CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + { + return CountAsync(_ => true, cancellation, collectionName: collectionName, collection: collection); - } + } - /// - /// Gets an accurate count of how many total entities are in the collection for a given entity type - /// - /// The entity type to get the count for - /// A filter definition - /// An optional cancellation token - /// An optional CountOptions object - /// Set to true if you'd like to ignore any global filters for this operation - public Task CountAsync(FilterDefinition filter, CancellationToken cancellation = default, CountOptions? options = null, bool ignoreGlobalFilters = false) where T : IEntity - { - filter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, filter); - return - Session == null - ? CollectionFor().CountDocumentsAsync(filter, options, cancellation) - : CollectionFor().CountDocumentsAsync(Session, filter, options, cancellation); - } + /// + /// Gets an accurate count of how many total entities are in the collection for a given entity type + /// + /// The entity type to get the count for + /// A filter definition + /// An optional cancellation token + /// An optional CountOptions object + /// Set to true if you'd like to ignore any global filters for this operation + /// + /// + public Task CountAsync(FilterDefinition filter, CancellationToken cancellation = default, CountOptions? options = null, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + { + filter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, filter); + return + Session == null + ? Collection(collectionName, collection).CountDocumentsAsync(filter, options, cancellation) + : Collection(collectionName, collection).CountDocumentsAsync(Session, filter, options, cancellation); + } - /// - /// Gets an accurate count of how many total entities are in the collection for a given entity type - /// - /// The entity type to get the count for - /// f => f.Eq(x => x.Prop, Value) & f.Gt(x => x.Prop, Value) - /// An optional cancellation token - /// An optional CountOptions object - /// Set to true if you'd like to ignore any global filters for this operation - public Task CountAsync(Func, FilterDefinition> filter, CancellationToken cancellation = default, CountOptions? options = null, bool ignoreGlobalFilters = false) where T : IEntity - { - return CountAsync(filter(Builders.Filter), cancellation, options, ignoreGlobalFilters); - } + /// + /// Gets an accurate count of how many total entities are in the collection for a given entity type + /// + /// The entity type to get the count for + /// f => f.Eq(x => x.Prop, Value) & f.Gt(x => x.Prop, Value) + /// An optional cancellation token + /// An optional CountOptions object + /// Set to true if you'd like to ignore any global filters for this operation + /// + /// + public Task CountAsync(Func, FilterDefinition> filter, CancellationToken cancellation = default, CountOptions? options = null, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + { + return CountAsync(filter(Builders.Filter), cancellation, options, ignoreGlobalFilters, collectionName: collectionName, collection: collection); } } diff --git a/MongoDB.Entities/DBContext/DBContext.Delete.cs b/MongoDB.Entities/DBContext/DBContext.Delete.cs index b8800ebf6..b1ea05eb5 100644 --- a/MongoDB.Entities/DBContext/DBContext.Delete.cs +++ b/MongoDB.Entities/DBContext/DBContext.Delete.cs @@ -6,164 +6,172 @@ using System.Threading; using System.Threading.Tasks; -namespace MongoDB.Entities +namespace MongoDB.Entities; + +public partial class DBContext { - public partial class DBContext + private static readonly int _deleteBatchSize = 100000; + private void ThrowIfCancellationNotSupported(CancellationToken cancellation = default) { - private static readonly int _deleteBatchSize = 100000; - private void ThrowIfCancellationNotSupported(CancellationToken cancellation = default) - { - if (cancellation != default && Session is null) - throw new NotSupportedException("Cancellation is only supported within transactions for delete operations!"); - } + if (cancellation != default && Session is null) + throw new NotSupportedException("Cancellation is only supported within transactions for delete operations!"); + } - private async Task DeleteCascadingAsync(IEnumerable IDs, CancellationToken cancellation = default) where T : IEntity - { - // note: cancellation should not be enabled outside of transactions because multiple collections are involved - // and premature cancellation could cause data inconsistencies. - // i.e. don't pass the cancellation token to delete methods below that don't take a session. - // also make consumers call ThrowIfCancellationNotSupported() before calling this method. + private async Task DeleteCascadingAsync(IEnumerable IDs, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + { + // note: cancellation should not be enabled outside of transactions because multiple collections are involved + // and premature cancellation could cause data inconsistencies. + // i.e. don't pass the cancellation token to delete methods below that don't take a session. + // also make consumers call ThrowIfCancellationNotSupported() before calling this method. - var db = Database; - var options = new ListCollectionNamesOptions - { - Filter = "{$and:[{name:/~/},{name:/" + Cache().CollectionName + "/}]}" - }; + var options = new ListCollectionNamesOptions + { + Filter = "{$and:[{name:/~/},{name:/" + collectionName ?? Cache().CollectionName + "/}]}" + }; - var tasks = new List(); + var tasks = new List(); - // note: db.listCollections() mongo command does not support transactions. - // so don't add session support here. - var collNamesCursor = await db.ListCollectionNamesAsync(options, cancellation).ConfigureAwait(false); + // note: db.listCollections() mongo command does not support transactions. + // so don't add session support here. + var collNamesCursor = await ListCollectionNamesAsync(options, cancellation).ConfigureAwait(false); - foreach (var cName in await collNamesCursor.ToListAsync(cancellation).ConfigureAwait(false)) - { - tasks.Add( - Session is null - ? db.GetCollection(cName).DeleteManyAsync(r => IDs.Contains(r.ChildID) || IDs.Contains(r.ParentID)) - : db.GetCollection(cName).DeleteManyAsync(Session, r => IDs.Contains(r.ChildID) || IDs.Contains(r.ParentID), null, cancellation)); - } + foreach (var cName in await collNamesCursor.ToListAsync(cancellation).ConfigureAwait(false)) + { + tasks.Add( + Session is null + ? Collection(cName).DeleteManyAsync(r => IDs.Contains(r.ChildID) || IDs.Contains(r.ParentID)) + : Collection(cName).DeleteManyAsync(Session, r => IDs.Contains(r.ChildID) || IDs.Contains(r.ParentID), null, cancellation)); + } - var delResTask = - Session == null - ? CollectionFor().DeleteManyAsync(x => IDs.Contains(x.ID)) - : CollectionFor().DeleteManyAsync(Session, x => IDs.Contains(x.ID), null, cancellation); + var delResTask = + Session == null + ? Collection(collectionName, collection).DeleteManyAsync(x => IDs.Contains(x.ID)) + : Collection(collectionName, collection).DeleteManyAsync(Session, x => IDs.Contains(x.ID), null, cancellation); - tasks.Add(delResTask); + tasks.Add(delResTask); - if (typeof(T).BaseType == typeof(FileEntity)) - { - tasks.Add( - Session is null - ? db.GetCollection(Cache().CollectionName).DeleteManyAsync(x => IDs.Contains(x.FileID)) - : db.GetCollection(Cache().CollectionName).DeleteManyAsync(Session, x => IDs.Contains(x.FileID), null, cancellation)); - } + if (typeof(FileEntity).IsAssignableFrom(typeof(T))) + { + tasks.Add( + Session is null + ? Collection().DeleteManyAsync(x => IDs.Contains(x.FileID)) + : Collection().DeleteManyAsync(Session, x => IDs.Contains(x.FileID), null, cancellation)); + } - await Task.WhenAll(tasks).ConfigureAwait(false); + await Task.WhenAll(tasks).ConfigureAwait(false); - return await delResTask.ConfigureAwait(false); - } + return await delResTask.ConfigureAwait(false); + } - /// - /// Deletes a single entity from MongoDB - /// HINT: If this entity is referenced by one-to-many/many-to-many relationships, those references are also deleted. - /// - /// The type of entity - /// The Id of the entity to delete - /// An optional cancellation token - /// Set to true if you'd like to ignore any global filters for this operation - public Task DeleteAsync(string ID, CancellationToken cancellation = default, bool ignoreGlobalFilters = false) where T : IEntity - { - return DeleteAsync(Builders.Filter.Eq(e => e.ID, ID), cancellation, ignoreGlobalFilters: ignoreGlobalFilters); - } + /// + /// Deletes a single entity from MongoDB + /// HINT: If this entity is referenced by one-to-many/many-to-many relationships, those references are also deleted. + /// + /// The type of entity + /// The Id of the entity to delete + /// An optional cancellation token + /// Set to true if you'd like to ignore any global filters for this operation + /// + /// + public Task DeleteAsync(string ID, CancellationToken cancellation = default, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + { + return DeleteAsync(Builders.Filter.Eq(e => e.ID, ID), cancellation, ignoreGlobalFilters: ignoreGlobalFilters, collection: collection, collectionName: collectionName); + } - /// - /// Deletes matching entities from MongoDB - /// HINT: If these entities are referenced by one-to-many/many-to-many relationships, those references are also deleted. - /// TIP: Try to keep the number of entities to delete under 100 in a single call - /// - /// The type of entity - /// An IEnumerable of entity IDs - /// An optional cancellation token - /// Set to true if you'd like to ignore any global filters for this operation - public Task DeleteAsync(IEnumerable IDs, CancellationToken cancellation = default, bool ignoreGlobalFilters = false) where T : IEntity - { - return DeleteAsync(Builders.Filter.In(e => e.ID, IDs), cancellation, ignoreGlobalFilters: ignoreGlobalFilters); - } + /// + /// Deletes matching entities from MongoDB + /// HINT: If these entities are referenced by one-to-many/many-to-many relationships, those references are also deleted. + /// TIP: Try to keep the number of entities to delete under 100 in a single call + /// + /// The type of entity + /// An IEnumerable of entity IDs + /// An optional cancellation token + /// Set to true if you'd like to ignore any global filters for this operation + /// + /// + public Task DeleteAsync(IEnumerable IDs, CancellationToken cancellation = default, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + { + return DeleteAsync(Builders.Filter.In(e => e.ID, IDs), cancellation, ignoreGlobalFilters: ignoreGlobalFilters, collection: collection, collectionName: collectionName); + } - /// - /// Deletes matching entities from MongoDB - /// HINT: If these entities are referenced by one-to-many/many-to-many relationships, those references are also deleted. - /// TIP: Try to keep the number of entities to delete under 100 in a single call - /// - /// The type of entity - /// A lambda expression for matching entities to delete. - /// An optional cancellation token - /// An optional collation object - /// Set to true if you'd like to ignore any global filters for this operation - public Task DeleteAsync(Expression> expression, CancellationToken cancellation = default, Collation? collation = null, bool ignoreGlobalFilters = false) where T : IEntity - { - return DeleteAsync(Builders.Filter.Where(expression), cancellation, collation, ignoreGlobalFilters); - } + /// + /// Deletes matching entities from MongoDB + /// HINT: If these entities are referenced by one-to-many/many-to-many relationships, those references are also deleted. + /// TIP: Try to keep the number of entities to delete under 100 in a single call + /// + /// The type of entity + /// A lambda expression for matching entities to delete. + /// An optional cancellation token + /// An optional collation object + /// Set to true if you'd like to ignore any global filters for this operation + /// + /// + public Task DeleteAsync(Expression> expression, CancellationToken cancellation = default, Collation? collation = null, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + { + return DeleteAsync(Builders.Filter.Where(expression), cancellation, collation, ignoreGlobalFilters, collection: collection, collectionName: collectionName); + } - /// - /// Deletes matching entities with a filter expression - /// HINT: If the expression matches more than 100,000 entities, they will be deleted in batches of 100k. - /// HINT: If these entities are referenced by one-to-many/many-to-many relationships, those references are also deleted. - /// - /// Any class that implements IEntity - /// f => f.Eq(x => x.Prop, Value) & f.Gt(x => x.Prop, Value) - /// An optional cancellation token - /// An optional collation object - /// Set to true if you'd like to ignore any global filters for this operation - public Task DeleteAsync(Func, FilterDefinition> filter, CancellationToken cancellation = default, Collation? collation = null, bool ignoreGlobalFilters = false) where T : IEntity - { - return DeleteAsync(filter(Builders.Filter), cancellation, collation, ignoreGlobalFilters); - } + /// + /// Deletes matching entities with a filter expression + /// HINT: If the expression matches more than 100,000 entities, they will be deleted in batches of 100k. + /// HINT: If these entities are referenced by one-to-many/many-to-many relationships, those references are also deleted. + /// + /// Any class that implements IEntity + /// f => f.Eq(x => x.Prop, Value) & f.Gt(x => x.Prop, Value) + /// An optional cancellation token + /// An optional collation object + /// Set to true if you'd like to ignore any global filters for this operation + /// + /// + public Task DeleteAsync(Func, FilterDefinition> filter, CancellationToken cancellation = default, Collation? collation = null, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + { + return DeleteAsync(filter(Builders.Filter), cancellation, collation, ignoreGlobalFilters, collection: collection, collectionName: collectionName); + } - /// - /// Deletes matching entities with a filter definition - /// HINT: If the expression matches more than 100,000 entities, they will be deleted in batches of 100k. - /// HINT: If these entities are referenced by one-to-many/many-to-many relationships, those references are also deleted. - /// - /// Any class that implements IEntity - /// A filter definition for matching entities to delete. - /// An optional cancellation token - /// An optional collation object - /// Set to true if you'd like to ignore any global filters for this operation - public async Task DeleteAsync(FilterDefinition filter, CancellationToken cancellation = default, Collation? collation = null, bool ignoreGlobalFilters = false) where T : IEntity - { - ThrowIfCancellationNotSupported(cancellation); + /// + /// Deletes matching entities with a filter definition + /// HINT: If the expression matches more than 100,000 entities, they will be deleted in batches of 100k. + /// HINT: If these entities are referenced by one-to-many/many-to-many relationships, those references are also deleted. + /// + /// Any class that implements IEntity + /// A filter definition for matching entities to delete. + /// An optional cancellation token + /// An optional collation object + /// Set to true if you'd like to ignore any global filters for this operation + /// + /// + public async Task DeleteAsync(FilterDefinition filter, CancellationToken cancellation = default, Collation? collation = null, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + { + ThrowIfCancellationNotSupported(cancellation); - var filterDef = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, filter); - var cursor = await new Find(this, CollectionFor()) - .Match(filter) - .Project(e => e.ID) - .Option(o => o.BatchSize = _deleteBatchSize) - .Option(o => o.Collation = collation) - .ExecuteCursorAsync(cancellation) - .ConfigureAwait(false); + var filterDef = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, filter); + var cursor = await new Find(this, Collection(collectionName, collection)) + .Match(filter) + .Project(e => e.ID) + .Option(o => o.BatchSize = _deleteBatchSize) + .Option(o => o.Collation = collation) + .ExecuteCursorAsync(cancellation) + .ConfigureAwait(false); - long deletedCount = 0; - DeleteResult? res = null; + long deletedCount = 0; + DeleteResult? res = null; - using (cursor) + using (cursor) + { + while (await cursor.MoveNextAsync(cancellation).ConfigureAwait(false)) { - while (await cursor.MoveNextAsync(cancellation).ConfigureAwait(false)) + if (cursor.Current.Any()) { - if (cursor.Current.Any()) - { - res = await DeleteCascadingAsync(cursor.Current, cancellation).ConfigureAwait(false); - deletedCount += res.DeletedCount; - } + res = await DeleteCascadingAsync(cursor.Current, cancellation).ConfigureAwait(false); + deletedCount += res.DeletedCount; } } + } - if (res?.IsAcknowledged == false) - return DeleteResult.Unacknowledged.Instance; + if (res?.IsAcknowledged == false) + return DeleteResult.Unacknowledged.Instance; - return new DeleteResult.Acknowledged(deletedCount); - } + return new DeleteResult.Acknowledged(deletedCount); } } diff --git a/MongoDB.Entities/DBContext/DBContext.Distinct.cs b/MongoDB.Entities/DBContext/DBContext.Distinct.cs index 3beaa8437..52b65b5b5 100644 --- a/MongoDB.Entities/DBContext/DBContext.Distinct.cs +++ b/MongoDB.Entities/DBContext/DBContext.Distinct.cs @@ -1,15 +1,16 @@ -namespace MongoDB.Entities +using MongoDB.Driver; + +namespace MongoDB.Entities; + +public partial class DBContext { - public partial class DBContext + /// + /// Represents a MongoDB Distinct command where you can get back distinct values for a given property of a given Entity + /// + /// Any Entity that implements IEntity interface + /// The type of the property of the entity you'd like to get unique values for + public Distinct Distinct(string? collectionName = null, IMongoCollection? collection = null) where T : IEntity { - /// - /// Represents a MongoDB Distinct command where you can get back distinct values for a given property of a given Entity - /// - /// Any Entity that implements IEntity interface - /// The type of the property of the entity you'd like to get unique values for - public Distinct Distinct() where T : IEntity - { - return new Distinct(this, CollectionFor()); - } + return new Distinct(this, Collection(collectionName, collection)); } } diff --git a/MongoDB.Entities/DBContext/DBContext.File.cs b/MongoDB.Entities/DBContext/DBContext.File.cs index 8826cc358..0c0c38598 100644 --- a/MongoDB.Entities/DBContext/DBContext.File.cs +++ b/MongoDB.Entities/DBContext/DBContext.File.cs @@ -1,21 +1,23 @@ using MongoDB.Bson; +using MongoDB.Driver; using System; -namespace MongoDB.Entities +namespace MongoDB.Entities; + +public partial class DBContext { - public partial class DBContext + /// + /// Returns a DataStreamer object to enable uploading/downloading file data directly by supplying the ID of the file entity + /// + /// The file entity type + /// The ID of the file entity + /// + /// + public DataStreamer File(string ID, string? collectionName = null, IMongoCollection? collection = null) where T : FileEntity, new() { - /// - /// Returns a DataStreamer object to enable uploading/downloading file data directly by supplying the ID of the file entity - /// - /// The file entity type - /// The ID of the file entity - public DataStreamer File(string ID) where T : FileEntity, new() - { - if (!ObjectId.TryParse(ID, out _)) - throw new ArgumentException("The ID passed in is not of the correct format!"); + if (!ObjectId.TryParse(ID, out _)) + throw new ArgumentException("The ID passed in is not of the correct format!"); - return new DataStreamer(new T() { ID = ID, UploadSuccessful = true }, this); - } + return new DataStreamer(new T() { ID = ID, UploadSuccessful = true }, this, Collection(collectionName, collection)); } } diff --git a/MongoDB.Entities/DBContext/DBContext.Find.cs b/MongoDB.Entities/DBContext/DBContext.Find.cs index 194f6cda9..85d10b264 100644 --- a/MongoDB.Entities/DBContext/DBContext.Find.cs +++ b/MongoDB.Entities/DBContext/DBContext.Find.cs @@ -1,24 +1,25 @@ -namespace MongoDB.Entities +using MongoDB.Driver; + +namespace MongoDB.Entities; + +public partial class DBContext { - public partial class DBContext + /// + /// Starts a find command for the given entity type + /// + /// The type of entity + public Find Find(string? collectionName = null, IMongoCollection? collection = null) where T : IEntity { - /// - /// Starts a find command for the given entity type - /// - /// The type of entity - public Find Find() where T : IEntity - { - return new Find(this, CollectionFor()); - } + return new Find(this, Collection(collectionName, collection)); + } - /// - /// Starts a find command with projection support for the given entity type - /// - /// The type of entity - /// The type of the end result - public Find Find() where T : IEntity - { - return new Find(this, CollectionFor()); - } + /// + /// Starts a find command with projection support for the given entity type + /// + /// The type of entity + /// The type of the end result + public Find Find(string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + { + return new Find(this, Collection(collectionName, collection)); } } diff --git a/MongoDB.Entities/DBContext/DBContext.Fluent.cs b/MongoDB.Entities/DBContext/DBContext.Fluent.cs index 649036c52..d6f3f05cf 100644 --- a/MongoDB.Entities/DBContext/DBContext.Fluent.cs +++ b/MongoDB.Entities/DBContext/DBContext.Fluent.cs @@ -10,13 +10,15 @@ public partial class DBContext /// The type of entity /// The options for the aggregation. This is not required. /// Set to true if you'd like to ignore any global filters for this operation - public IAggregateFluent Fluent(AggregateOptions? options = null, bool ignoreGlobalFilters = false) where T : IEntity + /// + /// + public IAggregateFluent Fluent(AggregateOptions? options = null, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity { var globalFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, Builders.Filter.Empty); var aggregate = Session is not IClientSessionHandle session - ? CollectionFor().Aggregate(options) - : CollectionFor().Aggregate(session, options); + ? Collection(collectionName, collection).Aggregate(options) + : Collection(collectionName, collection).Aggregate(session, options); if (globalFilter != Builders.Filter.Empty) { @@ -36,7 +38,9 @@ public IAggregateFluent Fluent(AggregateOptions? options = null, bool igno /// The language for the search (optional) /// Options for finding documents (not required) /// Set to true if you'd like to ignore any global filters for this operation - public IAggregateFluent FluentTextSearch(Search searchType, string searchTerm, bool caseSensitive = false, bool diacriticSensitive = false, string? language = null, AggregateOptions? options = null, bool ignoreGlobalFilters = false) where T : IEntity + /// + /// + public IAggregateFluent FluentTextSearch(Search searchType, string searchTerm, bool caseSensitive = false, bool diacriticSensitive = false, string? language = null, AggregateOptions? options = null, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity { var globalFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, Builders.Filter.Empty); @@ -58,8 +62,8 @@ public IAggregateFluent FluentTextSearch(Search searchType, string searchT }); var aggregate = Session is not IClientSessionHandle session - ? CollectionFor().Aggregate(options).Match(filter) - : CollectionFor().Aggregate(session, options).Match(filter); + ? Collection(collectionName, collection).Aggregate(options).Match(filter) + : Collection(collectionName, collection).Aggregate(session, options).Match(filter); if (globalFilter != Builders.Filter.Empty) diff --git a/MongoDB.Entities/DBContext/DBContext.GeoNear.cs b/MongoDB.Entities/DBContext/DBContext.GeoNear.cs index 8e9238d6f..bc1274aa9 100644 --- a/MongoDB.Entities/DBContext/DBContext.GeoNear.cs +++ b/MongoDB.Entities/DBContext/DBContext.GeoNear.cs @@ -23,7 +23,9 @@ public partial class DBContext /// The options for the aggregation. This is not required. /// The type of entity /// Set to true if you'd like to ignore any global filters for this operation - public IAggregateFluent GeoNear(Coordinates2D NearCoordinates, Expression>? DistanceField, bool Spherical = true, double? MaxDistance = null, double? MinDistance = null, int? Limit = null, BsonDocument? Query = null, double? DistanceMultiplier = null, Expression>? IncludeLocations = null, string? IndexKey = null, AggregateOptions? options = null, bool ignoreGlobalFilters = false) where T : IEntity + /// + /// + public IAggregateFluent GeoNear(Coordinates2D NearCoordinates, Expression>? DistanceField, bool Spherical = true, double? MaxDistance = null, double? MinDistance = null, int? Limit = null, BsonDocument? Query = null, double? DistanceMultiplier = null, Expression>? IncludeLocations = null, string? IndexKey = null, AggregateOptions? options = null, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity { var globalFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, Builders.Filter.Empty); @@ -40,7 +42,7 @@ public IAggregateFluent GeoNear(Coordinates2D NearCoordinates, Expression< includeLocs = IncludeLocations?.FullPath(), key = IndexKey, } - .ToFluent(this, options); + .ToFluent(this, options, collectionName: collectionName, collection: collection); if (globalFilter != Builders.Filter.Empty) { diff --git a/MongoDB.Entities/DBContext/DBContext.IMongoDatabase.cs b/MongoDB.Entities/DBContext/DBContext.IMongoDatabase.cs index 73a2cdbea..20663f796 100644 --- a/MongoDB.Entities/DBContext/DBContext.IMongoDatabase.cs +++ b/MongoDB.Entities/DBContext/DBContext.IMongoDatabase.cs @@ -111,7 +111,11 @@ public Task DropCollectionAsync(IClientSessionHandle session, string name, Cance return Database.DropCollectionAsync(session, name, cancellationToken); } - public IMongoCollection GetCollection(string name, MongoCollectionSettings? settings = null) + public DBCollection GetCollection(string name, MongoCollectionSettings? settings = null) + { + return new(Database.GetCollection(name, settings)); + } + IMongoCollection IMongoDatabase.GetCollection(string name, MongoCollectionSettings? settings) { return Database.GetCollection(name, settings); } @@ -126,12 +130,12 @@ IAsyncCursor IMongoDatabase.ListCollectionNames(IClientSessionHandle ses return Database.ListCollectionNames(session, options, cancellationToken); } - Task> IMongoDatabase.ListCollectionNamesAsync(ListCollectionNamesOptions options, CancellationToken cancellationToken) + public Task> ListCollectionNamesAsync(ListCollectionNamesOptions options, CancellationToken cancellationToken) { return Database.ListCollectionNamesAsync(options, cancellationToken); } - Task> IMongoDatabase.ListCollectionNamesAsync(IClientSessionHandle session, ListCollectionNamesOptions options, CancellationToken cancellationToken) + public Task> ListCollectionNamesAsync(IClientSessionHandle session, ListCollectionNamesOptions options, CancellationToken cancellationToken) { return Database.ListCollectionNamesAsync(session, options, cancellationToken); } diff --git a/MongoDB.Entities/DBContext/DBContext.Index.cs b/MongoDB.Entities/DBContext/DBContext.Index.cs index 9898d7cdd..8f500cd67 100644 --- a/MongoDB.Entities/DBContext/DBContext.Index.cs +++ b/MongoDB.Entities/DBContext/DBContext.Index.cs @@ -1,4 +1,6 @@ -namespace MongoDB.Entities +using MongoDB.Driver; + +namespace MongoDB.Entities { public partial class DBContext { @@ -7,9 +9,9 @@ public partial class DBContext /// TIP: Define the keys first with .Key() method and finally call the .Create() method. /// /// Any class that implements IEntity - public Index Index() where T : IEntity + public Index Index(string? collectionName = null, IMongoCollection? collection = null) where T : IEntity { - return new Index(this, CollectionFor()); + return new Index(this, Collection(collectionName, collection)); } } } diff --git a/MongoDB.Entities/DBContext/DBContext.Insert.cs b/MongoDB.Entities/DBContext/DBContext.Insert.cs index da90c4884..4e66f8163 100644 --- a/MongoDB.Entities/DBContext/DBContext.Insert.cs +++ b/MongoDB.Entities/DBContext/DBContext.Insert.cs @@ -1,5 +1,8 @@ using MongoDB.Driver; +using System; using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; @@ -7,6 +10,47 @@ namespace MongoDB.Entities { public partial class DBContext { + private static readonly BulkWriteOptions _unOrdBlkOpts = new() { IsOrdered = false }; + private static readonly UpdateOptions _updateOptions = new() { IsUpsert = true }; + private Task SavePartial(T entity, Expression> members, CancellationToken cancellation, bool excludeMode = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + { + PrepAndCheckIfInsert(entity); //just prep. we don't care about inserts here + return + Session == null + ? Collection(collectionName, collection).UpdateOneAsync(e => e.ID == entity.ID, Builders.Update.Combine(Logic.BuildUpdateDefs(entity, members, excludeMode)), _updateOptions, cancellation) + : Collection(collectionName, collection).UpdateOneAsync(Session, e => e.ID == entity.ID, Builders.Update.Combine(Logic.BuildUpdateDefs(entity, members, excludeMode)), _updateOptions, cancellation); + } + + private Task> SavePartial(IEnumerable entities, Expression> members, CancellationToken cancellation, bool excludeMode = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + { + var models = entities.Select(ent => + { + PrepAndCheckIfInsert(ent); //just prep. we don't care about inserts here + return new UpdateOneModel( + filter: Builders.Filter.Eq(e => e.ID, ent.ID), + update: Builders.Update.Combine(Logic.BuildUpdateDefs(ent, members, excludeMode))) + { IsUpsert = true }; + }).ToList(); + return Session == null + ? Collection(collectionName, collection).BulkWriteAsync(models, _unOrdBlkOpts, cancellation) + : Collection(collectionName, collection).BulkWriteAsync(Session, models, _unOrdBlkOpts, cancellation); + } + + private bool PrepAndCheckIfInsert(T entity) where T : IEntity + { + var cache = Cache(); + if (string.IsNullOrEmpty(entity.ID)) + { + entity.ID = entity.GenerateNewID(); + if (cache.HasCreatedOn) ((ICreatedOn)entity).CreatedOn = DateTime.UtcNow; + if (cache.HasModifiedOn) ((IModifiedOn)entity).ModifiedOn = DateTime.UtcNow; + return true; + } + + if (cache.HasModifiedOn) ((IModifiedOn)entity).ModifiedOn = DateTime.UtcNow; + return false; + } + /// /// Saves a complete entity replacing an existing entity or creating a new one if it does not exist. /// If ID value is null, a new entity is created. If ID has a value, then existing entity is replaced. @@ -14,11 +58,16 @@ public partial class DBContext /// The type of entity /// The instance to persist /// And optional cancellation token - public Task InsertAsync(T entity, CancellationToken cancellation = default) where T : IEntity + /// + /// + public Task InsertAsync(T entity, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity { SetModifiedBySingle(entity); - OnBeforeSave()?.Invoke(entity); - return DB.InsertAsync(entity, Session, cancellation); + OnBeforeSave(entity); + PrepAndCheckIfInsert(entity); + return Session == null + ? Collection(collectionName, collection).InsertOneAsync(entity, null, cancellation) + : Collection(collectionName, collection).InsertOneAsync(Session, entity, null, cancellation); } /// @@ -28,11 +77,22 @@ public Task InsertAsync(T entity, CancellationToken cancellation = default) w /// The type of entity /// The entities to persist /// And optional cancellation token - public Task> InsertAsync(IEnumerable entities, CancellationToken cancellation = default) where T : IEntity + /// + /// + public Task> InsertAsync(IEnumerable entities, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity { SetModifiedByMultiple(entities); - foreach (var ent in entities) OnBeforeSave()?.Invoke(ent); - return DB.InsertAsync(entities, Session, cancellation); + foreach (var ent in entities) OnBeforeSave(ent); + + var models = entities.Select(ent => + { + PrepAndCheckIfInsert(ent); + return new InsertOneModel(ent); + }).ToList(); + + return Session == null + ? Collection(collectionName, collection).BulkWriteAsync(models, _unOrdBlkOpts, cancellation) + : Collection(collectionName, collection).BulkWriteAsync(Session, models, _unOrdBlkOpts, cancellation); } } } diff --git a/MongoDB.Entities/DBContext/DBContext.PagedSearch.cs b/MongoDB.Entities/DBContext/DBContext.PagedSearch.cs index 99818fb0c..891df99cd 100644 --- a/MongoDB.Entities/DBContext/DBContext.PagedSearch.cs +++ b/MongoDB.Entities/DBContext/DBContext.PagedSearch.cs @@ -1,4 +1,6 @@ -namespace MongoDB.Entities +using MongoDB.Driver; + +namespace MongoDB.Entities { public partial class DBContext { @@ -6,9 +8,9 @@ public partial class DBContext /// Represents an aggregation query that retrieves results with easy paging support. /// /// Any class that implements IEntity - public PagedSearch PagedSearch() where T : IEntity + public PagedSearch PagedSearch(string? collectionName = null, IMongoCollection? collection = null) where T : IEntity { - return new PagedSearch(Session, _globalFilters, tenantPrefix); + return new PagedSearch(this, Collection(collectionName, collection)); } /// @@ -16,9 +18,9 @@ public PagedSearch PagedSearch() where T : IEntity /// /// Any class that implements IEntity /// The type you'd like to project the results to. - public PagedSearch PagedSearch() where T : IEntity + public PagedSearch PagedSearch(string? collectionName = null, IMongoCollection? collection = null) where T : IEntity { - return new PagedSearch(Session, _globalFilters, tenantPrefix); + return new PagedSearch(this, Collection(collectionName, collection)); } } } diff --git a/MongoDB.Entities/DBContext/DBContext.Pipeline.cs b/MongoDB.Entities/DBContext/DBContext.Pipeline.cs index 927d9eeab..cd35efeed 100644 --- a/MongoDB.Entities/DBContext/DBContext.Pipeline.cs +++ b/MongoDB.Entities/DBContext/DBContext.Pipeline.cs @@ -2,6 +2,7 @@ using MongoDB.Bson.Serialization; using MongoDB.Driver; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -19,9 +20,14 @@ public partial class DBContext /// The options for the aggregation. This is not required. /// An optional cancellation token /// Set to true if you'd like to ignore any global filters for this operation - public Task> PipelineCursorAsync(Template template, AggregateOptions options = null, CancellationToken cancellation = default, bool ignoreGlobalFilters = false) where T : IEntity + /// + /// + public Task> PipelineCursorAsync(Template template, AggregateOptions? options = null, CancellationToken cancellation = default, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity { - return DB.PipelineCursorAsync(MergeGlobalFilter(template, ignoreGlobalFilters), options, Session, cancellation); + template = MergeTemplateGlobalFilter(template, ignoreGlobalFilters); + return Session == null + ? Collection(collectionName, collection).AggregateAsync(template.ToPipeline(), options, cancellation) + : Collection(collectionName, collection).AggregateAsync(Session, template.ToPipeline(), options, cancellation); } /// @@ -34,9 +40,19 @@ public Task> PipelineCursorAsync(TemplateThe options for the aggregation. This is not required. /// An optional cancellation token /// Set to true if you'd like to ignore any global filters for this operation - public Task> PipelineAsync(Template template, AggregateOptions options = null, CancellationToken cancellation = default, bool ignoreGlobalFilters = false) where T : IEntity + /// + /// + public async Task> PipelineAsync(Template template, AggregateOptions? options = null, CancellationToken cancellation = default, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity { - return DB.PipelineAsync(MergeGlobalFilter(template, ignoreGlobalFilters), options, Session, cancellation); + var list = new List(); + using (var cursor = await PipelineCursorAsync(template, options, cancellation, ignoreGlobalFilters: ignoreGlobalFilters, collectionName: collectionName, collection: collection).ConfigureAwait(false)) + { + while (await cursor.MoveNextAsync(cancellation).ConfigureAwait(false)) + { + list.AddRange(cursor.Current); + } + } + return list; } /// @@ -49,9 +65,17 @@ public Task> PipelineAsync(Template templa /// The options for the aggregation. This is not required. /// An optional cancellation token /// Set to true if you'd like to ignore any global filters for this operation - public Task PipelineSingleAsync(Template template, AggregateOptions options = null, CancellationToken cancellation = default, bool ignoreGlobalFilters = false) where T : IEntity + /// + /// + public async Task PipelineSingleAsync(Template template, AggregateOptions? options = null, CancellationToken cancellation = default, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity { - return DB.PipelineSingleAsync(MergeGlobalFilter(template, ignoreGlobalFilters), options, Session, cancellation); + + AggregateOptions opts = options ?? new AggregateOptions(); + opts.BatchSize = 2; + + using var cursor = await PipelineCursorAsync(template, opts, cancellation, ignoreGlobalFilters: ignoreGlobalFilters, collectionName: collectionName, collection: collection).ConfigureAwait(false); + await cursor.MoveNextAsync(cancellation).ConfigureAwait(false); + return cursor.Current.SingleOrDefault(); } /// @@ -64,19 +88,25 @@ public Task PipelineSingleAsync(Template templa /// The options for the aggregation. This is not required. /// An optional cancellation token /// Set to true if you'd like to ignore any global filters for this operation - public Task PipelineFirstAsync(Template template, AggregateOptions options = null, CancellationToken cancellation = default, bool ignoreGlobalFilters = false) where T : IEntity + public async Task PipelineFirstAsync(Template template, AggregateOptions? options = null, CancellationToken cancellation = default, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity { - return DB.PipelineFirstAsync(MergeGlobalFilter(template, ignoreGlobalFilters), options, Session, cancellation); + + var opts = options ?? new AggregateOptions(); + opts.BatchSize = 1; + + using var cursor = await PipelineCursorAsync(template, opts, cancellation, ignoreGlobalFilters: ignoreGlobalFilters, collectionName: collectionName, collection: collection).ConfigureAwait(false); + await cursor.MoveNextAsync(cancellation).ConfigureAwait(false); + return cursor.Current.SingleOrDefault(); } - private Template MergeGlobalFilter(Template template, bool ignoreGlobalFilters) where T : IEntity + private Template MergeTemplateGlobalFilter(Template template, bool ignoreGlobalFilters) where T : IEntity { //WARNING: this has to do the same thing as Logic.MergeGlobalFilter method // if the following logic changes, update the other method also - if (!ignoreGlobalFilters && _globalFilters.Count > 0 && _globalFilters.TryGetValue(typeof(T), out var gFilter)) + if (!ignoreGlobalFilters && GlobalFilters.Count > 0 && GlobalFilters.TryGetValue(typeof(T), out var gFilter)) { - BsonDocument filter = null; + BsonDocument? filter = null; switch (gFilter.filterDef) { diff --git a/MongoDB.Entities/DBContext/DBContext.Queryable.cs b/MongoDB.Entities/DBContext/DBContext.Queryable.cs index 608c0627a..cb79264ca 100644 --- a/MongoDB.Entities/DBContext/DBContext.Queryable.cs +++ b/MongoDB.Entities/DBContext/DBContext.Queryable.cs @@ -11,13 +11,15 @@ public partial class DBContext /// The aggregate options /// The type of entity /// Set to true if you'd like to ignore any global filters for this operation - public IMongoQueryable Queryable(AggregateOptions? options = null, bool ignoreGlobalFilters = false) where T : IEntity + /// + /// + public IMongoQueryable Queryable(AggregateOptions? options = null, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity { var globalFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, Builders.Filter.Empty); - + collection = Collection(collectionName, collection); var q = Session == null - ? CollectionFor().AsQueryable(options) - : CollectionFor().AsQueryable(Session, options); + ? collection.AsQueryable(options) + : collection.AsQueryable(Session, options); if (globalFilter != Builders.Filter.Empty) { diff --git a/MongoDB.Entities/DBContext/DBContext.Replace.cs b/MongoDB.Entities/DBContext/DBContext.Replace.cs index f8eb9e984..b831a2f90 100644 --- a/MongoDB.Entities/DBContext/DBContext.Replace.cs +++ b/MongoDB.Entities/DBContext/DBContext.Replace.cs @@ -1,4 +1,6 @@ -namespace MongoDB.Entities +using MongoDB.Driver; + +namespace MongoDB.Entities { public partial class DBContext { @@ -7,10 +9,10 @@ public partial class DBContext /// TIP: Only the first matched entity will be replaced /// /// The type of entity - public Replace Replace() where T : IEntity + public Replace Replace(string? collectionName = null, IMongoCollection? collection = null) where T : IEntity { ThrowIfModifiedByIsEmpty(); - return new Replace(Session, ModifiedBy, _globalFilters, OnBeforeSave(), tenantPrefix); + return new Replace(this, Collection(collectionName, collection), OnBeforeSave); } } } diff --git a/MongoDB.Entities/DBContext/DBContext.Save.cs b/MongoDB.Entities/DBContext/DBContext.Save.cs index c79ebd972..bde3f9848 100644 --- a/MongoDB.Entities/DBContext/DBContext.Save.cs +++ b/MongoDB.Entities/DBContext/DBContext.Save.cs @@ -3,6 +3,7 @@ using MongoDB.Driver; using System; using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; @@ -18,11 +19,23 @@ public partial class DBContext /// The type of entity /// The instance to persist /// And optional cancellation token - public Task SaveAsync(T entity, CancellationToken cancellation = default) where T : IEntity + /// + /// + public Task SaveAsync(T entity, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity { SetModifiedBySingle(entity); - OnBeforeSave()?.Invoke(entity); - return DB.SaveAsync(entity, Session, cancellation); + OnBeforeSave(entity); + collection = Collection(collectionName, collection); + if (PrepAndCheckIfInsert(entity)) + { + return Session is null + ? collection.InsertOneAsync(entity, null, cancellation) + : collection.InsertOneAsync(Session, entity, null, cancellation); + } + + return Session == null + ? collection.ReplaceOneAsync(x => x.ID == entity.ID, entity, new ReplaceOptions { IsUpsert = true }, cancellation) + : collection.ReplaceOneAsync(Session, x => x.ID == entity.ID, entity, new ReplaceOptions { IsUpsert = true }, cancellation); } /// @@ -32,11 +45,32 @@ public Task SaveAsync(T entity, CancellationToken cancellation = default) whe /// The type of entity /// The entities to persist /// And optional cancellation token - public Task> SaveAsync(IEnumerable entities, CancellationToken cancellation = default) where T : IEntity + /// + /// + public Task> SaveAsync(IEnumerable entities, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity { SetModifiedByMultiple(entities); - foreach (var ent in entities) OnBeforeSave()?.Invoke(ent); - return DB.SaveAsync(entities, Session, cancellation); + foreach (var ent in entities) OnBeforeSave(ent); + + + var models = entities.Select>(ent => + { + if (PrepAndCheckIfInsert(ent)) + { + return new InsertOneModel(ent); + } + else + { + return new ReplaceOneModel( + filter: Builders.Filter.Eq(e => e.ID, ent.ID), + replacement: ent) + { IsUpsert = true }; + } + }); + + return Session == null + ? Collection(collectionName, collection).BulkWriteAsync(models, _unOrdBlkOpts, cancellation) + : Collection(collectionName, collection).BulkWriteAsync(Session, models, _unOrdBlkOpts, cancellation); } /// @@ -49,11 +83,14 @@ public Task> SaveAsync(IEnumerable entities, Cancellati /// The entity to save /// x => new { x.PropOne, x.PropTwo } /// An optional cancellation token - public Task SaveOnlyAsync(T entity, Expression> members, CancellationToken cancellation = default) where T : IEntity + /// + /// + public Task SaveOnlyAsync(T entity, Expression> members, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity { SetModifiedBySingle(entity); - OnBeforeSave()?.Invoke(entity); - return DB.SaveOnlyAsync(entity, members, Session, cancellation); + OnBeforeSave(entity); + return SavePartial(entity, members, cancellation, collectionName: collectionName, collection: collection); + } /// @@ -66,11 +103,13 @@ public Task SaveOnlyAsync(T entity, Expression> /// The batch of entities to save /// x => new { x.PropOne, x.PropTwo } /// An optional cancellation token - public Task> SaveOnlyAsync(IEnumerable entities, Expression> members, CancellationToken cancellation = default) where T : IEntity + /// + /// + public Task> SaveOnlyAsync(IEnumerable entities, Expression> members, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity { SetModifiedByMultiple(entities); - foreach (var ent in entities) OnBeforeSave()?.Invoke(ent); - return DB.SaveOnlyAsync(entities, members, Session, cancellation); + foreach (var ent in entities) OnBeforeSave(ent); + return SavePartial(entities, members, cancellation, collectionName: collectionName, collection: collection); } /// @@ -83,11 +122,13 @@ public Task> SaveOnlyAsync(IEnumerable entities, Expres /// The entity to save /// x => new { x.PropOne, x.PropTwo } /// An optional cancellation token - public Task SaveExceptAsync(T entity, Expression> members, CancellationToken cancellation = default) where T : IEntity + /// + /// + public Task SaveExceptAsync(T entity, Expression> members, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity { SetModifiedBySingle(entity); - OnBeforeSave()?.Invoke(entity); - return DB.SaveExceptAsync(entity, members, Session, cancellation); + OnBeforeSave(entity); + return SavePartial(entity, members, cancellation, true, collectionName: collectionName, collection: collection); } /// @@ -100,11 +141,13 @@ public Task SaveExceptAsync(T entity, ExpressionThe batch of entities to save /// x => new { x.PropOne, x.PropTwo } /// An optional cancellation token - public Task> SaveExceptAsync(IEnumerable entities, Expression> members, CancellationToken cancellation = default) where T : IEntity + /// + /// + public Task> SaveExceptAsync(IEnumerable entities, Expression> members, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity { SetModifiedByMultiple(entities); - foreach (var ent in entities) OnBeforeSave()?.Invoke(ent); - return DB.SaveExceptAsync(entities, members, Session, cancellation); + foreach (var ent in entities) OnBeforeSave(ent); + return SavePartial(entities, members, cancellation, true, collectionName: collectionName, collection: collection); } /// @@ -114,18 +157,64 @@ public Task> SaveExceptAsync(IEnumerable entities, Expr /// The type of entity /// The entity to save /// An optional cancellation token - public Task SavePreservingAsync(T entity, CancellationToken cancellation = default) where T : IEntity + /// + /// + public Task SavePreservingAsync(T entity, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity { SetModifiedBySingle(entity); - OnBeforeSave()?.Invoke(entity); - return DB.SavePreservingAsync(entity, Session, cancellation); + OnBeforeSave(entity); + entity.ThrowIfUnsaved(); + var cache = Cache(); + var propsToUpdate = cache.UpdatableProps(entity); + + IEnumerable propsToPreserve = new string[0]; + + var dontProps = propsToUpdate.Where(p => p.IsDefined(typeof(DontPreserveAttribute), false)).Select(p => p.Name); + var presProps = propsToUpdate.Where(p => p.IsDefined(typeof(PreserveAttribute), false)).Select(p => p.Name); + + if (dontProps.Any() && presProps.Any()) + throw new NotSupportedException("[Preseve] and [DontPreserve] attributes cannot be used together on the same entity!"); + + if (dontProps.Any()) + propsToPreserve = propsToUpdate.Where(p => !dontProps.Contains(p.Name)).Select(p => p.Name); + + if (presProps.Any()) + propsToPreserve = propsToUpdate.Where(p => presProps.Contains(p.Name)).Select(p => p.Name); + + if (!propsToPreserve.Any()) + throw new ArgumentException("No properties are being preserved. Please use .SaveAsync() method instead!"); + + propsToUpdate = propsToUpdate.Where(p => !propsToPreserve.Contains(p.Name)); + + var propsToUpdateCount = propsToUpdate.Count(); + + if (propsToUpdateCount == 0) + throw new ArgumentException("At least one property must be not preserved!"); + + var defs = new List>(propsToUpdateCount); + + foreach (var p in propsToUpdate) + { + if (p.Name == cache.ModifiedOnPropName) + defs.Add(Builders.Update.CurrentDate(cache.ModifiedOnPropName)); + else + defs.Add(Builders.Update.Set(p.Name, p.GetValue(entity))); + } + + return + Session == null + ? Collection(collectionName, collection).UpdateOneAsync(e => e.ID == entity.ID, Builders.Update.Combine(defs), _updateOptions, cancellation) + : Collection(collectionName, collection).UpdateOneAsync(Session, e => e.ID == entity.ID, Builders.Update.Combine(defs), _updateOptions, cancellation); } private void SetModifiedBySingle(T entity) where T : IEntity { - ThrowIfModifiedByIsEmpty(); var cache = Cache(); - cache.ModifiedByProp?.SetValue( + if (cache.ModifiedByProp is null) + return; + ThrowIfModifiedByIsEmpty(); + + cache.ModifiedByProp.SetValue( entity, BsonSerializer.Deserialize(ModifiedBy.ToBson(), cache.ModifiedByProp.PropertyType)); //note: we can't use an IModifiedBy interface because the above line needs a concrete type @@ -135,7 +224,7 @@ private void SetModifiedBySingle(T entity) where T : IEntity private void SetModifiedByMultiple(IEnumerable entities) where T : IEntity { var cache = Cache(); - if (Cache().ModifiedByProp is null) + if (cache.ModifiedByProp is null) return; ThrowIfModifiedByIsEmpty(); diff --git a/MongoDB.Entities/DBContext/DBContext.Sequence.cs b/MongoDB.Entities/DBContext/DBContext.Sequence.cs index 1f3ac8307..3d4f8f454 100644 --- a/MongoDB.Entities/DBContext/DBContext.Sequence.cs +++ b/MongoDB.Entities/DBContext/DBContext.Sequence.cs @@ -5,16 +5,15 @@ namespace MongoDB.Entities { public partial class DBContext { - //NOTE: transaction support will not be added due to unpredictability with concurrency. - /// /// Returns an atomically generated sequential number for the given Entity type everytime the method is called /// /// The type of entity to get the next sequential number for /// An optional cancellation token + /// transaction support will not be added due to unpredictability with concurrency. public Task NextSequentialNumberAsync(CancellationToken cancellation = default) where T : IEntity { - return DB.NextSequentialNumberAsync(DB.CollectionName(), cancellation, tenantPrefix); + return NextSequentialNumberAsync(CollectionName(), cancellation); } /// @@ -22,9 +21,15 @@ public Task NextSequentialNumberAsync(CancellationToken cancellation = /// /// The name of the sequence to get the next number for /// An optional cancellation token + /// transaction support will not be added due to unpredictability with concurrency. public Task NextSequentialNumberAsync(string sequenceName, CancellationToken cancellation = default) { - return DB.NextSequentialNumberAsync(sequenceName, cancellation, tenantPrefix); + return new UpdateAndGet(this, Collection(), onUpdateAction: null, defs: null) + .Match(s => s.ID == sequenceName) + .Modify(b => b.Inc(s => s.Count, 1ul)) + .Option(o => o.IsUpsert = true) + .Project(s => s.Count) + .ExecuteAsync(cancellation); } } } diff --git a/MongoDB.Entities/DBContext/DBContext.Transaction.cs b/MongoDB.Entities/DBContext/DBContext.Transaction.cs index 3f03ebb57..9cfda4bcc 100644 --- a/MongoDB.Entities/DBContext/DBContext.Transaction.cs +++ b/MongoDB.Entities/DBContext/DBContext.Transaction.cs @@ -7,20 +7,18 @@ namespace MongoDB.Entities { public partial class DBContext { - /// /// Starts a transaction and returns a session object. /// WARNING: Only one transaction is allowed per DBContext instance. /// Call Session.Dispose() and assign a null to it before calling this method a second time. /// Trying to start a second transaction for this DBContext instance will throw an exception. /// - /// The name of the database to use for this transaction. default db is used if not specified /// Client session options for this transaction - public IClientSessionHandle Transaction(string database = default, ClientSessionOptions options = null) + public IClientSessionHandle Transaction(ClientSessionOptions? options = null) { if (Session is null) { - Session = DB.Database(database).Client.StartSession(options); + Session = Client.StartSession(options); Session.StartTransaction(); return Session; } @@ -29,29 +27,33 @@ public IClientSessionHandle Transaction(string database = default, ClientSession "Only one transaction is allowed per DBContext instance. Dispose and nullify the Session before calling this method again!"); } - /// - /// Starts a transaction and returns a session object for a given entity type. - /// WARNING: Only one transaction is allowed per DBContext instance. - /// Call Session.Dispose() and assign a null to it before calling this method a second time. - /// Trying to start a second transaction for this DBContext instance will throw an exception. - /// - /// The entity type to determine the database from for the transaction - /// Client session options (not required) - public IClientSessionHandle Transaction(ClientSessionOptions options = null) where T : IEntity - { - return Transaction(DB.DatabaseName(tenantPrefix), options); - } /// /// Commits a transaction to MongoDB /// /// An optional cancellation token - public Task CommitAsync(CancellationToken cancellation = default) => Session.CommitTransactionAsync(cancellation); + public Task CommitAsync(CancellationToken cancellation = default) + { + if (Session is null) + { + throw new ArgumentNullException(nameof(Session), "Please call Transaction() first before committing"); + } + + return Session.CommitTransactionAsync(cancellation); + } /// /// Aborts and rolls back a transaction /// /// An optional cancellation token - public Task AbortAsync(CancellationToken cancellation = default) => Session.AbortTransactionAsync(cancellation); + public Task AbortAsync(CancellationToken cancellation = default) + { + if (Session is null) + { + throw new ArgumentNullException(nameof(Session), "Please call Transaction() first before aborting"); + } + + return Session.AbortTransactionAsync(cancellation); + } } } diff --git a/MongoDB.Entities/DBContext/DBContext.Update.cs b/MongoDB.Entities/DBContext/DBContext.Update.cs index 928b0fe87..f38e78087 100644 --- a/MongoDB.Entities/DBContext/DBContext.Update.cs +++ b/MongoDB.Entities/DBContext/DBContext.Update.cs @@ -1,4 +1,7 @@ -namespace MongoDB.Entities +using MongoDB.Driver; +using System.Reflection; + +namespace MongoDB.Entities { public partial class DBContext { @@ -6,13 +9,13 @@ public partial class DBContext /// Starts an update command for the given entity type /// /// The type of entity - public Update Update() where T : IEntity + public Update Update(string? collectionName = null, IMongoCollection? collection = null) where T : IEntity { - var cmd = new Update(this, CollectionFor(), _globalFilters, OnBeforeUpdate>()); - if (Cache().ModifiedByProp != null) + var cmd = new Update(this, Collection(collectionName, collection), OnBeforeUpdate>); + if (Cache().ModifiedByProp is PropertyInfo ModifiedByProp) { ThrowIfModifiedByIsEmpty(); - cmd.Modify(b => b.Set(Cache().ModifiedByProp.Name, ModifiedBy)); + cmd.Modify(b => b.Set(ModifiedByProp.Name, ModifiedBy)); } return cmd; } @@ -21,9 +24,9 @@ public Update Update() where T : IEntity /// Starts an update-and-get command for the given entity type /// /// The type of entity - public UpdateAndGet UpdateAndGet() where T : IEntity + public UpdateAndGet UpdateAndGet(string? collectionName = null, IMongoCollection? collection = null) where T : IEntity { - return UpdateAndGet(); + return UpdateAndGet(collectionName, collection); } /// @@ -31,13 +34,13 @@ public UpdateAndGet UpdateAndGet() where T : IEntity /// /// The type of entity /// The type of the end result - public UpdateAndGet UpdateAndGet() where T : IEntity + public UpdateAndGet UpdateAndGet(string? collectionName = null, IMongoCollection? collection = null) where T : IEntity { - var cmd = new UpdateAndGet(this, CollectionFor(), _globalFilters, OnBeforeUpdate>()); - if (Cache().ModifiedByProp != null) + var cmd = new UpdateAndGet(this, Collection(collectionName, collection), OnBeforeUpdate>); + if (Cache().ModifiedByProp is PropertyInfo ModifiedByProp) { ThrowIfModifiedByIsEmpty(); - cmd.Modify(b => b.Set(Cache().ModifiedByProp.Name, ModifiedBy)); + cmd.Modify(b => b.Set(ModifiedByProp.Name, ModifiedBy)); } return cmd; } diff --git a/MongoDB.Entities/DBContext/DBContext.Watcher.cs b/MongoDB.Entities/DBContext/DBContext.Watcher.cs index b1801837b..451c0821f 100644 --- a/MongoDB.Entities/DBContext/DBContext.Watcher.cs +++ b/MongoDB.Entities/DBContext/DBContext.Watcher.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using MongoDB.Driver; +using System.Collections.Generic; namespace MongoDB.Entities { @@ -11,15 +12,23 @@ public partial class DBContext /// /// The entity type to get a watcher for /// A unique name for the watcher of this entity type. Names can be duplicate among different entity types. - public Watcher Watcher(string name) where T : IEntity + /// + /// + public Watcher Watcher(string name, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity { - return DB.Watcher(name, tenantPrefix); + var cache = Cache(); + if (cache.Watchers.TryGetValue(name.ToLowerInvariant().Trim(), out Watcher watcher)) + return watcher; + + watcher = new Watcher(name.ToLowerInvariant().Trim(), this, Collection(collectionName, collection)); + cache.Watchers.TryAdd(name, watcher); + return watcher; } /// /// Returns all the watchers for a given entity type /// /// The entity type to get the watcher of - public IEnumerable> Watchers() where T : IEntity => DB.Watchers(); + public IEnumerable> Watchers() where T : IEntity => Cache().Watchers.Values; } } diff --git a/MongoDB.Entities/DBContext/DBContext.cs b/MongoDB.Entities/DBContext/DBContext.cs index 4f06b90d9..28f618cf8 100644 --- a/MongoDB.Entities/DBContext/DBContext.cs +++ b/MongoDB.Entities/DBContext/DBContext.cs @@ -1,4 +1,7 @@ -using MongoDB.Bson.Serialization; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Conventions; +using MongoDB.Bson.Serialization.Serializers; using MongoDB.Driver; using System; using System.Collections.Concurrent; @@ -8,295 +11,334 @@ using System.Threading; using System.Threading.Tasks; #nullable enable -namespace MongoDB.Entities +namespace MongoDB.Entities; + +/// +/// Wraps an IMongoDatabase to provide extra functionality +/// +public partial class DBContext : IMongoDatabase { /// - /// This db context class can be used as an alternative entry point instead of the DB static class. + /// Returns the session object used for transactions /// - public partial class DBContext : IMongoDatabase - { - /// - /// Returns the session object used for transactions - /// - public IClientSessionHandle? Session { get; protected set; } + public IClientSessionHandle? Session { get; protected set; } - public MongoServerContext MongoServerContext { get; set; } - public IMongoDatabase Database { get; set; } - public DBContextOptions Options { get; set; } + public MongoServerContext MongoServerContext { get; } + public IMongoDatabase Database { get; } + public DBContextOptions Options { get; } - /// - /// wrapper around so that we don't break the public api - /// - public ModifiedBy? ModifiedBy + /// + /// wrapper around so that we don't break the public api + /// + public ModifiedBy? ModifiedBy + { + get { - get - { - return MongoServerContext.ModifiedBy; - } - [Obsolete("Use MongoContext.Options.ModifiedBy = value instead")] - set - { - MongoServerContext.Options.ModifiedBy = value; - } + return MongoServerContext.ModifiedBy; + } + [Obsolete("Use MongoContext.Options.ModifiedBy = value instead")] + set + { + MongoServerContext.Options.ModifiedBy = value; } + } - public IMongoClient Client => MongoServerContext; + public IMongoClient Client => MongoServerContext; - public DatabaseNamespace DatabaseNamespace => Database.DatabaseNamespace; + public DatabaseNamespace DatabaseNamespace => Database.DatabaseNamespace; - public MongoDatabaseSettings Settings => Database.Settings; + public MongoDatabaseSettings Settings => Database.Settings; - private Dictionary? _globalFilters; - internal Dictionary GlobalFilters => _globalFilters ??= new(); + private Dictionary? _globalFilters; + internal Dictionary GlobalFilters => _globalFilters ??= new(); - /// - /// Copy constructor - /// - /// - public DBContext(DBContext other) - { - MongoServerContext = other.MongoServerContext; - Database = other.Database; - Options = other.Options; - } - public DBContext(MongoServerContext mongoContext, IMongoDatabase database, DBContextOptions? options = null) - { - MongoServerContext = mongoContext; - Database = database; - Options = options ?? new(); - } - public DBContext(MongoServerContext mongoContext, string database, MongoDatabaseSettings? settings = null, DBContextOptions? options = null) - { - MongoServerContext = mongoContext; - Options = options ?? new(); - Database = mongoContext.GetDatabase(database, settings); - } + /// + /// Copy constructor + /// + /// + public DBContext(DBContext other) + { + MongoServerContext = other.MongoServerContext; + Database = other.Database; + Options = other.Options; + } + public DBContext(MongoServerContext mongoContext, IMongoDatabase database, DBContextOptions? options = null) + { + MongoServerContext = mongoContext; + Database = database; + Options = options ?? new(); + } + public DBContext(MongoServerContext mongoContext, string database, MongoDatabaseSettings? settings = null, DBContextOptions? options = null) + { + MongoServerContext = mongoContext; + Options = options ?? new(); + Database = mongoContext.Client.GetDatabase(database, settings); + } - /// - /// Initializes a DBContext instance with the given connection parameters. - /// TIP: network connection is deferred until the first actual operation. - /// - /// Name of the database - /// Address of the MongoDB server - /// Port number of the server - /// An optional ModifiedBy instance. - /// When supplied, all save/update operations performed via this DBContext instance will set the value on entities that has a property of type ModifiedBy. - /// You can even inherit from the ModifiedBy class and add your own properties to it. - /// Only one ModifiedBy property is allowed on a single entity type. - public DBContext(string database, string host = "127.0.0.1", int port = 27017, ModifiedBy? modifiedBy = null) - { - MongoServerContext = new MongoServerContext( - client: new MongoClient( - new MongoClientSettings - { - Server = new MongoServerAddress(host, port) - }), - options: new() + /// + /// Initializes a DBContext instance with the given connection parameters. + /// TIP: network connection is deferred until the first actual operation. + /// + /// Name of the database + /// Address of the MongoDB server + /// Port number of the server + /// An optional ModifiedBy instance. + /// When supplied, all save/update operations performed via this DBContext instance will set the value on entities that has a property of type ModifiedBy. + /// You can even inherit from the ModifiedBy class and add your own properties to it. + /// Only one ModifiedBy property is allowed on a single entity type. + public DBContext(string database, string host = "127.0.0.1", int port = 27017, ModifiedBy? modifiedBy = null) + { + MongoServerContext = new MongoServerContext( + client: new MongoClient( + new MongoClientSettings { - ModifiedBy = modifiedBy - }); - Options = new(); - Database = MongoServerContext.GetDatabase(database); - } + Server = new MongoServerAddress(host, port) + }), + options: new() + { + ModifiedBy = modifiedBy + }); + Options = new(); + Database = MongoServerContext.Client.GetDatabase(database); + } - /// - /// Initializes a DBContext instance with the given connection parameters. - /// TIP: network connection is deferred until the first actual operation. - /// - /// Name of the database - /// A MongoClientSettings object - /// An optional ModifiedBy instance. - /// When supplied, all save/update operations performed via this DBContext instance will set the value on entities that has a property of type ModifiedBy. - /// You can even inherit from the ModifiedBy class and add your own properties to it. - /// Only one ModifiedBy property is allowed on a single entity type. - public DBContext(string database, MongoClientSettings settings, ModifiedBy? modifiedBy = null) - { - MongoServerContext = new MongoServerContext( - client: new MongoClient(settings), - options: new() - { - ModifiedBy = modifiedBy - }); - Options = new(); - Database = MongoServerContext.GetDatabase(database); - } + /// + /// Initializes a DBContext instance with the given connection parameters. + /// TIP: network connection is deferred until the first actual operation. + /// + /// Name of the database + /// A MongoClientSettings object + /// An optional ModifiedBy instance. + /// When supplied, all save/update operations performed via this DBContext instance will set the value on entities that has a property of type ModifiedBy. + /// You can even inherit from the ModifiedBy class and add your own properties to it. + /// Only one ModifiedBy property is allowed on a single entity type. + public DBContext(string database, MongoClientSettings settings, ModifiedBy? modifiedBy = null) + { + MongoServerContext = new MongoServerContext( + client: new MongoClient(settings), + options: new() + { + ModifiedBy = modifiedBy + }); + Options = new(); + Database = MongoServerContext.Client.GetDatabase(database); + } + static DBContext() + { + InitStatic(); + } + private static bool _isInit; + public static void InitStatic() + { + if (_isInit) + return; + _isInit = true; + BsonSerializer.RegisterSerializer(new DateSerializer()); + BsonSerializer.RegisterSerializer(new FuzzyStringSerializer()); + BsonSerializer.RegisterSerializer(typeof(decimal), new DecimalSerializer(BsonType.Decimal128)); + BsonSerializer.RegisterSerializer(typeof(decimal?), new NullableSerializer(new DecimalSerializer(BsonType.Decimal128))); + + ConventionRegistry.Register( + "DefaultConventions", + new ConventionPack + { + new IgnoreExtraElementsConvention(true), + new IgnoreManyPropsConvention() + }, + _ => true); + } - /// - /// Instantiates a DBContext instance - /// TIP: will throw an error if no connections have been initialized - /// - /// An optional ModifiedBy instance. - /// When supplied, all save/update operations performed via this DBContext instance will set the value on entities that has a property of type ModifiedBy. - /// You can even inherit from the ModifiedBy class and add your own properties to it. - /// Only one ModifiedBy property is allowed on a single entity type. - [Obsolete("This constructor is obsolete, you can only create a DBContext after knowing the database name")] - public DBContext(ModifiedBy? modifiedBy = null) : this("default", modifiedBy: modifiedBy) - { - } - /// - /// This event hook will be trigged right before an entity is persisted - /// - /// Any entity that implements IEntity - protected virtual Action? OnBeforeSave() where T : IEntity - { - return null; - } + /// + /// Instantiates a DBContext instance + /// TIP: will throw an error if no connections have been initialized + /// + /// An optional ModifiedBy instance. + /// When supplied, all save/update operations performed via this DBContext instance will set the value on entities that has a property of type ModifiedBy. + /// You can even inherit from the ModifiedBy class and add your own properties to it. + /// Only one ModifiedBy property is allowed on a single entity type. + [Obsolete("This constructor is obsolete, you can only create a DBContext after knowing the database name")] + public DBContext(ModifiedBy? modifiedBy = null) : this("default", modifiedBy: modifiedBy) + { + } - /// - /// This event hook will be triggered right before an update/replace command is executed - /// - /// Any entity that implements IEntity - /// Any entity that implements IEntity - protected virtual Action>? OnBeforeUpdate() where T : IEntity where TSelf : UpdateBase - { - return null; - } + /// + /// This event hook will be trigged right before an entity is persisted + /// + /// Any entity that implements IEntity + protected virtual void OnBeforeSave(T entity) where T : IEntity + { + } + /// + /// This event hook will be triggered right before an update/replace command is executed + /// + /// Any entity that implements IEntity + /// Any entity that implements IEntity + protected virtual void OnBeforeUpdate(UpdateBase updateBase) where T : IEntity where TSelf : UpdateBase + { + } - /// - /// Specify a global filter to be applied to all operations performed with this DBContext - /// - /// The type of Entity this global filter should be applied to - /// x => x.Prop1 == "some value" - /// Set to true if you want to prepend this global filter to your operation filters instead of being appended - protected void SetGlobalFilter(Expression> filter, bool prepend = false) where T : IEntity - { - SetGlobalFilter(Builders.Filter.Where(filter), prepend); - } - /// - /// Specify a global filter to be applied to all operations performed with this DBContext - /// - /// The type of Entity this global filter should be applied to - /// b => b.Eq(x => x.Prop1, "some value") - /// Set to true if you want to prepend this global filter to your operation filters instead of being appended - protected void SetGlobalFilter(Func, FilterDefinition> filter, bool prepend = false) where T : IEntity - { - SetGlobalFilter(filter(Builders.Filter), prepend); - } - /// - /// Specify a global filter to be applied to all operations performed with this DBContext - /// - /// The type of Entity this global filter should be applied to - /// A filter definition to be applied - /// Set to true if you want to prepend this global filter to your operation filters instead of being appended - protected void SetGlobalFilter(FilterDefinition filter, bool prepend = false) where T : IEntity - { - AddFilter(typeof(T), (filter, prepend)); - } + /// + /// Specify a global filter to be applied to all operations performed with this DBContext + /// + /// The type of Entity this global filter should be applied to + /// x => x.Prop1 == "some value" + /// Set to true if you want to prepend this global filter to your operation filters instead of being appended + protected void SetGlobalFilter(Expression> filter, bool prepend = false) where T : IEntity + { + SetGlobalFilter(Builders.Filter.Where(filter), prepend); + } - /// - /// Specify a global filter to be applied to all operations performed with this DBContext - /// - /// The type of Entity this global filter should be applied to - /// A JSON string filter definition to be applied - /// Set to true if you want to prepend this global filter to your operation filters instead of being appended - protected void SetGlobalFilter(Type type, string jsonString, bool prepend = false) - { - AddFilter(type, (jsonString, prepend)); - } + /// + /// Specify a global filter to be applied to all operations performed with this DBContext + /// + /// The type of Entity this global filter should be applied to + /// b => b.Eq(x => x.Prop1, "some value") + /// Set to true if you want to prepend this global filter to your operation filters instead of being appended + protected void SetGlobalFilter(Func, FilterDefinition> filter, bool prepend = false) where T : IEntity + { + SetGlobalFilter(filter(Builders.Filter), prepend); + } + + /// + /// Specify a global filter to be applied to all operations performed with this DBContext + /// + /// The type of Entity this global filter should be applied to + /// A filter definition to be applied + /// Set to true if you want to prepend this global filter to your operation filters instead of being appended + protected void SetGlobalFilter(FilterDefinition filter, bool prepend = false) where T : IEntity + { + AddFilter(typeof(T), (filter, prepend)); + } + /// + /// Specify a global filter to be applied to all operations performed with this DBContext + /// + /// The type of Entity this global filter should be applied to + /// A JSON string filter definition to be applied + /// Set to true if you want to prepend this global filter to your operation filters instead of being appended + protected void SetGlobalFilter(Type type, string jsonString, bool prepend = false) + { + AddFilter(type, (jsonString, prepend)); + } - /// - /// Specify a global filter to be applied to all operations performed with this DBContext - /// - /// The type of the base class - /// b => b.Eq(x => x.Prop1, "some value") - /// Set to true if you want to prepend this global filter to your operation filters instead of being appended - protected void SetGlobalFilterForBaseClass(Expression> filter, bool prepend = false) where TBase : IEntity - { - SetGlobalFilterForBaseClass(Builders.Filter.Where(filter), prepend); - } - /// - /// Specify a global filter to be applied to all operations performed with this DBContext - /// - /// The type of the base class - /// b => b.Eq(x => x.Prop1, "some value") - /// Set to true if you want to prepend this global filter to your operation filters instead of being appended - protected void SetGlobalFilterForBaseClass(Func, FilterDefinition> filter, bool prepend = false) where TBase : IEntity - { - SetGlobalFilterForBaseClass(filter(Builders.Filter), prepend); - } + /// + /// Specify a global filter to be applied to all operations performed with this DBContext + /// + /// The type of the base class + /// b => b.Eq(x => x.Prop1, "some value") + /// Set to true if you want to prepend this global filter to your operation filters instead of being appended + protected void SetGlobalFilterForBaseClass(Expression> filter, bool prepend = false) where TBase : IEntity + { + SetGlobalFilterForBaseClass(Builders.Filter.Where(filter), prepend); + } + + /// + /// Specify a global filter to be applied to all operations performed with this DBContext + /// + /// The type of the base class + /// b => b.Eq(x => x.Prop1, "some value") + /// Set to true if you want to prepend this global filter to your operation filters instead of being appended + protected void SetGlobalFilterForBaseClass(Func, FilterDefinition> filter, bool prepend = false) where TBase : IEntity + { + SetGlobalFilterForBaseClass(filter(Builders.Filter), prepend); + } - /// - /// Specify a global filter to be applied to all operations performed with this DBContext - /// - /// The type of the base class - /// A filter definition to be applied - /// Set to true if you want to prepend this global filter to your operation filters instead of being appended - protected void SetGlobalFilterForBaseClass(FilterDefinition filter, bool prepend = false) where TBase : IEntity + /// + /// Specify a global filter to be applied to all operations performed with this DBContext + /// + /// The type of the base class + /// A filter definition to be applied + /// Set to true if you want to prepend this global filter to your operation filters instead of being appended + protected void SetGlobalFilterForBaseClass(FilterDefinition filter, bool prepend = false) where TBase : IEntity + { + foreach (var entType in MongoServerContext.AllEntitiyTypes.Where(t => t.IsSubclassOf(typeof(TBase)))) { - foreach (var entType in MongoServerContext.AllEntitiyTypes.Where(t => t.IsSubclassOf(typeof(TBase)))) - { - var bsonDoc = filter.Render( - BsonSerializer.SerializerRegistry.GetSerializer(), - BsonSerializer.SerializerRegistry); + var bsonDoc = filter.Render( + BsonSerializer.SerializerRegistry.GetSerializer(), + BsonSerializer.SerializerRegistry); - AddFilter(entType, (bsonDoc, prepend)); - } + AddFilter(entType, (bsonDoc, prepend)); } + } - /// - /// Specify a global filter for all entity types that implements a given interface - /// - /// The interface type to target. Will throw if supplied argument is not an interface type - /// A JSON string filter definition to be applied - /// Set to true if you want to prepend this global filter to your operation filters instead of being appended - protected void SetGlobalFilterForInterface(string jsonString, bool prepend = false) - { - var targetType = typeof(TInterface); + /// + /// Specify a global filter for all entity types that implements a given interface + /// + /// The interface type to target. Will throw if supplied argument is not an interface type + /// A JSON string filter definition to be applied + /// Set to true if you want to prepend this global filter to your operation filters instead of being appended + protected void SetGlobalFilterForInterface(string jsonString, bool prepend = false) + { + var targetType = typeof(TInterface); - if (!targetType.IsInterface) throw new ArgumentException("Only interfaces are allowed!", "TInterface"); + if (!targetType.IsInterface) throw new ArgumentException("Only interfaces are allowed!", "TInterface"); - foreach (var entType in MongoServerContext.AllEntitiyTypes.Where(t => targetType.IsAssignableFrom(t))) - { - AddFilter(entType, (jsonString, prepend)); - } + foreach (var entType in MongoServerContext.AllEntitiyTypes.Where(t => targetType.IsAssignableFrom(t))) + { + AddFilter(entType, (jsonString, prepend)); } + } - private void ThrowIfModifiedByIsEmpty() where T : IEntity + private void ThrowIfModifiedByIsEmpty() where T : IEntity + { + var cache = Cache(); + if (cache.ModifiedByProp is not null && ModifiedBy is null) { - if (Cache().ModifiedByProp != null && ModifiedBy is null) - { - throw new InvalidOperationException( - $"A value for [{Cache().ModifiedByProp.Name}] must be specified when saving/updating entities of type [{Cache().CollectionName}]"); - } + throw new InvalidOperationException( + $"A value for [{cache.ModifiedByProp.Name}] must be specified when saving/updating entities of type [{cache.CollectionName}]"); } + } - private void AddFilter(Type type, (object filterDef, bool prepend) filter) - { - GlobalFilters[type] = filter; - } + private void AddFilter(Type type, (object filterDef, bool prepend) filter) + { + GlobalFilters[type] = filter; + } - private readonly ConcurrentDictionary _cache = new(); - internal Cache Cache() where T : IEntity + private readonly ConcurrentDictionary _cache = new(); + internal EntityCache Cache() where T : IEntity + { + if (!_cache.TryGetValue(typeof(T), out var c)) { - if (!_cache.TryGetValue(typeof(T), out var c)) - { - c = new Cache(); - } - return (Cache)c; + c = new EntityCache(); } - public virtual string CollectionNameFor() where T : IEntity + return (EntityCache)c; + } + public virtual string CollectionName() where T : IEntity + { + return Cache().CollectionName; + } + + public virtual IMongoCollection Collection(string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + { + return collection ?? GetCollection(collectionName ?? CollectionName()); + } + + public async Task PingNetwork() + { + try { - return Cache().CollectionName; + await Database.RunCommandAsync((Command)"{ping:1}").ConfigureAwait(false); + return true; } - public virtual IMongoCollection CollectionFor() where T : IEntity + catch (Exception) { - return Database.GetCollection(CollectionNameFor()); + return false; } + } } diff --git a/MongoDB.Entities/DBContext/TenantContext.cs b/MongoDB.Entities/DBContext/TenantContext.cs deleted file mode 100644 index 82df7a106..000000000 --- a/MongoDB.Entities/DBContext/TenantContext.cs +++ /dev/null @@ -1,69 +0,0 @@ -using MongoDB.Driver; -using System.Threading.Tasks; - -namespace MongoDB.Entities -{ - /// - /// Use this class as the main entrypoint when using multi-tenancy - /// - public class TenantContext : DBContext - { - /// - /// If you use this ctor, make sure to call SetTenantPrefix() method and specify the tenant prefix - /// - public TenantContext() { } - - /// - /// Instantiate a tenant context with the given tenant prefix value. - /// - /// The tenant prefix to be prepended to database names - public TenantContext(string tenantPrefix) - { - this.tenantPrefix = tenantPrefix; - } - - /// - /// Set the tenant prefix - /// - /// The tenant prefix to be prepended to database names - public void SetTenantPrefix(string tenantPrefix) - => this.tenantPrefix = tenantPrefix; - - /// - /// Configure this tenant context to be able to connect to a particular database/server. - /// TIP: network connection is deferred until the first actual operation. - /// - /// Name of the database (without tenant prefix) - /// Address of the MongoDB server - /// Port number of the server - /// An optional ModifiedBy instance. - /// When supplied, all save/update operations performed via this DBContext instance will set the value on entities that has a property of type ModifiedBy. - /// You can even inherit from the ModifiedBy class and add your own properties to it. - /// Only one ModifiedBy property is allowed on a single entity type. - public void Init(string dbName, string host = "127.0.0.1", int port = 27017, ModifiedBy modifiedBy = null) - { - ModifiedBy = modifiedBy; - DB.Initialize( - new MongoClientSettings { Server = new MongoServerAddress(host, port) }, - $"{tenantPrefix}~{dbName}", - true - ).GetAwaiter().GetResult(); - } - - /// - /// Configure this tenant context to be able to connect to a particular database/server. - /// TIP: network connection is deferred until the first actual operation. - /// - /// Name of the database (without tenant prefix) - /// A MongoClientSettings object - /// An optional ModifiedBy instance. - /// When supplied, all save/update operations performed via this DBContext instance will set the value on entities that has a property of type ModifiedBy. - /// You can even inherit from the ModifiedBy class and add your own properties to it. - /// Only one ModifiedBy property is allowed on a single entity type. - public void Init(string dbName, MongoClientSettings settings, ModifiedBy modifiedBy = null) - { - ModifiedBy = modifiedBy; - DB.Initialize(settings, $"{tenantPrefix}~{dbName}", true).GetAwaiter().GetResult(); - } - } -} diff --git a/MongoDB.Entities/MongoContext/MongoContext.IMongoClient.cs b/MongoDB.Entities/MongoContext/MongoContext.IMongoClient.cs index a3cbe7bfd..bcc830df9 100644 --- a/MongoDB.Entities/MongoContext/MongoContext.IMongoClient.cs +++ b/MongoDB.Entities/MongoContext/MongoContext.IMongoClient.cs @@ -36,7 +36,8 @@ Task IMongoClient.DropDatabaseAsync(IClientSessionHandle session, string name, C return Client.DropDatabaseAsync(session, name, cancellationToken); } - public IMongoDatabase GetDatabase(string name, MongoDatabaseSettings? settings = null) + + IMongoDatabase IMongoClient.GetDatabase(string name, MongoDatabaseSettings settings) { return Client.GetDatabase(name, settings); } @@ -146,7 +147,7 @@ Task> IMongoClient.WatchAsync(PipelineDefi return Client.WatchAsync(pipeline, options, cancellationToken); } - Task> IMongoClient.WatchAsync(IClientSessionHandle session, PipelineDefinition, TResult> pipeline, ChangeStreamOptions options, CancellationToken cancellationToken = default) + Task> IMongoClient.WatchAsync(IClientSessionHandle session, PipelineDefinition, TResult> pipeline, ChangeStreamOptions options, CancellationToken cancellationToken) { return Client.WatchAsync(session, pipeline, options, cancellationToken); } diff --git a/MongoDB.Entities/MongoContext/MongoContext.cs b/MongoDB.Entities/MongoContext/MongoContext.cs index 6833ff3e6..957afc8a5 100644 --- a/MongoDB.Entities/MongoContext/MongoContext.cs +++ b/MongoDB.Entities/MongoContext/MongoContext.cs @@ -23,6 +23,7 @@ public MongoServerContext(IMongoClient client, MongoContextOptions? options = nu Options = options ?? new(); } + /// /// The backing client /// @@ -32,7 +33,10 @@ public MongoServerContext(IMongoClient client, MongoContextOptions? options = nu /// public ModifiedBy? ModifiedBy => Options.ModifiedBy; - + public DBContext GetDatabase(string name, MongoDatabaseSettings? settings = null, DBContextOptions? options = null) + { + return new(this, Client.GetDatabase(name, settings), options); + } public async Task> AllDatabaseNamesAsync() { return await (await @@ -43,8 +47,6 @@ public async Task> AllDatabaseNamesAsync() private Type[]? _allEntitiyTypes; public Type[] AllEntitiyTypes => _allEntitiyTypes ??= GetAllEntityTypes(); - - private static Type[] GetAllEntityTypes() { var excludes = new[] @@ -69,6 +71,11 @@ private static Type[] GetAllEntityTypes() .ToArray(); } + //key: entity type + //val: database name without tenant prefix (will be null if not specifically set using DB.DatabaseFor() method) + internal readonly ConcurrentDictionary _typeToDbName = new(); + internal void MapTypeToDb(string dbNameWithoutTenantPrefix) where T : IEntity + => _typeToDbName[typeof(T)] = dbNameWithoutTenantPrefix; } } diff --git a/MongoDB.Entities/MongoDB.Entities.csproj b/MongoDB.Entities/MongoDB.Entities.csproj index a6002511a..fe0c9639b 100644 --- a/MongoDB.Entities/MongoDB.Entities.csproj +++ b/MongoDB.Entities/MongoDB.Entities.csproj @@ -27,7 +27,7 @@ README.md mongodb mongodb-orm mongodb-repo mongodb-repository entities nosql orm linq netcore repository aspnetcore netcore2 netcore3 dotnetstandard database persistance dal repo true - 9.0 + latest diff --git a/MongoDB.Entities/Relationships/JoinRecord.cs b/MongoDB.Entities/Relationships/JoinRecord.cs index 6e92f4421..2611c256b 100644 --- a/MongoDB.Entities/Relationships/JoinRecord.cs +++ b/MongoDB.Entities/Relationships/JoinRecord.cs @@ -10,12 +10,12 @@ public class JoinRecord : Entity /// The ID of the parent IEntity for both one-to-many and the owner side of many-to-many relationships. /// [AsObjectId] - public string ParentID { get; set; } + public string ParentID { get; set; } = null!; /// /// The ID of the child IEntity in one-to-many relationships and the ID of the inverse side IEntity in many-to-many relationships. /// [AsObjectId] - public string ChildID { get; set; } + public string ChildID { get; set; } = null!; } } diff --git a/MongoDB.Entities/Relationships/Many.Remove.cs b/MongoDB.Entities/Relationships/Many.Remove.cs index 6272727b8..edce4b7de 100644 --- a/MongoDB.Entities/Relationships/Many.Remove.cs +++ b/MongoDB.Entities/Relationships/Many.Remove.cs @@ -47,7 +47,7 @@ public Task RemoveAsync(IEnumerable children, IClientSessionHandle sessi /// The IDs of the child Entities to remove the references of /// An optional session if using within a transaction /// An optional cancellation token - public Task RemoveAsync(IEnumerable childIDs, IClientSessionHandle session = null, CancellationToken cancellation = default) + public Task RemoveAsync(IEnumerable childIDs, IClientSessionHandle? session = null, CancellationToken cancellation = default) { var filter = isInverse From 2558d59f7883476c722eb21cc4accda003b9130e Mon Sep 17 00:00:00 2001 From: Ahmed Fwela Date: Wed, 10 Nov 2021 13:34:02 +0200 Subject: [PATCH 16/26] Moved transaction logic to MongoServerContext --- .../DBContext/DBContext.Transaction.cs | 24 +------- MongoDB.Entities/DBContext/DBContext.cs | 4 +- MongoDB.Entities/MongoContext/MongoContext.cs | 56 +++++++++++++++++++ 3 files changed, 61 insertions(+), 23 deletions(-) diff --git a/MongoDB.Entities/DBContext/DBContext.Transaction.cs b/MongoDB.Entities/DBContext/DBContext.Transaction.cs index 9cfda4bcc..daca1bd55 100644 --- a/MongoDB.Entities/DBContext/DBContext.Transaction.cs +++ b/MongoDB.Entities/DBContext/DBContext.Transaction.cs @@ -16,15 +16,7 @@ public partial class DBContext /// Client session options for this transaction public IClientSessionHandle Transaction(ClientSessionOptions? options = null) { - if (Session is null) - { - Session = Client.StartSession(options); - Session.StartTransaction(); - return Session; - } - - throw new NotSupportedException( - "Only one transaction is allowed per DBContext instance. Dispose and nullify the Session before calling this method again!"); + return MongoServerContext.Transaction(options); } @@ -34,12 +26,7 @@ public IClientSessionHandle Transaction(ClientSessionOptions? options = null) /// An optional cancellation token public Task CommitAsync(CancellationToken cancellation = default) { - if (Session is null) - { - throw new ArgumentNullException(nameof(Session), "Please call Transaction() first before committing"); - } - - return Session.CommitTransactionAsync(cancellation); + return MongoServerContext.CommitAsync(cancellation); } /// @@ -48,12 +35,7 @@ public Task CommitAsync(CancellationToken cancellation = default) /// An optional cancellation token public Task AbortAsync(CancellationToken cancellation = default) { - if (Session is null) - { - throw new ArgumentNullException(nameof(Session), "Please call Transaction() first before aborting"); - } - - return Session.AbortTransactionAsync(cancellation); + return MongoServerContext.AbortAsync(cancellation); } } } diff --git a/MongoDB.Entities/DBContext/DBContext.cs b/MongoDB.Entities/DBContext/DBContext.cs index 28f618cf8..fb2bd1e61 100644 --- a/MongoDB.Entities/DBContext/DBContext.cs +++ b/MongoDB.Entities/DBContext/DBContext.cs @@ -19,9 +19,9 @@ namespace MongoDB.Entities; public partial class DBContext : IMongoDatabase { /// - /// Returns the session object used for transactions + /// Returns the session object used for transactions /// - public IClientSessionHandle? Session { get; protected set; } + public IClientSessionHandle? Session => MongoServerContext.Session; public MongoServerContext MongoServerContext { get; } diff --git a/MongoDB.Entities/MongoContext/MongoContext.cs b/MongoDB.Entities/MongoContext/MongoContext.cs index 957afc8a5..571608d45 100644 --- a/MongoDB.Entities/MongoContext/MongoContext.cs +++ b/MongoDB.Entities/MongoContext/MongoContext.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; #nullable enable namespace MongoDB.Entities @@ -76,6 +77,61 @@ private static Type[] GetAllEntityTypes() internal readonly ConcurrentDictionary _typeToDbName = new(); internal void MapTypeToDb(string dbNameWithoutTenantPrefix) where T : IEntity => _typeToDbName[typeof(T)] = dbNameWithoutTenantPrefix; + + + /// + /// Returns the session object used for transactions + /// + public IClientSessionHandle? Session { get; protected set; } + + /// + /// Starts a transaction and returns a session object. + /// WARNING: Only one transaction is allowed per DBContext instance. + /// Call Session.Dispose() and assign a null to it before calling this method a second time. + /// Trying to start a second transaction for this DBContext instance will throw an exception. + /// + /// Client session options for this transaction + public IClientSessionHandle Transaction(ClientSessionOptions? options = null) + { + if (Session is null) + { + Session = Client.StartSession(options); + Session.StartTransaction(); + return Session; + } + + throw new NotSupportedException( + "Only one transaction is allowed per DBContext instance. Dispose and nullify the Session before calling this method again!"); + } + + + /// + /// Commits a transaction to MongoDB + /// + /// An optional cancellation token + public Task CommitAsync(CancellationToken cancellation = default) + { + if (Session is null) + { + throw new ArgumentNullException(nameof(Session), "Please call Transaction() first before committing"); + } + + return Session.CommitTransactionAsync(cancellation); + } + + /// + /// Aborts and rolls back a transaction + /// + /// An optional cancellation token + public Task AbortAsync(CancellationToken cancellation = default) + { + if (Session is null) + { + throw new ArgumentNullException(nameof(Session), "Please call Transaction() first before aborting"); + } + + return Session.AbortTransactionAsync(cancellation); + } } } From b42c201405be9b776ca59b4d05ec024b4ce1b4c4 Mon Sep 17 00:00:00 2001 From: Ahmed Fwela Date: Wed, 10 Nov 2021 13:42:47 +0200 Subject: [PATCH 17/26] Improved transactions --- .../DBContext/DBContext.Transaction.cs | 10 ++++++ MongoDB.Entities/DBContext/DBContext.cs | 1 + MongoDB.Entities/MongoContext/MongoContext.cs | 31 +++++++++++++++++-- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/MongoDB.Entities/DBContext/DBContext.Transaction.cs b/MongoDB.Entities/DBContext/DBContext.Transaction.cs index daca1bd55..ed40c8710 100644 --- a/MongoDB.Entities/DBContext/DBContext.Transaction.cs +++ b/MongoDB.Entities/DBContext/DBContext.Transaction.cs @@ -19,6 +19,16 @@ public IClientSessionHandle Transaction(ClientSessionOptions? options = null) return MongoServerContext.Transaction(options); } + /// + /// Creates a new DBContext and a new MongoServerContext and Starts a transaction on the new instance. + /// + /// Client session options for this transaction + public DBContext TransactionCopy(ClientSessionOptions? options = null) + { + var server = new MongoServerContext(MongoServerContext); + server.Transaction(options); + return new DBContext(server, Database, Options); + } /// /// Commits a transaction to MongoDB diff --git a/MongoDB.Entities/DBContext/DBContext.cs b/MongoDB.Entities/DBContext/DBContext.cs index fb2bd1e61..9a428e96e 100644 --- a/MongoDB.Entities/DBContext/DBContext.cs +++ b/MongoDB.Entities/DBContext/DBContext.cs @@ -341,4 +341,5 @@ public async Task PingNetwork() } } + } diff --git a/MongoDB.Entities/MongoContext/MongoContext.cs b/MongoDB.Entities/MongoContext/MongoContext.cs index 571608d45..71fb87390 100644 --- a/MongoDB.Entities/MongoContext/MongoContext.cs +++ b/MongoDB.Entities/MongoContext/MongoContext.cs @@ -11,10 +11,10 @@ namespace MongoDB.Entities /// /// MongoContext is a wrapper around an /// - public partial class MongoServerContext : IMongoClient + public partial class MongoServerContext : IMongoClient, IDisposable { /// - /// Creates a new context + /// Creates a new server context /// /// The backing client, usually a /// The options to configure the context @@ -24,6 +24,16 @@ public MongoServerContext(IMongoClient client, MongoContextOptions? options = nu Options = options ?? new(); } + /// + /// Copies a new MongoServerContext without the Session + /// + /// + public MongoServerContext(MongoServerContext other) + { + Client = other.Client; + Options = other.Options; + } + /// /// The backing client @@ -104,6 +114,17 @@ public IClientSessionHandle Transaction(ClientSessionOptions? options = null) "Only one transaction is allowed per DBContext instance. Dispose and nullify the Session before calling this method again!"); } + /// + /// Creates a new MongoServerContext and Starts a transaction on the new instance. + /// + /// Client session options for this transaction + public MongoServerContext TransactionCopy(ClientSessionOptions? options = null) + { + var res = new MongoServerContext(this); + res.Transaction(options); + return res; + } + /// /// Commits a transaction to MongoDB @@ -132,6 +153,12 @@ public Task AbortAsync(CancellationToken cancellation = default) return Session.AbortTransactionAsync(cancellation); } + + public void Dispose() + { + Session?.Dispose(); + Session = null; + } } } From 93e38248581a7eb322656dbd4d46bb3fb9272931 Mon Sep 17 00:00:00 2001 From: Ahmed Fwela Date: Wed, 10 Nov 2021 14:27:33 +0200 Subject: [PATCH 18/26] changed One api --- MongoDB.Entities/Core/Entity.cs | 4 +- MongoDB.Entities/Core/IEntity.cs | 2 +- MongoDB.Entities/Extensions/Entity.cs | 2 +- MongoDB.Entities/Relationships/One.cs | 130 ++++++++++++++++---------- 4 files changed, 85 insertions(+), 53 deletions(-) diff --git a/MongoDB.Entities/Core/Entity.cs b/MongoDB.Entities/Core/Entity.cs index a7b7b5073..614e646fc 100644 --- a/MongoDB.Entities/Core/Entity.cs +++ b/MongoDB.Entities/Core/Entity.cs @@ -7,12 +7,12 @@ namespace MongoDB.Entities /// Inherit this class for all entities you want to store in their own collection. /// public abstract class Entity : IEntity - { + { /// /// This property is auto managed. A new ID will be assigned for new entities upon saving. /// [BsonId, AsObjectId] - public string ID { get; set; } + public string? ID { get; set; } /// /// Override this method in order to control the generation of IDs for new entities. diff --git a/MongoDB.Entities/Core/IEntity.cs b/MongoDB.Entities/Core/IEntity.cs index 90e799495..5589e08fe 100644 --- a/MongoDB.Entities/Core/IEntity.cs +++ b/MongoDB.Entities/Core/IEntity.cs @@ -9,7 +9,7 @@ public interface IEntity /// The ID property for this entity type. /// IMPORTANT: make sure to decorate this property with the [BsonId] attribute when implementing this interface /// - string ID { get; set; } + string? ID { get; set; } /// /// Generate and return a new ID string from this method. It will be used when saving new entities that don't have their ID set. diff --git a/MongoDB.Entities/Extensions/Entity.cs b/MongoDB.Entities/Extensions/Entity.cs index 80e0bf4a6..c0ddf3fb2 100644 --- a/MongoDB.Entities/Extensions/Entity.cs +++ b/MongoDB.Entities/Extensions/Entity.cs @@ -30,7 +30,7 @@ private static T Duplicate(this T source) ).Data; } - internal static void ThrowIfUnsaved(this string entityID) + internal static void ThrowIfUnsaved(this string? entityID) { if (string.IsNullOrWhiteSpace(entityID)) throw new InvalidOperationException("Please save the entity before performing this operation!"); diff --git a/MongoDB.Entities/Relationships/One.cs b/MongoDB.Entities/Relationships/One.cs index e6d2f6220..99f4aa7f2 100644 --- a/MongoDB.Entities/Relationships/One.cs +++ b/MongoDB.Entities/Relationships/One.cs @@ -13,14 +13,31 @@ namespace MongoDB.Entities /// Any type that implements IEntity public class One where T : IEntity { + private T? _cache; + /// /// The Id of the entity referenced by this instance. /// [AsObjectId] - public string ID { get; set; } + public string? ID { get; set; } + + public T? Cache + { + get => _cache; + set + { + value?.ThrowIfUnsaved(); + _cache = value; + if (ID != value?.ID) + { + ID = value?.ID!; + } + } + } public One() - { } + { + } /// /// Initializes a reference to an entity in MongoDB. @@ -28,8 +45,7 @@ public One() /// The actual entity this reference represents. internal One(T entity) { - entity.ThrowIfUnsaved(); - ID = entity.ID; + Cache = entity; } /// @@ -38,7 +54,7 @@ internal One(T entity) /// The ID to create a new One<T> with public static implicit operator One(string id) { - return new One { ID = id }; + return new One() { ID = id }; } /// @@ -50,51 +66,67 @@ public static implicit operator One(T entity) return new One(entity); } - //TODO: revamp One/Many api - ///// - ///// Fetches the actual entity this reference represents from the database. - ///// - ///// An optional session - ///// An optional cancellation token - ///// Optional tenant prefix if using multi-tenancy - ///// A Task containing the actual entity - //public Task ToEntityAsync(IClientSessionHandle session = null, CancellationToken cancellation = default, string tenantPrefix = null) - //{ - // return new Find(session, null, tenantPrefix).OneAsync(ID, cancellation); - //} + /// + /// Fetches the actual entity this reference represents from the database. + /// + /// + /// An optional cancellation token + /// + /// + /// A Task containing the actual entity + public async Task ToEntityAsync(DBContext context, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) + { + if (ID is null) + { + return default; + } + + return Cache = await new Find(context, collection ?? context.Collection(collectionName)).OneAsync(ID, cancellation); + } + + /// + /// Fetches the actual entity this reference represents from the database with a projection. + /// + /// + /// x => new Test { PropName = x.Prop } + /// An optional cancellation token + /// + /// + /// A Task containing the actual projected entity + public async Task ToEntityAsync(DBContext context, Expression> projection, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) where TFrom : IEntity + { + if (ID is null) + { + return default; + } - ///// - ///// Fetches the actual entity this reference represents from the database with a projection. - ///// - ///// x => new Test { PropName = x.Prop } - ///// An optional session if using within a transaction - ///// An optional cancellation token - ///// Optional tenant prefix if using multi-tenancy - ///// A Task containing the actual projected entity - //public async Task ToEntityAsync(Expression> projection, IClientSessionHandle session = null, CancellationToken cancellation = default, string tenantPrefix = null) - //{ - // return (await new Find(session, null, tenantPrefix) - // .Match(ID) - // .Project(projection) - // .ExecuteAsync(cancellation).ConfigureAwait(false)) - // .SingleOrDefault(); - //} + return Cache = (await new Find(context, collection ?? context.Collection(collectionName)) + .Match(ID) + .Project(projection) + .ExecuteAsync(cancellation).ConfigureAwait(false)) + .SingleOrDefault(); + } - ///// - ///// Fetches the actual entity this reference represents from the database with a projection. - ///// - ///// p=> p.Include("Prop1").Exclude("Prop2") - ///// An optional session if using within a transaction - ///// An optional cancellation token - ///// Optional tenant prefix if using multi-tenancy - ///// A Task containing the actual projected entity - //public async Task ToEntityAsync(Func, ProjectionDefinition> projection, IClientSessionHandle session = null, CancellationToken cancellation = default, string tenantPrefix = null) - //{ - // return (await new Find(session, null, tenantPrefix) - // .Match(ID) - // .Project(projection) - // .ExecuteAsync(cancellation).ConfigureAwait(false)) - // .SingleOrDefault(); - //} + /// + /// Fetches the actual entity this reference represents from the database with a projection. + /// + /// + /// p=> p.Include("Prop1").Exclude("Prop2") + /// An optional cancellation token + /// + /// + /// A Task containing the actual projected entity + public async Task ToEntityAsync(DBContext context, Func, ProjectionDefinition> projection, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) where TFrom : IEntity + { + if (ID is null) + { + return default; + } + return Cache = (await new Find(context, collection ?? context.Collection(collectionName)) + .Match(ID) + .Project(projection) + .ExecuteAsync(cancellation).ConfigureAwait(false)) + .SingleOrDefault(); + } } } From a30b9be6691fd9d14e2cac9aa5bb1ef73839050e Mon Sep 17 00:00:00 2001 From: Ahmed Fwela Date: Wed, 10 Nov 2021 22:52:12 +0200 Subject: [PATCH 19/26] WIP relation support --- MongoDB.Entities/Builders/UpdateAndGet.cs | 5 +- MongoDB.Entities/DB/DB.Update.cs | 18 +- .../DBContext/DBContext.Update.cs | 2 +- MongoDB.Entities/Extensions/Database.cs | 87 +++--- MongoDB.Entities/Relationships/Many.cs | 274 +++++++++--------- MongoDB.Entities/Relationships/NewMany.cs | 179 ++++++++++++ 6 files changed, 368 insertions(+), 197 deletions(-) create mode 100644 MongoDB.Entities/Relationships/NewMany.cs diff --git a/MongoDB.Entities/Builders/UpdateAndGet.cs b/MongoDB.Entities/Builders/UpdateAndGet.cs index 26ec002e6..63ce7e30a 100644 --- a/MongoDB.Entities/Builders/UpdateAndGet.cs +++ b/MongoDB.Entities/Builders/UpdateAndGet.cs @@ -36,7 +36,7 @@ public class UpdateAndGet : UpdateBase> _stages = new(); private protected readonly FindOneAndUpdateOptions _options = new() { ReturnDocument = ReturnDocument.After }; - public DBContext Context { get; } + public override DBContext Context { get; } public IMongoCollection Collection { get; } internal UpdateAndGet(DBContext context, IMongoCollection collection, UpdateBase> other) : base(other) @@ -51,8 +51,7 @@ internal UpdateAndGet(DBContext context, IMongoCollection collection, Action< Collection = collection; } - private EntityCache? _cache; - internal override EntityCache Cache() => _cache ??= Context.Cache(); + /// /// Specify an update pipeline with multiple stages using a Template to modify the Entities. diff --git a/MongoDB.Entities/DB/DB.Update.cs b/MongoDB.Entities/DB/DB.Update.cs index a0aeaa63a..06570c5fe 100644 --- a/MongoDB.Entities/DB/DB.Update.cs +++ b/MongoDB.Entities/DB/DB.Update.cs @@ -9,10 +9,8 @@ public static partial class DB /// TIP: Specify a filter first with the .Match() method. Then set property values with .Modify() and finally call .Execute() to run the command. /// /// Any class that implements IEntity - /// An optional session if using within a transaction - /// Optional tenant prefix if using multi-tenancy - public static Update Update(IClientSessionHandle session = null, string tenantPrefix = null) where T : IEntity - => new(session, null, null, tenantPrefix); + public static Update Update() where T : IEntity + => Context.Update(); /// /// Update and retrieve the first document that was updated. @@ -20,19 +18,15 @@ public static Update Update(IClientSessionHandle session = null, string te /// /// Any class that implements IEntity /// The type to project to - /// An optional session if using within a transaction - /// Optional tenant prefix if using multi-tenancy - public static UpdateAndGet UpdateAndGet(IClientSessionHandle session = null, string tenantPrefix = null) where T : IEntity - => new(session, null, null, tenantPrefix); + public static UpdateAndGet UpdateAndGet() where T : IEntity + => Context.UpdateAndGet(); /// /// Update and retrieve the first document that was updated. /// TIP: Specify a filter first with the .Match(). Then set property values with .Modify() and finally call .Execute() to run the command. /// /// Any class that implements IEntity - /// An optional session if using within a transaction - /// Optional tenant prefix if using multi-tenancy - public static UpdateAndGet UpdateAndGet(IClientSessionHandle session = null, string tenantPrefix = null) where T : IEntity - => new(session, null, null, tenantPrefix); + public static UpdateAndGet UpdateAndGet() where T : IEntity + => Context.UpdateAndGet(); } } diff --git a/MongoDB.Entities/DBContext/DBContext.Update.cs b/MongoDB.Entities/DBContext/DBContext.Update.cs index f38e78087..1e0bc984e 100644 --- a/MongoDB.Entities/DBContext/DBContext.Update.cs +++ b/MongoDB.Entities/DBContext/DBContext.Update.cs @@ -24,7 +24,7 @@ public Update Update(string? collectionName = null, IMongoCollection? c /// Starts an update-and-get command for the given entity type /// /// The type of entity - public UpdateAndGet UpdateAndGet(string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + public UpdateAndGet UpdateAndGet(string? collectionName = null, IMongoCollection? collection = null) where T : IEntity { return UpdateAndGet(collectionName, collection); } diff --git a/MongoDB.Entities/Extensions/Database.cs b/MongoDB.Entities/Extensions/Database.cs index 331ba7197..6e3883e03 100644 --- a/MongoDB.Entities/Extensions/Database.cs +++ b/MongoDB.Entities/Extensions/Database.cs @@ -4,58 +4,57 @@ using System.Threading; using System.Threading.Tasks; -namespace MongoDB.Entities +namespace MongoDB.Entities; + +public static partial class Extensions { - public static partial class Extensions - { - /// - /// Gets the IMongoDatabase for the given entity type - /// - /// The type of entity - public static IMongoDatabase Database(this T _, string tenantPrefix) where T : IEntity => DB.Database(tenantPrefix); + /// + /// Gets the IMongoDatabase for the given entity type + /// + /// The type of entity + [Obsolete("This method returns the current Context", error: true)] + public static IMongoDatabase Database(this T _, string tenantPrefix) where T : IEntity => DB.Database(); - /// - /// Gets the name of the database this entity is attached to. Returns name of default database if not specifically attached. - /// - public static string DatabaseName(this T _, string tenantPrefix) where T : IEntity => DB.DatabaseName(tenantPrefix); + /// + /// Gets the name of the database this entity is attached to. Returns name of default database if not specifically attached. + /// + [Obsolete("This method returns the current DatabaseName in the Context")] + public static string DatabaseName(this T _, string tenantPrefix) where T : IEntity => DB.DatabaseName(); - /// - /// Pings the mongodb server to check if it's still connectable - /// - /// The number of seconds to keep trying - public static async Task IsAccessibleAsync(this IMongoDatabase db, int timeoutSeconds = 5) + /// + /// Pings the mongodb server to check if it's still connectable + /// + /// The number of seconds to keep trying + public static async Task IsAccessibleAsync(this IMongoDatabase db, int timeoutSeconds = 5) + { + using var cts = new CancellationTokenSource(timeoutSeconds * 1000); + try { - using (var cts = new CancellationTokenSource(timeoutSeconds * 1000)) - { - try - { - var res = await db.RunCommandAsync((Command)"{ping:1}", null, cts.Token).ConfigureAwait(false); - return res["ok"] == 1; - } - catch (Exception) - { - return false; - } - } + var res = await db.RunCommandAsync((Command)"{ping:1}", null, cts.Token).ConfigureAwait(false); + return res["ok"] == 1; } + catch (Exception) + { + return false; + } + } - /// - /// Checks to see if the database already exists on the mongodb server - /// - /// The number of seconds to keep trying - public static async Task ExistsAsync(this IMongoDatabase db, int timeoutSeconds = 5) + /// + /// Checks to see if the database already exists on the mongodb server + /// + /// The number of seconds to keep trying + public static async Task ExistsAsync(this IMongoDatabase db, int timeoutSeconds = 5) + { + using (var cts = new CancellationTokenSource(timeoutSeconds * 1000)) { - using (var cts = new CancellationTokenSource(timeoutSeconds * 1000)) + try + { + var dbs = await (await db.Client.ListDatabaseNamesAsync(cts.Token).ConfigureAwait(false)).ToListAsync().ConfigureAwait(false); + return dbs.Contains(db.DatabaseNamespace.DatabaseName); + } + catch (Exception) { - try - { - var dbs = await (await db.Client.ListDatabaseNamesAsync(cts.Token).ConfigureAwait(false)).ToListAsync().ConfigureAwait(false); - return dbs.Contains(db.DatabaseNamespace.DatabaseName); - } - catch (Exception) - { - return false; - } + return false; } } } diff --git a/MongoDB.Entities/Relationships/Many.cs b/MongoDB.Entities/Relationships/Many.cs index 4b5f4a2da..ee5493d96 100644 --- a/MongoDB.Entities/Relationships/Many.cs +++ b/MongoDB.Entities/Relationships/Many.cs @@ -6,143 +6,144 @@ using System.Threading; using System.Threading.Tasks; -namespace MongoDB.Entities -{ +namespace MongoDB.Entities; + +/// +/// Base class providing shared state for Many'1 classes +/// +public abstract class ManyBase +{ + //shared state for all Many instances + internal static ConcurrentBag indexedCollections = new(); + internal static string PropTypeName = typeof(Many).Name; +} + +/// +/// Represents a one-to-many/many-to-many relationship between two Entities. +/// WARNING: You have to initialize all instances of this class before accessing any of it's members. +/// Initialize from the constructor of the parent entity as follows: +/// this.InitOneToMany(() => Property); +/// this.InitManyToMany(() => Property, x => x.OtherProperty); +/// +/// +/// Type of the child IEntity. +public sealed partial class Many : ManyBase where TChild : IEntity +{ + private static readonly BulkWriteOptions unOrdBlkOpts = new() { IsOrdered = false }; + private bool isInverse; + private IEntity? parent; + /// - /// Base class providing shared state for Many'1 classes + /// Gets the IMongoCollection of JoinRecords for this relationship. + /// TIP: Try never to use this unless really neccessary. /// - public abstract class ManyBase - { - //shared state for all Many instances - internal static ConcurrentBag indexedCollections = new(); - internal static string PropTypeName = typeof(Many).Name; - } + //public IMongoCollection JoinCollection { get; private set; } + + public string TargetCollectionName { get; set; } /// - /// Represents a one-to-many/many-to-many relationship between two Entities. - /// WARNING: You have to initialize all instances of this class before accessing any of it's members. - /// Initialize from the constructor of the parent entity as follows: - /// this.InitOneToMany(() => Property); - /// this.InitManyToMany(() => Property, x => x.OtherProperty); + /// Get the number of children for a relationship /// - /// Type of the child IEntity. - public sealed partial class Many : ManyBase where TChild : IEntity - { - private static readonly BulkWriteOptions unOrdBlkOpts = new() { IsOrdered = false }; - private bool isInverse; - private IEntity parent; - - /// - /// Gets the IMongoCollection of JoinRecords for this relationship. - /// TIP: Try never to use this unless really neccessary. - /// - public IMongoCollection JoinCollection { get; private set; } - - public string TargetCollectionName { get; set; } - - /// - /// Get the number of children for a relationship - /// - /// An optional session if using within a transaction - /// An optional AggregateOptions object - /// An optional cancellation token - public Task ChildrenCountAsync(IClientSessionHandle session = null, CountOptions options = null, CancellationToken cancellation = default) - { - parent.ThrowIfUnsaved(); - - if (isInverse) - { - return session == null - ? JoinCollection.CountDocumentsAsync(j => j.ChildID == parent.ID, options, cancellation) - : JoinCollection.CountDocumentsAsync(session, j => j.ChildID == parent.ID, options, cancellation); - } - else - { - return session == null - ? JoinCollection.CountDocumentsAsync(j => j.ParentID == parent.ID, options, cancellation) - : JoinCollection.CountDocumentsAsync(session, j => j.ParentID == parent.ID, options, cancellation); - } - } - - /// - /// Creates an instance of Many<TChild> - /// This is only needed in VB.Net - /// - public Many() { } - - #region one-to-many-initializers - internal Many(object parent, string property) - { - Init((dynamic)parent, property); - } - - private void Init(TParent parent, string property) where TParent : IEntity - { - if (DB.DatabaseName(null) != DB.DatabaseName(null)) - throw new NotSupportedException("Cross database relationships are not supported!"); - - this.parent = parent; - isInverse = false; - JoinCollection = DB.GetRefCollection($"[{DB.CollectionName()}~{DB.CollectionName()}({property})]"); - CreateIndexesAsync(JoinCollection); - } - - /// - /// Use this method to initialize the Many<TChild> properties with VB.Net - /// - /// The type of the parent - /// The parent entity instance - /// Function(x) x.PropName - public void VB_InitOneToMany(TParent parent, Expression> property) where TParent : IEntity - { - Init(parent, Prop.Property(property)); - } - #endregion - - #region many-to-many initializers - internal Many(object parent, string propertyParent, string propertyChild, bool isInverse) - { - Init((dynamic)parent, propertyParent, propertyChild, isInverse); - } - - private void Init(TParent parent, string propertyParent, string propertyChild, bool isInverse) where TParent : IEntity - { - this.parent = parent; - this.isInverse = isInverse; - - JoinCollection = isInverse - ? DB.GetRefCollection($"[({propertyParent}){DB.CollectionName()}~{DB.CollectionName()}({propertyChild})]") - : DB.GetRefCollection($"[({propertyChild}){DB.CollectionName()}~{DB.CollectionName()}({propertyParent})]"); - - CreateIndexesAsync(JoinCollection); - } - - /// - /// Use this method to initialize the Many<TChild> properties with VB.Net - /// - /// The type of the parent - /// The parent entity instance - /// Function(x) x.ParentProp - /// Function(x) x.ChildProp - /// Specify if this is the inverse side of the relationship or not - public void VB_InitManyToMany( - TParent parent, - Expression> propertyParent, - Expression> propertyChild, - bool isInverse) where TParent : IEntity + /// An optional session if using within a transaction + /// An optional AggregateOptions object + /// An optional cancellation token + public Task ChildrenCountAsync(DBContext context, CountOptions? options = null, CancellationToken cancellation = default) + { + parent.ThrowIfUnsaved(); + + //if (isInverse) + //{ + // return session == null + // ? JoinCollection.CountDocumentsAsync(j => j.ChildID == parent.ID, options, cancellation) + // : JoinCollection.CountDocumentsAsync(session, j => j.ChildID == parent.ID, options, cancellation); + //} + //else + //{ + // return session == null + // ? JoinCollection.CountDocumentsAsync(j => j.ParentID == parent.ID, options, cancellation) + // : JoinCollection.CountDocumentsAsync(session, j => j.ParentID == parent.ID, options, cancellation); + //} + } + + /// + /// Creates an instance of Many<TChild> + /// This is only needed in VB.Net + /// + public Many() { } + + #region one-to-many-initializers + internal Many(object parent, string property) + { + Init((dynamic)parent, property); + } + + private void Init(TParent parent, string property) where TParent : IEntity + { + if (DB.DatabaseName(null) != DB.DatabaseName(null)) + throw new NotSupportedException("Cross database relationships are not supported!"); + + this.parent = parent; + isInverse = false; + JoinCollection = DB.GetRefCollection($"[{DB.CollectionName()}~{DB.CollectionName()}({property})]"); + CreateIndexesAsync(JoinCollection); + } + + /// + /// Use this method to initialize the Many<TChild> properties with VB.Net + /// + /// The type of the parent + /// The parent entity instance + /// Function(x) x.PropName + public void VB_InitOneToMany(TParent parent, Expression> property) where TParent : IEntity + { + Init(parent, Prop.Property(property)); + } + #endregion + + #region many-to-many initializers + internal Many(object parent, string propertyParent, string propertyChild, bool isInverse) + { + Init((dynamic)parent, propertyParent, propertyChild, isInverse); + } + + private void Init(TParent parent, string propertyParent, string propertyChild, bool isInverse) where TParent : IEntity + { + this.parent = parent; + this.isInverse = isInverse; + + JoinCollection = isInverse + ? DB.GetRefCollection($"[({propertyParent}){DB.CollectionName()}~{DB.CollectionName()}({propertyChild})]") + : DB.GetRefCollection($"[({propertyChild}){DB.CollectionName()}~{DB.CollectionName()}({propertyParent})]"); + + CreateIndexesAsync(JoinCollection); + } + + /// + /// Use this method to initialize the Many<TChild> properties with VB.Net + /// + /// The type of the parent + /// The parent entity instance + /// Function(x) x.ParentProp + /// Function(x) x.ChildProp + /// Specify if this is the inverse side of the relationship or not + public void VB_InitManyToMany( + TParent parent, + Expression> propertyParent, + Expression> propertyChild, + bool isInverse) where TParent : IEntity + { + Init(parent, Prop.Property(propertyParent), Prop.Property(propertyChild), isInverse); + } + #endregion + + private static Task CreateIndexesAsync(IMongoCollection collection) + { + //only create indexes once (best effort) per unique ref collection + if (!indexedCollections.Contains(collection.CollectionNamespace.CollectionName)) { - Init(parent, Prop.Property(propertyParent), Prop.Property(propertyChild), isInverse); - } - #endregion - - private static Task CreateIndexesAsync(IMongoCollection collection) - { - //only create indexes once (best effort) per unique ref collection - if (!indexedCollections.Contains(collection.CollectionNamespace.CollectionName)) - { - indexedCollections.Add(collection.CollectionNamespace.CollectionName); - collection.Indexes.CreateManyAsync( - new[] { + indexedCollections.Add(collection.CollectionNamespace.CollectionName); + collection.Indexes.CreateManyAsync( + new[] { new CreateIndexModel( Builders.IndexKeys.Ascending(r => r.ParentID), new CreateIndexOptions @@ -157,10 +158,9 @@ private static Task CreateIndexesAsync(IMongoCollection collection) { Background = true, Name = "[ChildID]" - }) - }); - } - return Task.CompletedTask; - } - } + }) + }); + } + return Task.CompletedTask; + } } diff --git a/MongoDB.Entities/Relationships/NewMany.cs b/MongoDB.Entities/Relationships/NewMany.cs new file mode 100644 index 000000000..da45852b4 --- /dev/null +++ b/MongoDB.Entities/Relationships/NewMany.cs @@ -0,0 +1,179 @@ +using MongoDB.Driver; +using MongoDB.Driver.Linq; +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; + +namespace MongoDB.Entities.NewMany; + + +/* + results in the following: + ========= + Collection A: + _id + BRef: { ID: xxxxxxxx } + ------ + Collection B: + _id + ------- + Collection C: + _id + ------ + Collection B~C(CRef): + ParentId: b._id + ChildId: c._id +*/ +/* +Queries needed: +One-Many (B-A) + * for a document B (parent) get children (A) {collection A where BRef == B.id} + * for a document A (child) get parent (B) {A.BRef} + * for multiple documents B (parents) get children (A) [to solve N+1 problem] {collection A where BRef.ID in b_ids} + * for multiple documents A (children) get parents (B) [to solve N+1 problem] {collection B where _id in a_BRefs} + * Add A to the Many list in B + * set A.BRef = B.Id + +Many(Parent)-Many(Child) (B-C) + * for a document B (parent) get children (C) {collection B~C(CRef) where ParentId==B.id} then {join result ChildId with collection C} + * for a document C (child) get parents (B) {collection B~C(CRef) where ChildId==C.id} then {join result ParentId with collection B} + * for multiple documents B (parents) get children (C) [to solve N+1 problem] {collection B~C(CRef) where ParentId in b_ids} then {join result ChildId with collection C} + * for multiple documents C (children) get parents (B) [to solve N+1 problem] {collection B~C(CRef) where ChildId in c_ids} then {join result ParentId with collection B} + * Add B to the Many list in C + * Add C to the Many list in B + * Add an arbitrary join record + */ +class A : Entity +{ + public One? SingleB1 { get; set; } + public One? SingleB2 { get; set; } +} +class B : Entity +{ + public Many ManyAVia1 { get; set; } + public Many ManyAVia2 { get; set; } + + [OwnerSide] + public Many ManyC { get; set; } + + public B() + { + ManyAVia1 = this.InitManyToOne(x => x.ManyAVia1, x => x.SingleB1); + ManyAVia2 = this.InitManyToOne(x => x.ManyAVia2, x => x.SingleB2); + + ManyC = this.InitManyToMany(x => x.ManyC, x => x.ManyB); + } +} +class C : Entity +{ + [InverseSide] + public Many ManyB { get; set; } + + public C() + { + ManyB = this.InitManyToMany(x => x.ManyB, x => x.ManyC); + } +} + +/// +/// Marker class +/// +/// +public abstract class Many where TChild : IEntity +{ + protected Many(PropertyInfo parentProperty, PropertyInfo childProperty) + { + ParentProperty = parentProperty; + ChildProperty = childProperty; + } + + public PropertyInfo ParentProperty { get; } + public PropertyInfo ChildProperty { get; } + + + public abstract IMongoQueryable GetChildrenQuery(DBContext context, string? childCollectionName = null, IMongoCollection? collection = null); + public abstract Find GetChildFind(DBContext context, string? childCollectionName = null, IMongoCollection? collection = null); + public abstract Find GetChildFind(DBContext context, string? childCollectionName = null, IMongoCollection? collection = null); +} +internal abstract class Many : Many where TParent : IEntity where TChild : IEntity +{ + protected Many(PropertyInfo parentProperty, PropertyInfo childProperty) : base(parentProperty, childProperty) + { + } +} +internal class ManyToMany : Many where TParent : IEntity where TChild : IEntity +{ + public ManyToMany(bool isParentOwner, PropertyInfo parentProperty, PropertyInfo childProperty) : base(parentProperty, childProperty) + { + IsParentOwner = isParentOwner; + } + + public bool IsParentOwner { get; } +} + +internal class ManyToOne : Many where TParent : IEntity where TChild : IEntity +{ + public ManyToOne(PropertyInfo parentProperty, PropertyInfo childProperty) : base(parentProperty, childProperty) + { + } +} + + +public static class RelationsExt +{ + public static PropertyInfo GetPropertyInfo(this Expression> propertyLambda) + { + Type type = typeof(TSource); + + if (propertyLambda.Body is not MemberExpression member) + throw new ArgumentException(string.Format( + "Expression '{0}' refers to a method, not a property.", + propertyLambda.ToString())); + + + if (member.Member is not PropertyInfo propInfo) + throw new ArgumentException(string.Format( + "Expression '{0}' refers to a field, not a property.", + propertyLambda.ToString())); + + if (type != propInfo.ReflectedType && + !type.IsSubclassOf(propInfo.ReflectedType)) + throw new ArgumentException(string.Format( + "Expression '{0}' refers to a property that is not from type {1}.", + propertyLambda.ToString(), + type)); + + return propInfo; + } + + public static Many InitManyToMany(this TParent _, Expression>> propertyExpression, Expression>> propertyOtherSide) where TParent : IEntity where TChild : IEntity + { + var property = propertyExpression.GetPropertyInfo(); + var hasOwnerAttrib = property.IsDefined(typeof(OwnerSideAttribute), false); + var hasInverseAttrib = property.IsDefined(typeof(InverseSideAttribute), false); + if (hasOwnerAttrib && hasInverseAttrib) throw new InvalidOperationException("Only one type of relationship side attribute is allowed on a property"); + if (!hasOwnerAttrib && !hasInverseAttrib) throw new InvalidOperationException("Missing attribute for determining relationship side of a many-to-many relationship"); + + var osProperty = propertyOtherSide.GetPropertyInfo(); + var osHasOwnerAttrib = osProperty.IsDefined(typeof(OwnerSideAttribute), false); + var osHasInverseAttrib = osProperty.IsDefined(typeof(InverseSideAttribute), false); + if (osHasOwnerAttrib && osHasInverseAttrib) throw new InvalidOperationException("Only one type of relationship side attribute is allowed on a property"); + if (!osHasOwnerAttrib && !osHasInverseAttrib) throw new InvalidOperationException("Missing attribute for determining relationship side of a many-to-many relationship"); + + if ((hasOwnerAttrib == osHasOwnerAttrib) || (hasInverseAttrib == osHasInverseAttrib)) throw new InvalidOperationException("Both sides of the relationship cannot have the same attribute"); + var res = new ManyToMany(hasInverseAttrib, property, osProperty); + //should we set the property ourself or let the user handle it ? + //property.SetValue(parent, res); + return res; + } + public static Many InitManyToOne(this TParent _, Expression>> propertyExpression, Expression?>> propertyOtherSide) where TParent : IEntity where TChild : IEntity + { + var property = propertyExpression.GetPropertyInfo(); + var osProperty = propertyOtherSide.GetPropertyInfo(); + + return new ManyToOne(property, osProperty); + + } +} \ No newline at end of file From bc27bc47fc2db5ad44abe03f7fc20a7f0c79f05b Mon Sep 17 00:00:00 2001 From: Ahmed Fwela Date: Wed, 10 Nov 2021 23:28:07 +0200 Subject: [PATCH 20/26] WIP relations --- MongoDB.Entities/Relationships/NewMany.cs | 57 +++++++++++++++++------ 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/MongoDB.Entities/Relationships/NewMany.cs b/MongoDB.Entities/Relationships/NewMany.cs index da45852b4..e2a98d641 100644 --- a/MongoDB.Entities/Relationships/NewMany.cs +++ b/MongoDB.Entities/Relationships/NewMany.cs @@ -89,35 +89,62 @@ protected Many(PropertyInfo parentProperty, PropertyInfo childProperty) ChildProperty = childProperty; } - public PropertyInfo ParentProperty { get; } - public PropertyInfo ChildProperty { get; } - + internal PropertyInfo ParentProperty { get; } + internal PropertyInfo ChildProperty { get; } public abstract IMongoQueryable GetChildrenQuery(DBContext context, string? childCollectionName = null, IMongoCollection? collection = null); - public abstract Find GetChildFind(DBContext context, string? childCollectionName = null, IMongoCollection? collection = null); - public abstract Find GetChildFind(DBContext context, string? childCollectionName = null, IMongoCollection? collection = null); + public Find GetChildrenFind(DBContext context, string? childCollectionName = null, IMongoCollection? collection = null) => GetChildrenFind(context, childCollectionName, collection); + public abstract Find GetChildrenFind(DBContext context, string? childCollectionName = null, IMongoCollection? collection = null); } -internal abstract class Many : Many where TParent : IEntity where TChild : IEntity +public abstract class Many : Many where TParent : IEntity where TChild : IEntity { - protected Many(PropertyInfo parentProperty, PropertyInfo childProperty) : base(parentProperty, childProperty) + public TParent Parent { get; } + protected Many(TParent parent, PropertyInfo parentProperty, PropertyInfo childProperty) : base(parentProperty, childProperty) { + Parent = parent; } + public FilterDefinition GetFilterForSingleDocument() => Builders.Filter.Eq(ChildProperty.Name, Parent.ID); + + } -internal class ManyToMany : Many where TParent : IEntity where TChild : IEntity +public sealed class ManyToMany : Many where TParent : IEntity where TChild : IEntity { - public ManyToMany(bool isParentOwner, PropertyInfo parentProperty, PropertyInfo childProperty) : base(parentProperty, childProperty) + public ManyToMany(bool isParentOwner, TParent parent, PropertyInfo parentProperty, PropertyInfo childProperty) : base(parent, parentProperty, childProperty) { IsParentOwner = isParentOwner; } public bool IsParentOwner { get; } + + public override Find GetChildrenFind(DBContext context, string? childCollectionName = null, IMongoCollection? collection = null) + { + throw new NotImplementedException(); + } + + public override IMongoQueryable GetChildrenQuery(DBContext context, string? childCollectionName = null, IMongoCollection? collection = null) + { + throw new NotImplementedException(); + } } -internal class ManyToOne : Many where TParent : IEntity where TChild : IEntity +public sealed class ManyToOne : Many where TParent : IEntity where TChild : IEntity { - public ManyToOne(PropertyInfo parentProperty, PropertyInfo childProperty) : base(parentProperty, childProperty) + public ManyToOne(TParent parent, PropertyInfo parentProperty, PropertyInfo childProperty) : base(parent, parentProperty, childProperty) + { + } + + public override Find GetChildrenFind(DBContext context, string? childCollectionName = null, IMongoCollection? childCollection = null) { + return context.Find(childCollectionName, childCollection) + .Match(GetFilterForSingleDocument()); //BRef==Parent.Id } + + public override IMongoQueryable GetChildrenQuery(DBContext context, string? childCollectionName = null, IMongoCollection? childCollection = null) + { + return context.Queryable(collectionName: childCollectionName, collection: childCollection) + .Where(_ => GetFilterForSingleDocument().Inject()); + } + } @@ -148,7 +175,7 @@ public static PropertyInfo GetPropertyInfo(this Expression InitManyToMany(this TParent _, Expression>> propertyExpression, Expression>> propertyOtherSide) where TParent : IEntity where TChild : IEntity + public static ManyToMany InitManyToMany(this TParent parent, Expression>> propertyExpression, Expression>> propertyOtherSide) where TParent : IEntity where TChild : IEntity { var property = propertyExpression.GetPropertyInfo(); var hasOwnerAttrib = property.IsDefined(typeof(OwnerSideAttribute), false); @@ -163,17 +190,17 @@ public static Many InitManyToMany(this TParent _, Expre if (!osHasOwnerAttrib && !osHasInverseAttrib) throw new InvalidOperationException("Missing attribute for determining relationship side of a many-to-many relationship"); if ((hasOwnerAttrib == osHasOwnerAttrib) || (hasInverseAttrib == osHasInverseAttrib)) throw new InvalidOperationException("Both sides of the relationship cannot have the same attribute"); - var res = new ManyToMany(hasInverseAttrib, property, osProperty); + var res = new ManyToMany(hasInverseAttrib, parent, property, osProperty); //should we set the property ourself or let the user handle it ? //property.SetValue(parent, res); return res; } - public static Many InitManyToOne(this TParent _, Expression>> propertyExpression, Expression?>> propertyOtherSide) where TParent : IEntity where TChild : IEntity + public static ManyToOne InitManyToOne(this TParent parent, Expression>> propertyExpression, Expression?>> propertyOtherSide) where TParent : IEntity where TChild : IEntity { var property = propertyExpression.GetPropertyInfo(); var osProperty = propertyOtherSide.GetPropertyInfo(); - return new ManyToOne(property, osProperty); + return new ManyToOne(parent, property, osProperty); } } \ No newline at end of file From 537303bfd2f13bf787df4b9fda39790fe6aee86e Mon Sep 17 00:00:00 2001 From: Ahmed Fwela Date: Thu, 11 Nov 2021 10:29:00 +0200 Subject: [PATCH 21/26] generic Id WIP --- MongoDB.Entities/Builders/Distinct.cs | 188 ++- MongoDB.Entities/Builders/FilterQueryBase.cs | 290 ++-- MongoDB.Entities/Builders/Find.cs | 530 ++++---- .../Builders/ICollectionRelated.cs | 26 +- MongoDB.Entities/Builders/Index.cs | 313 +++-- MongoDB.Entities/Builders/PagedSearch.cs | 424 +++--- MongoDB.Entities/Builders/Replace.cs | 219 ++- .../Builders/SortFilterQueryBase.cs | 77 +- MongoDB.Entities/Builders/Update.cs | 616 +++++---- MongoDB.Entities/Builders/UpdateAndGet.cs | 38 +- MongoDB.Entities/Core/Attributes.cs | 215 ++- MongoDB.Entities/Core/DBContextOptions.cs | 13 +- MongoDB.Entities/Core/Date.cs | 164 ++- MongoDB.Entities/Core/DoubleMetaphone.cs | 1199 ++++++++--------- MongoDB.Entities/Core/Entity.cs | 48 +- MongoDB.Entities/Core/EntityCache.cs | 184 +-- MongoDB.Entities/Core/FileEntity.cs | 461 +++---- MongoDB.Entities/Core/FuzzyString.cs | 151 +-- MongoDB.Entities/Core/GeoNear.cs | 108 +- MongoDB.Entities/Core/ICreatedOn.cs | 19 +- MongoDB.Entities/Core/IEntity.cs | 60 +- MongoDB.Entities/Core/IModifiedOn.cs | 19 +- .../Core/IgnoreManyPropsConvention.cs | 18 +- MongoDB.Entities/Core/LevenshteinDistance.cs | 83 +- MongoDB.Entities/Core/Logic.cs | 80 +- MongoDB.Entities/Core/ModifiedBy.cs | 11 +- MongoDB.Entities/Core/Prop.cs | 242 ++-- MongoDB.Entities/Core/SequenceCounter.cs | 22 +- MongoDB.Entities/Core/Template.cs | 12 +- MongoDB.Entities/Core/Transaction.cs | 76 +- MongoDB.Entities/Core/Watcher.cs | 795 ++++++----- MongoDB.Entities/DB/DB.Collection.cs | 4 +- .../DBContext/DBContext.Collection.cs | 4 +- MongoDB.Entities/DBContext/DBContext.Count.cs | 12 +- .../DBContext/DBContext.Delete.cs | 52 +- .../DBContext/DBContext.Distinct.cs | 7 +- MongoDB.Entities/DBContext/DBContext.File.cs | 10 +- MongoDB.Entities/DBContext/DBContext.Find.cs | 14 +- .../DBContext/DBContext.Fluent.cs | 4 +- .../DBContext/DBContext.GeoNear.cs | 2 +- MongoDB.Entities/DBContext/DBContext.Index.cs | 7 +- .../DBContext/DBContext.Insert.cs | 40 +- .../DBContext/DBContext.PagedSearch.cs | 43 +- .../DBContext/DBContext.Pipeline.cs | 12 +- .../DBContext/DBContext.Queryable.cs | 2 +- .../DBContext/DBContext.Replace.cs | 7 +- MongoDB.Entities/DBContext/DBContext.Save.cs | 65 +- .../DBContext/DBContext.Sequence.cs | 4 +- .../DBContext/DBContext.Transaction.cs | 8 +- .../DBContext/DBContext.Update.cs | 79 +- .../DBContext/DBContext.Watcher.cs | 50 +- MongoDB.Entities/DBContext/DBContext.cs | 37 +- MongoDB.Entities/Extensions/Entity.cs | 9 +- MongoDB.Entities/GlobalUsing.cs | 8 + MongoDB.Entities/MongoDB.Entities.csproj | 3 + MongoDB.Entities/Relationships/JoinRecord.cs | 43 +- .../Relationships/Many.Queryable.cs | 253 ++-- MongoDB.Entities/Relationships/Many.cs | 5 +- MongoDB.Entities/Relationships/NewMany.cs | 155 ++- MongoDB.Entities/Relationships/One.cs | 41 +- 60 files changed, 3904 insertions(+), 3777 deletions(-) create mode 100644 MongoDB.Entities/GlobalUsing.cs diff --git a/MongoDB.Entities/Builders/Distinct.cs b/MongoDB.Entities/Builders/Distinct.cs index 9f4f63577..f2bffb03f 100644 --- a/MongoDB.Entities/Builders/Distinct.cs +++ b/MongoDB.Entities/Builders/Distinct.cs @@ -1,119 +1,117 @@ -using MongoDB.Driver; -using System; -using System.Collections.Generic; -using System.Linq.Expressions; -using System.Threading; -using System.Threading.Tasks; +namespace MongoDB.Entities; -namespace MongoDB.Entities +public class DistinctBase : FilterQueryBase + where TId : IComparable, IEquatable + where T : IEntity + where TSelf : DistinctBase { - public class DistinctBase : FilterQueryBase where T : IEntity where TSelf : DistinctBase - { - internal DistinctOptions _options = new(); - internal FieldDefinition? _field; - - internal DistinctBase(DistinctBase other) : base(other) - { - _options = other._options; - _field = other._field; - } - internal DistinctBase(Dictionary globalFilters) : base(globalFilters) - { - _globalFilters = globalFilters; - } + internal DistinctOptions _options = new(); + internal FieldDefinition? _field; + internal DistinctBase(DistinctBase other) : base(other) + { + _options = other._options; + _field = other._field; + } + internal DistinctBase(Dictionary globalFilters) : base(globalFilters) + { + _globalFilters = globalFilters; + } - private TSelf This => (TSelf)this; + private TSelf This => (TSelf)this; - /// - /// Specify an option for this find command (use multiple times if needed) - /// - /// x => x.OptionName = OptionValue - public TSelf Option(Action option) - { - option(_options); - return This; - } - /// - /// Specify the property you want to get the unique values for (as a string path) - /// - /// ex: "Address.Street" - public TSelf Property(string property) - { - _field = property; - return This; - } + /// + /// Specify an option for this find command (use multiple times if needed) + /// + /// x => x.OptionName = OptionValue + public TSelf Option(Action option) + { + option(_options); + return This; + } - /// - /// Specify the property you want to get the unique values for (as a member expression) - /// - /// x => x.Address.Street - public TSelf Property(Expression> property) - { - _field = property.FullPath(); - return This; - } + /// + /// Specify the property you want to get the unique values for (as a string path) + /// + /// ex: "Address.Street" + public TSelf Property(string property) + { + _field = property; + return This; } /// - /// Represents a MongoDB Distinct command where you can get back distinct values for a given property of a given Entity. + /// Specify the property you want to get the unique values for (as a member expression) /// - /// Any Entity that implements IEntity interface - /// The type of the property of the entity you'd like to get unique values for - public class Distinct : DistinctBase>, ICollectionRelated where T : IEntity + /// x => x.Address.Street + public TSelf Property(Expression> property) { - public DBContext Context { get; } - public IMongoCollection Collection { get; } + _field = property.FullPath(); + return This; + } +} - internal Distinct( - DBContext context, - IMongoCollection collection, - DistinctBase> other) : base(other) - { - Context = context; - Collection = collection; - } - internal Distinct( - DBContext context, - IMongoCollection collection) : base(globalFilters: context.GlobalFilters) - { - Context = context; - Collection = collection; - } +/// +/// Represents a MongoDB Distinct command where you can get back distinct values for a given property of a given Entity. +/// +/// Any Entity that implements IEntity interface +/// The type of the property of the entity you'd like to get unique values for +/// The type of the id +public class Distinct : DistinctBase>, ICollectionRelated + where TId : IComparable, IEquatable + where T : IEntity +{ + public DBContext Context { get; } + public IMongoCollection Collection { get; } - /// - /// Run the Distinct command in MongoDB server and get a cursor instead of materialized results - /// - /// An optional cancellation token - public Task> ExecuteCursorAsync(CancellationToken cancellation = default) - { - if (_field == null) - throw new InvalidOperationException("Please use the .Property() method to specify the field to use for obtaining unique values for!"); + internal Distinct( + DBContext context, + IMongoCollection collection, + DistinctBase> other) : base(other) + { + Context = context; + Collection = collection; + } + internal Distinct( + DBContext context, + IMongoCollection collection) : base(globalFilters: context.GlobalFilters) + { + Context = context; + Collection = collection; + } - var mergedFilter = Logic.MergeWithGlobalFilter(_ignoreGlobalFilters, _globalFilters, _filter); + /// + /// Run the Distinct command in MongoDB server and get a cursor instead of materialized results + /// + /// An optional cancellation token + public Task> ExecuteCursorAsync(CancellationToken cancellation = default) + { + if (_field == null) + throw new InvalidOperationException("Please use the .Property() method to specify the field to use for obtaining unique values for!"); - return Context.Session is IClientSessionHandle session - ? Collection.DistinctAsync(session, _field, mergedFilter, _options, cancellation) - : Collection.DistinctAsync(_field, mergedFilter, _options, cancellation); - } + var mergedFilter = Logic.MergeWithGlobalFilter(_ignoreGlobalFilters, _globalFilters, _filter); - /// - /// Run the Distinct command in MongoDB server and get a list of unique property values - /// - /// An optional cancellation token - public async Task> ExecuteAsync(CancellationToken cancellation = default) + return Context.Session is IClientSessionHandle session + ? Collection.DistinctAsync(session, _field, mergedFilter, _options, cancellation) + : Collection.DistinctAsync(_field, mergedFilter, _options, cancellation); + } + + /// + /// Run the Distinct command in MongoDB server and get a list of unique property values + /// + /// An optional cancellation token + public async Task> ExecuteAsync(CancellationToken cancellation = default) + { + var list = new List(); + using (var csr = await ExecuteCursorAsync(cancellation).ConfigureAwait(false)) { - var list = new List(); - using (var csr = await ExecuteCursorAsync(cancellation).ConfigureAwait(false)) + while (await csr.MoveNextAsync(cancellation).ConfigureAwait(false)) { - while (await csr.MoveNextAsync(cancellation).ConfigureAwait(false)) - { - list.AddRange(csr.Current); - } + list.AddRange(csr.Current); } - return list; } + return list; } } diff --git a/MongoDB.Entities/Builders/FilterQueryBase.cs b/MongoDB.Entities/Builders/FilterQueryBase.cs index ece5a1c99..eaa32de9a 100644 --- a/MongoDB.Entities/Builders/FilterQueryBase.cs +++ b/MongoDB.Entities/Builders/FilterQueryBase.cs @@ -1,169 +1,167 @@ -using MongoDB.Driver; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -namespace MongoDB.Entities +namespace MongoDB.Entities; + +public abstract class FilterQueryBase + where TId : IComparable, IEquatable + where T : IEntity + where TSelf : FilterQueryBase + { - public abstract class FilterQueryBase where T : IEntity where TSelf : FilterQueryBase + internal FilterDefinition _filter = Builders.Filter.Empty; + internal Dictionary _globalFilters; + internal bool _ignoreGlobalFilters; + + internal FilterQueryBase(FilterQueryBase other) : this(globalFilters: other._globalFilters) { - internal FilterDefinition _filter = Builders.Filter.Empty; - internal Dictionary _globalFilters; - internal bool _ignoreGlobalFilters; + _filter = other._filter; + _ignoreGlobalFilters = other._ignoreGlobalFilters; + } + internal FilterQueryBase(Dictionary globalFilters) + { + _globalFilters = globalFilters; + } - internal FilterQueryBase(FilterQueryBase other) : this(globalFilters: other._globalFilters) - { - _filter = other._filter; - _ignoreGlobalFilters = other._ignoreGlobalFilters; - } - internal FilterQueryBase(Dictionary globalFilters) - { - _globalFilters = globalFilters; - } + protected FilterDefinition MergedFilter => Logic.MergeWithGlobalFilter(_ignoreGlobalFilters, _globalFilters, _filter); + private TSelf This => (TSelf)this; - protected FilterDefinition MergedFilter => Logic.MergeWithGlobalFilter(_ignoreGlobalFilters, _globalFilters, _filter); - private TSelf This => (TSelf)this; + /// + /// Specify that this operation should ignore any global filters + /// + public TSelf IgnoreGlobalFilters() + { + _ignoreGlobalFilters = true; + return This; + } - /// - /// Specify that this operation should ignore any global filters - /// - public TSelf IgnoreGlobalFilters() - { - _ignoreGlobalFilters = true; - return This; - } + /// + /// Specify an IEntity ID as the matching criteria + /// + /// A unique IEntity ID + public TSelf Match(TId ID) + { + return Match(f => f.Eq(t => t.ID, ID)); + } - /// - /// Specify an IEntity ID as the matching criteria - /// - /// A unique IEntity ID - public TSelf Match(string ID) - { - return Match(f => f.Eq(t => t.ID, ID)); - } + /// + /// Specify the matching criteria with a lambda expression + /// + /// x => x.Property == Value + public TSelf Match(Expression> expression) + { + return Match(f => f.Where(expression)); + } - /// - /// Specify the matching criteria with a lambda expression - /// - /// x => x.Property == Value - public TSelf Match(Expression> expression) - { - return Match(f => f.Where(expression)); - } + /// + /// Specify the matching criteria with a filter expression + /// + /// f => f.Eq(x => x.Prop, Value) & f.Gt(x => x.Prop, Value) + public TSelf Match(Func, FilterDefinition> filter) + { + _filter &= filter(Builders.Filter); + return This; + } - /// - /// Specify the matching criteria with a filter expression - /// - /// f => f.Eq(x => x.Prop, Value) & f.Gt(x => x.Prop, Value) - public TSelf Match(Func, FilterDefinition> filter) - { - _filter &= filter(Builders.Filter); - return This; - } + /// + /// Specify the matching criteria with a filter definition + /// + /// A filter definition + public TSelf Match(FilterDefinition filterDefinition) + { + _filter &= filterDefinition; + return This; + } - /// - /// Specify the matching criteria with a filter definition - /// - /// A filter definition - public TSelf Match(FilterDefinition filterDefinition) - { - _filter &= filterDefinition; - return This; - } + /// + /// Specify the matching criteria with a template + /// + /// A Template with a find query + public TSelf Match(Template template) + { + _filter &= template.RenderToString(); + return This; + } - /// - /// Specify the matching criteria with a template - /// - /// A Template with a find query - public TSelf Match(Template template) + /// + /// Specify a search term to find results from the text index of this particular collection. + /// TIP: Make sure to define a text index with DB.Index<T>() before searching + /// + /// The type of text matching to do + /// The search term + /// Case sensitivity of the search (optional) + /// Diacritic sensitivity of the search (optional) + /// The language for the search (optional) + public TSelf Match(Search searchType, string searchTerm, bool caseSensitive = false, bool diacriticSensitive = false, string? language = null) + { + if (searchType == Search.Fuzzy) { - _filter &= template.RenderToString(); - return This; + searchTerm = searchTerm.ToDoubleMetaphoneHash(); + caseSensitive = false; + diacriticSensitive = false; + language = null; } - /// - /// Specify a search term to find results from the text index of this particular collection. - /// TIP: Make sure to define a text index with DB.Index<T>() before searching - /// - /// The type of text matching to do - /// The search term - /// Case sensitivity of the search (optional) - /// Diacritic sensitivity of the search (optional) - /// The language for the search (optional) - public TSelf Match(Search searchType, string searchTerm, bool caseSensitive = false, bool diacriticSensitive = false, string? language = null) - { - if (searchType == Search.Fuzzy) - { - searchTerm = searchTerm.ToDoubleMetaphoneHash(); - caseSensitive = false; - diacriticSensitive = false; - language = null; - } - - return Match( - f => f.Text( - searchTerm, - new TextSearchOptions - { - CaseSensitive = caseSensitive, - DiacriticSensitive = diacriticSensitive, - Language = language - })); - } + return Match( + f => f.Text( + searchTerm, + new TextSearchOptions + { + CaseSensitive = caseSensitive, + DiacriticSensitive = diacriticSensitive, + Language = language + })); + } - /// - /// Specify criteria for matching entities based on GeoSpatial data (longitude & latitude) - /// TIP: Make sure to define a Geo2DSphere index with DB.Index<T>() before searching - /// Note: DB.FluentGeoNear() supports more advanced options - /// - /// The property where 2DCoordinates are stored - /// The search point - /// Maximum distance in meters from the search point - /// Minimum distance in meters from the search point - public TSelf Match(Expression> coordinatesProperty, Coordinates2D nearCoordinates, double? maxDistance = null, double? minDistance = null) - { - return Match(f => f.Near(coordinatesProperty, nearCoordinates.ToGeoJsonPoint(), maxDistance, minDistance)); - } + /// + /// Specify criteria for matching entities based on GeoSpatial data (longitude & latitude) + /// TIP: Make sure to define a Geo2DSphere index with DB.Index<T>() before searching + /// Note: DB.FluentGeoNear() supports more advanced options + /// + /// The property where 2DCoordinates are stored + /// The search point + /// Maximum distance in meters from the search point + /// Minimum distance in meters from the search point + public TSelf Match(Expression> coordinatesProperty, Coordinates2D nearCoordinates, double? maxDistance = null, double? minDistance = null) + { + return Match(f => f.Near(coordinatesProperty, nearCoordinates.ToGeoJsonPoint(), maxDistance, minDistance)); + } - /// - /// Specify the matching criteria with an aggregation expression (i.e. $expr) - /// - /// { $gt: ['$Property1', '$Property2'] } - public TSelf MatchExpression(string expression) - { - _filter &= "{$expr:" + expression + "}"; - return This; - } + /// + /// Specify the matching criteria with an aggregation expression (i.e. $expr) + /// + /// { $gt: ['$Property1', '$Property2'] } + public TSelf MatchExpression(string expression) + { + _filter &= "{$expr:" + expression + "}"; + return This; + } - /// - /// Specify the matching criteria with a Template - /// - /// A Template object - public TSelf MatchExpression(Template template) - { - return MatchExpression(template.RenderToString()); - } + /// + /// Specify the matching criteria with a Template + /// + /// A Template object + public TSelf MatchExpression(Template template) + { + return MatchExpression(template.RenderToString()); + } - /// - /// Specify an IEntity ID as the matching criteria - /// - /// A unique IEntity ID - public TSelf MatchID(string ID) - { - return Match(f => f.Eq(t => t.ID, ID)); - } + /// + /// Specify an IEntity ID as the matching criteria + /// + /// A unique IEntity ID + public TSelf MatchID(TId ID) + { + return Match(f => f.Eq(t => t.ID, ID)); + } - /// - /// Specify the matching criteria with a JSON string - /// - /// { Title : 'The Power Of Now' } - public TSelf MatchString(string jsonString) - { - _filter &= jsonString; - return This; - } + /// + /// Specify the matching criteria with a JSON string + /// + /// { Title : 'The Power Of Now' } + public TSelf MatchString(string jsonString) + { + _filter &= jsonString; + return This; } } diff --git a/MongoDB.Entities/Builders/Find.cs b/MongoDB.Entities/Builders/Find.cs index cd51a3335..f77d73f67 100644 --- a/MongoDB.Entities/Builders/Find.cs +++ b/MongoDB.Entities/Builders/Find.cs @@ -1,319 +1,317 @@ -using MongoDB.Bson; -using MongoDB.Bson.Serialization; -using MongoDB.Driver; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Threading; -using System.Threading.Tasks; -#nullable enable -namespace MongoDB.Entities -{ - public abstract class FindBase : SortFilterQueryBase where T : IEntity where TSelf : FindBase - { - internal FindOptions _options = new(); - - internal FindBase(FindBase other) : base(other) - { - _options = other._options; - } - internal FindBase(Dictionary globalFilters) : base(globalFilters: globalFilters) - { - _globalFilters = globalFilters; - } - public abstract DBContext Context { get; } - private TSelf This => (TSelf)this; - - /// - /// Specify how many entities to skip - /// - /// The number to skip - public TSelf Skip(int skipCount) - { - _options.Skip = skipCount; - return This; - } +namespace MongoDB.Entities; - /// - /// Specify how many entities to Take/Limit - /// - /// The number to limit/take - public TSelf Limit(int takeCount) - { - _options.Limit = takeCount; - return This; - } +public abstract class FindBase : SortFilterQueryBase + where TId : IComparable, IEquatable + where T : IEntity + where TSelf : FindBase +{ + internal FindOptions _options = new(); - /// - /// Specify how to project the results using a lambda expression - /// - /// x => new Test { PropName = x.Prop } - public TSelf Project(Expression> expression) - { - return Project(p => p.Expression(expression)); - } + internal FindBase(FindBase other) : base(other) + { + _options = other._options; + } + internal FindBase(Dictionary globalFilters) : base(globalFilters: globalFilters) + { + _globalFilters = globalFilters; + } + public abstract DBContext Context { get; } + private TSelf This => (TSelf)this; - /// - /// Specify how to project the results using a projection expression - /// - /// p => p.Include("Prop1").Exclude("Prop2") - public TSelf Project(Func, ProjectionDefinition> projection) - { - _options.Projection = projection(Builders.Projection); - return This; - } + /// + /// Specify how many entities to skip + /// + /// The number to skip + public TSelf Skip(int skipCount) + { + _options.Skip = skipCount; + return This; + } - /// - /// Specify to automatically include all properties marked with [BsonRequired] attribute on the entity in the final projection. - /// HINT: this method should only be called after the .Project() method. - /// - public TSelf IncludeRequiredProps() - { - if (typeof(T) != typeof(TProjection)) - throw new InvalidOperationException("IncludeRequiredProps() cannot be used when projecting to a different type."); + /// + /// Specify how many entities to Take/Limit + /// + /// The number to limit/take + public TSelf Limit(int takeCount) + { + _options.Limit = takeCount; + return This; + } - _options.Projection = Context.Cache().CombineWithRequiredProps(_options.Projection); - return This; - } + /// + /// Specify how to project the results using a lambda expression + /// + /// x => new Test { PropName = x.Prop } + public TSelf Project(Expression> expression) + { + return Project(p => p.Expression(expression)); + } - /// - /// Specify how to project the results using an exclusion projection expression. - /// - /// x => new { x.PropToExclude, x.AnotherPropToExclude } - public TSelf ProjectExcluding(Expression> exclusion) - { - var props = (exclusion.Body as NewExpression)?.Arguments - .Select(a => a.ToString().Split('.')[1]); + /// + /// Specify how to project the results using a projection expression + /// + /// p => p.Include("Prop1").Exclude("Prop2") + public TSelf Project(Func, ProjectionDefinition> projection) + { + _options.Projection = projection(Builders.Projection); + return This; + } - if (props == null || !props.Any()) - throw new ArgumentException("Unable to get any properties from the exclusion expression!"); + /// + /// Specify to automatically include all properties marked with [BsonRequired] attribute on the entity in the final projection. + /// HINT: this method should only be called after the .Project() method. + /// + public TSelf IncludeRequiredProps() + { + if (typeof(T) != typeof(TProjection)) + throw new InvalidOperationException("IncludeRequiredProps() cannot be used when projecting to a different type."); - var defs = new List>(props.Count()); + _options.Projection = Context.Cache().CombineWithRequiredProps(_options.Projection); + return This; + } - foreach (var prop in props) - { - defs.Add(Builders.Projection.Exclude(prop)); - } + /// + /// Specify how to project the results using an exclusion projection expression. + /// + /// x => new { x.PropToExclude, x.AnotherPropToExclude } + public TSelf ProjectExcluding(Expression> exclusion) + { + var props = (exclusion.Body as NewExpression)?.Arguments + .Select(a => a.ToString().Split('.')[1]); - _options.Projection = Builders.Projection.Combine(defs); + if (props == null || !props.Any()) + throw new ArgumentException("Unable to get any properties from the exclusion expression!"); - return This; - } + var defs = new List>(props.Count()); - /// - /// Specify an option for this find command (use multiple times if needed) - /// - /// x => x.OptionName = OptionValue - public TSelf Option(Action> option) + foreach (var prop in props) { - option(_options); - return This; + defs.Add(Builders.Projection.Exclude(prop)); } - private void AddTxtScoreToProjection(string propName) - { - if (_options.Projection == null) _options.Projection = "{}"; + _options.Projection = Builders.Projection.Combine(defs); - _options.Projection = - _options.Projection - .Render(BsonSerializer.SerializerRegistry.GetSerializer(), BsonSerializer.SerializerRegistry) - .Document.Add(propName, new BsonDocument { { "$meta", "textScore" } }); - } + return This; + } - /// - /// Sort the results of a text search by the MetaTextScore - /// TIP: Use this method after .Project() if you need to do a projection also - /// - public TSelf SortByTextScore() - { - return SortByTextScore(null); - } + /// + /// Specify an option for this find command (use multiple times if needed) + /// + /// x => x.OptionName = OptionValue + public TSelf Option(Action> option) + { + option(_options); + return This; + } - /// - /// Sort the results of a text search by the MetaTextScore and get back the score as well - /// TIP: Use this method after .Project() if you need to do a projection also - /// - /// x => x.TextScoreProp - public TSelf SortByTextScore(Expression>? scoreProperty) - { - switch (scoreProperty) - { - case null: - AddTxtScoreToProjection("_Text_Match_Score_"); - return Sort(s => s.MetaTextScore("_Text_Match_Score_")); + private void AddTxtScoreToProjection(string propName) + { + if (_options.Projection == null) _options.Projection = "{}"; - default: - AddTxtScoreToProjection(Prop.Path(scoreProperty)); - return Sort(s => s.MetaTextScore(Prop.Path(scoreProperty))); - } - } + _options.Projection = + _options.Projection + .Render(BsonSerializer.SerializerRegistry.GetSerializer(), BsonSerializer.SerializerRegistry) + .Document.Add(propName, new BsonDocument { { "$meta", "textScore" } }); } /// - /// Represents a MongoDB Find command. - /// TIP: Specify your criteria using .Match() .Sort() .Skip() .Take() .Project() .Option() methods and finally call .Execute() - /// Note: For building queries, use the DB.Fluent* interfaces + /// Sort the results of a text search by the MetaTextScore + /// TIP: Use this method after .Project() if you need to do a projection also /// - /// Any class that implements IEntity - public class Find : Find where T : IEntity + public TSelf SortByTextScore() { - internal Find(DBContext context, IMongoCollection collection) - : base(context, collection) { } - - internal Find(DBContext context, IMongoCollection collection, FindBase> baseQuery) - : base(context, collection, baseQuery) { } + return SortByTextScore(null); } - /// - /// Represents a MongoDB Find command with the ability to project to a different result type. - /// TIP: Specify your criteria using .Match() .Sort() .Skip() .Take() .Project() .Option() methods and finally call .Execute() + /// Sort the results of a text search by the MetaTextScore and get back the score as well + /// TIP: Use this method after .Project() if you need to do a projection also /// - /// Any class that implements IEntity - /// The type you'd like to project the results to. - public class Find : FindBase>, ICollectionRelated where T : IEntity + /// x => x.TextScoreProp + public TSelf SortByTextScore(Expression>? scoreProperty) { - - /// - /// copy constructor - /// - /// - /// - /// - internal Find(DBContext context, IMongoCollection collection, FindBase> other) : base(other) - { - Context = context; - Collection = collection; - } - internal Find(DBContext context, IMongoCollection collection) : base(context.GlobalFilters) + switch (scoreProperty) { - Context = context; - Collection = collection; + case null: + AddTxtScoreToProjection("_Text_Match_Score_"); + return Sort(s => s.MetaTextScore("_Text_Match_Score_")); + + default: + AddTxtScoreToProjection(Prop.Path(scoreProperty)); + return Sort(s => s.MetaTextScore(Prop.Path(scoreProperty))); } + } +} - public override DBContext Context { get; } - public IMongoCollection Collection { get; private set; } +/// +/// Represents a MongoDB Find command. +/// TIP: Specify your criteria using .Match() .Sort() .Skip() .Take() .Project() .Option() methods and finally call .Execute() +/// Note: For building queries, use the DB.Fluent* interfaces +/// +/// Any class that implements IEntity +/// Id type +public class Find : Find + where TId : IComparable, IEquatable + where T : IEntity +{ + internal Find(DBContext context, IMongoCollection collection) + : base(context, collection) { } + internal Find(DBContext context, IMongoCollection collection, FindBase> baseQuery) + : base(context, collection, baseQuery) { } +} - /// - /// Find a single IEntity by ID - /// - /// The unique ID of an IEntity - /// An optional cancellation token - /// A single entity or null if not found - public Task OneAsync(string ID, CancellationToken cancellation = default) - { - Match(ID); - return ExecuteSingleAsync(cancellation); - } +/// +/// Represents a MongoDB Find command with the ability to project to a different result type. +/// TIP: Specify your criteria using .Match() .Sort() .Skip() .Take() .Project() .Option() methods and finally call .Execute() +/// +/// Any class that implements IEntity +/// ID type +/// The type you'd like to project the results to. +public class Find : FindBase>, ICollectionRelated + where TId : IComparable, IEquatable + where T : IEntity +{ - /// - /// Find entities by supplying a lambda expression - /// - /// x => x.Property == Value - /// An optional cancellation token - /// A list of Entities - public Task> ManyAsync(Expression> expression, CancellationToken cancellation = default) - { - Match(expression); - return ExecuteAsync(cancellation); - } + /// + /// copy constructor + /// + /// + /// + /// + internal Find(DBContext context, IMongoCollection collection, FindBase> other) : base(other) + { + Context = context; + Collection = collection; + } + internal Find(DBContext context, IMongoCollection collection) : base(context.GlobalFilters) + { + Context = context; + Collection = collection; + } - /// - /// Find entities by supplying a filter expression - /// - /// f => f.Eq(x => x.Prop, Value) & f.Gt(x => x.Prop, Value) - /// An optional cancellation token - /// A list of Entities - public Task> ManyAsync(Func, FilterDefinition> filter, CancellationToken cancellation = default) - { - Match(filter); - return ExecuteAsync(cancellation); - } + public override DBContext Context { get; } + public IMongoCollection Collection { get; private set; } - /// - /// Run the Find command in MongoDB server and get a list of results - /// - /// An optional cancellation token - public async Task> ExecuteAsync(CancellationToken cancellation = default) - { - var list = new List(); - using (var cursor = await ExecuteCursorAsync(cancellation).ConfigureAwait(false)) - { - while (await cursor.MoveNextAsync(cancellation).ConfigureAwait(false)) - { - list.AddRange(cursor.Current); - } - } - return list; - } - /// - /// Run the Find command in MongoDB server and get a single result or the default value if not found. - /// If more than one entity is found, it will throw an exception. - /// - /// An optional cancellation token - public async Task ExecuteSingleAsync(CancellationToken cancellation = default) - { - Limit(2); - using var cursor = await ExecuteCursorAsync(cancellation).ConfigureAwait(false); - await cursor.MoveNextAsync(cancellation).ConfigureAwait(false); - return cursor.Current.SingleOrDefault(); - } - /// - /// Run the Find command in MongoDB server and get the first result or the default value if not found - /// - /// An optional cancellation token - public async Task ExecuteFirstAsync(CancellationToken cancellation = default) - { - Limit(1); - using var cursor = await ExecuteCursorAsync(cancellation).ConfigureAwait(false); - await cursor.MoveNextAsync(cancellation).ConfigureAwait(false); - return cursor.Current.SingleOrDefault(); //because we're limiting to 1 - } + /// + /// Find a single IEntity by ID + /// + /// The unique ID of an IEntity + /// An optional cancellation token + /// A single entity or null if not found + public Task OneAsync(TId ID, CancellationToken cancellation = default) + { + Match(ID); + return ExecuteSingleAsync(cancellation); + } - /// - /// Run the Find command and get back a bool indicating whether any entities matched the query - /// - /// An optional cancellation token - public async Task ExecuteAnyAsync(CancellationToken cancellation = default) - { - Project(b => b.Include(x => x.ID)); - Limit(1); - using var cursor = await ExecuteCursorAsync(cancellation).ConfigureAwait(false); - await cursor.MoveNextAsync(cancellation).ConfigureAwait(false); - return cursor.Current.Any(); - } + /// + /// Find entities by supplying a lambda expression + /// + /// x => x.Property == Value + /// An optional cancellation token + /// A list of Entities + public Task> ManyAsync(Expression> expression, CancellationToken cancellation = default) + { + Match(expression); + return ExecuteAsync(cancellation); + } - /// - /// Run the Find command in MongoDB server and get a cursor instead of materialized results - /// - /// An optional cancellation token - public Task> ExecuteCursorAsync(CancellationToken cancellation = default) + /// + /// Find entities by supplying a filter expression + /// + /// f => f.Eq(x => x.Prop, Value) & f.Gt(x => x.Prop, Value) + /// An optional cancellation token + /// A list of Entities + public Task> ManyAsync(Func, FilterDefinition> filter, CancellationToken cancellation = default) + { + Match(filter); + return ExecuteAsync(cancellation); + } + + /// + /// Run the Find command in MongoDB server and get a list of results + /// + /// An optional cancellation token + public async Task> ExecuteAsync(CancellationToken cancellation = default) + { + var list = new List(); + using (var cursor = await ExecuteCursorAsync(cancellation).ConfigureAwait(false)) { - if (_sorts.Count > 0) - _options.Sort = Builders.Sort.Combine(_sorts); + while (await cursor.MoveNextAsync(cancellation).ConfigureAwait(false)) + { + list.AddRange(cursor.Current); + } + } + return list; + } - var mergedFilter = Logic.MergeWithGlobalFilter(_ignoreGlobalFilters, _globalFilters, _filter); + /// + /// Run the Find command in MongoDB server and get a single result or the default value if not found. + /// If more than one entity is found, it will throw an exception. + /// + /// An optional cancellation token + public async Task ExecuteSingleAsync(CancellationToken cancellation = default) + { + Limit(2); + using var cursor = await ExecuteCursorAsync(cancellation).ConfigureAwait(false); + await cursor.MoveNextAsync(cancellation).ConfigureAwait(false); + return cursor.Current.SingleOrDefault(); + } - return this.Session() is not IClientSessionHandle session ? - Collection.FindAsync(mergedFilter, _options, cancellation) : - Collection.FindAsync(session, mergedFilter, _options, cancellation); - } + /// + /// Run the Find command in MongoDB server and get the first result or the default value if not found + /// + /// An optional cancellation token + public async Task ExecuteFirstAsync(CancellationToken cancellation = default) + { + Limit(1); + using var cursor = await ExecuteCursorAsync(cancellation).ConfigureAwait(false); + await cursor.MoveNextAsync(cancellation).ConfigureAwait(false); + return cursor.Current.SingleOrDefault(); //because we're limiting to 1 } - public enum Order + /// + /// Run the Find command and get back a bool indicating whether any entities matched the query + /// + /// An optional cancellation token + public async Task ExecuteAnyAsync(CancellationToken cancellation = default) { - Ascending, - Descending + Project(b => b.Include(x => x.ID)); + Limit(1); + using var cursor = await ExecuteCursorAsync(cancellation).ConfigureAwait(false); + await cursor.MoveNextAsync(cancellation).ConfigureAwait(false); + return cursor.Current.Any(); } - public enum Search + /// + /// Run the Find command in MongoDB server and get a cursor instead of materialized results + /// + /// An optional cancellation token + public Task> ExecuteCursorAsync(CancellationToken cancellation = default) { - Fuzzy, - Full + if (_sorts.Count > 0) + _options.Sort = Builders.Sort.Combine(_sorts); + + var mergedFilter = Logic.MergeWithGlobalFilter(_ignoreGlobalFilters, _globalFilters, _filter); + + return this.Session() is not IClientSessionHandle session ? + Collection.FindAsync(mergedFilter, _options, cancellation) : + Collection.FindAsync(session, mergedFilter, _options, cancellation); } } + +public enum Order +{ + Ascending, + Descending +} + +public enum Search +{ + Fuzzy, + Full +} diff --git a/MongoDB.Entities/Builders/ICollectionRelated.cs b/MongoDB.Entities/Builders/ICollectionRelated.cs index 5ca08ca05..4a31b2ce5 100644 --- a/MongoDB.Entities/Builders/ICollectionRelated.cs +++ b/MongoDB.Entities/Builders/ICollectionRelated.cs @@ -1,21 +1,15 @@ -using MongoDB.Driver; -using System; -using System.Collections.Generic; -using System.Text; -#nullable enable -namespace MongoDB.Entities +namespace MongoDB.Entities; + +internal interface ICollectionRelated +{ + public DBContext Context { get; } + public IMongoCollection Collection { get; } +} +internal static class ICollectionRelatedExt { - internal interface ICollectionRelated where T : IEntity + public static IClientSessionHandle? Session(this ICollectionRelated collectionRelated) { - public DBContext Context { get; } - public IMongoCollection Collection { get; } + return collectionRelated.Context.Session; } - internal static class ICollectionRelatedExt - { - public static IClientSessionHandle? Session(this ICollectionRelated collectionRelated) where T : IEntity - { - return collectionRelated.Context.Session; - } - } } diff --git a/MongoDB.Entities/Builders/Index.cs b/MongoDB.Entities/Builders/Index.cs index 3ed2209b6..0e1fbf1ca 100644 --- a/MongoDB.Entities/Builders/Index.cs +++ b/MongoDB.Entities/Builders/Index.cs @@ -1,191 +1,188 @@ -using MongoDB.Driver; -using System; -using System.Collections.Generic; -using System.Linq.Expressions; -using System.Threading; -using System.Threading.Tasks; - -namespace MongoDB.Entities +namespace MongoDB.Entities; + +/// +/// Represents an index creation command +/// TIP: Define the keys first with .Key() method and finally call the .Create() method. +/// +/// Any class that implements IEntity +/// Id type +public class Index : ICollectionRelated + where TId : IComparable, IEquatable + where T : IEntity { + internal List> Keys { get; set; } = new(); + public DBContext Context { get; } + public IMongoCollection Collection { get; } + + private readonly CreateIndexOptions _options = new() { Background = true }; + + internal Index(DBContext context, IMongoCollection collection) + { + Context = context; + Collection = collection; + } + /// - /// Represents an index creation command - /// TIP: Define the keys first with .Key() method and finally call the .Create() method. + /// Call this method to finalize defining the index after setting the index keys and options. /// - /// Any class that implements IEntity - public class Index : ICollectionRelated where T : IEntity + /// An optional cancellation token + /// The name of the created index + public async Task CreateAsync(CancellationToken cancellation = default) { - internal List> Keys { get; set; } = new List>(); - public DBContext Context { get; } - public IMongoCollection Collection { get; } - - private readonly CreateIndexOptions _options = new() { Background = true }; + if (Keys.Count == 0) throw new ArgumentException("Please define keys before calling this method."); - internal Index(DBContext context, IMongoCollection collection) - { - Context = context; - Collection = collection; - } + var propNames = new List(); + var keyDefs = new List>(); + var isTextIndex = false; - /// - /// Call this method to finalize defining the index after setting the index keys and options. - /// - /// An optional cancellation token - /// The name of the created index - public async Task CreateAsync(CancellationToken cancellation = default) + foreach (var key in Keys) { - if (Keys.Count == 0) throw new ArgumentException("Please define keys before calling this method."); - - var propNames = new List(); - var keyDefs = new List>(); - var isTextIndex = false; - - foreach (var key in Keys) - { - string keyType = string.Empty; - - switch (key.Type) - { - case KeyType.Ascending: - keyDefs.Add(Builders.IndexKeys.Ascending(key.PropertyName)); - keyType = "(Asc)"; - break; - case KeyType.Descending: - keyDefs.Add(Builders.IndexKeys.Descending(key.PropertyName)); - keyType = "(Dsc)"; - break; - case KeyType.Geo2D: - keyDefs.Add(Builders.IndexKeys.Geo2D(key.PropertyName)); - keyType = "(G2d)"; - break; - case KeyType.Geo2DSphere: - keyDefs.Add(Builders.IndexKeys.Geo2DSphere(key.PropertyName)); - keyType = "(Gsp)"; - break; - case KeyType.Hashed: - keyDefs.Add(Builders.IndexKeys.Hashed(key.PropertyName)); - keyType = "(Hsh)"; - break; - case KeyType.Text: - keyDefs.Add(Builders.IndexKeys.Text(key.PropertyName)); - isTextIndex = true; - break; - case KeyType.Wildcard: - keyDefs.Add(Builders.IndexKeys.Wildcard(key.PropertyName)); - keyType = "(Wld)"; - break; - } - propNames.Add(key.PropertyName + keyType); - } - - if (string.IsNullOrEmpty(_options.Name)) - { - if (isTextIndex) - _options.Name = "[TEXT]"; - else - _options.Name = string.Join(" | ", propNames); - } + string keyType = string.Empty; - var model = new CreateIndexModel( - Builders.IndexKeys.Combine(keyDefs), - _options); - - try - { - await CreateAsync(model, cancellation).ConfigureAwait(false); - } - catch (MongoCommandException x) when (x.Code == 85 || x.Code == 86) + switch (key.Type) { - await DropAsync(_options.Name, cancellation).ConfigureAwait(false); - await CreateAsync(model, cancellation).ConfigureAwait(false); + case KeyType.Ascending: + keyDefs.Add(Builders.IndexKeys.Ascending(key.PropertyName)); + keyType = "(Asc)"; + break; + case KeyType.Descending: + keyDefs.Add(Builders.IndexKeys.Descending(key.PropertyName)); + keyType = "(Dsc)"; + break; + case KeyType.Geo2D: + keyDefs.Add(Builders.IndexKeys.Geo2D(key.PropertyName)); + keyType = "(G2d)"; + break; + case KeyType.Geo2DSphere: + keyDefs.Add(Builders.IndexKeys.Geo2DSphere(key.PropertyName)); + keyType = "(Gsp)"; + break; + case KeyType.Hashed: + keyDefs.Add(Builders.IndexKeys.Hashed(key.PropertyName)); + keyType = "(Hsh)"; + break; + case KeyType.Text: + keyDefs.Add(Builders.IndexKeys.Text(key.PropertyName)); + isTextIndex = true; + break; + case KeyType.Wildcard: + keyDefs.Add(Builders.IndexKeys.Wildcard(key.PropertyName)); + keyType = "(Wld)"; + break; } - - return _options.Name; + propNames.Add(key.PropertyName + keyType); } - /// - /// Set the options for this index definition - /// TIP: Setting options is not required. - /// - /// x => x.OptionName = OptionValue - public Index Option(Action> option) + if (string.IsNullOrEmpty(_options.Name)) { - option(_options); - return this; + if (isTextIndex) + _options.Name = "[TEXT]"; + else + _options.Name = string.Join(" | ", propNames); } - /// - /// Adds a key definition to the index - /// TIP: At least one key definition is required - /// - /// x => x.PropertyName - /// The type of the key - public Index Key(Expression> propertyToIndex, KeyType type) - { - Keys.Add(new Key(propertyToIndex, type)); - return this; - } + var model = new CreateIndexModel( + Builders.IndexKeys.Combine(keyDefs), + _options); - /// - /// Drops an index by name for this entity type - /// - /// The name of the index to drop - /// An optional cancellation token - public async Task DropAsync(string name, CancellationToken cancellation = default) + try { - await Collection.Indexes.DropOneAsync(name, cancellation).ConfigureAwait(false); + await CreateAsync(model, cancellation).ConfigureAwait(false); } - - /// - /// Drops all indexes for this entity type - /// - /// An optional cancellation token - public async Task DropAllAsync(CancellationToken cancellation = default) + catch (MongoCommandException x) when (x.Code == 85 || x.Code == 86) { - await Collection.Indexes.DropAllAsync(cancellation).ConfigureAwait(false); + await DropAsync(_options.Name, cancellation).ConfigureAwait(false); + await CreateAsync(model, cancellation).ConfigureAwait(false); } - private Task CreateAsync(CreateIndexModel model, CancellationToken cancellation = default) - { - return Collection.Indexes.CreateOneAsync(model, cancellationToken: cancellation); - } + return _options.Name; } - internal class Key where T : IEntity + /// + /// Set the options for this index definition + /// TIP: Setting options is not required. + /// + /// x => x.OptionName = OptionValue + public Index Option(Action> option) { - internal string PropertyName { get; set; } - internal KeyType Type { get; set; } + option(_options); + return this; + } - internal Key(Expression> expression, KeyType type) - { - Type = type; + /// + /// Adds a key definition to the index + /// TIP: At least one key definition is required + /// + /// x => x.PropertyName + /// The type of the key + public Index Key(Expression> propertyToIndex, KeyType type) + { + Keys.Add(new Key(propertyToIndex, type)); + return this; + } - if (expression.Body.NodeType == ExpressionType.Parameter && type == KeyType.Text) - { - PropertyName = "$**"; - return; - } + /// + /// Drops an index by name for this entity type + /// + /// The name of the index to drop + /// An optional cancellation token + public async Task DropAsync(string name, CancellationToken cancellation = default) + { + await Collection.Indexes.DropOneAsync(name, cancellation).ConfigureAwait(false); + } - if (expression.Body.NodeType == ExpressionType.MemberAccess && type == KeyType.Text) - { - if (expression.PropertyInfo().PropertyType == typeof(FuzzyString)) - PropertyName = expression.FullPath() + ".Hash"; - else - PropertyName = expression.FullPath(); - return; - } + /// + /// Drops all indexes for this entity type + /// + /// An optional cancellation token + public async Task DropAllAsync(CancellationToken cancellation = default) + { + await Collection.Indexes.DropAllAsync(cancellation).ConfigureAwait(false); + } - PropertyName = expression.FullPath(); - } + private Task CreateAsync(CreateIndexModel model, CancellationToken cancellation = default) + { + return Collection.Indexes.CreateOneAsync(model, cancellationToken: cancellation); } +} + +internal class Key + where TId : IComparable, IEquatable + where T : IEntity +{ + internal string PropertyName { get; set; } + internal KeyType Type { get; set; } - public enum KeyType + internal Key(Expression> expression, KeyType type) { - Ascending, - Descending, - Geo2D, - Geo2DSphere, - Hashed, - Text, - Wildcard + Type = type; + + if (expression.Body.NodeType == ExpressionType.Parameter && type == KeyType.Text) + { + PropertyName = "$**"; + return; + } + + if (expression.Body.NodeType == ExpressionType.MemberAccess && type == KeyType.Text) + { + if (expression.PropertyInfo().PropertyType == typeof(FuzzyString)) + PropertyName = expression.FullPath() + ".Hash"; + else + PropertyName = expression.FullPath(); + return; + } + + PropertyName = expression.FullPath(); } } + +public enum KeyType +{ + Ascending, + Descending, + Geo2D, + Geo2DSphere, + Hashed, + Text, + Wildcard +} diff --git a/MongoDB.Entities/Builders/PagedSearch.cs b/MongoDB.Entities/Builders/PagedSearch.cs index 760d4b24e..73a7f0bb5 100644 --- a/MongoDB.Entities/Builders/PagedSearch.cs +++ b/MongoDB.Entities/Builders/PagedSearch.cs @@ -1,269 +1,267 @@ -using MongoDB.Bson; -using MongoDB.Bson.Serialization; -using MongoDB.Driver; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Threading; -using System.Threading.Tasks; - -namespace MongoDB.Entities +namespace MongoDB.Entities; + +public class PagedSearchBase : SortFilterQueryBase + where TId : IComparable, IEquatable + where T : IEntity + where TSelf : PagedSearchBase { - public class PagedSearchBase : SortFilterQueryBase where TSelf : PagedSearchBase where T : IEntity + internal PagedSearchBase(PagedSearchBase other) : base(other) { - internal PagedSearchBase(PagedSearchBase other) : base(other) - { - } + } - internal PagedSearchBase(Dictionary globalFilters) : base(globalFilters) - { - } - private TSelf This => (TSelf)this; - - internal IAggregateFluent? _fluentPipeline; - internal AggregateOptions _options = new(); - internal PipelineStageDefinition? _projectionStage; - internal int _pageNumber = 1, _pageSize = 100; - - /// - /// Begins the paged search aggregation pipeline with the provided fluent pipeline. - /// TIP: This method must be first in the chain and it cannot be used with .Match() - /// - /// The type of the input pipeline - /// The input IAggregateFluent pipeline - public TSelf WithFluent(TFluent fluentPipeline) where TFluent : IAggregateFluent - { - this._fluentPipeline = fluentPipeline; - return This; - } + internal PagedSearchBase(Dictionary globalFilters) : base(globalFilters) + { + } + private TSelf This => (TSelf)this; + internal IAggregateFluent? _fluentPipeline; + internal AggregateOptions _options = new(); + internal PipelineStageDefinition? _projectionStage; + internal int _pageNumber = 1, _pageSize = 100; + /// + /// Begins the paged search aggregation pipeline with the provided fluent pipeline. + /// TIP: This method must be first in the chain and it cannot be used with .Match() + /// + /// The type of the input pipeline + /// The input IAggregateFluent pipeline + public TSelf WithFluent(TFluent fluentPipeline) where TFluent : IAggregateFluent + { + this._fluentPipeline = fluentPipeline; + return This; + } - /// - /// Sort the results of a text search by the MetaTextScore - /// TIP: Use this method after .Project() if you need to do a projection also - /// - public TSelf SortByTextScore() - { - return SortByTextScore(null); - } - /// - /// Sort the results of a text search by the MetaTextScore and get back the score as well - /// TIP: Use this method after .Project() if you need to do a projection also - /// - /// x => x.TextScoreProp - public TSelf SortByTextScore(Expression>? scoreProperty) - { - switch (scoreProperty) - { - case null: - AddTxtScoreToProjection("_Text_Match_Score_"); - return Sort(s => s.MetaTextScore("_Text_Match_Score_")); - - default: - AddTxtScoreToProjection(Prop.Path(scoreProperty)); - return Sort(s => s.MetaTextScore(Prop.Path(scoreProperty))); - } - } - private void AddTxtScoreToProjection(string fieldName) - { - if (_projectionStage == null) - { - _projectionStage = $"{{ $set : {{ {fieldName} : {{ $meta : 'textScore' }} }} }}"; - return; - } + /// + /// Sort the results of a text search by the MetaTextScore + /// TIP: Use this method after .Project() if you need to do a projection also + /// + public TSelf SortByTextScore() + { + return SortByTextScore(null); + } - var renderedStage = _projectionStage.Render( - BsonSerializer.SerializerRegistry.GetSerializer(), - BsonSerializer.SerializerRegistry); + /// + /// Sort the results of a text search by the MetaTextScore and get back the score as well + /// TIP: Use this method after .Project() if you need to do a projection also + /// + /// x => x.TextScoreProp + public TSelf SortByTextScore(Expression>? scoreProperty) + { + switch (scoreProperty) + { + case null: + AddTxtScoreToProjection("_Text_Match_Score_"); + return Sort(s => s.MetaTextScore("_Text_Match_Score_")); - renderedStage.Document["$project"][fieldName] = new BsonDocument { { "$meta", "textScore" } }; + default: + AddTxtScoreToProjection(Prop.Path(scoreProperty)); + return Sort(s => s.MetaTextScore(Prop.Path(scoreProperty))); + } + } - _projectionStage = renderedStage.Document; + private void AddTxtScoreToProjection(string fieldName) + { + if (_projectionStage == null) + { + _projectionStage = $"{{ $set : {{ {fieldName} : {{ $meta : 'textScore' }} }} }}"; + return; } + var renderedStage = _projectionStage.Render( + BsonSerializer.SerializerRegistry.GetSerializer(), + BsonSerializer.SerializerRegistry); + renderedStage.Document["$project"][fieldName] = new BsonDocument { { "$meta", "textScore" } }; - /// - /// Specify the page number to get - /// - /// The page number - public TSelf PageNumber(int pageNumber) - { - this._pageNumber = pageNumber; - return This; - } + _projectionStage = renderedStage.Document; + } - /// - /// Specify the number of items per page - /// - /// The size of a page - public TSelf PageSize(int pageSize) - { - this._pageSize = pageSize; - return This; - } - /// - /// Specify how to project the results using a lambda expression - /// - /// x => new Test { PropName = x.Prop } - public TSelf Project(Expression> expression) - { - _projectionStage = PipelineStageDefinitionBuilder.Project(expression); - return This; - } - /// - /// Specify how to project the results using a projection expression - /// - /// p => p.Include("Prop1").Exclude("Prop2") - public TSelf Project(Func, ProjectionDefinition> projection) - { - _projectionStage = PipelineStageDefinitionBuilder.Project(projection(Builders.Projection)); - return This; - } + /// + /// Specify the page number to get + /// + /// The page number + public TSelf PageNumber(int pageNumber) + { + this._pageNumber = pageNumber; + return This; + } - /// - /// Specify how to project the results using an exclusion projection expression. - /// - /// x => new { x.PropToExclude, x.AnotherPropToExclude } - public TSelf ProjectExcluding(Expression> exclusion) - { - var props = (exclusion.Body as NewExpression)?.Arguments - .Select(a => a.ToString().Split('.')[1]); + /// + /// Specify the number of items per page + /// + /// The size of a page + public TSelf PageSize(int pageSize) + { + this._pageSize = pageSize; + return This; + } - if (props == null || !props.Any()) - throw new ArgumentException("Unable to get any properties from the exclusion expression!"); + /// + /// Specify how to project the results using a lambda expression + /// + /// x => new Test { PropName = x.Prop } + public TSelf Project(Expression> expression) + { + _projectionStage = PipelineStageDefinitionBuilder.Project(expression); + return This; + } - var defs = new List>(props.Count()); + /// + /// Specify how to project the results using a projection expression + /// + /// p => p.Include("Prop1").Exclude("Prop2") + public TSelf Project(Func, ProjectionDefinition> projection) + { + _projectionStage = PipelineStageDefinitionBuilder.Project(projection(Builders.Projection)); + return This; + } - foreach (var prop in props) - { - defs.Add(Builders.Projection.Exclude(prop)); - } + /// + /// Specify how to project the results using an exclusion projection expression. + /// + /// x => new { x.PropToExclude, x.AnotherPropToExclude } + public TSelf ProjectExcluding(Expression> exclusion) + { + var props = (exclusion.Body as NewExpression)?.Arguments + .Select(a => a.ToString().Split('.')[1]); - _projectionStage = PipelineStageDefinitionBuilder.Project(Builders.Projection.Combine(defs)); + if (props == null || !props.Any()) + throw new ArgumentException("Unable to get any properties from the exclusion expression!"); - return This; - } + var defs = new List>(props.Count()); - /// - /// Specify an option for this find command (use multiple times if needed) - /// - /// x => x.OptionName = OptionValue - public TSelf Option(Action option) + foreach (var prop in props) { - option(_options); - return This; + defs.Add(Builders.Projection.Exclude(prop)); } + _projectionStage = PipelineStageDefinitionBuilder.Project(Builders.Projection.Combine(defs)); + return This; } /// - /// Represents an aggregation query that retrieves results with easy paging support. + /// Specify an option for this find command (use multiple times if needed) /// - /// Any class that implements IEntity - public class PagedSearch : PagedSearch where T : IEntity + /// x => x.OptionName = OptionValue + public TSelf Option(Action option) { - internal PagedSearch( - DBContext context, IMongoCollection collection) - : base(context, collection) { } + option(_options); + return This; } + +} + +/// +/// Represents an aggregation query that retrieves results with easy paging support. +/// +/// Any class that implements IEntity +/// Id type +public class PagedSearch : PagedSearch + where TId : IComparable, IEquatable + where T : IEntity +{ + internal PagedSearch( + DBContext context, IMongoCollection collection) + : base(context, collection) { } +} + +/// +/// Represents an aggregation query that retrieves results with easy paging support. +/// +/// Any class that implements IEntity +/// Id type +/// The type you'd like to project the results to. +public class PagedSearch : PagedSearchBase> + where TId : IComparable, IEquatable + where T : IEntity +{ + + public DBContext Context { get; set; } + public IMongoCollection Collection { get; set; } + + internal PagedSearch(DBContext context, IMongoCollection collection) : base(context.GlobalFilters) + { + var type = typeof(TProjection); + if (type.IsPrimitive || type.IsValueType || (type == typeof(string))) + throw new NotSupportedException("Projecting to primitive types is not supported!"); + Context = context; + Collection = collection; + } + + + /// - /// Represents an aggregation query that retrieves results with easy paging support. + /// Run the aggregation search command in MongoDB server and get a page of results and total + page count /// - /// Any class that implements IEntity - /// The type you'd like to project the results to. - public class PagedSearch : PagedSearchBase> where T : IEntity + /// An optional cancellation token + public async Task<(IReadOnlyList Results, long TotalCount, int PageCount)> ExecuteAsync(CancellationToken cancellation = default) { + if (_filter != Builders.Filter.Empty && _fluentPipeline != null) + throw new InvalidOperationException(".Match() and .WithFluent() cannot be used together!"); - public DBContext Context { get; set; } - public IMongoCollection Collection { get; set; } + var pipelineStages = new List(4); - internal PagedSearch(DBContext context, IMongoCollection collection) : base(context.GlobalFilters) - { - var type = typeof(TProjection); - if (type.IsPrimitive || type.IsValueType || (type == typeof(string))) - throw new NotSupportedException("Projecting to primitive types is not supported!"); - Context = context; - Collection = collection; - } + if (_sorts.Count == 0) + throw new InvalidOperationException("Paging without sorting is a sin!"); + else + pipelineStages.Add(PipelineStageDefinitionBuilder.Sort(Builders.Sort.Combine(_sorts))); + pipelineStages.Add(PipelineStageDefinitionBuilder.Skip((_pageNumber - 1) * _pageSize)); + pipelineStages.Add(PipelineStageDefinitionBuilder.Limit(_pageSize)); + if (_projectionStage != null) + pipelineStages.Add(_projectionStage); - /// - /// Run the aggregation search command in MongoDB server and get a page of results and total + page count - /// - /// An optional cancellation token - public async Task<(IReadOnlyList Results, long TotalCount, int PageCount)> ExecuteAsync(CancellationToken cancellation = default) - { - if (_filter != Builders.Filter.Empty && _fluentPipeline != null) - throw new InvalidOperationException(".Match() and .WithFluent() cannot be used together!"); + var resultsFacet = AggregateFacet.Create("_results", pipelineStages); - var pipelineStages = new List(4); + var countFacet = AggregateFacet.Create("_count", + PipelineDefinition.Create(new[] + { + PipelineStageDefinitionBuilder.Count() + })); - if (_sorts.Count == 0) - throw new InvalidOperationException("Paging without sorting is a sin!"); - else - pipelineStages.Add(PipelineStageDefinitionBuilder.Sort(Builders.Sort.Combine(_sorts))); + AggregateFacetResults facetResult; - pipelineStages.Add(PipelineStageDefinitionBuilder.Skip((_pageNumber - 1) * _pageSize)); - pipelineStages.Add(PipelineStageDefinitionBuilder.Limit(_pageSize)); + if (_fluentPipeline == null) //.Match() used + { + var filterDef = Logic.MergeWithGlobalFilter(_ignoreGlobalFilters, _globalFilters, _filter); - if (_projectionStage != null) - pipelineStages.Add(_projectionStage); + facetResult = + Context.Session is not IClientSessionHandle session + ? await Collection.Aggregate(_options).Match(filterDef).Facet(countFacet, resultsFacet).SingleAsync(cancellation).ConfigureAwait(false) + : await Collection.Aggregate(session, _options).Match(filterDef).Facet(countFacet, resultsFacet).SingleAsync(cancellation).ConfigureAwait(false); + } + else //.WithFluent() used + { + facetResult = await _fluentPipeline + .Facet(countFacet, resultsFacet) + .SingleAsync(cancellation) + .ConfigureAwait(false); + } - var resultsFacet = AggregateFacet.Create("_results", pipelineStages); + long matchCount = ( + facetResult.Facets + .Single(x => x.Name == "_count") + .Output().FirstOrDefault()?.Count + ) ?? 0; - var countFacet = AggregateFacet.Create("_count", - PipelineDefinition.Create(new[] - { - PipelineStageDefinitionBuilder.Count() - })); + int pageCount = + matchCount > 0 && matchCount <= _pageSize + ? 1 + : (int)Math.Ceiling((double)matchCount / _pageSize); - AggregateFacetResults facetResult; + var results = facetResult.Facets + .First(x => x.Name == "_results") + .Output(); - if (_fluentPipeline == null) //.Match() used - { - var filterDef = Logic.MergeWithGlobalFilter(_ignoreGlobalFilters, _globalFilters, _filter); - - facetResult = - Context.Session is not IClientSessionHandle session - ? await Collection.Aggregate(_options).Match(filterDef).Facet(countFacet, resultsFacet).SingleAsync(cancellation).ConfigureAwait(false) - : await Collection.Aggregate(session, _options).Match(filterDef).Facet(countFacet, resultsFacet).SingleAsync(cancellation).ConfigureAwait(false); - } - else //.WithFluent() used - { - facetResult = await _fluentPipeline - .Facet(countFacet, resultsFacet) - .SingleAsync(cancellation) - .ConfigureAwait(false); - } - - long matchCount = ( - facetResult.Facets - .Single(x => x.Name == "_count") - .Output().FirstOrDefault()?.Count - ) ?? 0; - - int pageCount = - matchCount > 0 && matchCount <= _pageSize - ? 1 - : (int)Math.Ceiling((double)matchCount / _pageSize); - - var results = facetResult.Facets - .First(x => x.Name == "_results") - .Output(); - - return (results, matchCount, pageCount); - } + return (results, matchCount, pageCount); } } diff --git a/MongoDB.Entities/Builders/Replace.cs b/MongoDB.Entities/Builders/Replace.cs index 1f02ea2d5..a47d203d0 100644 --- a/MongoDB.Entities/Builders/Replace.cs +++ b/MongoDB.Entities/Builders/Replace.cs @@ -1,136 +1,129 @@ -using MongoDB.Bson; -using MongoDB.Bson.Serialization; -using MongoDB.Driver; -using System; -using System.Collections.Generic; -using System.Linq.Expressions; -using System.Threading; -using System.Threading.Tasks; - -namespace MongoDB.Entities +namespace MongoDB.Entities; + +/// +/// Represents an UpdateOne command, which can replace the first matched document with a given entity +/// TIP: Specify a filter first with the .Match(). Then set entity with .WithEntity() and finally call .Execute() to run the command. +/// +/// Any class that implements IEntity +/// ID type +public class Replace : FilterQueryBase>, ICollectionRelated + where TId : IComparable, IEquatable + where T : IEntity { + private ReplaceOptions _options = new(); + private readonly List> _models = new(); + private readonly ModifiedBy? _modifiedBy; + private readonly Action? _onSaveAction; + private T? _entity; + + public DBContext Context { get; } + public IMongoCollection Collection { get; } + + internal Replace( + DBContext context, + IMongoCollection collection, + Action? onSaveAction) : base(context.GlobalFilters) + { + Context = context; + Collection = collection; + _modifiedBy = context.ModifiedBy; + _onSaveAction = onSaveAction; + } + + /// - /// Represents an UpdateOne command, which can replace the first matched document with a given entity - /// TIP: Specify a filter first with the .Match(). Then set entity with .WithEntity() and finally call .Execute() to run the command. + /// Supply the entity to replace the first matched document with + /// TIP: If the entity ID is empty, a new ID will be generated before being stored /// - /// Any class that implements IEntity - public class Replace : FilterQueryBase>, ICollectionRelated where T : IEntity + /// + public Replace WithEntity(T entity) { - private ReplaceOptions _options = new(); - private readonly List> _models = new(); - private readonly ModifiedBy? _modifiedBy; - private readonly Action? _onSaveAction; - private T? _entity; - - public DBContext Context { get; } - public IMongoCollection Collection { get; } - - internal Replace( - DBContext context, - IMongoCollection collection, - Action? onSaveAction) : base(context.GlobalFilters) - { - Context = context; - Collection = collection; - _modifiedBy = context.ModifiedBy; - _onSaveAction = onSaveAction; - } + if (entity.ID is null) + throw new InvalidOperationException("Cannot replace an entity with an empty ID value!"); + _onSaveAction?.Invoke(entity); - /// - /// Supply the entity to replace the first matched document with - /// TIP: If the entity ID is empty, a new ID will be generated before being stored - /// - /// - public Replace WithEntity(T entity) - { - if (string.IsNullOrEmpty(entity.ID)) - throw new InvalidOperationException("Cannot replace an entity with an empty ID value!"); + _entity = entity; + return this; + } - _onSaveAction?.Invoke(entity); + /// + /// Specify an option for this replace command (use multiple times if needed) + /// TIP: Setting options is not required + /// + /// x => x.OptionName = OptionValue + public Replace Option(Action option) + { + option(_options); + return this; + } - _entity = entity; - return this; - } - /// - /// Specify an option for this replace command (use multiple times if needed) - /// TIP: Setting options is not required - /// - /// x => x.OptionName = OptionValue - public Replace Option(Action option) + /// + /// Queue up a replace command for bulk execution later. + /// + public Replace AddToQueue() + { + var mergedFilter = Logic.MergeWithGlobalFilter(_ignoreGlobalFilters, _globalFilters, _filter); + if (mergedFilter == Builders.Filter.Empty) throw new ArgumentException("Please use Match() method first!"); + if (_entity == null) throw new ArgumentException("Please use WithEntity() method first!"); + SetModOnAndByValues(); + + _models.Add(new ReplaceOneModel(mergedFilter, _entity) { - option(_options); - return this; - } + Collation = _options.Collation, + Hint = _options.Hint, + IsUpsert = _options.IsUpsert + }); + _filter = Builders.Filter.Empty; + _entity = default; + _options = new ReplaceOptions(); + return this; + } + + /// + /// Run the replace command in MongoDB. + /// + /// An optional cancellation token + public async Task ExecuteAsync(CancellationToken cancellation = default) + { + if (_models.Count > 0) + { + var bulkWriteResult = await ( + this.Session() is not IClientSessionHandle session + ? Collection.BulkWriteAsync(_models, null, cancellation) + : Collection.BulkWriteAsync(session, _models, null, cancellation) + ).ConfigureAwait(false); + _models.Clear(); - /// - /// Queue up a replace command for bulk execution later. - /// - public Replace AddToQueue() + if (!bulkWriteResult.IsAcknowledged) + return ReplaceOneResult.Unacknowledged.Instance; + + return new ReplaceOneResult.Acknowledged(bulkWriteResult.MatchedCount, bulkWriteResult.ModifiedCount, null); + } + else { - var mergedFilter = Logic.MergeWithGlobalFilter(_ignoreGlobalFilters, _globalFilters, _filter); + var mergedFilter = MergedFilter; if (mergedFilter == Builders.Filter.Empty) throw new ArgumentException("Please use Match() method first!"); if (_entity == null) throw new ArgumentException("Please use WithEntity() method first!"); SetModOnAndByValues(); - _models.Add(new ReplaceOneModel(mergedFilter, _entity) - { - Collation = _options.Collation, - Hint = _options.Hint, - IsUpsert = _options.IsUpsert - }); - _filter = Builders.Filter.Empty; - _entity = default; - _options = new ReplaceOptions(); - return this; - } - - /// - /// Run the replace command in MongoDB. - /// - /// An optional cancellation token - public async Task ExecuteAsync(CancellationToken cancellation = default) - { - if (_models.Count > 0) - { - var bulkWriteResult = await ( - this.Session() is not IClientSessionHandle session - ? Collection.BulkWriteAsync(_models, null, cancellation) - : Collection.BulkWriteAsync(session, _models, null, cancellation) - ).ConfigureAwait(false); - - _models.Clear(); - - if (!bulkWriteResult.IsAcknowledged) - return ReplaceOneResult.Unacknowledged.Instance; - - return new ReplaceOneResult.Acknowledged(bulkWriteResult.MatchedCount, bulkWriteResult.ModifiedCount, null); - } - else - { - var mergedFilter = MergedFilter; - if (mergedFilter == Builders.Filter.Empty) throw new ArgumentException("Please use Match() method first!"); - if (_entity == null) throw new ArgumentException("Please use WithEntity() method first!"); - SetModOnAndByValues(); - - return this.Session() is not IClientSessionHandle session - ? await Collection.ReplaceOneAsync(mergedFilter, _entity, _options, cancellation).ConfigureAwait(false) - : await Collection.ReplaceOneAsync(session, mergedFilter, _entity, _options, cancellation).ConfigureAwait(false); - } + return this.Session() is not IClientSessionHandle session + ? await Collection.ReplaceOneAsync(mergedFilter, _entity, _options, cancellation).ConfigureAwait(false) + : await Collection.ReplaceOneAsync(session, mergedFilter, _entity, _options, cancellation).ConfigureAwait(false); } + } - private void SetModOnAndByValues() + private void SetModOnAndByValues() + { + var cache = Context.Cache(); + if (cache.HasModifiedOn && _entity is IModifiedOn _entityModifiedOn) _entityModifiedOn.ModifiedOn = DateTime.UtcNow; + if (cache.ModifiedByProp != null && _modifiedBy != null) { - var cache = Context.Cache(); - if (cache.HasModifiedOn && _entity is IModifiedOn _entityModifiedOn) _entityModifiedOn.ModifiedOn = DateTime.UtcNow; - if (cache.ModifiedByProp != null && _modifiedBy != null) - { - cache.ModifiedByProp.SetValue( - _entity, - BsonSerializer.Deserialize(_modifiedBy.ToBson(), cache.ModifiedByProp.PropertyType)); - } + cache.ModifiedByProp.SetValue( + _entity, + BsonSerializer.Deserialize(_modifiedBy.ToBson(), cache.ModifiedByProp.PropertyType)); } } } diff --git a/MongoDB.Entities/Builders/SortFilterQueryBase.cs b/MongoDB.Entities/Builders/SortFilterQueryBase.cs index 08eb7d9ab..0a1e97d64 100644 --- a/MongoDB.Entities/Builders/SortFilterQueryBase.cs +++ b/MongoDB.Entities/Builders/SortFilterQueryBase.cs @@ -1,48 +1,45 @@ -using MongoDB.Driver; -using System; -using System.Collections.Generic; -using System.Linq.Expressions; -#nullable enable -namespace MongoDB.Entities +namespace MongoDB.Entities; + +public abstract class SortFilterQueryBase : FilterQueryBase + where TId : IComparable, IEquatable + where T : IEntity + where TSelf : SortFilterQueryBase { - public abstract class SortFilterQueryBase : FilterQueryBase where T : IEntity where TSelf : SortFilterQueryBase - { - internal List> _sorts = new(); - private TSelf This => (TSelf)this; + internal List> _sorts = new(); + private TSelf This => (TSelf)this; - internal SortFilterQueryBase(SortFilterQueryBase other) : base(other) - { - _sorts = other._sorts; - } - internal SortFilterQueryBase(Dictionary globalFilters) : base(globalFilters: globalFilters) - { - } + internal SortFilterQueryBase(SortFilterQueryBase other) : base(other) + { + _sorts = other._sorts; + } + internal SortFilterQueryBase(Dictionary globalFilters) : base(globalFilters: globalFilters) + { + } - /// - /// Specify which property and order to use for sorting (use multiple times if needed) - /// - /// x => x.Prop - /// The sort order - public TSelf Sort(Expression> propertyToSortBy, Order sortOrder) + /// + /// Specify which property and order to use for sorting (use multiple times if needed) + /// + /// x => x.Prop + /// The sort order + public TSelf Sort(Expression> propertyToSortBy, Order sortOrder) + { + return sortOrder switch { - return sortOrder switch - { - Order.Ascending => Sort(s => s.Ascending(propertyToSortBy)), - Order.Descending => Sort(s => s.Descending(propertyToSortBy)), - _ => This, - }; - } + Order.Ascending => Sort(s => s.Ascending(propertyToSortBy)), + Order.Descending => Sort(s => s.Descending(propertyToSortBy)), + _ => This, + }; + } - /// - /// Specify how to sort using a sort expression - /// - /// s => s.Ascending("Prop1").MetaTextScore("Prop2") - /// - public TSelf Sort(Func, SortDefinition> sortFunction) - { - _sorts.Add(sortFunction(Builders.Sort)); - return This; - } + /// + /// Specify how to sort using a sort expression + /// + /// s => s.Ascending("Prop1").MetaTextScore("Prop2") + /// + public TSelf Sort(Func, SortDefinition> sortFunction) + { + _sorts.Add(sortFunction(Builders.Sort)); + return This; } } diff --git a/MongoDB.Entities/Builders/Update.cs b/MongoDB.Entities/Builders/Update.cs index 0f9426fbb..e9fb8891e 100644 --- a/MongoDB.Entities/Builders/Update.cs +++ b/MongoDB.Entities/Builders/Update.cs @@ -1,364 +1,360 @@ -using MongoDB.Bson.Serialization; -using MongoDB.Driver; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Threading; -using System.Threading.Tasks; - -namespace MongoDB.Entities -{ - public abstract class UpdateBase : FilterQueryBase where T : IEntity where TSelf : UpdateBase - { - protected readonly List> defs; - protected readonly Action? onUpdateAction; - - public abstract DBContext Context { get; } - private EntityCache? _cache; - internal EntityCache Cache() => _cache ??= Context.Cache(); +namespace MongoDB.Entities; - internal UpdateBase(UpdateBase other) : base(other) - { - onUpdateAction = other.onUpdateAction; - defs = other.defs; - } - private TSelf This => (TSelf)this; - internal UpdateBase(Dictionary globalFilters, Action? onUpdateAction = null, List>? defs = null) : base(globalFilters) - { - this.onUpdateAction = onUpdateAction; - this.defs = defs ?? new(); - } +public abstract class UpdateBase : FilterQueryBase + where TId : IComparable, IEquatable + where T : IEntity + where TSelf : UpdateBase +{ + protected readonly List> defs; + protected readonly Action? onUpdateAction; - /// - /// Specify the property and it's value to modify (use multiple times if needed) - /// - /// x => x.Property - /// The value to set on the property - public void AddModification(Expression> property, TProp value) - { - defs.Add(Builders.Update.Set(property, value)); - } + public abstract DBContext Context { get; } + private EntityCache? _cache; + internal EntityCache Cache() => _cache ??= Context.Cache(); - /// - /// Specify the update definition builder operation to modify the Entities (use multiple times if needed) - /// - /// b => b.Inc(x => x.PropName, Value) - public void AddModification(Func, UpdateDefinition> operation) - { - defs.Add(operation(Builders.Update)); - } + internal UpdateBase(UpdateBase other) : base(other) + { + onUpdateAction = other.onUpdateAction; + defs = other.defs; + } + private TSelf This => (TSelf)this; + internal UpdateBase(Dictionary globalFilters, Action? onUpdateAction = null, List>? defs = null) : base(globalFilters) + { + this.onUpdateAction = onUpdateAction; + this.defs = defs ?? new(); + } - /// - /// Specify an update (json string) to modify the Entities (use multiple times if needed) - /// - /// { $set: { 'RootProp.$[x].SubProp' : 321 } } - public void AddModification(string update) - { - defs.Add(update); - } + /// + /// Specify the property and it's value to modify (use multiple times if needed) + /// + /// x => x.Property + /// The value to set on the property + public void AddModification(Expression> property, TProp value) + { + defs.Add(Builders.Update.Set(property, value)); + } - /// - /// Specify an update with a Template to modify the Entities (use multiple times if needed) - /// - /// A Template with a single update - public void AddModification(Template template) - { - AddModification(template.RenderToString()); - } + /// + /// Specify the update definition builder operation to modify the Entities (use multiple times if needed) + /// + /// b => b.Inc(x => x.PropName, Value) + public void AddModification(Func, UpdateDefinition> operation) + { + defs.Add(operation(Builders.Update)); + } + /// + /// Specify an update (json string) to modify the Entities (use multiple times if needed) + /// + /// { $set: { 'RootProp.$[x].SubProp' : 321 } } + public void AddModification(string update) + { + defs.Add(update); + } - /// - /// Specify the property and it's value to modify (use multiple times if needed) - /// - /// x => x.Property - /// The value to set on the property - public TSelf Modify(Expression> property, TProp value) - { - AddModification(property, value); - return This; - } + /// + /// Specify an update with a Template to modify the Entities (use multiple times if needed) + /// + /// A Template with a single update + public void AddModification(Template template) + { + AddModification(template.RenderToString()); + } - /// - /// Specify the update definition builder operation to modify the Entities (use multiple times if needed) - /// - /// b => b.Inc(x => x.PropName, Value) - /// - public TSelf Modify(Func, UpdateDefinition> operation) - { - AddModification(operation); - return This; - } - /// - /// Specify an update (json string) to modify the Entities (use multiple times if needed) - /// - /// { $set: { 'RootProp.$[x].SubProp' : 321 } } - public TSelf Modify(string update) - { - AddModification(update); - return This; - } + /// + /// Specify the property and it's value to modify (use multiple times if needed) + /// + /// x => x.Property + /// The value to set on the property + public TSelf Modify(Expression> property, TProp value) + { + AddModification(property, value); + return This; + } - /// - /// Specify an update with a Template to modify the Entities (use multiple times if needed) - /// - /// A Template with a single update - public TSelf Modify(Template template) - { - AddModification(template.RenderToString()); - return This; - } + /// + /// Specify the update definition builder operation to modify the Entities (use multiple times if needed) + /// + /// b => b.Inc(x => x.PropName, Value) + /// + public TSelf Modify(Func, UpdateDefinition> operation) + { + AddModification(operation); + return This; + } - /// - /// Modify ALL properties with the values from the supplied entity instance. - /// - /// The entity instance to read the property values from - public TSelf ModifyWith(T entity) - { + /// + /// Specify an update (json string) to modify the Entities (use multiple times if needed) + /// + /// { $set: { 'RootProp.$[x].SubProp' : 321 } } + public TSelf Modify(string update) + { + AddModification(update); + return This; + } - if (Cache().HasModifiedOn) ((IModifiedOn)entity).ModifiedOn = DateTime.UtcNow; - defs.AddRange(Logic.BuildUpdateDefs(entity, Context)); - return This; - } + /// + /// Specify an update with a Template to modify the Entities (use multiple times if needed) + /// + /// A Template with a single update + public TSelf Modify(Template template) + { + AddModification(template.RenderToString()); + return This; + } - /// - /// Modify ONLY the specified properties with the values from a given entity instance. - /// - /// A new expression with the properties to include. Ex: x => new { x.PropOne, x.PropTwo } - /// The entity instance to read the corresponding values from - public TSelf ModifyOnly(Expression> members, T entity) - { - if (Cache().HasModifiedOn) ((IModifiedOn)entity).ModifiedOn = DateTime.UtcNow; - defs.AddRange(Logic.BuildUpdateDefs(entity, members)); - return This; - } + /// + /// Modify ALL properties with the values from the supplied entity instance. + /// + /// The entity instance to read the property values from + public TSelf ModifyWith(T entity) + { - /// - /// Modify all EXCEPT the specified properties with the values from a given entity instance. - /// - /// Supply a new expression with the properties to exclude. Ex: x => new { x.Prop1, x.Prop2 } - /// The entity instance to read the corresponding values from - public TSelf ModifyExcept(Expression> members, T entity) - { - if (Cache().HasModifiedOn) ((IModifiedOn)entity).ModifiedOn = DateTime.UtcNow; - defs.AddRange(Logic.BuildUpdateDefs(entity, members, excludeMode: true)); - return This; - } + if (Cache().HasModifiedOn) ((IModifiedOn)entity).ModifiedOn = DateTime.UtcNow; + defs.AddRange(Logic.BuildUpdateDefs(entity, Context)); + return This; } /// - /// Represents an update command - /// TIP: Specify a filter first with the .Match(). Then set property values with .Modify() and finally call .Execute() to run the command. + /// Modify ONLY the specified properties with the values from a given entity instance. /// - /// Any class that implements IEntity - public class Update : UpdateBase>, ICollectionRelated where T : IEntity + /// A new expression with the properties to include. Ex: x => new { x.PropOne, x.PropTwo } + /// The entity instance to read the corresponding values from + public TSelf ModifyOnly(Expression> members, T entity) { - private readonly List> _stages = new(); - private UpdateOptions _options = new(); - private readonly List> _models = new(); + if (Cache().HasModifiedOn) ((IModifiedOn)entity).ModifiedOn = DateTime.UtcNow; + defs.AddRange(Logic.BuildUpdateDefs(entity, members, Context)); + return This; + } - internal Update(DBContext context, IMongoCollection collection, UpdateBase> other) : base(other) - { - Context = context; - Collection = collection; - } + /// + /// Modify all EXCEPT the specified properties with the values from a given entity instance. + /// + /// Supply a new expression with the properties to exclude. Ex: x => new { x.Prop1, x.Prop2 } + /// The entity instance to read the corresponding values from + public TSelf ModifyExcept(Expression> members, T entity) + { + if (Cache().HasModifiedOn) ((IModifiedOn)entity).ModifiedOn = DateTime.UtcNow; + defs.AddRange(Logic.BuildUpdateDefs(entity, members, Context, excludeMode: true)); + return This; + } +} - internal Update(DBContext context, IMongoCollection collection, Action>? onUpdateAction, List>? defs = null) : base(context.GlobalFilters, onUpdateAction, defs) - { - Context = context; - Collection = collection; - } +/// +/// Represents an update command +/// TIP: Specify a filter first with the .Match(). Then set property values with .Modify() and finally call .Execute() to run the command. +/// +/// Any class that implements IEntity +/// ID type +public class Update : UpdateBase>, ICollectionRelated + where TId : IComparable, IEquatable + where T : IEntity +{ + private readonly List> _stages = new(); + private UpdateOptions _options = new(); + private readonly List> _models = new(); - public override DBContext Context { get; } - public IMongoCollection Collection { get; } + internal Update(DBContext context, IMongoCollection collection, UpdateBase> other) : base(other) + { + Context = context; + Collection = collection; + } + internal Update(DBContext context, IMongoCollection collection, Action>? onUpdateAction, List>? defs = null) : base(context.GlobalFilters, onUpdateAction, defs) + { + Context = context; + Collection = collection; + } - /// - /// Specify an update pipeline with multiple stages using a Template to modify the Entities. - /// NOTE: pipeline updates and regular updates cannot be used together. - /// - /// A Template object containing multiple pipeline stages - public Update WithPipeline(Template template) - { - foreach (var stage in template.ToStages()) - { - _stages.Add(stage); - } + public override DBContext Context { get; } + public IMongoCollection Collection { get; } - return this; - } - /// - /// Specify an update pipeline stage to modify the Entities (use multiple times if needed) - /// NOTE: pipeline updates and regular updates cannot be used together. - /// - /// { $set: { FullName: { $concat: ['$Name', ' ', '$Surname'] } } } - public Update WithPipelineStage(string stage) + /// + /// Specify an update pipeline with multiple stages using a Template to modify the Entities. + /// NOTE: pipeline updates and regular updates cannot be used together. + /// + /// A Template object containing multiple pipeline stages + public Update WithPipeline(Template template) + { + foreach (var stage in template.ToStages()) { _stages.Add(stage); - return this; } - /// - /// Specify an update pipeline stage using a Template to modify the Entities (use multiple times if needed) - /// NOTE: pipeline updates and regular updates cannot be used together. - /// - /// A Template object containing a pipeline stage - public Update WithPipelineStage(Template template) - { - return WithPipelineStage(template.RenderToString()); - } + return this; + } - /// - /// Specify an array filter to target nested entities for updates (use multiple times if needed). - /// - /// { 'x.SubProp': { $gte: 123 } } - public Update WithArrayFilter(string filter) - { - ArrayFilterDefinition def = filter; + /// + /// Specify an update pipeline stage to modify the Entities (use multiple times if needed) + /// NOTE: pipeline updates and regular updates cannot be used together. + /// + /// { $set: { FullName: { $concat: ['$Name', ' ', '$Surname'] } } } + public Update WithPipelineStage(string stage) + { + _stages.Add(stage); + return this; + } - _options.ArrayFilters = - _options.ArrayFilters == null - ? new[] { def } - : _options.ArrayFilters.Concat(new[] { def }); + /// + /// Specify an update pipeline stage using a Template to modify the Entities (use multiple times if needed) + /// NOTE: pipeline updates and regular updates cannot be used together. + /// + /// A Template object containing a pipeline stage + public Update WithPipelineStage(Template template) + { + return WithPipelineStage(template.RenderToString()); + } - return this; - } + /// + /// Specify an array filter to target nested entities for updates (use multiple times if needed). + /// + /// { 'x.SubProp': { $gte: 123 } } + public Update WithArrayFilter(string filter) + { + ArrayFilterDefinition def = filter; - /// - /// Specify a single array filter using a Template to target nested entities for updates - /// - /// - public Update WithArrayFilter(Template template) - { - WithArrayFilter(template.RenderToString()); - return this; - } + _options.ArrayFilters = + _options.ArrayFilters == null + ? new[] { def } + : _options.ArrayFilters.Concat(new[] { def }); - /// - /// Specify multiple array filters with a Template to target nested entities for updates. - /// - /// The template with an array [...] of filters - public Update WithArrayFilters(Template template) - { - var defs = template.ToArrayFilters(); + return this; + } - _options.ArrayFilters = - _options.ArrayFilters == null - ? defs - : _options.ArrayFilters.Concat(defs); + /// + /// Specify a single array filter using a Template to target nested entities for updates + /// + /// + public Update WithArrayFilter(Template template) + { + WithArrayFilter(template.RenderToString()); + return this; + } - return this; - } + /// + /// Specify multiple array filters with a Template to target nested entities for updates. + /// + /// The template with an array [...] of filters + public Update WithArrayFilters(Template template) + { + var defs = template.ToArrayFilters(); - /// - /// Specify an option for this update command (use multiple times if needed) - /// TIP: Setting options is not required - /// - /// x => x.OptionName = OptionValue - public Update Option(Action option) - { - option(_options); - return this; - } + _options.ArrayFilters = + _options.ArrayFilters == null + ? defs + : _options.ArrayFilters.Concat(defs); + + return this; + } + + /// + /// Specify an option for this update command (use multiple times if needed) + /// TIP: Setting options is not required + /// + /// x => x.OptionName = OptionValue + public Update Option(Action option) + { + option(_options); + return this; + } - /// - /// Queue up an update command for bulk execution later. - /// - public Update AddToQueue() + /// + /// Queue up an update command for bulk execution later. + /// + public Update AddToQueue() + { + var mergedFilter = MergedFilter; + if (mergedFilter == Builders.Filter.Empty) throw new ArgumentException("Please use Match() method first!"); + if (defs.Count == 0) throw new ArgumentException("Please use Modify() method first!"); + if (Cache().HasModifiedOn) Modify(b => b.CurrentDate(Cache().ModifiedOnPropName)); + onUpdateAction?.Invoke(this); + _models.Add(new UpdateManyModel(mergedFilter, Builders.Update.Combine(defs)) { - var mergedFilter = MergedFilter; - if (mergedFilter == Builders.Filter.Empty) throw new ArgumentException("Please use Match() method first!"); - if (defs.Count == 0) throw new ArgumentException("Please use Modify() method first!"); - if (Cache().HasModifiedOn) Modify(b => b.CurrentDate(Cache().ModifiedOnPropName)); - onUpdateAction?.Invoke(this); - _models.Add(new UpdateManyModel(mergedFilter, Builders.Update.Combine(defs)) - { - ArrayFilters = _options.ArrayFilters, - Collation = _options.Collation, - Hint = _options.Hint, - IsUpsert = _options.IsUpsert - }); - _filter = Builders.Filter.Empty; - defs.Clear(); - _options = new UpdateOptions(); - return this; - } + ArrayFilters = _options.ArrayFilters, + Collation = _options.Collation, + Hint = _options.Hint, + IsUpsert = _options.IsUpsert + }); + _filter = Builders.Filter.Empty; + defs.Clear(); + _options = new UpdateOptions(); + return this; + } - /// - /// Run the update command in MongoDB. - /// - /// An optional cancellation token - public async Task ExecuteAsync(CancellationToken cancellation = default) + /// + /// Run the update command in MongoDB. + /// + /// An optional cancellation token + public async Task ExecuteAsync(CancellationToken cancellation = default) + { + if (_models.Count > 0) { - if (_models.Count > 0) - { - var bulkWriteResult = await ( - this.Session() is not IClientSessionHandle session - ? Collection.BulkWriteAsync(_models, null, cancellation) - : Collection.BulkWriteAsync(session, _models, null, cancellation) - ).ConfigureAwait(false); - - _models.Clear(); - - if (!bulkWriteResult.IsAcknowledged) - return UpdateResult.Unacknowledged.Instance; - - return new UpdateResult.Acknowledged(bulkWriteResult.MatchedCount, bulkWriteResult.ModifiedCount, null); - } - else - { - var mergedFilter = MergedFilter; - if (mergedFilter == Builders.Filter.Empty) throw new ArgumentException("Please use Match() method first!"); - if (defs.Count == 0) throw new ArgumentException("Please use a Modify() method first!"); - if (_stages.Count > 0) throw new ArgumentException("Regular updates and Pipeline updates cannot be used together!"); - if (ShouldSetModDate()) Modify(b => b.CurrentDate(Cache().ModifiedOnPropName)); - - onUpdateAction?.Invoke(this); - return await UpdateAsync(mergedFilter, Builders.Update.Combine(defs), _options, cancellation).ConfigureAwait(false); - } - } + var bulkWriteResult = await ( + this.Session() is not IClientSessionHandle session + ? Collection.BulkWriteAsync(_models, null, cancellation) + : Collection.BulkWriteAsync(session, _models, null, cancellation) + ).ConfigureAwait(false); + + _models.Clear(); + + if (!bulkWriteResult.IsAcknowledged) + return UpdateResult.Unacknowledged.Instance; - /// - /// Run the update command with pipeline stages - /// - /// An optional cancellation token - public Task ExecutePipelineAsync(CancellationToken cancellation = default) + return new UpdateResult.Acknowledged(bulkWriteResult.MatchedCount, bulkWriteResult.ModifiedCount, null); + } + else { var mergedFilter = MergedFilter; if (mergedFilter == Builders.Filter.Empty) throw new ArgumentException("Please use Match() method first!"); - if (_stages.Count == 0) throw new ArgumentException("Please use WithPipelineStage() method first!"); - if (defs.Count > 0) throw new ArgumentException("Pipeline updates cannot be used together with regular updates!"); - if (ShouldSetModDate()) WithPipelineStage($"{{ $set: {{ '{Cache().ModifiedOnPropName}': new Date() }} }}"); - - return UpdateAsync( - mergedFilter, - Builders.Update.Pipeline(_stages.ToArray()), - _options, - cancellation); - } + if (defs.Count == 0) throw new ArgumentException("Please use a Modify() method first!"); + if (_stages.Count > 0) throw new ArgumentException("Regular updates and Pipeline updates cannot be used together!"); + if (ShouldSetModDate()) Modify(b => b.CurrentDate(Cache().ModifiedOnPropName)); - private bool ShouldSetModDate() - { - //only set mod date by library if user hasn't done anything with the ModifiedOn property - - return - Cache().HasModifiedOn && - !defs.Any(d => d - .Render(BsonSerializer.SerializerRegistry.GetSerializer(), BsonSerializer.SerializerRegistry) - .ToString() - .Contains($"\"{Cache().ModifiedOnPropName}\"")); + onUpdateAction?.Invoke(this); + return await UpdateAsync(mergedFilter, Builders.Update.Combine(defs), _options, cancellation).ConfigureAwait(false); } + } - private Task UpdateAsync(FilterDefinition filter, UpdateDefinition definition, UpdateOptions options, CancellationToken cancellation = default) - { - return Context.Session is null - ? Collection.UpdateManyAsync(filter, definition, options, cancellation) - : Collection.UpdateManyAsync(Context.Session, filter, definition, options, cancellation); - } + /// + /// Run the update command with pipeline stages + /// + /// An optional cancellation token + public Task ExecutePipelineAsync(CancellationToken cancellation = default) + { + var mergedFilter = MergedFilter; + if (mergedFilter == Builders.Filter.Empty) throw new ArgumentException("Please use Match() method first!"); + if (_stages.Count == 0) throw new ArgumentException("Please use WithPipelineStage() method first!"); + if (defs.Count > 0) throw new ArgumentException("Pipeline updates cannot be used together with regular updates!"); + if (ShouldSetModDate()) WithPipelineStage($"{{ $set: {{ '{Cache().ModifiedOnPropName}': new Date() }} }}"); + + return UpdateAsync( + mergedFilter, + Builders.Update.Pipeline(_stages.ToArray()), + _options, + cancellation); + } + private bool ShouldSetModDate() + { + //only set mod date by library if user hasn't done anything with the ModifiedOn property + + return + Cache().HasModifiedOn && + !defs.Any(d => d + .Render(BsonSerializer.SerializerRegistry.GetSerializer(), BsonSerializer.SerializerRegistry) + .ToString() + .Contains($"\"{Cache().ModifiedOnPropName}\"")); } + + private Task UpdateAsync(FilterDefinition filter, UpdateDefinition definition, UpdateOptions options, CancellationToken cancellation = default) + { + return Context.Session is null + ? Collection.UpdateManyAsync(filter, definition, options, cancellation) + : Collection.UpdateManyAsync(Context.Session, filter, definition, options, cancellation); + } + } diff --git a/MongoDB.Entities/Builders/UpdateAndGet.cs b/MongoDB.Entities/Builders/UpdateAndGet.cs index 63ce7e30a..6bea924c3 100644 --- a/MongoDB.Entities/Builders/UpdateAndGet.cs +++ b/MongoDB.Entities/Builders/UpdateAndGet.cs @@ -14,13 +14,16 @@ namespace MongoDB.Entities /// TIP: Specify a filter first with the .Match(). Then set property values with .Modify() and finally call .Execute() to run the command. /// /// Any class that implements IEntity - public class UpdateAndGet : UpdateAndGet where T : IEntity + /// ID type + public class UpdateAndGet : UpdateAndGet + where TId : IComparable, IEquatable + where T : IEntity { - internal UpdateAndGet(DBContext context, IMongoCollection collection, UpdateBase> other) : base(context, collection, other) + internal UpdateAndGet(DBContext context, IMongoCollection collection, UpdateBase> other) : base(context, collection, other) { } - internal UpdateAndGet(DBContext context, IMongoCollection collection, Action>? onUpdateAction, List>? defs) : base(context, collection, onUpdateAction, defs) + internal UpdateAndGet(DBContext context, IMongoCollection collection, Action>? onUpdateAction, List>? defs) : base(context, collection, onUpdateAction, defs) { } } @@ -31,7 +34,10 @@ internal UpdateAndGet(DBContext context, IMongoCollection collection, Action< /// /// Any class that implements IEntity /// The type to project to - public class UpdateAndGet : UpdateBase>, ICollectionRelated where T : IEntity + /// ID type + public class UpdateAndGet : UpdateBase>, ICollectionRelated + where TId : IComparable, IEquatable + where T : IEntity { private readonly List> _stages = new(); private protected readonly FindOneAndUpdateOptions _options = new() { ReturnDocument = ReturnDocument.After }; @@ -39,13 +45,13 @@ public class UpdateAndGet : UpdateBase Collection { get; } - internal UpdateAndGet(DBContext context, IMongoCollection collection, UpdateBase> other) : base(other) + internal UpdateAndGet(DBContext context, IMongoCollection collection, UpdateBase> other) : base(other) { Context = context; Collection = collection; } - internal UpdateAndGet(DBContext context, IMongoCollection collection, Action>? onUpdateAction = null, List>? defs = null) : base(context.GlobalFilters, onUpdateAction, defs) + internal UpdateAndGet(DBContext context, IMongoCollection collection, Action>? onUpdateAction = null, List>? defs = null) : base(context.GlobalFilters, onUpdateAction, defs) { Context = context; Collection = collection; @@ -58,7 +64,7 @@ internal UpdateAndGet(DBContext context, IMongoCollection collection, Action< /// NOTE: pipeline updates and regular updates cannot be used together. /// /// A Template object containing multiple pipeline stages - public UpdateAndGet WithPipeline(Template template) + public UpdateAndGet WithPipeline(Template template) { foreach (var stage in template.ToStages()) { @@ -73,7 +79,7 @@ public UpdateAndGet WithPipeline(Template template) /// NOTE: pipeline updates and regular updates cannot be used together. /// /// { $set: { FullName: { $concat: ['$Name', ' ', '$Surname'] } } } - public UpdateAndGet WithPipelineStage(string stage) + public UpdateAndGet WithPipelineStage(string stage) { _stages.Add(stage); return this; @@ -84,7 +90,7 @@ public UpdateAndGet WithPipelineStage(string stage) /// NOTE: pipeline updates and regular updates cannot be used together. /// /// A Template object containing a pipeline stage - public UpdateAndGet WithPipelineStage(Template template) + public UpdateAndGet WithPipelineStage(Template template) { return WithPipelineStage(template.RenderToString()); } @@ -93,7 +99,7 @@ public UpdateAndGet WithPipelineStage(Template template) /// Specify an array filter to target nested entities for updates (use multiple times if needed). /// /// { 'x.SubProp': { $gte: 123 } } - public UpdateAndGet WithArrayFilter(string filter) + public UpdateAndGet WithArrayFilter(string filter) { ArrayFilterDefinition def = filter; @@ -109,7 +115,7 @@ public UpdateAndGet WithArrayFilter(string filter) /// Specify a single array filter using a Template to target nested entities for updates /// /// - public UpdateAndGet WithArrayFilter(Template template) + public UpdateAndGet WithArrayFilter(Template template) { WithArrayFilter(template.RenderToString()); return this; @@ -119,7 +125,7 @@ public UpdateAndGet WithArrayFilter(Template template) /// Specify multiple array filters with a Template to target nested entities for updates. /// /// The template with an array [...] of filters - public UpdateAndGet WithArrayFilters(Template template) + public UpdateAndGet WithArrayFilters(Template template) { var defs = template.ToArrayFilters(); @@ -136,7 +142,7 @@ public UpdateAndGet WithArrayFilters(Template template) /// TIP: Setting options is not required /// /// x => x.OptionName = OptionValue - public UpdateAndGet Option(Action> option) + public UpdateAndGet Option(Action> option) { option(_options); return this; @@ -146,7 +152,7 @@ public UpdateAndGet Option(Action /// x => new Test { PropName = x.Prop } - public UpdateAndGet Project(Expression> expression) + public UpdateAndGet Project(Expression> expression) { return Project(p => p.Expression(expression)); } @@ -155,7 +161,7 @@ public UpdateAndGet Project(Expression> exp /// Specify how to project the results using a projection expression /// /// p => p.Include("Prop1").Exclude("Prop2") - public UpdateAndGet Project(Func, ProjectionDefinition> projection) + public UpdateAndGet Project(Func, ProjectionDefinition> projection) { _options.Projection = projection(Builders.Projection); return this; @@ -165,7 +171,7 @@ public UpdateAndGet Project(Func, /// Specify to automatically include all properties marked with [BsonRequired] attribute on the entity in the final projection. /// HINT: this method should only be called after the .Project() method. /// - public UpdateAndGet IncludeRequiredProps() + public UpdateAndGet IncludeRequiredProps() { if (typeof(T) != typeof(TProjection)) throw new InvalidOperationException("IncludeRequiredProps() cannot be used when projecting to a different type."); diff --git a/MongoDB.Entities/Core/Attributes.cs b/MongoDB.Entities/Core/Attributes.cs index 06019c9ef..7fd269443 100644 --- a/MongoDB.Entities/Core/Attributes.cs +++ b/MongoDB.Entities/Core/Attributes.cs @@ -1,130 +1,123 @@ -using MongoDB.Bson; -using MongoDB.Bson.Serialization; -using MongoDB.Bson.Serialization.Attributes; -using MongoDB.Bson.Serialization.Serializers; -using System; +namespace MongoDB.Entities; + +/// +/// Use this attribute to ignore a property when persisting an entity to the database. +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] +public class IgnoreAttribute : BsonIgnoreAttribute { } + +/// +/// Use this attribute to ignore a property when persisting an entity to the database if the value is null/default. +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] +public class IgnoreDefaultAttribute : BsonIgnoreIfDefaultAttribute { } + +/// +/// Specifies the field name and/or the order of the persisted document. +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] +public class FieldAttribute : BsonElementAttribute +{ + public FieldAttribute(int fieldOrder) { Order = fieldOrder; } + public FieldAttribute(string fieldName) : base(fieldName) { } + public FieldAttribute(string fieldName, int fieldOrder) : base(fieldName) { Order = fieldOrder; } +} -namespace MongoDB.Entities +/// +/// Indicates that this property is the owner side of a many-to-many relationship +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] +public class OwnerSideAttribute : Attribute { } + +/// +/// Indicates that this property is the inverse side of a many-to-many relationship +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] +public class InverseSideAttribute : Attribute { } + +/// +/// Specifies a custom MongoDB collection name for an entity type. +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] +public class CollectionAttribute : Attribute { - /// - /// Use this attribute to ignore a property when persisting an entity to the database. - /// - [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] - public class IgnoreAttribute : BsonIgnoreAttribute { } - - /// - /// Use this attribute to ignore a property when persisting an entity to the database if the value is null/default. - /// - [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] - public class IgnoreDefaultAttribute : BsonIgnoreIfDefaultAttribute { } - - /// - /// Specifies the field name and/or the order of the persisted document. - /// - [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] - public class FieldAttribute : BsonElementAttribute - { - public FieldAttribute(int fieldOrder) { Order = fieldOrder; } - public FieldAttribute(string fieldName) : base(fieldName) { } - public FieldAttribute(string fieldName, int fieldOrder) : base(fieldName) { Order = fieldOrder; } - } + public string Name { get; } - /// - /// Indicates that this property is the owner side of a many-to-many relationship - /// - [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] - public class OwnerSideAttribute : Attribute { } - - /// - /// Indicates that this property is the inverse side of a many-to-many relationship - /// - [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] - public class InverseSideAttribute : Attribute { } - - /// - /// Specifies a custom MongoDB collection name for an entity type. - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - public class CollectionAttribute : Attribute + public CollectionAttribute(string name) { - public string Name { get; } - - public CollectionAttribute(string name) - { - if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name)); - Name = name; - } + if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name)); + Name = name; } +} - /// - /// Use this attribute on properties that you want to omit when using SavePreserving() instead of supplying an expression. - /// TIP: These attribute decorations are only effective if you do not specify a preservation expression when calling SavePreserving() - /// - [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] - public class PreserveAttribute : Attribute { } - - /// - /// Properties that don't have this attribute will be omitted when using SavePreserving() - /// TIP: These attribute decorations are only effective if you do not specify a preservation expression when calling SavePreserving() - /// - [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] - public class DontPreserveAttribute : Attribute { } - - /// - /// Use this attribute to mark a property in order to save it in MongoDB server as ObjectId - /// - [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] - public class ObjectIdAttribute : BsonRepresentationAttribute - { - public ObjectIdAttribute() : base(BsonType.ObjectId) - { } - } +/// +/// Use this attribute on properties that you want to omit when using SavePreserving() instead of supplying an expression. +/// TIP: These attribute decorations are only effective if you do not specify a preservation expression when calling SavePreserving() +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] +public class PreserveAttribute : Attribute { } + +/// +/// Properties that don't have this attribute will be omitted when using SavePreserving() +/// TIP: These attribute decorations are only effective if you do not specify a preservation expression when calling SavePreserving() +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] +public class DontPreserveAttribute : Attribute { } + +/// +/// Use this attribute to mark a property in order to save it in MongoDB server as ObjectId +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] +public class ObjectIdAttribute : BsonRepresentationAttribute +{ + public ObjectIdAttribute() : base(BsonType.ObjectId) + { } +} - /// - /// Use this attribute to mark a string property to store the value in MongoDB as ObjectID if it is a valid ObjectId string. - /// If it is not a valid ObjectId string, it will be stored as string. This is useful when using custom formats for the ID field. - /// - [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] - public class AsObjectIdAttribute : BsonSerializerAttribute - { - public AsObjectIdAttribute() : base(typeof(ObjectIdSerializer)) { } +/// +/// Use this attribute to mark a string property to store the value in MongoDB as ObjectID if it is a valid ObjectId string. +/// If it is not a valid ObjectId string, it will be stored as string. This is useful when using custom formats for the ID field. +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] +public class AsObjectIdAttribute : BsonSerializerAttribute +{ + public AsObjectIdAttribute() : base(typeof(ObjectIdSerializer)) { } - private class ObjectIdSerializer : SerializerBase + private class ObjectIdSerializer : SerializerBase + { + public override void Serialize(BsonSerializationContext ctx, BsonSerializationArgs args, string? value) { - public override void Serialize(BsonSerializationContext ctx, BsonSerializationArgs args, string value) + if (value is null) + { + ctx.Writer.WriteNull(); + return; + } + + if (value.Length == 24 && ObjectId.TryParse(value, out var oID)) { - if (value == null) - { - ctx.Writer.WriteNull(); - return; - } - - if (value.Length == 24 && ObjectId.TryParse(value, out var oID)) - { - ctx.Writer.WriteObjectId(oID); - return; - } - - ctx.Writer.WriteString(value); + ctx.Writer.WriteObjectId(oID); + return; } - public override string Deserialize(BsonDeserializationContext ctx, BsonDeserializationArgs args) + ctx.Writer.WriteString(value); + } + + public override string? Deserialize(BsonDeserializationContext ctx, BsonDeserializationArgs args) + { + switch (ctx.Reader.CurrentBsonType) { - switch (ctx.Reader.CurrentBsonType) - { - case BsonType.String: - return ctx.Reader.ReadString(); + case BsonType.String: + return ctx.Reader.ReadString(); - case BsonType.ObjectId: - return ctx.Reader.ReadObjectId().ToString(); + case BsonType.ObjectId: + return ctx.Reader.ReadObjectId().ToString(); - case BsonType.Null: - ctx.Reader.ReadNull(); - return null; + case BsonType.Null: + ctx.Reader.ReadNull(); + return default; - default: - throw new BsonSerializationException($"'{ctx.Reader.CurrentBsonType}' values are not valid on properties decorated with an [AsObjectId] attribute!"); - } + default: + throw new BsonSerializationException($"'{ctx.Reader.CurrentBsonType}' values are not valid on properties decorated with an [AsObjectId] attribute!"); } } } diff --git a/MongoDB.Entities/Core/DBContextOptions.cs b/MongoDB.Entities/Core/DBContextOptions.cs index db734ce92..a289b1ae9 100644 --- a/MongoDB.Entities/Core/DBContextOptions.cs +++ b/MongoDB.Entities/Core/DBContextOptions.cs @@ -1,12 +1,7 @@ -#nullable enable +namespace MongoDB.Entities; -using MongoDB.Driver; - -namespace MongoDB.Entities +public class DBContextOptions { - public class DBContextOptions - { - - - } + + } diff --git a/MongoDB.Entities/Core/Date.cs b/MongoDB.Entities/Core/Date.cs index 572d0591d..f8e3f1409 100644 --- a/MongoDB.Entities/Core/Date.cs +++ b/MongoDB.Entities/Core/Date.cs @@ -1,109 +1,103 @@ -using MongoDB.Bson; -using MongoDB.Bson.IO; -using MongoDB.Bson.Serialization; -using MongoDB.Bson.Serialization.Serializers; -using System; +using MongoDB.Bson.IO; -namespace MongoDB.Entities +namespace MongoDB.Entities; + +internal class DateSerializer : SerializerBase, IBsonDocumentSerializer { - internal class DateSerializer : SerializerBase, IBsonDocumentSerializer - { - private static readonly Int64Serializer longSerializer = new(); - private static readonly DateTimeSerializer dtSerializer = new(); + private static readonly Int64Serializer _longSerializer = new(); + private static readonly DateTimeSerializer _dtSerializer = new(); - public override void Serialize(BsonSerializationContext ctx, BsonSerializationArgs args, Date date) + public override void Serialize(BsonSerializationContext ctx, BsonSerializationArgs args, Date? date) + { + if (date == null) { - if (date == null) - { - ctx.Writer.WriteNull(); - } - else - { - var dtUTC = BsonUtils.ToUniversalTime(date.DateTime); - ctx.Writer.WriteStartDocument(); - ctx.Writer.WriteDateTime("DateTime", BsonUtils.ToMillisecondsSinceEpoch(dtUTC)); - ctx.Writer.WriteInt64("Ticks", dtUTC.Ticks); - ctx.Writer.WriteEndDocument(); - } + ctx.Writer.WriteNull(); } - - public override Date Deserialize(BsonDeserializationContext ctx, BsonDeserializationArgs args) + else { - var bsonType = ctx.Reader.GetCurrentBsonType(); + var dtUTC = BsonUtils.ToUniversalTime(date.DateTime); + ctx.Writer.WriteStartDocument(); + ctx.Writer.WriteDateTime(nameof(Date.DateTime), BsonUtils.ToMillisecondsSinceEpoch(dtUTC)); + ctx.Writer.WriteInt64(nameof(Date.Ticks), dtUTC.Ticks); + ctx.Writer.WriteEndDocument(); + } + } - switch (bsonType) - { - case BsonType.Document: + public override Date? Deserialize(BsonDeserializationContext ctx, BsonDeserializationArgs args) + { + var bsonType = ctx.Reader.GetCurrentBsonType(); - long ticks = 0; + switch (bsonType) + { + case BsonType.Document: - ctx.Reader.ReadStartDocument(); - while (ctx.Reader.ReadBsonType() != BsonType.EndOfDocument) - { - if (ctx.Reader.ReadName() == "Ticks") - ticks = ctx.Reader.ReadInt64(); - else - ctx.Reader.SkipValue(); - } - ctx.Reader.ReadEndDocument(); + long ticks = 0; - return new Date() { DateTime = new DateTime(ticks, DateTimeKind.Utc) }; + ctx.Reader.ReadStartDocument(); + while (ctx.Reader.ReadBsonType() != BsonType.EndOfDocument) + { + if (ctx.Reader.ReadName() == nameof(Date.Ticks)) + ticks = ctx.Reader.ReadInt64(); + else + ctx.Reader.SkipValue(); + } + ctx.Reader.ReadEndDocument(); - case BsonType.Null: - ctx.Reader.ReadNull(); - return null; + return new Date() { DateTime = new DateTime(ticks, DateTimeKind.Utc) }; - default: - throw new FormatException($"Cannot deserialize a 'Date' from a [{bsonType}]"); - } - } + case BsonType.Null: + ctx.Reader.ReadNull(); + return null; - public bool TryGetMemberSerializationInfo(string memberName, out BsonSerializationInfo serializationInfo) - { - switch (memberName) - { - case "Ticks": - serializationInfo = new BsonSerializationInfo("Ticks", longSerializer, typeof(long)); - return true; - case "DateTime": - serializationInfo = new BsonSerializationInfo("DateTime", dtSerializer, typeof(DateTime)); - return true; - default: - serializationInfo = null; - return false; - } + default: + throw new FormatException($"Cannot deserialize a 'Date' from a [{bsonType}]"); } } - /// - /// A custom date/time type for precision datetime handling - /// - public class Date + public bool TryGetMemberSerializationInfo(string memberName, out BsonSerializationInfo? serializationInfo) { - private long ticks; - private DateTime date = new(); - - public long Ticks + switch (memberName) { - get => ticks; - set { date = new DateTime(value); ticks = value; } + case nameof(Date.Ticks): + serializationInfo = new BsonSerializationInfo(nameof(Date.Ticks), _longSerializer, typeof(long)); + return true; + case nameof(Date.DateTime): + serializationInfo = new BsonSerializationInfo(nameof(Date.DateTime), _dtSerializer, typeof(DateTime)); + return true; + default: + serializationInfo = null; + return false; } + } +} - public DateTime DateTime - { - get => date; - set { date = value; ticks = value.Ticks; } - } +/// +/// A custom date/time type for precision datetime handling +/// +public class Date +{ + private long _ticks; + private DateTime _date = new(); - public static implicit operator Date(DateTime datetime) - { - return new Date { DateTime = datetime }; - } + public long Ticks + { + get => _ticks; + set { _date = new DateTime(value); _ticks = value; } + } - public static implicit operator DateTime(Date date) - { - if (date == null) throw new NullReferenceException("The [Date] instance is Null!"); - return new DateTime(date.Ticks); - } + public DateTime DateTime + { + get => _date; + set { _date = value; _ticks = value.Ticks; } + } + + public static implicit operator Date(DateTime datetime) + { + return new Date { DateTime = datetime }; + } + + public static implicit operator DateTime(Date date) + { + return new DateTime(date.Ticks); } } diff --git a/MongoDB.Entities/Core/DoubleMetaphone.cs b/MongoDB.Entities/Core/DoubleMetaphone.cs index 5ea650970..06b0fc087 100644 --- a/MongoDB.Entities/Core/DoubleMetaphone.cs +++ b/MongoDB.Entities/Core/DoubleMetaphone.cs @@ -1,765 +1,764 @@ -using System; -using System.Collections.Generic; -using System.Text; +using System.Text; using System.Text.RegularExpressions; -namespace MongoDB.Entities +#pragma warning disable IDE1006 // Naming Styles + +namespace MongoDB.Entities; + +internal static class DoubleMetaphone { - internal static class DoubleMetaphone + private static readonly string[] GN_KN_PN_WR_PS = new[] { "GN", "KN", "PN", "WR", "PS" }; + private static readonly string[] ACH = new[] { "ACH" }; + private static readonly string[] BACHER_MACHER = new[] { "BACHER", "MACHER" }; + private static readonly string[] CAESAR = new[] { "CAESAR" }; + private static readonly string[] CHIA = new[] { "CHIA" }; + private static readonly string[] CH = new[] { "CH" }; + private static readonly string[] CHAE = new[] { "CHAE" }; + private static readonly string[] HARAC_HARIS_HOR_HYM_HIA_HEM = new[] { "HARAC", "HARIS", "HOR", "HYM", "HIA", "HEM" }; + private static readonly string[] CHORE = new[] { "CHORE" }; + private static readonly string[] SCH = new[] { "SCH" }; + private static readonly string[] VAN__VON__SCH = new[] { "VAN ", "VON ", "SCH" }; + private static readonly string[] ORCHES_ARCHIT_ORCHID = new[] { "ORCHES", "ARCHIT", "ORCHID" }; + private static readonly string[] T_S = new[] { "T", "S" }; + private static readonly string[] A_O = new[] { "A", "O" }; + private static readonly string[] A_O_U_E = new[] { "A", "O", "U", "E" }; + private static readonly string[] L_R_N_M_B_H_F_V_W__ = new[] { "L", "R", "N", "M", "B", "H", "F", "V", "W", " " }; + private static readonly string[] MC = new[] { "MC" }; + private static readonly string[] CZ = new[] { "CZ" }; + private static readonly string[] WICZ = new[] { "WICZ" }; + private static readonly string[] CIA = new[] { "CIA" }; + private static readonly string[] CC = new[] { "CC" }; + private static readonly string[] I_E_H = new[] { "I", "E", "H" }; + private static readonly string[] HU = new[] { "HU" }; + private static readonly string[] UCCEE_UCCES = new[] { "UCCEE", "UCCES" }; + private static readonly string[] CK_CG_CQ = new[] { "CK", "CG", "CQ" }; + private static readonly string[] CI_CE_CY = new[] { "CI", "CE", "CY" }; + private static readonly string[] CIO_CIE_CIA = new[] { "CIO", "CIE", "CIA" }; + private static readonly string[] _C__Q__G = new[] { " C", " Q", " G" }; + private static readonly string[] C_K_Q = new[] { "C", "K", "Q" }; + private static readonly string[] CE_CI = new[] { "CE", "CI" }; + private static readonly string[] DG = new[] { "DG" }; + private static readonly string[] I_E_Y = new[] { "I", "E", "Y" }; + private static readonly string[] DT_DD = new[] { "DT", "DD" }; + private static readonly string[] B_H_D = new[] { "B", "H", "D" }; + private static readonly string[] B_H = new[] { "B", "H" }; + private static readonly string[] C_G_L_R_T = new[] { "C", "G", "L", "R", "T" }; + private static readonly string[] EY = new[] { "EY" }; + private static readonly string[] LI = new[] { "LI" }; + private static readonly string[] Y_ES_EP_EB_EL_EY_IB_IL_IN_IE_EI_ER = new[] { "Y", "ES", "EP", "EB", "EL", "EY", "IB", "IL", "IN", "IE", "EI", "ER" }; + private static readonly string[] Y_ER = new[] { "Y", "ER" }; + private static readonly string[] DANGER_RANGER_MANGER = new[] { "DANGER", "RANGER", "MANGER" }; + private static readonly string[] E_I = new[] { "E", "I" }; + private static readonly string[] RGY_OGY = new[] { "RGY", "OGY" }; + private static readonly string[] E_I_Y = new[] { "E", "I", "Y" }; + private static readonly string[] AGGI_OGGI = new[] { "AGGI", "OGGI" }; + private static readonly string[] ET = new[] { "ET" }; + private static readonly string[] JOSE = new[] { "JOSE" }; + private static readonly string[] SAN_ = new[] { "SAN " }; + private static readonly string[] L_T_K_S_N_M_B_Z = new[] { "L", "T", "K", "S", "N", "M", "B", "Z" }; + private static readonly string[] S_K_L = new[] { "S", "K", "L" }; + private static readonly string[] ILLO_ILLA_ALLE = new[] { "ILLO", "ILLA", "ALLE" }; + private static readonly string[] AS_OS = new[] { "AS", "OS" }; + private static readonly string[] ALLE = new[] { "ALLE" }; + private static readonly string[] UMB = new[] { "UMB" }; + private static readonly string[] P_B = new[] { "P", "B" }; + private static readonly string[] IE = new[] { "IE" }; + private static readonly string[] IER = new[] { "IER" }; + private static readonly string[] ER = new[] { "ER" }; + private static readonly string[] ME_MA = new[] { "ME", "MA" }; + private static readonly string[] ISL_YSL = new[] { "ISL", "YSL" }; + private static readonly string[] SUGAR = new[] { "SUGAR" }; + private static readonly string[] SH = new[] { "SH" }; + private static readonly string[] HEIM_HOEK_HOLM_HOLZ = new[] { "HEIM", "HOEK", "HOLM", "HOLZ" }; + private static readonly string[] SIO_SIA = new[] { "SIO", "SIA" }; + private static readonly string[] SIAN = new[] { "SIAN" }; + private static readonly string[] M_N_L_W = new[] { "M", "N", "L", "W" }; + private static readonly string[] SC = new[] { "SC" }; + private static readonly string[] OO_ER_EN_UY_ED_EM = new[] { "OO", "ER", "EN", "UY", "ED", "EM" }; + private static readonly string[] ER_EN = new[] { "ER", "EN" }; + private static readonly string[] AI_OI = new[] { "AI", "OI" }; + private static readonly string[] S_Z = new[] { "S", "Z" }; + private static readonly string[] TION = new[] { "TION" }; + private static readonly string[] TIA_TCH = new[] { "TIA", "TCH" }; + private static readonly string[] TH_TTH = new[] { "TH", "TTH" }; + private static readonly string[] OM_AM = new[] { "OM", "AM" }; + private static readonly string[] T_D = new[] { "T", "D" }; + private static readonly string[] WR = new[] { "WR" }; + private static readonly string[] WH = new[] { "WH" }; + private static readonly string[] EWSKI_EWSKY_OWSKI_OWSKY = new[] { "EWSKI", "EWSKY", "OWSKI", "OWSKY" }; + private static readonly string[] WICZ_WITZ = new[] { "WICZ", "WITZ" }; + private static readonly string[] IAU_EAU = new[] { "IAU", "EAU" }; + private static readonly string[] AU_OU = new[] { "AU", "OU" }; + private static readonly string[] C_X = new[] { "C", "X" }; + private static readonly string[] ZO_ZI_ZA = new[] { "ZO", "ZI", "ZA" }; + + private static readonly string[] EmptyKeys = new string[0]; + private const int MaxLength = 4; + + private static readonly Regex regex = new(@"\w(? 0) { - sbPrimary.Append(main); - sbSecondary.Append(main); + hasAlternate = true; + if (!alternate.Equals(" ")) + sbSecondary.Append(alternate); } - - private static void Add(string main, string alternate, ref StringBuilder sbPrimary, ref StringBuilder sbSecondary, ref bool hasAlternate) + else { - sbPrimary.Append(main); - if (alternate.Length > 0) - { - hasAlternate = true; - if (!alternate.Equals(" ")) - sbSecondary.Append(alternate); - } - else - { - if (main.Length > 0 && !main.Equals(" ")) - sbSecondary.Append(main); - } + if (main.Length > 0 && !main.Equals(" ")) + sbSecondary.Append(main); } + } - private static bool Match(string stringRenamed, int pos, string[] strings) + private static bool Match(string stringRenamed, int pos, string[] strings) + { + if (0 <= pos && pos < stringRenamed.Length) { - if (0 <= pos && pos < stringRenamed.Length) + for (int n = strings.Length - 1; n >= 0; n--) { - for (int n = strings.Length - 1; n >= 0; n--) - { - if (string.Compare(stringRenamed, pos, strings[n], 0, strings[n].Length) == 0) - return true; - } + if (string.Compare(stringRenamed, pos, strings[n], 0, strings[n].Length) == 0) + return true; } - return false; } + return false; + } - private static bool Match(string stringRenamed, int pos, char c) - { - return 0 <= pos && pos < stringRenamed.Length && stringRenamed[pos] == c; - } + private static bool Match(string stringRenamed, int pos, char c) + { + return 0 <= pos && pos < stringRenamed.Length && stringRenamed[pos] == c; + } - private static bool IsSlavoGermanic(string stringRenamed) - { - return (stringRenamed.IndexOf('W') >= 0) || (stringRenamed.IndexOf('K') >= 0) || (stringRenamed.IndexOf("CZ", StringComparison.Ordinal) >= 0) || (stringRenamed.IndexOf("WITZ", StringComparison.Ordinal) >= 0); - } + private static bool IsSlavoGermanic(string stringRenamed) + { + return (stringRenamed.IndexOf('W') >= 0) || (stringRenamed.IndexOf('K') >= 0) || (stringRenamed.IndexOf("CZ", StringComparison.Ordinal) >= 0) || (stringRenamed.IndexOf("WITZ", StringComparison.Ordinal) >= 0); + } - private static bool IsVowel(string stringRenamed, int pos) - { - if (pos < 0 || stringRenamed.Length <= pos) - return false; + private static bool IsVowel(string stringRenamed, int pos) + { + if (pos < 0 || stringRenamed.Length <= pos) + return false; - char c = stringRenamed[pos]; - return c == 'A' || c == 'E' || c == 'I' || c == 'O' || c == 'U'; - } + char c = stringRenamed[pos]; + return c == 'A' || c == 'E' || c == 'I' || c == 'O' || c == 'U'; + } - private static string[] BuildKeys(string word) - { - if (string.IsNullOrEmpty(word)) - return EmptyKeys; + private static string[] BuildKeys(string word) + { + if (string.IsNullOrEmpty(word)) + return EmptyKeys; - word = word.ToUpper(); + word = word.ToUpper(); - var sbPrimary = new StringBuilder(word.Length); - var sbSecondary = new StringBuilder(word.Length); - bool hasAlternate = false; - int length = word.Length; - int last = length - 1; - bool isSlavoGermanic = IsSlavoGermanic(word); - int n = 0; + var sbPrimary = new StringBuilder(word.Length); + var sbSecondary = new StringBuilder(word.Length); + bool hasAlternate = false; + int length = word.Length; + int last = length - 1; + bool isSlavoGermanic = IsSlavoGermanic(word); + int n = 0; - if (Match(word, 0, GN_KN_PN_WR_PS)) - n++; + if (Match(word, 0, GN_KN_PN_WR_PS)) + n++; - if (Match(word, 0, 'X')) - { - Add("S", ref sbPrimary, ref sbSecondary); - n++; - } + if (Match(word, 0, 'X')) + { + Add("S", ref sbPrimary, ref sbSecondary); + n++; + } - while (n < length && (MaxLength < 0 || (sbPrimary.Length < MaxLength && sbSecondary.Length < MaxLength))) + while (n < length && (MaxLength < 0 || (sbPrimary.Length < MaxLength && sbSecondary.Length < MaxLength))) + { + switch (word[n]) { - switch (word[n]) - { - case 'A': - case 'E': - case 'I': - case 'O': - case 'U': - case 'Y': - if (n == 0) - Add("A", ref sbPrimary, ref sbSecondary); - n++; - break; - case 'B': - Add("P", ref sbPrimary, ref sbSecondary); - n += Match(word, n + 1, 'B') ? 2 : 1; + case 'A': + case 'E': + case 'I': + case 'O': + case 'U': + case 'Y': + if (n == 0) + Add("A", ref sbPrimary, ref sbSecondary); + n++; + break; + case 'B': + Add("P", ref sbPrimary, ref sbSecondary); + n += Match(word, n + 1, 'B') ? 2 : 1; + break; + case 'Ç': + Add("S", ref sbPrimary, ref sbSecondary); + n++; + break; + case 'C': + if ((n > 1) && !IsVowel(word, n - 2) && Match(word, n - 1, ACH) && !Match(word, n + 2, 'I') && (!Match(word, n + 2, 'E') || Match(word, n - 2, BACHER_MACHER))) + { + Add("K", ref sbPrimary, ref sbSecondary); + n += 2; break; - case 'Ç': + } + + if ((n == 0) && Match(word, n, CAESAR)) + { Add("S", ref sbPrimary, ref sbSecondary); - n++; + n += 2; + break; + } + + if (Match(word, n, CHIA)) + { + Add("K", ref sbPrimary, ref sbSecondary); + n += 2; break; - case 'C': - if ((n > 1) && !IsVowel(word, n - 2) && Match(word, n - 1, ACH) && !Match(word, n + 2, 'I') && (!Match(word, n + 2, 'E') || Match(word, n - 2, BACHER_MACHER))) + } + + if (Match(word, n, CH)) + { + if ((n > 0) && Match(word, n, CHAE)) { - Add("K", ref sbPrimary, ref sbSecondary); + Add("K", "X", ref sbPrimary, ref sbSecondary, ref hasAlternate); n += 2; break; } - if ((n == 0) && Match(word, n, CAESAR)) + if ((n == 0) && Match(word, n + 1, HARAC_HARIS_HOR_HYM_HIA_HEM) && !Match(word, 0, CHORE)) { - Add("S", ref sbPrimary, ref sbSecondary); + Add("K", ref sbPrimary, ref sbSecondary); n += 2; break; } - if (Match(word, n, CHIA)) + if (Match(word, 0, VAN__VON__SCH) || Match(word, n - 2, ORCHES_ARCHIT_ORCHID) || Match(word, n + 2, T_S) || (((n == 0) || Match(word, n - 1, A_O_U_E)) && Match(word, n + 2, L_R_N_M_B_H_F_V_W__))) { Add("K", ref sbPrimary, ref sbSecondary); - n += 2; - break; } - - if (Match(word, n, CH)) + else { - if ((n > 0) && Match(word, n, CHAE)) + if (n > 0) { - Add("K", "X", ref sbPrimary, ref sbSecondary, ref hasAlternate); - n += 2; - break; - } - - if ((n == 0) && Match(word, n + 1, HARAC_HARIS_HOR_HYM_HIA_HEM) && !Match(word, 0, CHORE)) - { - Add("K", ref sbPrimary, ref sbSecondary); - n += 2; - break; - } - - if (Match(word, 0, VAN__VON__SCH) || Match(word, n - 2, ORCHES_ARCHIT_ORCHID) || Match(word, n + 2, T_S) || (((n == 0) || Match(word, n - 1, A_O_U_E)) && Match(word, n + 2, L_R_N_M_B_H_F_V_W__))) - { - Add("K", ref sbPrimary, ref sbSecondary); + if (Match(word, 0, MC)) + Add("K", ref sbPrimary, ref sbSecondary); + else + Add("X", "K", ref sbPrimary, ref sbSecondary, ref hasAlternate); } else { - if (n > 0) - { - if (Match(word, 0, MC)) - Add("K", ref sbPrimary, ref sbSecondary); - else - Add("X", "K", ref sbPrimary, ref sbSecondary, ref hasAlternate); - } - else - { - Add("X", ref sbPrimary, ref sbSecondary); - } + Add("X", ref sbPrimary, ref sbSecondary); } - n += 2; - break; - } - - if (Match(word, n, CZ) && !Match(word, n - 2, WICZ)) - { - Add("S", "X", ref sbPrimary, ref sbSecondary, ref hasAlternate); - n += 2; - break; - } - - if (Match(word, n + 1, CIA)) - { - Add("X", ref sbPrimary, ref sbSecondary); - n += 3; - break; } + n += 2; + break; + } - if (Match(word, n, CC) && !((n == 1) && Match(word, 0, 'M'))) - { - if (Match(word, n + 2, I_E_H) && !Match(word, n + 2, HU)) - { - if (((n == 1) && Match(word, n - 1, 'A')) || Match(word, n - 1, UCCEE_UCCES)) - Add("KS", ref sbPrimary, ref sbSecondary); - else - Add("X", ref sbPrimary, ref sbSecondary); - n += 3; - break; - } - Add("K", ref sbPrimary, ref sbSecondary); - n += 2; - break; - } + if (Match(word, n, CZ) && !Match(word, n - 2, WICZ)) + { + Add("S", "X", ref sbPrimary, ref sbSecondary, ref hasAlternate); + n += 2; + break; + } - if (Match(word, n, CK_CG_CQ)) - { - Add("K", ref sbPrimary, ref sbSecondary); - n += 2; - break; - } + if (Match(word, n + 1, CIA)) + { + Add("X", ref sbPrimary, ref sbSecondary); + n += 3; + break; + } - if (Match(word, n, CI_CE_CY)) + if (Match(word, n, CC) && !((n == 1) && Match(word, 0, 'M'))) + { + if (Match(word, n + 2, I_E_H) && !Match(word, n + 2, HU)) { - if (Match(word, n, CIO_CIE_CIA)) - Add("S", "X", ref sbPrimary, ref sbSecondary, ref hasAlternate); + if (((n == 1) && Match(word, n - 1, 'A')) || Match(word, n - 1, UCCEE_UCCES)) + Add("KS", ref sbPrimary, ref sbSecondary); else - Add("S", ref sbPrimary, ref sbSecondary); - n += 2; + Add("X", ref sbPrimary, ref sbSecondary); + n += 3; break; } + Add("K", ref sbPrimary, ref sbSecondary); + n += 2; + break; + } + if (Match(word, n, CK_CG_CQ)) + { Add("K", ref sbPrimary, ref sbSecondary); + n += 2; + break; + } - if (Match(word, n + 1, _C__Q__G)) - n += 3; + if (Match(word, n, CI_CE_CY)) + { + if (Match(word, n, CIO_CIE_CIA)) + Add("S", "X", ref sbPrimary, ref sbSecondary, ref hasAlternate); else - n += (Match(word, n + 1, C_K_Q) && !Match(word, n + 1, CE_CI)) ? 2 : 1; + Add("S", ref sbPrimary, ref sbSecondary); + n += 2; break; + } - case 'D': - if (Match(word, n, DG)) - { - if (Match(word, n + 2, I_E_Y)) - { - Add("J", ref sbPrimary, ref sbSecondary); - n += 3; - break; - } - Add("TK", ref sbPrimary, ref sbSecondary); - n += 2; - break; - } + Add("K", ref sbPrimary, ref sbSecondary); - if (Match(word, n, DT_DD)) + if (Match(word, n + 1, _C__Q__G)) + n += 3; + else + n += (Match(word, n + 1, C_K_Q) && !Match(word, n + 1, CE_CI)) ? 2 : 1; + break; + + case 'D': + if (Match(word, n, DG)) + { + if (Match(word, n + 2, I_E_Y)) { - Add("T", ref sbPrimary, ref sbSecondary); - n += 2; + Add("J", ref sbPrimary, ref sbSecondary); + n += 3; break; } + Add("TK", ref sbPrimary, ref sbSecondary); + n += 2; + break; + } + if (Match(word, n, DT_DD)) + { Add("T", ref sbPrimary, ref sbSecondary); - n++; + n += 2; break; - case 'F': - n += Match(word, n + 1, 'F') ? 2 : 1; - Add("F", ref sbPrimary, ref sbSecondary); - break; - case 'G': - if (Match(word, n + 1, 'H')) + } + + Add("T", ref sbPrimary, ref sbSecondary); + n++; + break; + case 'F': + n += Match(word, n + 1, 'F') ? 2 : 1; + Add("F", ref sbPrimary, ref sbSecondary); + break; + case 'G': + if (Match(word, n + 1, 'H')) + { + if ((n > 0) && !IsVowel(word, n - 1)) { - if ((n > 0) && !IsVowel(word, n - 1)) - { - Add("K", ref sbPrimary, ref sbSecondary); - n += 2; - break; - } - - if (n < 3) - { - if (n == 0) - { - Add(Match(word, n + 2, 'I') ? "J" : "K", ref sbPrimary, ref sbSecondary); - n += 2; - break; - } - } - - if (((n > 1) && Match(word, n - 2, B_H_D)) || ((n > 2) && Match(word, n - 3, B_H_D)) || ((n > 3) && Match(word, n - 4, B_H))) - { - n += 2; - break; - } - if ((n > 2) && Match(word, n - 1, 'U') && Match(word, n - 3, C_G_L_R_T)) - { - Add("F", ref sbPrimary, ref sbSecondary); - } - else if ((n > 0) && !Match(word, n - 1, 'I')) - { - Add("K", ref sbPrimary, ref sbSecondary); - } - + Add("K", ref sbPrimary, ref sbSecondary); n += 2; break; } - if (Match(word, n + 1, 'N')) + if (n < 3) { - if ((n == 1) && IsVowel(word, 0) && !isSlavoGermanic) - { - Add("KN", "N", ref sbPrimary, ref sbSecondary, ref hasAlternate); - } - else + if (n == 0) { - if (!Match(word, n + 2, EY) && !Match(word, n + 1, 'Y') && !isSlavoGermanic) - { - Add("N", "KN", ref sbPrimary, ref sbSecondary, ref hasAlternate); - } - else - { - Add("KN", ref sbPrimary, ref sbSecondary); - } + Add(Match(word, n + 2, 'I') ? "J" : "K", ref sbPrimary, ref sbSecondary); + n += 2; + break; } - n += 2; - break; - } - - if (Match(word, n + 1, LI) && !isSlavoGermanic) - { - Add("KL", "L", ref sbPrimary, ref sbSecondary, ref hasAlternate); - n += 2; - break; } - if ((n == 0) && Match(word, n + 1, Y_ES_EP_EB_EL_EY_IB_IL_IN_IE_EI_ER)) + if (((n > 1) && Match(word, n - 2, B_H_D)) || ((n > 2) && Match(word, n - 3, B_H_D)) || ((n > 3) && Match(word, n - 4, B_H))) { - Add("K", "J", ref sbPrimary, ref sbSecondary, ref hasAlternate); n += 2; break; } - - if (Match(word, n + 1, Y_ER) && !Match(word, 0, DANGER_RANGER_MANGER) && !Match(word, n - 1, E_I) && !Match(word, n - 1, RGY_OGY)) + if ((n > 2) && Match(word, n - 1, 'U') && Match(word, n - 3, C_G_L_R_T)) { - Add("K", "J", ref sbPrimary, ref sbSecondary, ref hasAlternate); - n += 2; - break; + Add("F", ref sbPrimary, ref sbSecondary); } - - if (Match(word, n + 1, E_I_Y) || Match(word, n - 1, AGGI_OGGI)) + else if ((n > 0) && !Match(word, n - 1, 'I')) { - if (Match(word, 0, VAN__VON__SCH) || Match(word, n + 1, ET)) - { - Add("K", ref sbPrimary, ref sbSecondary); - } - else - { - if (Match(word, n + 1, IER)) - Add("J", ref sbPrimary, ref sbSecondary); - else - Add("J", "K", ref sbPrimary, ref sbSecondary, ref hasAlternate); - } - n += 2; - break; + Add("K", ref sbPrimary, ref sbSecondary); } - Add("K", ref sbPrimary, ref sbSecondary); - n += Match(word, n + 1, 'G') ? 2 : 1; - break; - case 'H': - if (((n == 0) || IsVowel(word, n - 1)) && IsVowel(word, n + 1)) - { - Add("H", ref sbPrimary, ref sbSecondary); - n += 2; - } - else - { - n++; - } + n += 2; break; - case 'J': - if (Match(word, n, JOSE) || Match(word, 0, SAN_)) - { - if (((n == 0) && Match(word, n + 4, ' ')) || Match(word, 0, SAN_)) - { - Add("H", ref sbPrimary, ref sbSecondary); - } - else - { - Add("J", "H", ref sbPrimary, ref sbSecondary, ref hasAlternate); - } - n++; - break; - } + } - if ((n == 0) && !Match(word, n, JOSE)) + if (Match(word, n + 1, 'N')) + { + if ((n == 1) && IsVowel(word, 0) && !isSlavoGermanic) { - Add("J", "A", ref sbPrimary, ref sbSecondary, ref hasAlternate); + Add("KN", "N", ref sbPrimary, ref sbSecondary, ref hasAlternate); } else { - if (IsVowel(word, n - 1) && !isSlavoGermanic && Match(word, n + 1, A_O)) + if (!Match(word, n + 2, EY) && !Match(word, n + 1, 'Y') && !isSlavoGermanic) { - Add("J", "H", ref sbPrimary, ref sbSecondary, ref hasAlternate); + Add("N", "KN", ref sbPrimary, ref sbSecondary, ref hasAlternate); } else { - if (n == last) - { - Add("J", " ", ref sbPrimary, ref sbSecondary, ref hasAlternate); - } - else - { - if (!Match(word, n + 1, L_T_K_S_N_M_B_Z) && !Match(word, n - 1, S_K_L)) - Add("J", ref sbPrimary, ref sbSecondary); - } + Add("KN", ref sbPrimary, ref sbSecondary); } } + n += 2; + break; + } - n += Match(word, n + 1, 'J') ? 2 : 1; + if (Match(word, n + 1, LI) && !isSlavoGermanic) + { + Add("KL", "L", ref sbPrimary, ref sbSecondary, ref hasAlternate); + n += 2; break; - case 'K': - n += Match(word, n + 1, 'K') ? 2 : 1; - Add("K", ref sbPrimary, ref sbSecondary); + } + + if ((n == 0) && Match(word, n + 1, Y_ES_EP_EB_EL_EY_IB_IL_IN_IE_EI_ER)) + { + Add("K", "J", ref sbPrimary, ref sbSecondary, ref hasAlternate); + n += 2; break; - case 'L': - if (Match(word, n + 1, 'L')) + } + + if (Match(word, n + 1, Y_ER) && !Match(word, 0, DANGER_RANGER_MANGER) && !Match(word, n - 1, E_I) && !Match(word, n - 1, RGY_OGY)) + { + Add("K", "J", ref sbPrimary, ref sbSecondary, ref hasAlternate); + n += 2; + break; + } + + if (Match(word, n + 1, E_I_Y) || Match(word, n - 1, AGGI_OGGI)) + { + if (Match(word, 0, VAN__VON__SCH) || Match(word, n + 1, ET)) { - if (((n == length - 3) && Match(word, n - 1, ILLO_ILLA_ALLE)) || ((Match(word, last - 1, AS_OS) || Match(word, last, A_O)) && Match(word, n - 1, ALLE))) - { - Add("L", " ", ref sbPrimary, ref sbSecondary, ref hasAlternate); - n += 2; - break; - } - n += 2; + Add("K", ref sbPrimary, ref sbSecondary); } else { - n++; + if (Match(word, n + 1, IER)) + Add("J", ref sbPrimary, ref sbSecondary); + else + Add("J", "K", ref sbPrimary, ref sbSecondary, ref hasAlternate); } - Add("L", ref sbPrimary, ref sbSecondary); + n += 2; break; - case 'M': - if ((Match(word, n - 1, UMB) && ((n + 1 == last) || Match(word, n + 2, ER))) || Match(word, n + 1, 'M')) + } + + Add("K", ref sbPrimary, ref sbSecondary); + n += Match(word, n + 1, 'G') ? 2 : 1; + break; + case 'H': + if (((n == 0) || IsVowel(word, n - 1)) && IsVowel(word, n + 1)) + { + Add("H", ref sbPrimary, ref sbSecondary); + n += 2; + } + else + { + n++; + } + break; + case 'J': + if (Match(word, n, JOSE) || Match(word, 0, SAN_)) + { + if (((n == 0) && Match(word, n + 4, ' ')) || Match(word, 0, SAN_)) { - n += 2; + Add("H", ref sbPrimary, ref sbSecondary); } else { - n++; + Add("J", "H", ref sbPrimary, ref sbSecondary, ref hasAlternate); } - Add("M", ref sbPrimary, ref sbSecondary); - break; - case 'N': - n += Match(word, n + 1, 'N') ? 2 : 1; - Add("N", ref sbPrimary, ref sbSecondary); - break; - case 'Ñ': n++; - Add("N", ref sbPrimary, ref sbSecondary); break; - case 'P': - if (Match(word, n + 1, 'H')) - { - Add("F", ref sbPrimary, ref sbSecondary); - n += 2; - break; - } + } - n += Match(word, n + 1, P_B) ? 2 : 1; - Add("P", ref sbPrimary, ref sbSecondary); - break; - case 'Q': - n += Match(word, n + 1, 'Q') ? 2 : 1; - Add("K", ref sbPrimary, ref sbSecondary); - break; - case 'R': - if ((n == last) && !isSlavoGermanic && Match(word, n - 2, IE) && !Match(word, n - 4, ME_MA)) + if ((n == 0) && !Match(word, n, JOSE)) + { + Add("J", "A", ref sbPrimary, ref sbSecondary, ref hasAlternate); + } + else + { + if (IsVowel(word, n - 1) && !isSlavoGermanic && Match(word, n + 1, A_O)) { - Add("", "R", ref sbPrimary, ref sbSecondary, ref hasAlternate); + Add("J", "H", ref sbPrimary, ref sbSecondary, ref hasAlternate); } else { - Add("R", ref sbPrimary, ref sbSecondary); - } - - n += Match(word, n + 1, 'R') ? 2 : 1; - break; - case 'S': - if (Match(word, n - 1, ISL_YSL)) - { - n++; - break; - } - - if ((n == 0) && Match(word, n, SUGAR)) - { - Add("X", "S", ref sbPrimary, ref sbSecondary, ref hasAlternate); - n++; - break; + if (n == last) + { + Add("J", " ", ref sbPrimary, ref sbSecondary, ref hasAlternate); + } + else + { + if (!Match(word, n + 1, L_T_K_S_N_M_B_Z) && !Match(word, n - 1, S_K_L)) + Add("J", ref sbPrimary, ref sbSecondary); + } } + } - if (Match(word, n, SH)) + n += Match(word, n + 1, 'J') ? 2 : 1; + break; + case 'K': + n += Match(word, n + 1, 'K') ? 2 : 1; + Add("K", ref sbPrimary, ref sbSecondary); + break; + case 'L': + if (Match(word, n + 1, 'L')) + { + if (((n == length - 3) && Match(word, n - 1, ILLO_ILLA_ALLE)) || ((Match(word, last - 1, AS_OS) || Match(word, last, A_O)) && Match(word, n - 1, ALLE))) { - Add(Match(word, n + 1, HEIM_HOEK_HOLM_HOLZ) ? "S" : "X", ref sbPrimary, ref sbSecondary); + Add("L", " ", ref sbPrimary, ref sbSecondary, ref hasAlternate); n += 2; break; } + n += 2; + } + else + { + n++; + } + Add("L", ref sbPrimary, ref sbSecondary); + break; + case 'M': + if ((Match(word, n - 1, UMB) && ((n + 1 == last) || Match(word, n + 2, ER))) || Match(word, n + 1, 'M')) + { + n += 2; + } + else + { + n++; + } + Add("M", ref sbPrimary, ref sbSecondary); + break; + case 'N': + n += Match(word, n + 1, 'N') ? 2 : 1; + Add("N", ref sbPrimary, ref sbSecondary); + break; + case 'Ñ': + n++; + Add("N", ref sbPrimary, ref sbSecondary); + break; + case 'P': + if (Match(word, n + 1, 'H')) + { + Add("F", ref sbPrimary, ref sbSecondary); + n += 2; + break; + } + + n += Match(word, n + 1, P_B) ? 2 : 1; + Add("P", ref sbPrimary, ref sbSecondary); + break; + case 'Q': + n += Match(word, n + 1, 'Q') ? 2 : 1; + Add("K", ref sbPrimary, ref sbSecondary); + break; + case 'R': + if ((n == last) && !isSlavoGermanic && Match(word, n - 2, IE) && !Match(word, n - 4, ME_MA)) + { + Add("", "R", ref sbPrimary, ref sbSecondary, ref hasAlternate); + } + else + { + Add("R", ref sbPrimary, ref sbSecondary); + } + + n += Match(word, n + 1, 'R') ? 2 : 1; + break; + case 'S': + if (Match(word, n - 1, ISL_YSL)) + { + n++; + break; + } - if (Match(word, n, SIO_SIA) || Match(word, n, SIAN)) - { - if (!isSlavoGermanic) - Add("S", "X", ref sbPrimary, ref sbSecondary, ref hasAlternate); - else - Add("S", ref sbPrimary, ref sbSecondary); - n += 3; - break; - } + if ((n == 0) && Match(word, n, SUGAR)) + { + Add("X", "S", ref sbPrimary, ref sbSecondary, ref hasAlternate); + n++; + break; + } - if (((n == 0) && Match(word, n + 1, M_N_L_W)) || Match(word, n + 1, 'Z')) - { + if (Match(word, n, SH)) + { + Add(Match(word, n + 1, HEIM_HOEK_HOLM_HOLZ) ? "S" : "X", ref sbPrimary, ref sbSecondary); + n += 2; + break; + } + + if (Match(word, n, SIO_SIA) || Match(word, n, SIAN)) + { + if (!isSlavoGermanic) Add("S", "X", ref sbPrimary, ref sbSecondary, ref hasAlternate); - n += Match(word, n + 1, 'Z') ? 2 : 1; - break; - } + else + Add("S", ref sbPrimary, ref sbSecondary); + n += 3; + break; + } + + if (((n == 0) && Match(word, n + 1, M_N_L_W)) || Match(word, n + 1, 'Z')) + { + Add("S", "X", ref sbPrimary, ref sbSecondary, ref hasAlternate); + n += Match(word, n + 1, 'Z') ? 2 : 1; + break; + } - if (Match(word, n, SC)) + if (Match(word, n, SC)) + { + if (Match(word, n + 2, 'H')) { - if (Match(word, n + 2, 'H')) + if (Match(word, n + 3, OO_ER_EN_UY_ED_EM)) { - if (Match(word, n + 3, OO_ER_EN_UY_ED_EM)) - { - if (Match(word, n + 3, ER_EN)) - Add("X", "SK", ref sbPrimary, ref sbSecondary, ref hasAlternate); - else - Add("SK", ref sbPrimary, ref sbSecondary); - n += 3; - break; - } - if ((n == 0) && !IsVowel(word, 3) && !Match(word, 3, 'W')) - Add("X", "S", ref sbPrimary, ref sbSecondary, ref hasAlternate); + if (Match(word, n + 3, ER_EN)) + Add("X", "SK", ref sbPrimary, ref sbSecondary, ref hasAlternate); else - Add("X", ref sbPrimary, ref sbSecondary); + Add("SK", ref sbPrimary, ref sbSecondary); n += 3; break; } - - Add(Match(word, n + 2, I_E_Y) ? "S" : "SK", ref sbPrimary, ref sbSecondary); + if ((n == 0) && !IsVowel(word, 3) && !Match(word, 3, 'W')) + Add("X", "S", ref sbPrimary, ref sbSecondary, ref hasAlternate); + else + Add("X", ref sbPrimary, ref sbSecondary); n += 3; break; } - if ((n == last) && Match(word, n - 2, AI_OI)) - Add("", "S", ref sbPrimary, ref sbSecondary, ref hasAlternate); - else - Add("S", ref sbPrimary, ref sbSecondary); - - n += Match(word, n + 1, S_Z) ? 2 : 1; + Add(Match(word, n + 2, I_E_Y) ? "S" : "SK", ref sbPrimary, ref sbSecondary); + n += 3; break; - case 'T': - if (Match(word, n, TION)) - { - Add("X", ref sbPrimary, ref sbSecondary); - n += 3; - break; - } - - if (Match(word, n, TIA_TCH)) - { - Add("X", ref sbPrimary, ref sbSecondary); - n += 3; - break; - } + } - if (Match(word, n, TH_TTH)) - { - if (Match(word, n + 2, OM_AM) || Match(word, 0, VAN__VON__SCH)) - Add("T", ref sbPrimary, ref sbSecondary); - else - Add("0", "T", ref sbPrimary, ref sbSecondary, ref hasAlternate); - n += 2; - break; - } + if ((n == last) && Match(word, n - 2, AI_OI)) + Add("", "S", ref sbPrimary, ref sbSecondary, ref hasAlternate); + else + Add("S", ref sbPrimary, ref sbSecondary); - n += Match(word, n + 1, T_D) ? 2 : 1; - Add("T", ref sbPrimary, ref sbSecondary); - break; - case 'V': - n += Match(word, n + 1, 'V') ? 2 : 1; - Add("F", ref sbPrimary, ref sbSecondary); + n += Match(word, n + 1, S_Z) ? 2 : 1; + break; + case 'T': + if (Match(word, n, TION)) + { + Add("X", ref sbPrimary, ref sbSecondary); + n += 3; break; - case 'W': - if (Match(word, n, WR)) - { - Add("R", ref sbPrimary, ref sbSecondary); - n += 2; - break; - } + } - if ((n == 0) && (IsVowel(word, n + 1) || Match(word, n, WH))) - { - if (IsVowel(word, n + 1)) - Add("A", "F", ref sbPrimary, ref sbSecondary, ref hasAlternate); - else - Add("A", ref sbPrimary, ref sbSecondary); - } + if (Match(word, n, TIA_TCH)) + { + Add("X", ref sbPrimary, ref sbSecondary); + n += 3; + break; + } - if (((n == last) && IsVowel(word, n - 1)) || Match(word, n - 1, EWSKI_EWSKY_OWSKI_OWSKY) || Match(word, 0, SCH)) - { - Add("", "F", ref sbPrimary, ref sbSecondary, ref hasAlternate); - n++; - break; - } + if (Match(word, n, TH_TTH)) + { + if (Match(word, n + 2, OM_AM) || Match(word, 0, VAN__VON__SCH)) + Add("T", ref sbPrimary, ref sbSecondary); + else + Add("0", "T", ref sbPrimary, ref sbSecondary, ref hasAlternate); + n += 2; + break; + } + + n += Match(word, n + 1, T_D) ? 2 : 1; + Add("T", ref sbPrimary, ref sbSecondary); + break; + case 'V': + n += Match(word, n + 1, 'V') ? 2 : 1; + Add("F", ref sbPrimary, ref sbSecondary); + break; + case 'W': + if (Match(word, n, WR)) + { + Add("R", ref sbPrimary, ref sbSecondary); + n += 2; + break; + } - if (Match(word, n, WICZ_WITZ)) - { - Add("TS", "FX", ref sbPrimary, ref sbSecondary, ref hasAlternate); - n += 4; - break; - } + if ((n == 0) && (IsVowel(word, n + 1) || Match(word, n, WH))) + { + if (IsVowel(word, n + 1)) + Add("A", "F", ref sbPrimary, ref sbSecondary, ref hasAlternate); + else + Add("A", ref sbPrimary, ref sbSecondary); + } + if (((n == last) && IsVowel(word, n - 1)) || Match(word, n - 1, EWSKI_EWSKY_OWSKI_OWSKY) || Match(word, 0, SCH)) + { + Add("", "F", ref sbPrimary, ref sbSecondary, ref hasAlternate); n++; break; - case 'X': - if (!((n == last) && (Match(word, n - 3, IAU_EAU) || Match(word, n - 2, AU_OU)))) - Add("KS", ref sbPrimary, ref sbSecondary); + } - n += Match(word, n + 1, C_X) ? 2 : 1; + if (Match(word, n, WICZ_WITZ)) + { + Add("TS", "FX", ref sbPrimary, ref sbSecondary, ref hasAlternate); + n += 4; break; - case 'Z': - if (Match(word, n + 1, 'H')) - { - Add("J", ref sbPrimary, ref sbSecondary); - n += 2; - break; - } - if (Match(word, n + 1, ZO_ZI_ZA) || (isSlavoGermanic && (n > 0) && !Match(word, n - 1, 'T'))) - { - Add("S", "TS", ref sbPrimary, ref sbSecondary, ref hasAlternate); - } - else - { - Add("S", ref sbPrimary, ref sbSecondary); - } - - n += Match(word, n + 1, 'Z') ? 2 : 1; - break; - default: - n++; + } + + n++; + break; + case 'X': + if (!((n == last) && (Match(word, n - 3, IAU_EAU) || Match(word, n - 2, AU_OU)))) + Add("KS", ref sbPrimary, ref sbSecondary); + + n += Match(word, n + 1, C_X) ? 2 : 1; + break; + case 'Z': + if (Match(word, n + 1, 'H')) + { + Add("J", ref sbPrimary, ref sbSecondary); + n += 2; break; - } - } + } + if (Match(word, n + 1, ZO_ZI_ZA) || (isSlavoGermanic && (n > 0) && !Match(word, n - 1, 'T'))) + { + Add("S", "TS", ref sbPrimary, ref sbSecondary, ref hasAlternate); + } + else + { + Add("S", ref sbPrimary, ref sbSecondary); + } - if (MaxLength < 0) - { - if (hasAlternate) - return new[] { sbPrimary.ToString(), sbSecondary.ToString() }; - return new[] { sbPrimary.ToString() }; - } - int primaryLength = Math.Min(MaxLength, sbPrimary.Length); - if (hasAlternate) - { - int secondaryLength = Math.Min(MaxLength, sbSecondary.Length); - return new[] { sbPrimary.ToString().Substring(0, primaryLength - 0), sbSecondary.ToString().Substring(0, secondaryLength - 0) }; + n += Match(word, n + 1, 'Z') ? 2 : 1; + break; + default: + n++; + break; } - return new[] { sbPrimary.ToString().Substring(0, primaryLength - 0) }; } - public static IEnumerable GetKeys(string phrase) + if (MaxLength < 0) { - var set = new HashSet(); - var keys = new List(); + if (hasAlternate) + return new[] { sbPrimary.ToString(), sbSecondary.ToString() }; + return new[] { sbPrimary.ToString() }; + } + int primaryLength = Math.Min(MaxLength, sbPrimary.Length); + if (hasAlternate) + { + int secondaryLength = Math.Min(MaxLength, sbSecondary.Length); + return new[] { sbPrimary.ToString().Substring(0, primaryLength - 0), sbSecondary.ToString().Substring(0, secondaryLength - 0) }; + } + return new[] { sbPrimary.ToString().Substring(0, primaryLength - 0) }; + } - foreach (Match m in regex.Matches(phrase)) - { - if (m.Value.Length > 2) set.Add(m.Value); - } + public static IEnumerable GetKeys(string phrase) + { + var set = new HashSet(); + var keys = new List(); + + foreach (Match m in regex.Matches(phrase)) + { + if (m.Value.Length > 2) set.Add(m.Value); + } - if (set.Count == 0) return keys; + if (set.Count == 0) return keys; - foreach (var word in set) + foreach (var word in set) + { + foreach (var key in BuildKeys(word)) { - foreach (var key in BuildKeys(word)) - { - keys.Add(key); - } + keys.Add(key); } - - return keys; } + + return keys; } } diff --git a/MongoDB.Entities/Core/Entity.cs b/MongoDB.Entities/Core/Entity.cs index 614e646fc..921dddd8f 100644 --- a/MongoDB.Entities/Core/Entity.cs +++ b/MongoDB.Entities/Core/Entity.cs @@ -1,23 +1,37 @@ -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; +namespace MongoDB.Entities; -namespace MongoDB.Entities +/// +/// Inherit this class for all entities you want to store in their own collection. +/// +public abstract class Entity : IEntity { /// - /// Inherit this class for all entities you want to store in their own collection. + /// This property is auto managed. A new ID will be assigned for new entities upon saving. /// - public abstract class Entity : IEntity - { - /// - /// This property is auto managed. A new ID will be assigned for new entities upon saving. - /// - [BsonId, AsObjectId] - public string? ID { get; set; } + [BsonId, AsObjectId] + public string? ID { get; set; } - /// - /// Override this method in order to control the generation of IDs for new entities. - /// - public virtual string GenerateNewID() - => ObjectId.GenerateNewId().ToString(); - } + /// + /// Override this method in order to control the generation of IDs for new entities. + /// + public virtual string GenerateNewID() + => ObjectId.GenerateNewId().ToString(); } + +/// +/// Inherit this class for all entities you want to store in their own collection. +/// +public abstract class Entity : IEntity + where TId : IComparable, IEquatable +{ + /// + /// This property is auto managed. A new ID will be assigned for new entities upon saving. + /// + [BsonId] + public TId? ID { get; set; } + + /// + /// Override this method in order to control the generation of IDs for new entities. + /// + public abstract TId GenerateNewID(); +} \ No newline at end of file diff --git a/MongoDB.Entities/Core/EntityCache.cs b/MongoDB.Entities/Core/EntityCache.cs index 0fc9c30a3..d230f4f2b 100644 --- a/MongoDB.Entities/Core/EntityCache.cs +++ b/MongoDB.Entities/Core/EntityCache.cs @@ -1,122 +1,122 @@ -using MongoDB.Bson.Serialization; -using MongoDB.Bson.Serialization.Attributes; -using MongoDB.Driver; -using MongoDB.Driver.Linq; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -#nullable enable -namespace MongoDB.Entities +using System.Collections.Concurrent; + +namespace MongoDB.Entities; + +internal class Cache { - internal abstract class Cache + protected PropertyInfo[] _updatableProps = null!; + + public bool HasCreatedOn { get; protected set; } + public bool HasModifiedOn { get; protected set; } + public string ModifiedOnPropName { get; } = nameof(IModifiedOn.ModifiedOn); + public PropertyInfo? ModifiedByProp { get; protected set; } + public bool HasIgnoreIfDefaultProps { get; protected set; } + public string CollectionName { get; protected set; } = null!; + public bool IsFileEntity { get; protected set; } + protected Cache(Type type) { - protected PropertyInfo[] _updatableProps = null!; - - public bool HasCreatedOn { get; protected set; } - public bool HasModifiedOn { get; protected set; } - public string ModifiedOnPropName { get; } = nameof(IModifiedOn.ModifiedOn); - public PropertyInfo? ModifiedByProp { get; protected set; } - public bool HasIgnoreIfDefaultProps { get; protected set; } - public string CollectionName { get; protected set; } = null!; - public bool IsFileEntity { get; protected set; } - } + var interfaces = type.GetInterfaces(); - internal class EntityCache : Cache where T : IEntity - { - + var collAttrb = type.GetCustomAttribute(false); - public ConcurrentDictionary> Watchers { get; } = new(); + CollectionName = collAttrb != null ? collAttrb.Name : type.Name; - private ProjectionDefinition? _requiredPropsProjection; + if (string.IsNullOrWhiteSpace(CollectionName) || CollectionName.Contains("~")) + throw new ArgumentException($"{CollectionName} is an illegal name for a collection!"); - public EntityCache() - { - var type = typeof(T); - var interfaces = type.GetInterfaces(); - var collAttrb = type.GetCustomAttribute(false); + HasCreatedOn = interfaces.Any(i => i == typeof(ICreatedOn)); + HasModifiedOn = interfaces.Any(i => i == typeof(IModifiedOn)); + IsFileEntity = typeof(FileEntity).IsAssignableFrom(type.BaseType); - CollectionName = collAttrb != null ? collAttrb.Name : type.Name; + _updatableProps = type.GetProperties() + .Where(p => + p.PropertyType.Name != ManyBase.PropTypeName && + !p.IsDefined(typeof(BsonIdAttribute), false) && + !p.IsDefined(typeof(BsonIgnoreAttribute), false)) + .ToArray(); - if (string.IsNullOrWhiteSpace(CollectionName) || CollectionName.Contains("~")) - throw new ArgumentException($"{CollectionName} is an illegal name for a collection!"); + HasIgnoreIfDefaultProps = _updatableProps.Any(p => + p.IsDefined(typeof(BsonIgnoreIfDefaultAttribute), false) || + p.IsDefined(typeof(BsonIgnoreIfNullAttribute), false)); + try + { + ModifiedByProp = _updatableProps.SingleOrDefault(p => typeof(ModifiedBy).IsAssignableFrom(p.PropertyType)); + } + catch (InvalidOperationException) + { + throw new InvalidOperationException("Multiple [ModifiedBy] properties are not allowed on entities!"); + } + } - HasCreatedOn = interfaces.Any(i => i == typeof(ICreatedOn)); - HasModifiedOn = interfaces.Any(i => i == typeof(IModifiedOn)); - IsFileEntity = typeof(FileEntity).IsAssignableFrom(type.BaseType); - _updatableProps = type.GetProperties() - .Where(p => - p.PropertyType.Name != ManyBase.PropTypeName && - !p.IsDefined(typeof(BsonIdAttribute), false) && - !p.IsDefined(typeof(BsonIgnoreAttribute), false)) - .ToArray(); - HasIgnoreIfDefaultProps = _updatableProps.Any(p => - p.IsDefined(typeof(BsonIgnoreIfDefaultAttribute), false) || - p.IsDefined(typeof(BsonIgnoreIfNullAttribute), false)); - try - { - ModifiedByProp = _updatableProps.SingleOrDefault(p => typeof(ModifiedBy).IsAssignableFrom(p.PropertyType)); - } - catch (InvalidOperationException) - { - throw new InvalidOperationException("Multiple [ModifiedBy] properties are not allowed on entities!"); - } - } +} - public IEnumerable UpdatableProps(T entity) +internal class EntityCache : Cache +{ + public ConcurrentDictionary> Watchers { get; } = new(); + + private static EntityCache? _default; + public static EntityCache Default => _default ??= new(); + + private EntityCache() : base(typeof(T)) + { + } + + public IEnumerable UpdatableProps(T entity) + { + if (HasIgnoreIfDefaultProps) { - if (HasIgnoreIfDefaultProps) - { - return _updatableProps.Where(p => - !(p.IsDefined(typeof(BsonIgnoreIfDefaultAttribute), false) && p.GetValue(entity) == default) && - !(p.IsDefined(typeof(BsonIgnoreIfNullAttribute), false) && p.GetValue(entity) == null)); - } - return _updatableProps; + return _updatableProps.Where(p => + !(p.IsDefined(typeof(BsonIgnoreIfDefaultAttribute), false) && p.GetValue(entity) == default) && + !(p.IsDefined(typeof(BsonIgnoreIfNullAttribute), false) && p.GetValue(entity) == null)); } + return _updatableProps; + } - public ProjectionDefinition CombineWithRequiredProps(ProjectionDefinition userProjection) - { - if (userProjection == null) - throw new InvalidOperationException("Please use .Project() method before .IncludeRequiredProps()"); + private ProjectionDefinition? _requiredPropsProjection; - if (_requiredPropsProjection is null) - { - _requiredPropsProjection = "{_id:1}"; + public ProjectionDefinition CombineWithRequiredProps(ProjectionDefinition userProjection) + { + if (userProjection == null) + throw new InvalidOperationException("Please use .Project() method before .IncludeRequiredProps()"); - var props = typeof(T) - .GetProperties() - .Where(p => p.IsDefined(typeof(BsonRequiredAttribute), false)); + if (_requiredPropsProjection is null) + { + _requiredPropsProjection = "{_id:1}"; + + var props = typeof(T) + .GetProperties() + .Where(p => p.IsDefined(typeof(BsonRequiredAttribute), false)); - if (!props.Any()) - throw new InvalidOperationException("Unable to find any entity properties marked with [BsonRequired] attribute!"); + if (!props.Any()) + throw new InvalidOperationException("Unable to find any entity properties marked with [BsonRequired] attribute!"); - FieldAttribute attr; - foreach (var p in props) - { - attr = p.GetCustomAttribute(); + FieldAttribute attr; + foreach (var p in props) + { + attr = p.GetCustomAttribute(); - if (attr is null) - _requiredPropsProjection = _requiredPropsProjection.Include(p.Name); - else - _requiredPropsProjection = _requiredPropsProjection.Include(attr.ElementName); - } + if (attr is null) + _requiredPropsProjection = _requiredPropsProjection.Include(p.Name); + else + _requiredPropsProjection = _requiredPropsProjection.Include(attr.ElementName); } + } - ProjectionDefinition userProj = userProjection.Render( - BsonSerializer.LookupSerializer(), - BsonSerializer.SerializerRegistry).Document; + ProjectionDefinition userProj = userProjection.Render( + BsonSerializer.LookupSerializer(), + BsonSerializer.SerializerRegistry).Document; - return Builders.Projection.Combine(new[] - { + return Builders.Projection.Combine(new[] + { _requiredPropsProjection, userProj }); - } } -} \ No newline at end of file + + +} diff --git a/MongoDB.Entities/Core/FileEntity.cs b/MongoDB.Entities/Core/FileEntity.cs index 97a2349c2..6ab699647 100644 --- a/MongoDB.Entities/Core/FileEntity.cs +++ b/MongoDB.Entities/Core/FileEntity.cs @@ -1,281 +1,284 @@ -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; -using MongoDB.Driver; -using System; -using System.Collections.Generic; -using System.IO; -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; using System.Security.Cryptography; -using System.Threading; -using System.Threading.Tasks; [assembly: InternalsVisibleTo("MongoDB.Entities.Tests")] -namespace MongoDB.Entities +namespace MongoDB.Entities; + +public class FileEntity : FileEntity { - /// - /// Inherit this base class in order to create your own File Entities - /// - public abstract class FileEntity : Entity + public override string GenerateNewID() { - /// - /// The total amount of data in bytes that has been uploaded so far - /// - [BsonElement] - public long FileSize { get; internal set; } - - /// - /// The number of chunks that have been created so far - /// - [BsonElement] - public int ChunkCount { get; internal set; } - - /// - /// Returns true only when all the chunks have been stored successfully in mongodb - /// - [BsonElement] - public bool UploadSuccessful { get; internal set; } - - /// - /// If this value is set, the uploaded data will be hashed and matched against this value. If the hash is not equal, an exception will be thrown by the UploadAsync() method. - /// - [IgnoreDefault] - public string? MD5 { get; set; } + return ObjectId.GenerateNewId().ToString(); } +} +/// +/// Inherit this base class in order to create your own File Entities +/// +[Collection("[BINARY_CHUNKS]")] +public abstract class FileEntity : Entity + where TId : IComparable, IEquatable +{ + /// + /// The total amount of data in bytes that has been uploaded so far + /// + [BsonElement] + public long FileSize { get; internal set; } - [Collection("[BINARY_CHUNKS]")] - internal class FileChunk : IEntity - { - [BsonId, ObjectId] - public string ID { get; set; } = null!; + /// + /// The number of chunks that have been created so far + /// + [BsonElement] + public int ChunkCount { get; internal set; } + + /// + /// Returns true only when all the chunks have been stored successfully in mongodb + /// + [BsonElement] + public bool UploadSuccessful { get; internal set; } + + /// + /// If this value is set, the uploaded data will be hashed and matched against this value. If the hash is not equal, an exception will be thrown by the UploadAsync() method. + /// + [IgnoreDefault] + public string? MD5 { get; set; } +} + +[Collection("[BINARY_CHUNKS]")] +internal class FileChunk : IEntity + where TFileId : IComparable, IEquatable +{ + [BsonId] + public ObjectId ID { get; set; } + + public TFileId? FileID { get; set; } - [AsObjectId] - public string FileID { get; set; } = null!; + public byte[] Data { get; set; } = Array.Empty(); - public byte[] Data { get; set; } = Array.Empty(); + public ObjectId GenerateNewID() + => ObjectId.GenerateNewId(); +} - public string GenerateNewID() - => ObjectId.GenerateNewId().ToString(); +/// +/// Provides the interface for uploading and downloading data chunks for file entities. +/// +public class DataStreamer + where T : FileEntity + where TId : IComparable, IEquatable +{ + private static readonly HashSet _indexedDBs = new(); + + private readonly T _parent; + private readonly DBContext _db; + private readonly IMongoCollection _collection; + private readonly IMongoCollection> _chunkCollection; + private FileChunk? _doc; + private int _chunkSize, _readCount; + private byte[]? _buffer; + private List? _dataChunk; + private MD5? _md5; + + internal DataStreamer(T parent, DBContext db, IMongoCollection collection) + { + _parent = parent; + _db = db; + _chunkCollection = db.Collection>(); + _collection = collection; + if (_indexedDBs.Add(db.DatabaseNamespace.DatabaseName)) + { + _ = _chunkCollection.Indexes.CreateOneAsync( + new CreateIndexModel>( + Builders>.IndexKeys.Ascending(c => c.FileID), + new CreateIndexOptions { Background = true, Name = $"{nameof(FileChunk.FileID)}(Asc)" })); + } } /// - /// Provides the interface for uploading and downloading data chunks for file entities. + /// Download binary data for this file entity from mongodb in chunks into a given stream with a timeout period. /// - public class DataStreamer where T : FileEntity + /// The output stream to write the data + /// The maximum number of seconds allowed for the operation to complete + /// + public Task DownloadWithTimeoutAsync(Stream stream, int timeOutSeconds, int batchSize = 1) { - private static readonly HashSet _indexedDBs = new(); - - private readonly T _parent; - private readonly DBContext _db; - private readonly IMongoCollection _collection; - private readonly IMongoCollection _chunkCollection; - private FileChunk? _doc; - private int _chunkSize, _readCount; - private byte[]? _buffer; - private List? _dataChunk; - private MD5? _md5; - - internal DataStreamer(T parent, DBContext db, IMongoCollection collection) - { - _parent = parent; - _db = db; - _chunkCollection = db.Collection(); - _collection = collection; - if (_indexedDBs.Add(db.DatabaseNamespace.DatabaseName)) - { - _ = _chunkCollection.Indexes.CreateOneAsync( - new CreateIndexModel( - Builders.IndexKeys.Ascending(c => c.FileID), - new CreateIndexOptions { Background = true, Name = $"{nameof(FileChunk.FileID)}(Asc)" })); - } - } + return DownloadAsync(stream, batchSize, new CancellationTokenSource(timeOutSeconds * 1000).Token); + } - /// - /// Download binary data for this file entity from mongodb in chunks into a given stream with a timeout period. - /// - /// The output stream to write the data - /// The maximum number of seconds allowed for the operation to complete - /// - public Task DownloadWithTimeoutAsync(Stream stream, int timeOutSeconds, int batchSize = 1) - { - return DownloadAsync(stream, batchSize, new CancellationTokenSource(timeOutSeconds * 1000).Token); - } + /// + /// Download binary data for this file entity from mongodb in chunks into a given stream. + /// + /// The output stream to write the data + /// The number of chunks you want returned at once + /// An optional cancellation token. + public async Task DownloadAsync(Stream stream, int batchSize = 1, CancellationToken cancellation = default) + { + _parent.ThrowIfUnsaved(); + if (!_parent.UploadSuccessful) throw new InvalidOperationException("Data for this file hasn't been uploaded successfully (yet)!"); + if (!stream.CanWrite) throw new NotSupportedException("The supplied stream is not writable!"); - /// - /// Download binary data for this file entity from mongodb in chunks into a given stream. - /// - /// The output stream to write the data - /// The number of chunks you want returned at once - /// An optional cancellation token. - public async Task DownloadAsync(Stream stream, int batchSize = 1, CancellationToken cancellation = default) + var filter = Builders>.Filter.Eq(c => c.FileID, _parent.ID); + var options = new FindOptions, byte[]> { - _parent.ThrowIfUnsaved(); - if (!_parent.UploadSuccessful) throw new InvalidOperationException("Data for this file hasn't been uploaded successfully (yet)!"); - if (!stream.CanWrite) throw new NotSupportedException("The supplied stream is not writable!"); - - var filter = Builders.Filter.Eq(c => c.FileID, _parent.ID); - var options = new FindOptions - { - BatchSize = batchSize, - Sort = Builders.Sort.Ascending(c => c.ID), - Projection = Builders.Projection.Expression(c => c.Data) - }; + BatchSize = batchSize, + Sort = Builders>.Sort.Ascending(c => c.ID), + Projection = Builders>.Projection.Expression(c => c.Data) + }; - var findTask = - _db.Session is not IClientSessionHandle session - ? _chunkCollection.FindAsync(filter, options, cancellation) - : _chunkCollection.FindAsync(session, filter, options, cancellation); + var findTask = + _db.Session is not IClientSessionHandle session + ? _chunkCollection.FindAsync(filter, options, cancellation) + : _chunkCollection.FindAsync(session, filter, options, cancellation); - using var cursor = await findTask.ConfigureAwait(false); - var hasChunks = false; + using var cursor = await findTask.ConfigureAwait(false); + var hasChunks = false; - while (await cursor.MoveNextAsync(cancellation).ConfigureAwait(false)) + while (await cursor.MoveNextAsync(cancellation).ConfigureAwait(false)) + { + foreach (var chunk in cursor.Current) { - foreach (var chunk in cursor.Current) - { - await stream.WriteAsync(chunk, 0, chunk.Length, cancellation).ConfigureAwait(false); - hasChunks = true; - } + await stream.WriteAsync(chunk, 0, chunk.Length, cancellation).ConfigureAwait(false); + hasChunks = true; } - - if (!hasChunks) throw new InvalidOperationException($"No data was found for file entity with ID: {_parent.ID}"); } - /// - /// Upload binary data for this file entity into mongodb in chunks from a given stream with a timeout period. - /// - /// The input stream to read the data from - /// The maximum number of seconds allowed for the operation to complete - /// The 'average' size of one chunk in KiloBytes - public Task UploadWithTimeoutAsync(Stream stream, int timeOutSeconds, int chunkSizeKB = 256) - { - return UploadAsync(stream, chunkSizeKB, new CancellationTokenSource(timeOutSeconds * 1000).Token); - } + if (!hasChunks) throw new InvalidOperationException($"No data was found for file entity with ID: {_parent.ID}"); + } - /// - /// Upload binary data for this file entity into mongodb in chunks from a given stream. - /// TIP: Make sure to save the entity before calling this method. - /// - /// The input stream to read the data from - /// The 'average' size of one chunk in KiloBytes - /// An optional cancellation token. - public async Task UploadAsync(Stream stream, int chunkSizeKB = 256, CancellationToken cancellation = default) - { - _parent.ThrowIfUnsaved(); - if (chunkSizeKB < 128 || chunkSizeKB > 4096) throw new ArgumentException("Please specify a chunk size from 128KB to 4096KB"); - if (!stream.CanRead) throw new NotSupportedException("The supplied stream is not readable!"); - await CleanUpAsync().ConfigureAwait(false); + /// + /// Upload binary data for this file entity into mongodb in chunks from a given stream with a timeout period. + /// + /// The input stream to read the data from + /// The maximum number of seconds allowed for the operation to complete + /// The 'average' size of one chunk in KiloBytes + public Task UploadWithTimeoutAsync(Stream stream, int timeOutSeconds, int chunkSizeKB = 256) + { + return UploadAsync(stream, chunkSizeKB, new CancellationTokenSource(timeOutSeconds * 1000).Token); + } - _doc = new FileChunk { FileID = _parent.ID }; - _chunkSize = chunkSizeKB * 1024; - _dataChunk = new List(_chunkSize); - _buffer = new byte[64 * 1024]; // 64kb read buffer - _readCount = 0; + /// + /// Upload binary data for this file entity into mongodb in chunks from a given stream. + /// TIP: Make sure to save the entity before calling this method. + /// + /// The input stream to read the data from + /// The 'average' size of one chunk in KiloBytes + /// An optional cancellation token. + public async Task UploadAsync(Stream stream, int chunkSizeKB = 256, CancellationToken cancellation = default) + { + _parent.ThrowIfUnsaved(); + if (chunkSizeKB < 128 || chunkSizeKB > 4096) throw new ArgumentException("Please specify a chunk size from 128KB to 4096KB"); + if (!stream.CanRead) throw new NotSupportedException("The supplied stream is not readable!"); + await CleanUpAsync().ConfigureAwait(false); - if (!string.IsNullOrEmpty(_parent.MD5)) - _md5 = MD5.Create(); + _doc = new FileChunk { FileID = _parent.ID }; + _chunkSize = chunkSizeKB * 1024; + _dataChunk = new List(_chunkSize); + _buffer = new byte[64 * 1024]; // 64kb read buffer + _readCount = 0; - try - { - if (stream.CanSeek && stream.Position > 0) stream.Position = 0; + if (!string.IsNullOrEmpty(_parent.MD5)) + _md5 = MD5.Create(); - while ((_readCount = await stream.ReadAsync(_buffer, 0, _buffer.Length, cancellation).ConfigureAwait(false)) > 0) - { - _md5?.TransformBlock(_buffer, 0, _readCount, null, 0); - await FlushToDBAsync(isLastChunk: false, cancellation).ConfigureAwait(false); - } + try + { + if (stream.CanSeek && stream.Position > 0) stream.Position = 0; - if (_parent.FileSize > 0) - { - _md5?.TransformFinalBlock(_buffer, 0, _readCount); - if (_md5 != null && !BitConverter.ToString(_md5.Hash).Replace("-", "").Equals(_parent.MD5, StringComparison.OrdinalIgnoreCase)) - { - throw new InvalidDataException("MD5 of uploaded data doesn't match with file entity MD5."); - } - await FlushToDBAsync(isLastChunk: true, cancellation).ConfigureAwait(false); - _parent.UploadSuccessful = true; - } - else - { - throw new InvalidOperationException("The supplied stream had no data to read (probably closed)"); - } + while ((_readCount = await stream.ReadAsync(_buffer, 0, _buffer.Length, cancellation).ConfigureAwait(false)) > 0) + { + _md5?.TransformBlock(_buffer, 0, _readCount, null, 0); + await FlushToDBAsync(isLastChunk: false, cancellation).ConfigureAwait(false); } - catch (Exception) + + if (_parent.FileSize > 0) { - await CleanUpAsync().ConfigureAwait(false); - throw; + _md5?.TransformFinalBlock(_buffer, 0, _readCount); + if (_md5 != null && !BitConverter.ToString(_md5.Hash).Replace("-", "").Equals(_parent.MD5, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidDataException("MD5 of uploaded data doesn't match with file entity MD5."); + } + await FlushToDBAsync(isLastChunk: true, cancellation).ConfigureAwait(false); + _parent.UploadSuccessful = true; } - finally + else { - await UpdateMetaDataAsync().ConfigureAwait(false); - _doc = null; - _buffer = null; - _dataChunk = null; - _md5?.Dispose(); - _md5 = null; + throw new InvalidOperationException("The supplied stream had no data to read (probably closed)"); } } - - /// - /// Deletes only the binary chunks stored in the database for this file entity. - /// - /// An optional cancellation token. - public Task DeleteBinaryChunks(CancellationToken cancellation = default) + catch (Exception) { - _parent.ThrowIfUnsaved(); - - if (cancellation != default && _db.Session == null) - throw new NotSupportedException("Cancellation is only supported within transactions for deleting binary chunks!"); - - return CleanUpAsync(cancellation); + await CleanUpAsync().ConfigureAwait(false); + throw; } - - private Task CleanUpAsync(CancellationToken cancellation = default) + finally { - _parent.FileSize = 0; - _parent.ChunkCount = 0; - _parent.UploadSuccessful = false; - return _db.Session is not IClientSessionHandle session - ? _chunkCollection.DeleteManyAsync(c => c.FileID == _parent.ID, cancellation) - : _chunkCollection.DeleteManyAsync(session, c => c.FileID == _parent.ID, null, cancellation); + await UpdateMetaDataAsync().ConfigureAwait(false); + _doc = null; + _buffer = null; + _dataChunk = null; + _md5?.Dispose(); + _md5 = null; } + } - private Task FlushToDBAsync(bool isLastChunk = false, CancellationToken cancellation = default) - { - if (!isLastChunk) - { - _dataChunk?.AddRange(new ArraySegment(_buffer, 0, _readCount)); - _parent.FileSize += _readCount; - } - if (_doc is null) - { - return Task.CompletedTask; - } - if (_dataChunk is not null && (_dataChunk.Count >= _chunkSize || isLastChunk)) - { + /// + /// Deletes only the binary chunks stored in the database for this file entity. + /// + /// An optional cancellation token. + public Task DeleteBinaryChunks(CancellationToken cancellation = default) + { + _parent.ThrowIfUnsaved(); - _doc.ID = _doc.GenerateNewID(); - _doc.Data = _dataChunk.ToArray(); - _dataChunk.Clear(); - _parent.ChunkCount++; - return _db.Session is not IClientSessionHandle session - ? _chunkCollection.InsertOneAsync(_doc, null, cancellation) - : _chunkCollection.InsertOneAsync(session, _doc, null, cancellation); - } + if (cancellation != default && _db.Session == null) + throw new NotSupportedException("Cancellation is only supported within transactions for deleting binary chunks!"); + + return CleanUpAsync(cancellation); + } + private Task CleanUpAsync(CancellationToken cancellation = default) + { + _parent.FileSize = 0; + _parent.ChunkCount = 0; + _parent.UploadSuccessful = false; + var filter = Builders>.Filter.Eq(c => c.FileID, _parent.ID); + return _db.Session is not IClientSessionHandle session + ? _chunkCollection.DeleteManyAsync(filter, cancellation) + : _chunkCollection.DeleteManyAsync(session, filter, cancellationToken: cancellation); + } + + private Task FlushToDBAsync(bool isLastChunk = false, CancellationToken cancellation = default) + { + if (!isLastChunk) + { + _dataChunk?.AddRange(new ArraySegment(_buffer, 0, _readCount)); + _parent.FileSize += _readCount; + } + if (_doc is null) + { return Task.CompletedTask; } - - private Task UpdateMetaDataAsync() + if (_dataChunk is not null && (_dataChunk.Count >= _chunkSize || isLastChunk)) { - var filter = Builders.Filter.Eq(e => e.ID, _parent.ID); - var update = Builders.Update - .Set(e => e.FileSize, _parent.FileSize) - .Set(e => e.ChunkCount, _parent.ChunkCount) - .Set(e => e.UploadSuccessful, _parent.UploadSuccessful); + _doc.ID = _doc.GenerateNewID(); + _doc.Data = _dataChunk.ToArray(); + _dataChunk.Clear(); + _parent.ChunkCount++; return _db.Session is not IClientSessionHandle session - ? _collection.UpdateOneAsync(filter, update) - : _collection.UpdateOneAsync(session, filter, update); + ? _chunkCollection.InsertOneAsync(_doc, null, cancellation) + : _chunkCollection.InsertOneAsync(session, _doc, null, cancellation); } + + return Task.CompletedTask; + } + + private Task UpdateMetaDataAsync() + { + var filter = Builders.Filter.Eq(e => e.ID, _parent.ID); + var update = Builders.Update + .Set(e => e.FileSize, _parent.FileSize) + .Set(e => e.ChunkCount, _parent.ChunkCount) + .Set(e => e.UploadSuccessful, _parent.UploadSuccessful); + + return _db.Session is not IClientSessionHandle session + ? _collection.UpdateOneAsync(filter, update) + : _collection.UpdateOneAsync(session, filter, update); } } diff --git a/MongoDB.Entities/Core/FuzzyString.cs b/MongoDB.Entities/Core/FuzzyString.cs index 21b71164d..3ca037037 100644 --- a/MongoDB.Entities/Core/FuzzyString.cs +++ b/MongoDB.Entities/Core/FuzzyString.cs @@ -1,102 +1,97 @@ -using MongoDB.Bson; -using MongoDB.Bson.IO; -using MongoDB.Bson.Serialization; -using MongoDB.Bson.Serialization.Serializers; -using System; +using MongoDB.Bson.IO; -namespace MongoDB.Entities +namespace MongoDB.Entities; + +internal class FuzzyStringSerializer : SerializerBase, IBsonDocumentSerializer { - internal class FuzzyStringSerializer : SerializerBase, IBsonDocumentSerializer - { - private static readonly StringSerializer strSerializer = new(); + private static readonly StringSerializer _strSerializer = new(); - public override void Serialize(BsonSerializationContext ctx, BsonSerializationArgs args, FuzzyString fString) + public override void Serialize(BsonSerializationContext ctx, BsonSerializationArgs args, FuzzyString? fString) + { + if (fString == null || string.IsNullOrWhiteSpace(fString.Value)) { - if (fString == null || string.IsNullOrWhiteSpace(fString.Value)) - { - ctx.Writer.WriteNull(); - } - else - { - if (fString.Value.Length > FuzzyString.CharacterLimit) - throw new NotSupportedException($"FuzzyString can only hold a maximum of {FuzzyString.CharacterLimit} characters!"); - - ctx.Writer.WriteStartDocument(); - ctx.Writer.WriteString("Value", fString.Value); - ctx.Writer.WriteString("Hash", fString.Value.ToDoubleMetaphoneHash()); - ctx.Writer.WriteEndDocument(); - } + ctx.Writer.WriteNull(); } - - public override FuzzyString Deserialize(BsonDeserializationContext ctx, BsonDeserializationArgs args) + else { - var bsonType = ctx.Reader.GetCurrentBsonType(); + if (fString.Value!.Length > FuzzyString.CharacterLimit) + throw new NotSupportedException($"FuzzyString can only hold a maximum of {FuzzyString.CharacterLimit} characters!"); - switch (bsonType) - { - case BsonType.Document: + ctx.Writer.WriteStartDocument(); + ctx.Writer.WriteString(nameof(FuzzyString.Value), fString.Value); + ctx.Writer.WriteString("Hash", fString.Value.ToDoubleMetaphoneHash()); + ctx.Writer.WriteEndDocument(); + } + } - string value = null; + public override FuzzyString? Deserialize(BsonDeserializationContext ctx, BsonDeserializationArgs args) + { + var bsonType = ctx.Reader.GetCurrentBsonType(); + + switch (bsonType) + { + case BsonType.Document: - ctx.Reader.ReadStartDocument(); - while (ctx.Reader.ReadBsonType() != BsonType.EndOfDocument) - { - if (ctx.Reader.ReadName() == "Value") - value = ctx.Reader.ReadString(); - else - ctx.Reader.SkipValue(); - } - ctx.Reader.ReadEndDocument(); + string? value = null; - if (value == null) - throw new FormatException("Unable to deserialize a value from the FuzzyString document!"); + ctx.Reader.ReadStartDocument(); + while (ctx.Reader.ReadBsonType() != BsonType.EndOfDocument) + { + if (ctx.Reader.ReadName() == nameof(FuzzyString.Value)) + value = ctx.Reader.ReadString(); + else + ctx.Reader.SkipValue(); + } + ctx.Reader.ReadEndDocument(); - return value; + if (value == null) + throw new FormatException("Unable to deserialize a value from the FuzzyString document!"); - case BsonType.Null: - ctx.Reader.ReadNull(); - return null; + return value; - default: - throw new FormatException($"Cannot deserialize a FuzzyString value from a [{bsonType}]"); - } + case BsonType.Null: + ctx.Reader.ReadNull(); + return null; + + default: + throw new FormatException($"Cannot deserialize a FuzzyString value from a [{bsonType}]"); } + } - public bool TryGetMemberSerializationInfo(string memberName, out BsonSerializationInfo serializationInfo) + public bool TryGetMemberSerializationInfo(string memberName, out BsonSerializationInfo? serializationInfo) + { + switch (memberName) { - switch (memberName) - { - case "Value": - serializationInfo = new BsonSerializationInfo("Value", strSerializer, typeof(string)); - return true; - default: - serializationInfo = null; - return false; - } + case "Value": + serializationInfo = new BsonSerializationInfo("Value", _strSerializer, typeof(string)); + return true; + default: + serializationInfo = null; + return false; } } +} - /// - /// Use this type to store strings if you need fuzzy text searching with MongoDB - /// TIP: There's a default limit of 250 characters for ensuring best performance. - /// If you exceed the default limit, an exception will be thrown. - /// You can increase the limit by sacrificing performance/resource utilization by setting the static property - /// FuzzyString.CharacterLimit = 500 at startup. - /// - public class FuzzyString - { - public static int CharacterLimit { get; set; } = 250; +/// +/// Use this type to store strings if you need fuzzy text searching with MongoDB +/// TIP: There's a default limit of 250 characters for ensuring best performance. +/// If you exceed the default limit, an exception will be thrown. +/// You can increase the limit by sacrificing performance/resource utilization by setting the static property +/// FuzzyString.CharacterLimit = 500 at startup. +/// +public class FuzzyString +{ + public static int CharacterLimit { get; set; } = 250; - public string Value { get; set; } + public string? Value { get; set; } - public static implicit operator FuzzyString(string value) - { - return new FuzzyString { Value = value }; - } + public static implicit operator FuzzyString(string value) + { + return new FuzzyString { Value = value }; + } - public static implicit operator string(FuzzyString fuzzyString) - { - return fuzzyString.Value; - } + public static implicit operator string?(FuzzyString fuzzyString) + { + return fuzzyString.Value; } } diff --git a/MongoDB.Entities/Core/GeoNear.cs b/MongoDB.Entities/Core/GeoNear.cs index cddf561d2..027d657a5 100644 --- a/MongoDB.Entities/Core/GeoNear.cs +++ b/MongoDB.Entities/Core/GeoNear.cs @@ -1,73 +1,69 @@ -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; -using MongoDB.Driver; -using MongoDB.Driver.GeoJsonObjectModel; +using MongoDB.Driver.GeoJsonObjectModel; -namespace MongoDB.Entities +namespace MongoDB.Entities; + +/// +/// Represents a 2D geographical coordinate consisting of longitude and latitude +/// +public class Coordinates2D { + [BsonElement("type")] + public string Type { get; set; } + + [BsonElement("coordinates")] + public double[] Coordinates { get; set; } + /// - /// Represents a 2D geographical coordinate consisting of longitude and latitude + /// Instantiate a new Coordinates2D instance with the supplied longtitude and latitude /// - public class Coordinates2D + public Coordinates2D(double longitude, double latitude) { - [BsonElement("type")] - public string Type { get; set; } - - [BsonElement("coordinates")] - public double[] Coordinates { get; set; } - - /// - /// Instantiate a new Coordinates2D instance with the supplied longtitude and latitude - /// - public Coordinates2D(double longitude, double latitude) - { - Type = "Point"; - Coordinates = new[] { longitude, latitude }; - } - - /// - /// Converts a Coordinates2D instance to a GeoJsonPoint of GeoJson2DGeographicCoordinates - /// - public GeoJsonPoint ToGeoJsonPoint() - { - return GeoJson.Point(GeoJson.Geographic(Coordinates[0], Coordinates[1])); - } + Type = "Point"; + Coordinates = new[] { longitude, latitude }; + } - /// - /// Create a GeoJsonPoint of GeoJson2DGeographicCoordinates with supplied longitude and latitude - /// - public static GeoJsonPoint GeoJsonPoint(double longitude, double latitude) - { - return GeoJson.Point(GeoJson.Geographic(longitude, latitude)); - } + /// + /// Converts a Coordinates2D instance to a GeoJsonPoint of GeoJson2DGeographicCoordinates + /// + public GeoJsonPoint ToGeoJsonPoint() + { + return GeoJson.Point(GeoJson.Geographic(Coordinates[0], Coordinates[1])); } /// - /// Fluent aggregation pipeline builder for GeoNear + /// Create a GeoJsonPoint of GeoJson2DGeographicCoordinates with supplied longitude and latitude /// - /// The type of entity - public class GeoNear where T : IEntity + public static GeoJsonPoint GeoJsonPoint(double longitude, double latitude) { + return GeoJson.Point(GeoJson.Geographic(longitude, latitude)); + } +} + +/// +/// Fluent aggregation pipeline builder for GeoNear +/// +/// The type of entity +public class GeoNear +{ #pragma warning disable IDE1006 - public Coordinates2D near { get; set; } = null!; - public string? distanceField { get; set; } - public bool spherical { get; set; } - [BsonIgnoreIfNull] public int? limit { get; set; } - [BsonIgnoreIfNull] public double? maxDistance { get; set; } - [BsonIgnoreIfNull] public BsonDocument? query { get; set; } - [BsonIgnoreIfNull] public double? distanceMultiplier { get; set; } - [BsonIgnoreIfNull] public string? includeLocs { get; set; } - [BsonIgnoreIfNull] public double? minDistance { get; set; } - [BsonIgnoreIfNull] public string? key { get; set; } + public Coordinates2D near { get; set; } = null!; + public string? distanceField { get; set; } + public bool spherical { get; set; } + [BsonIgnoreIfNull] public int? limit { get; set; } + [BsonIgnoreIfNull] public double? maxDistance { get; set; } + [BsonIgnoreIfNull] public BsonDocument? query { get; set; } + [BsonIgnoreIfNull] public double? distanceMultiplier { get; set; } + [BsonIgnoreIfNull] public string? includeLocs { get; set; } + [BsonIgnoreIfNull] public double? minDistance { get; set; } + [BsonIgnoreIfNull] public string? key { get; set; } #pragma warning restore IDE1006 - internal IAggregateFluent ToFluent(DBContext context, AggregateOptions? options = null, string? collectionName = null, IMongoCollection? collection = null) - { - var stage = new BsonDocument { { "$geoNear", this.ToBsonDocument() } }; + internal IAggregateFluent ToFluent(DBContext context, AggregateOptions? options = null, string? collectionName = null, IMongoCollection? collection = null) + { + var stage = new BsonDocument { { "$geoNear", this.ToBsonDocument() } }; - return context.Session == null - ? context.Collection(collectionName, collection).Aggregate(options).AppendStage(stage) - : context.Collection(collectionName, collection).Aggregate(context.Session, options).AppendStage(stage); - } + return context.Session == null + ? context.Collection(collectionName, collection).Aggregate(options).AppendStage(stage) + : context.Collection(collectionName, collection).Aggregate(context.Session, options).AppendStage(stage); } } diff --git a/MongoDB.Entities/Core/ICreatedOn.cs b/MongoDB.Entities/Core/ICreatedOn.cs index d20b33e44..80d94781b 100644 --- a/MongoDB.Entities/Core/ICreatedOn.cs +++ b/MongoDB.Entities/Core/ICreatedOn.cs @@ -1,16 +1,13 @@ -using System; +namespace MongoDB.Entities; -namespace MongoDB.Entities +/// +/// Implement this interface on entities you want the library to automatically store the creation date with +/// +public interface ICreatedOn { /// - /// Implement this interface on entities you want the library to automatically store the creation date with + /// This property will be automatically set by the library when an entity is created. + /// TIP: This property is useful when sorting by creation date. /// - public interface ICreatedOn - { - /// - /// This property will be automatically set by the library when an entity is created. - /// TIP: This property is useful when sorting by creation date. - /// - DateTime CreatedOn { get; set; } - } + DateTime CreatedOn { get; set; } } diff --git a/MongoDB.Entities/Core/IEntity.cs b/MongoDB.Entities/Core/IEntity.cs index 5589e08fe..cc9399b8d 100644 --- a/MongoDB.Entities/Core/IEntity.cs +++ b/MongoDB.Entities/Core/IEntity.cs @@ -1,21 +1,47 @@ -namespace MongoDB.Entities +namespace MongoDB.Entities; + +/// +/// The contract for Entity classes +/// +public interface IEntity : IEntity +{ + +} + +/// +/// The contract for Entity classes +/// +public interface IEntity + where TId : IComparable, IEquatable { /// - /// The contract for Entity classes + /// The ID property for this entity type. + /// IMPORTANT: make sure to decorate this property with the [BsonId] attribute when implementing this interface + /// + TId? ID { get; set; } + + /// + /// Generate and return a new ID string from this method. It will be used when saving new entities that don't have their ID set. + /// That is, if an entity has a null ID, this method will be called for getting a new ID value. + /// If you're not doing custom ID generation, simply do return ObjectId.GenerateNewId().ToString() /// - public interface IEntity - { - /// - /// The ID property for this entity type. - /// IMPORTANT: make sure to decorate this property with the [BsonId] attribute when implementing this interface - /// - string? ID { get; set; } + TId GenerateNewID(); +} - /// - /// Generate and return a new ID string from this method. It will be used when saving new entities that don't have their ID set. - /// That is, if an entity has a null ID, this method will be called for getting a new ID value. - /// If you're not doing custom ID generation, simply do return ObjectId.GenerateNewId().ToString() - /// - string GenerateNewID(); - } -} \ No newline at end of file +//public class MultipleIdEntity : IEntity> +//{ +// public (Guid, ObjectId) ID { get; set; } +// [BsonIgnore] +// public Guid Id1 +// { +// get => ID.Item1; +// set => ID = (value, ID.Item2); +// } +// [BsonIgnore] +// public ObjectId Id2 +// { +// get => ID.Item2; +// set => ID = (ID.Item1, value); +// } +// public (Guid, ObjectId) GenerateNewID() => (Guid.NewGuid(), ObjectId.GenerateNewId()); +//} \ No newline at end of file diff --git a/MongoDB.Entities/Core/IModifiedOn.cs b/MongoDB.Entities/Core/IModifiedOn.cs index dd305629e..d00e29cbb 100644 --- a/MongoDB.Entities/Core/IModifiedOn.cs +++ b/MongoDB.Entities/Core/IModifiedOn.cs @@ -1,16 +1,13 @@ -using System; +namespace MongoDB.Entities; -namespace MongoDB.Entities +/// +/// Implement this interface on entities you want the library to automatically store the modified date with +/// +public interface IModifiedOn { /// - /// Implement this interface on entities you want the library to automatically store the modified date with + /// This property will be automatically set by the library when an entity is updated. + /// TIP: This property is useful when sorting by update date. /// - public interface IModifiedOn - { - /// - /// This property will be automatically set by the library when an entity is updated. - /// TIP: This property is useful when sorting by update date. - /// - DateTime ModifiedOn { get; set; } - } + DateTime ModifiedOn { get; set; } } diff --git a/MongoDB.Entities/Core/IgnoreManyPropsConvention.cs b/MongoDB.Entities/Core/IgnoreManyPropsConvention.cs index 1d9b7f9f0..859ab0c03 100644 --- a/MongoDB.Entities/Core/IgnoreManyPropsConvention.cs +++ b/MongoDB.Entities/Core/IgnoreManyPropsConvention.cs @@ -1,16 +1,14 @@ -using MongoDB.Bson.Serialization; -using MongoDB.Bson.Serialization.Conventions; +using MongoDB.Bson.Serialization.Conventions; -namespace MongoDB.Entities +namespace MongoDB.Entities; + +internal class IgnoreManyPropsConvention : ConventionBase, IMemberMapConvention { - internal class IgnoreManyPropsConvention : ConventionBase, IMemberMapConvention + public void Apply(BsonMemberMap mMap) { - public void Apply(BsonMemberMap mMap) + if (mMap.MemberType.Name == ManyBase.PropTypeName) { - if (mMap.MemberType.Name == ManyBase.PropTypeName) - { - _ = mMap.SetShouldSerializeMethod(_ => false); - } + _ = mMap.SetShouldSerializeMethod(_ => false); } } -} \ No newline at end of file +} diff --git a/MongoDB.Entities/Core/LevenshteinDistance.cs b/MongoDB.Entities/Core/LevenshteinDistance.cs index e51364d3c..b86366ea1 100644 --- a/MongoDB.Entities/Core/LevenshteinDistance.cs +++ b/MongoDB.Entities/Core/LevenshteinDistance.cs @@ -1,65 +1,64 @@ -namespace MongoDB.Entities +namespace MongoDB.Entities; + +internal class Levenshtein { - internal class Levenshtein + private readonly string _storedValue; + private readonly int[] _costs; + + public Levenshtein(string value) + { + _storedValue = value.ToLower(); + _costs = new int[_storedValue.Length]; + } + + public int DistanceFrom(string value) { - private readonly string storedValue; - private readonly int[] costs; + value = value.ToLower(); - public Levenshtein(string value) + if (_costs.Length == 0) { - storedValue = value.ToLower(); - costs = new int[storedValue.Length]; + return value.Length; } - public int DistanceFrom(string value) + for (int i = 0; i < _costs.Length;) { - value = value.ToLower(); + _costs[i] = ++i; + } - if (costs.Length == 0) - { - return value.Length; - } + for (int i = 0; i < value.Length; i++) + { + int cost = i; + int addationCost = i; - for (int i = 0; i < costs.Length;) - { - costs[i] = ++i; - } + char value1Char = value[i]; - for (int i = 0; i < value.Length; i++) + for (int j = 0; j < _storedValue.Length; j++) { - int cost = i; - int addationCost = i; + int insertionCost = cost; - char value1Char = value[i]; + cost = addationCost; - for (int j = 0; j < storedValue.Length; j++) - { - int insertionCost = cost; - - cost = addationCost; - - addationCost = costs[j]; + addationCost = _costs[j]; - if (value1Char != storedValue[j]) + if (value1Char != _storedValue[j]) + { + if (insertionCost < cost) { - if (insertionCost < cost) - { - cost = insertionCost; - } - - if (addationCost < cost) - { - cost = addationCost; - } + cost = insertionCost; + } - ++cost; + if (addationCost < cost) + { + cost = addationCost; } - costs[j] = cost; + ++cost; } - } - return costs[costs.Length - 1]; + _costs[j] = cost; + } } + + return _costs[_costs.Length - 1]; } } diff --git a/MongoDB.Entities/Core/Logic.cs b/MongoDB.Entities/Core/Logic.cs index abb12cd7d..dac217f7f 100644 --- a/MongoDB.Entities/Core/Logic.cs +++ b/MongoDB.Entities/Core/Logic.cs @@ -1,62 +1,54 @@ -using MongoDB.Bson; -using MongoDB.Driver; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; - -namespace MongoDB.Entities +namespace MongoDB.Entities; + +internal static class Logic { - internal static class Logic + internal static IEnumerable> BuildUpdateDefs(T entity, DBContext context) { - internal static IEnumerable> BuildUpdateDefs(T entity, DBContext? context = null) where T : IEntity - { - if (entity == null) - throw new ArgumentException("The supplied entity cannot be null!"); + if (entity == null) + throw new ArgumentException("The supplied entity cannot be null!"); - var props = (context?.Cache() ?? EntityCache.Instance).UpdatableProps(entity); + var props = context.Cache().UpdatableProps(entity); - return props.Select(p => Builders.Update.Set(p.Name, p.GetValue(entity))); - } + return props.Select(p => Builders.Update.Set(p.Name, p.GetValue(entity))); + } - internal static IEnumerable> BuildUpdateDefs(T entity, Expression> members, bool excludeMode = false, DBContext? context = null) where T : IEntity - { - var propNames = (members?.Body as NewExpression)?.Arguments - .Select(a => a.ToString().Split('.')[1]); + internal static IEnumerable> BuildUpdateDefs(T entity, Expression> members, DBContext context, bool excludeMode = false) + { + var propNames = (members?.Body as NewExpression)?.Arguments + .Select(a => a.ToString().Split('.')[1]); - if (!propNames.Any()) - throw new ArgumentException("Unable to get any properties from the members expression!"); + if (!propNames.Any()) + throw new ArgumentException("Unable to get any properties from the members expression!"); - var props = (context?.Cache() ?? EntityCache.Instance).UpdatableProps(entity); + var props = context.Cache().UpdatableProps(entity); - if (excludeMode) - props = props.Where(p => !propNames.Contains(p.Name)); - else - props = props.Where(p => propNames.Contains(p.Name)); + if (excludeMode) + props = props.Where(p => !propNames.Contains(p.Name)); + else + props = props.Where(p => propNames.Contains(p.Name)); - return props.Select(p => Builders.Update.Set(p.Name, p.GetValue(entity))); - } + return props.Select(p => Builders.Update.Set(p.Name, p.GetValue(entity))); + } - internal static FilterDefinition MergeWithGlobalFilter(bool ignoreGlobalFilters, Dictionary? globalFilters, FilterDefinition filter) where T : IEntity - { - //WARNING: this has to do the same thing as DBContext.Pipeline.MergeWithGlobalFilter method - // if the following logic changes, update the other method also + internal static FilterDefinition MergeWithGlobalFilter(bool ignoreGlobalFilters, Dictionary? globalFilters, FilterDefinition filter) + { + //WARNING: this has to do the same thing as DBContext.Pipeline.MergeWithGlobalFilter method + // if the following logic changes, update the other method also - if (!ignoreGlobalFilters && globalFilters is not null && globalFilters.Count > 0 && globalFilters.TryGetValue(typeof(T), out var gFilter)) + if (!ignoreGlobalFilters && globalFilters is not null && globalFilters.Count > 0 && globalFilters.TryGetValue(typeof(T), out var gFilter)) + { + switch (gFilter.filterDef) { - switch (gFilter.filterDef) - { - case FilterDefinition definition: - return (gFilter.prepend) ? definition & filter : filter & definition; + case FilterDefinition definition: + return (gFilter.prepend) ? definition & filter : filter & definition; - case BsonDocument bsonDoc: - return (gFilter.prepend) ? bsonDoc & filter : filter & bsonDoc; + case BsonDocument bsonDoc: + return (gFilter.prepend) ? bsonDoc & filter : filter & bsonDoc; - case string jsonString: - return (gFilter.prepend) ? jsonString & filter : filter & jsonString; - } + case string jsonString: + return (gFilter.prepend) ? jsonString & filter : filter & jsonString; } - return filter; } + return filter; } } diff --git a/MongoDB.Entities/Core/ModifiedBy.cs b/MongoDB.Entities/Core/ModifiedBy.cs index ebb6506ce..c94244460 100644 --- a/MongoDB.Entities/Core/ModifiedBy.cs +++ b/MongoDB.Entities/Core/ModifiedBy.cs @@ -1,8 +1,7 @@ -namespace MongoDB.Entities +namespace MongoDB.Entities; + +public class ModifiedBy { - public class ModifiedBy - { - [AsObjectId] public string UserID { get; set; } - public string UserName { get; set; } - } + [AsObjectId] public string UserID { get; set; } + public string UserName { get; set; } } diff --git a/MongoDB.Entities/Core/Prop.cs b/MongoDB.Entities/Core/Prop.cs index 81a92d6a1..b2291f6c0 100644 --- a/MongoDB.Entities/Core/Prop.cs +++ b/MongoDB.Entities/Core/Prop.cs @@ -1,147 +1,135 @@ -using System; -using System.Linq.Expressions; -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; -namespace MongoDB.Entities +namespace MongoDB.Entities; + +/// +/// This class provides methods to generate property path strings from lambda expression. +/// +public static class Prop { - /// - /// This class provides methods to generate property path strings from lambda expression. - /// - public static class Prop - { - private static readonly Regex rxOne = new(@"(?:\.(?:\w+(?:[[(]\d+[)\]])?))+", RegexOptions.Compiled);//matched result: One.Two[1].Three.get_Item(2).Four - private static readonly Regex rxTwo = new(@".get_Item\((\d+)\)", RegexOptions.Compiled);//replaced result: One.Two[1].Three[2].Four - private static readonly Regex rxThree = new(@"\[\d+\]", RegexOptions.Compiled); - private static readonly Regex rxFour = new(@"\[(\d+)\]", RegexOptions.Compiled); + private static readonly Regex _rxOne = new(@"(?:\.(?:\w+(?:[[(]\d+[)\]])?))+", RegexOptions.Compiled);//matched result: One.Two[1].Three.get_Item(2).Four + private static readonly Regex _rxTwo = new(@".get_Item\((\d+)\)", RegexOptions.Compiled);//replaced result: One.Two[1].Three[2].Four + private static readonly Regex _rxThree = new(@"\[\d+\]", RegexOptions.Compiled); + private static readonly Regex _rxFour = new(@"\[(\d+)\]", RegexOptions.Compiled); - private static string ToLowerCaseLetter(long n) + private static string? ToLowerCaseLetter(long n) + { + string? val = null; + const char c = 'a'; + while (n >= 0) { - string val = null; - const char c = 'a'; - while (n >= 0) - { - val = (char)(c + (n % 26)) + val; - n /= 26; - n--; - } - - return val; + val = (char)(c + (n % 26)) + val; + n /= 26; + n--; } - private static void ThrowIfInvalid(Expression> expression) - { - if (expression == null) - throw new ArgumentNullException(nameof(expression), "The supplied expression is null!"); + return val; + } - if (expression.Body.NodeType == ExpressionType.Parameter) - throw new ArgumentException("Cannot generate property path from lambda parameter!"); - } + private static void ThrowIfInvalid(Expression> expression) + { + if (expression == null) + throw new ArgumentNullException(nameof(expression), "The supplied expression is null!"); - private static string GetPath(Expression> expression) - { - ThrowIfInvalid(expression); + if (expression.Body.NodeType == ExpressionType.Parameter) + throw new ArgumentException("Cannot generate property path from lambda parameter!"); + } - return rxTwo.Replace( - rxOne.Match(expression.ToString()).Value.Substring(1), - m => "[" + m.Groups[1].Value + "]"); - } + private static string GetPath(Expression> expression) + { + ThrowIfInvalid(expression); - internal static string GetPath(string expString) - { - return - rxThree.Replace( - rxTwo.Replace( - rxOne.Match(expString).Value.Substring(1), - m => "[" + m.Groups[1].Value + "]"), - ""); - } + return _rxTwo.Replace( + _rxOne.Match(expression.ToString()).Value.Substring(1), + m => "[" + m.Groups[1].Value + "]"); + } - /// - /// Returns the collection/entity name of a given entity type - /// - /// The type of the entity to get the collection name of - public static string Collection() where T : IEntity - { - return EntityCache.CollectionName; - } + internal static string GetPath(string expString) + { + return + _rxThree.Replace( + _rxTwo.Replace( + _rxOne.Match(expString).Value.Substring(1), + m => "[" + m.Groups[1].Value + "]"), + ""); + } - /// - /// Returns the name of the property for a given expression. - /// EX: Authors[0].Books[0].Title > Title - /// - /// x => x.SomeList[0].SomeProp - public static string Property(Expression> expression) - { - ThrowIfInvalid(expression); - return expression.MemberInfo().Name; - } + /// + /// Returns the name of the property for a given expression. + /// EX: Authors[0].Books[0].Title > Title + /// + /// x => x.SomeList[0].SomeProp + public static string Property(Expression> expression) + { + ThrowIfInvalid(expression); + return expression.MemberInfo().Name; + } - /// - /// Returns the full dotted path for a given expression. - /// EX: Authors[0].Books[0].Title > Authors.Books.Title - /// - /// x => x.SomeList[0].SomeProp - public static string Path(Expression> expression) - { - return rxThree.Replace(GetPath(expression), ""); - } + /// + /// Returns the full dotted path for a given expression. + /// EX: Authors[0].Books[0].Title > Authors.Books.Title + /// + /// x => x.SomeList[0].SomeProp + public static string Path(Expression> expression) + { + return _rxThree.Replace(GetPath(expression), ""); + } - /// - /// Returns a path with filtered positional identifiers $[x] for a given expression. - /// EX: Authors[0].Name > Authors.$[a].Name - /// EX: Authors[1].Age > Authors.$[b].Age - /// EX: Authors[2].Books[3].Title > Authors.$[c].Books.$[d].Title - /// TIP: Index positions start from [0] which is converted to $[a] and so on. - /// - /// x => x.SomeList[0].SomeProp - public static string PosFiltered(Expression> expression) - { - return rxFour.Replace( - GetPath(expression), - m => ".$[" + ToLowerCaseLetter(int.Parse(m.Groups[1].Value)) + "]"); - } + /// + /// Returns a path with filtered positional identifiers $[x] for a given expression. + /// EX: Authors[0].Name > Authors.$[a].Name + /// EX: Authors[1].Age > Authors.$[b].Age + /// EX: Authors[2].Books[3].Title > Authors.$[c].Books.$[d].Title + /// TIP: Index positions start from [0] which is converted to $[a] and so on. + /// + /// x => x.SomeList[0].SomeProp + public static string PosFiltered(Expression> expression) + { + return _rxFour.Replace( + GetPath(expression), + m => ".$[" + ToLowerCaseLetter(int.Parse(m.Groups[1].Value)) + "]"); + } - /// - /// Returns a path with the all positional operator $[] for a given expression. - /// EX: Authors[0].Name > Authors.$[].Name - /// - /// x => x.SomeList[0].SomeProp - public static string PosAll(Expression> expression) - { - return rxThree.Replace(GetPath(expression), ".$[]"); - } + /// + /// Returns a path with the all positional operator $[] for a given expression. + /// EX: Authors[0].Name > Authors.$[].Name + /// + /// x => x.SomeList[0].SomeProp + public static string PosAll(Expression> expression) + { + return _rxThree.Replace(GetPath(expression), ".$[]"); + } - /// - /// Returns a path with the first positional operator $ for a given expression. - /// EX: Authors[0].Name > Authors.$.Name - /// - /// x => x.SomeList[0].SomeProp - public static string PosFirst(Expression> expression) - { - return rxThree.Replace(GetPath(expression), ".$"); - } + /// + /// Returns a path with the first positional operator $ for a given expression. + /// EX: Authors[0].Name > Authors.$.Name + /// + /// x => x.SomeList[0].SomeProp + public static string PosFirst(Expression> expression) + { + return _rxThree.Replace(GetPath(expression), ".$"); + } - /// - /// Returns a path without any filtered positional identifier prepended to it. - /// EX: b => b.Tags > Tags - /// - /// x => x.SomeProp - public static string Elements(Expression> expression) - { - return Path(expression); - } + /// + /// Returns a path without any filtered positional identifier prepended to it. + /// EX: b => b.Tags > Tags + /// + /// x => x.SomeProp + public static string Elements(Expression> expression) + { + return Path(expression); + } - /// - /// Returns a path with the filtered positional identifier prepended to the property path. - /// EX: 0, x => x.Rating > a.Rating - /// EX: 1, x => x.Rating > b.Rating - /// TIP: Index positions start from '0' which is converted to 'a' and so on. - /// - /// 0=a 1=b 2=c 3=d and so on... - /// x => x.SomeProp - public static string Elements(int index, Expression> expression) - { - return $"{ToLowerCaseLetter(index)}.{Path(expression)}"; - } + /// + /// Returns a path with the filtered positional identifier prepended to the property path. + /// EX: 0, x => x.Rating > a.Rating + /// EX: 1, x => x.Rating > b.Rating + /// TIP: Index positions start from '0' which is converted to 'a' and so on. + /// + /// 0=a 1=b 2=c 3=d and so on... + /// x => x.SomeProp + public static string Elements(int index, Expression> expression) + { + return $"{ToLowerCaseLetter(index)}.{Path(expression)}"; } } diff --git a/MongoDB.Entities/Core/SequenceCounter.cs b/MongoDB.Entities/Core/SequenceCounter.cs index 1331b3e47..53528d9f1 100644 --- a/MongoDB.Entities/Core/SequenceCounter.cs +++ b/MongoDB.Entities/Core/SequenceCounter.cs @@ -1,18 +1,14 @@ -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; +namespace MongoDB.Entities; -namespace MongoDB.Entities +[Collection("[SEQUENCE_COUNTERS]")] +internal class SequenceCounter : IEntity { - [Collection("[SEQUENCE_COUNTERS]")] - internal class SequenceCounter : IEntity - { - [BsonId] - public string ID { get; set; } = null!; + [BsonId] + public string ID { get; set; } = null!; - [BsonRepresentation(BsonType.Int64)] - public ulong Count { get; set; } + [BsonRepresentation(BsonType.Int64)] + public ulong Count { get; set; } - public string GenerateNewID() - => throw new System.NotImplementedException(); - } + public string GenerateNewID() + => throw new NotImplementedException(); } diff --git a/MongoDB.Entities/Core/Template.cs b/MongoDB.Entities/Core/Template.cs index f5aed1e45..e3e8373cc 100644 --- a/MongoDB.Entities/Core/Template.cs +++ b/MongoDB.Entities/Core/Template.cs @@ -15,7 +15,7 @@ namespace MongoDB.Entities /// A helper class to build a JSON command from a string with tag replacement /// /// Any type that implements IEntity - public class Template : Template where T : IEntity + public class Template : Template { /// /// Initializes a template with a tagged input string. @@ -29,7 +29,7 @@ public Template(string template) : base(template) { } /// /// The input type /// The output type - public class Template : Template where TInput : IEntity + public class Template : Template { /// /// Initializes a template with a tagged input string. @@ -361,11 +361,11 @@ public void AppendStage(string pipelineStageString) /// Gets the collection name of a given entity type and replaces matching tags in the template such as "<EntityName>" /// /// The type of entity to get the collection name of - public Template Collection() where TEntity : IEntity + public Template Collection() { if (cacheHit) return this; - return ReplacePath(Prop.Collection()); + return ReplacePath(EntityCache.Default.CollectionName); } /// @@ -393,7 +393,7 @@ public Template Properties(Expression> expression) .Cast() .Select(e => e.Member.Name); - if (!props.Any()) + if (props == null || !props.Any()) throw new ArgumentException("Unable to parse any property names from the supplied `new` expression!"); foreach (var p in props) @@ -426,7 +426,7 @@ public Template Paths(Expression> expression) .Arguments .Select(a => Prop.GetPath(a.ToString())); - if (!paths.Any()) + if (paths is null || !paths.Any()) throw new ArgumentException("Unable to parse any property paths from the supplied `new` expression!"); foreach (var p in paths) diff --git a/MongoDB.Entities/Core/Transaction.cs b/MongoDB.Entities/Core/Transaction.cs index 0b78eef09..5f5d421f7 100644 --- a/MongoDB.Entities/Core/Transaction.cs +++ b/MongoDB.Entities/Core/Transaction.cs @@ -1,53 +1,55 @@ -using MongoDB.Driver; -using System; +namespace MongoDB.Entities; -namespace MongoDB.Entities +/// +/// Represents a transaction used to carry out inter-related write operations. +/// TIP: Remember to always call .Dispose() after use or enclose in a 'Using' statement. +/// IMPORTANT: Use the methods on this transaction to perform operations and not the methods on the DB class. +/// +public class Transaction : DBContext, IDisposable { /// - /// Represents a transaction used to carry out inter-related write operations. - /// TIP: Remember to always call .Dispose() after use or enclose in a 'Using' statement. - /// IMPORTANT: Use the methods on this transaction to perform operations and not the methods on the DB class. + /// Instantiates and begins a transaction. /// - public class Transaction : DBContext, IDisposable + /// + /// The name of the database to use for this transaction. default db is used if not specified + /// Client session options for this transaction + public Transaction(MongoServerContext context, string database, ClientSessionOptions? options = null) : base(mongoContext: new(context), database: database) { - /// - /// Instantiates and begins a transaction. - /// - /// The name of the database to use for this transaction. default db is used if not specified - /// Client session options for this transaction - /// An optional ModifiedBy instance. - /// When supplied, all save/update operations performed via this DBContext instance will set the value on entities that has a property of type ModifiedBy. - /// You can inherit from the ModifiedBy class and add your own properties to it. - /// Only one ModifiedBy property is allowed on a single entity type. - public Transaction(string database = default, ClientSessionOptions options = null, ModifiedBy modifiedBy = null) - { - Session = DB.Database(database).Client.StartSession(options); - Session.StartTransaction(); - ModifiedBy = modifiedBy; - } + MongoServerContext.Transaction(options); + } + + /// + /// Instantiates and begins a transaction. + /// + /// + /// The name of the database to use for this transaction. default db is used if not specified + /// Client session options for this transaction + public Transaction(MongoServerContext context, IMongoDatabase database, ClientSessionOptions? options = null) : base(mongoContext: new(context), database: database) + { + MongoServerContext.Transaction(options); + } - #region IDisposable Support + #region IDisposable Support - private bool disposedValue; + private bool _disposedValue; - protected virtual void Dispose(bool disposing) + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) { - if (!disposedValue) + if (disposing) { - if (disposing) - { - Session.Dispose(); - } - - disposedValue = true; + MongoServerContext.Dispose(); } - } - public void Dispose() - { - Dispose(true); + _disposedValue = true; } + } - #endregion + public void Dispose() + { + Dispose(true); } + + #endregion } diff --git a/MongoDB.Entities/Core/Watcher.cs b/MongoDB.Entities/Core/Watcher.cs index 42397fccf..da17ce42f 100644 --- a/MongoDB.Entities/Core/Watcher.cs +++ b/MongoDB.Entities/Core/Watcher.cs @@ -1,293 +1,283 @@ -using MongoDB.Bson; -using MongoDB.Bson.Serialization; -using MongoDB.Driver; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Threading; -using System.Threading.Tasks; - -namespace MongoDB.Entities +namespace MongoDB.Entities; + +public delegate Task AsyncEventHandler(TEventArgs args); +public static class AsyncEventHandlerExtensions { - public delegate Task AsyncEventHandler(TEventArgs args); - public static class AsyncEventHandlerExtensions - { - public static IEnumerable> GetHandlers(this AsyncEventHandler handler) - => handler.GetInvocationList().Cast>(); + public static IEnumerable> GetHandlers(this AsyncEventHandler handler) + => handler.GetInvocationList().Cast>(); + + public static Task InvokeAllAsync(this AsyncEventHandler handler, TEventArgs args) + => Task.WhenAll(handler.GetHandlers().Select(h => h(args))); +} + +/// +/// Watcher for subscribing to mongodb change streams. +/// +/// The type of entity +public class Watcher +{ + /// + /// This event is fired when the desired types of events have occured. Will have a list of 'entities' that was received as input. + /// + public event Action>? OnChanges; + + /// + /// This event is fired when the desired types of events have occured. Will have a list of 'entities' that was received as input. + /// + public event AsyncEventHandler>? OnChangesAsync; + + /// + /// This event is fired when the desired types of events have occured. Will have a list of 'ChangeStreamDocuments' that was received as input. + /// + public event Action>>? OnChangesCSD; + + /// + /// This event is fired when the desired types of events have occured. Will have a list of 'ChangeStreamDocuments' that was received as input. + /// + public event AsyncEventHandler>>? OnChangesCSDAsync; + + /// + /// This event is fired when an exception is thrown in the change-stream. + /// + public event Action? OnError; + + /// + /// This event is fired when the internal cursor get closed due to an 'invalidate' event or cancellation is requested via the cancellation token. + /// + public event Action? OnStop; + + /// + /// The name of this watcher instance + /// + public string Name { get; } + + /// + /// Indicates whether this watcher has already been initialized or not. + /// + public bool IsInitialized { get => _initialized; } + + /// + /// Returns true if watching can be restarted if it was stopped due to an error or invalidate event. + /// Will always return false after cancellation is requested via the cancellation token. + /// + public bool CanRestart { get => !_cancelToken.IsCancellationRequested; } - public static Task InvokeAllAsync(this AsyncEventHandler handler, TEventArgs args) - => Task.WhenAll(handler.GetHandlers().Select(h => h(args))); + /// + /// The last resume token received from mongodb server. Can be used to resume watching with .StartWithToken() method. + /// + public BsonDocument? ResumeToken => _options?.StartAfter; + + private PipelineDefinition, ChangeStreamDocument>? _pipeline; + private ChangeStreamOptions? _options; + private bool _resume; + private CancellationToken _cancelToken; + private bool _initialized; + private readonly IMongoCollection _collection; + internal Watcher(string name, IMongoCollection collection) + { + Name = name; + _collection = collection; } /// - /// Watcher for subscribing to mongodb change streams. + /// Starts the watcher instance with the supplied parameters + /// + /// Type of event to watch for. Specify multiple like: EventType.Created | EventType.Updated | EventType.Deleted + /// x => x.FullDocument.Prop1 == "SomeValue" + /// The max number of entities to receive for a single event occurence + /// Set to true if you don't want the complete entity details. All properties except the ID will then be null. + /// Set to false if you'd like to skip the changes that happened while the watching was stopped. This will also make you unable to retrieve a ResumeToken. + /// A cancellation token for ending the watching/change stream + public void Start( + EventType eventTypes, + Expression, bool>>? filter = null, + int batchSize = 25, + bool onlyGetIDs = false, + bool autoResume = true, + CancellationToken cancellation = default) + => Init(null, eventTypes, filter, null, batchSize, onlyGetIDs, autoResume, cancellation); + + /// + /// Starts the watcher instance with the supplied parameters. Supports projection. + /// + /// Type of event to watch for. Specify multiple like: EventType.Created | EventType.Updated | EventType.Deleted + /// A projection expression for the entity + /// x => x.FullDocument.Prop1 == "SomeValue" + /// The max number of entities to receive for a single event occurence + /// Set to false if you'd like to skip the changes that happened while the watching was stopped. This will also make you unable to retrieve a ResumeToken. + /// A cancellation token for ending the watching/change stream + public void Start( + EventType eventTypes, + Expression>? projection, + Expression, bool>>? filter = null, + int batchSize = 25, + bool autoResume = true, + CancellationToken cancellation = default) + => Init(null, eventTypes, filter, projection, batchSize, false, autoResume, cancellation); + + /// + /// Starts the watcher instance with the supplied parameters + /// + /// Type of event to watch for. Specify multiple like: EventType.Created | EventType.Updated | EventType.Deleted + /// b => b.Eq(d => d.FullDocument.Prop1, "value") + /// The max number of entities to receive for a single event occurence + /// Set to true if you don't want the complete entity details. All properties except the ID will then be null. + /// Set to false if you'd like to skip the changes that happened while the watching was stopped. This will also make you unable to retrieve a ResumeToken. + /// A cancellation token for ending the watching/change stream + public void Start( + EventType eventTypes, + Func>, FilterDefinition>> filter, + int batchSize = 25, + bool onlyGetIDs = false, + bool autoResume = true, + CancellationToken cancellation = default) + => Init(null, eventTypes, filter(Builders>.Filter), null, batchSize, onlyGetIDs, autoResume, cancellation); + + /// + /// Starts the watcher instance with the supplied parameters. Supports projection. + /// + /// Type of event to watch for. Specify multiple like: EventType.Created | EventType.Updated | EventType.Deleted + /// A projection expression for the entity + /// b => b.Eq(d => d.FullDocument.Prop1, "value") + /// The max number of entities to receive for a single event occurence + /// Set to false if you'd like to skip the changes that happened while the watching was stopped. This will also make you unable to retrieve a ResumeToken. + /// A cancellation token for ending the watching/change stream + public void Start( + EventType eventTypes, + Expression> projection, + Func>, FilterDefinition>> filter, + int batchSize = 25, + bool autoResume = true, + CancellationToken cancellation = default) + => Init(null, eventTypes, filter(Builders>.Filter), projection, batchSize, false, autoResume, cancellation); + + /// + /// Starts the watcher instance with the supplied configuration + /// + /// A resume token to start receiving changes after some point back in time + /// Type of event to watch for. Specify multiple like: EventType.Created | EventType.Updated | EventType.Deleted + /// x => x.FullDocument.Prop1 == "SomeValue" + /// The max number of entities to receive for a single event occurence + /// Set to true if you don't want the complete entity details. All properties except the ID will then be null. + /// A cancellation token for ending the watching/change stream + public void StartWithToken( + BsonDocument resumeToken, + EventType eventTypes, + Expression, bool>>? filter = null, + int batchSize = 25, + bool onlyGetIDs = false, + CancellationToken cancellation = default) + => Init(resumeToken, eventTypes, filter, null, batchSize, onlyGetIDs, true, cancellation); + + /// + /// Starts the watcher instance with the supplied configuration. Supports projection. + /// + /// A resume token to start receiving changes after some point back in time + /// Type of event to watch for. Specify multiple like: EventType.Created | EventType.Updated | EventType.Deleted + /// A projection expression for the entity + /// x => x.FullDocument.Prop1 == "SomeValue" + /// The max number of entities to receive for a single event occurence + /// A cancellation token for ending the watching/change stream + public void StartWithToken( + BsonDocument resumeToken, + EventType eventTypes, + Expression> projection, + Expression, bool>>? filter = null, + int batchSize = 25, + CancellationToken cancellation = default) + => Init(resumeToken, eventTypes, filter, projection, batchSize, false, true, cancellation); + + /// + /// Starts the watcher instance with the supplied configuration + /// + /// A resume token to start receiving changes after some point back in time + /// Type of event to watch for. Specify multiple like: EventType.Created | EventType.Updated | EventType.Deleted + /// b => b.Eq(d => d.FullDocument.Prop1, "value") + /// The max number of entities to receive for a single event occurence + /// Set to true if you don't want the complete entity details. All properties except the ID will then be null. + /// A cancellation token for ending the watching/change stream + public void StartWithToken( + BsonDocument resumeToken, + EventType eventTypes, + Func>, FilterDefinition>> filter, + int batchSize = 25, + bool onlyGetIDs = false, + CancellationToken cancellation = default) + => Init(resumeToken, eventTypes, filter(Builders>.Filter), null, batchSize, onlyGetIDs, true, cancellation); + + /// + /// Starts the watcher instance with the supplied configuration. Supports projection. /// - /// The type of entity - public class Watcher where T : IEntity + /// A resume token to start receiving changes after some point back in time + /// Type of event to watch for. Specify multiple like: EventType.Created | EventType.Updated | EventType.Deleted + /// A projection expression for the entity + /// b => b.Eq(d => d.FullDocument.Prop1, "value") + /// The max number of entities to receive for a single event occurence + /// A cancellation token for ending the watching/change stream + public void StartWithToken( + BsonDocument resumeToken, + EventType eventTypes, + Expression> projection, + Func>, FilterDefinition>> filter, + int batchSize = 25, + CancellationToken cancellation = default) + => Init(resumeToken, eventTypes, filter(Builders>.Filter), projection, batchSize, false, true, cancellation); + + private void Init( + BsonDocument? resumeToken, + EventType eventTypes, + FilterDefinition> filter, + Expression>? projection, + int batchSize, + bool onlyGetIDs, + bool autoResume, + CancellationToken cancellation) { - /// - /// This event is fired when the desired types of events have occured. Will have a list of 'entities' that was received as input. - /// - public event Action>? OnChanges; - - /// - /// This event is fired when the desired types of events have occured. Will have a list of 'entities' that was received as input. - /// - public event AsyncEventHandler>? OnChangesAsync; - - /// - /// This event is fired when the desired types of events have occured. Will have a list of 'ChangeStreamDocuments' that was received as input. - /// - public event Action>>? OnChangesCSD; - - /// - /// This event is fired when the desired types of events have occured. Will have a list of 'ChangeStreamDocuments' that was received as input. - /// - public event AsyncEventHandler>>? OnChangesCSDAsync; - - /// - /// This event is fired when an exception is thrown in the change-stream. - /// - public event Action? OnError; - - /// - /// This event is fired when the internal cursor get closed due to an 'invalidate' event or cancellation is requested via the cancellation token. - /// - public event Action? OnStop; - - /// - /// The name of this watcher instance - /// - public string Name { get; } - - /// - /// Indicates whether this watcher has already been initialized or not. - /// - public bool IsInitialized { get => _initialized; } - - /// - /// Returns true if watching can be restarted if it was stopped due to an error or invalidate event. - /// Will always return false after cancellation is requested via the cancellation token. - /// - public bool CanRestart { get => !_cancelToken.IsCancellationRequested; } - - /// - /// The last resume token received from mongodb server. Can be used to resume watching with .StartWithToken() method. - /// - public BsonDocument? ResumeToken => _options?.StartAfter; - - private PipelineDefinition, ChangeStreamDocument>? _pipeline; - private ChangeStreamOptions? _options; - private bool _resume; - private CancellationToken _cancelToken; - private bool _initialized; - private readonly IMongoCollection _collection; - internal Watcher(string name, DBContext context, IMongoCollection collection) - { - Name = name; - _collection = collection; - } + if (_initialized) + throw new InvalidOperationException("This watcher has already been initialized!"); - /// - /// Starts the watcher instance with the supplied parameters - /// - /// Type of event to watch for. Specify multiple like: EventType.Created | EventType.Updated | EventType.Deleted - /// x => x.FullDocument.Prop1 == "SomeValue" - /// The max number of entities to receive for a single event occurence - /// Set to true if you don't want the complete entity details. All properties except the ID will then be null. - /// Set to false if you'd like to skip the changes that happened while the watching was stopped. This will also make you unable to retrieve a ResumeToken. - /// A cancellation token for ending the watching/change stream - public void Start( - EventType eventTypes, - Expression, bool>>? filter = null, - int batchSize = 25, - bool onlyGetIDs = false, - bool autoResume = true, - CancellationToken cancellation = default) - => Init(null, eventTypes, filter, null, batchSize, onlyGetIDs, autoResume, cancellation); - - /// - /// Starts the watcher instance with the supplied parameters. Supports projection. - /// - /// Type of event to watch for. Specify multiple like: EventType.Created | EventType.Updated | EventType.Deleted - /// A projection expression for the entity - /// x => x.FullDocument.Prop1 == "SomeValue" - /// The max number of entities to receive for a single event occurence - /// Set to false if you'd like to skip the changes that happened while the watching was stopped. This will also make you unable to retrieve a ResumeToken. - /// A cancellation token for ending the watching/change stream - public void Start( - EventType eventTypes, - Expression>? projection, - Expression, bool>>? filter = null, - int batchSize = 25, - bool autoResume = true, - CancellationToken cancellation = default) - => Init(null, eventTypes, filter, projection, batchSize, false, autoResume, cancellation); - - /// - /// Starts the watcher instance with the supplied parameters - /// - /// Type of event to watch for. Specify multiple like: EventType.Created | EventType.Updated | EventType.Deleted - /// b => b.Eq(d => d.FullDocument.Prop1, "value") - /// The max number of entities to receive for a single event occurence - /// Set to true if you don't want the complete entity details. All properties except the ID will then be null. - /// Set to false if you'd like to skip the changes that happened while the watching was stopped. This will also make you unable to retrieve a ResumeToken. - /// A cancellation token for ending the watching/change stream - public void Start( - EventType eventTypes, - Func>, FilterDefinition>> filter, - int batchSize = 25, - bool onlyGetIDs = false, - bool autoResume = true, - CancellationToken cancellation = default) - => Init(null, eventTypes, filter(Builders>.Filter), null, batchSize, onlyGetIDs, autoResume, cancellation); - - /// - /// Starts the watcher instance with the supplied parameters. Supports projection. - /// - /// Type of event to watch for. Specify multiple like: EventType.Created | EventType.Updated | EventType.Deleted - /// A projection expression for the entity - /// b => b.Eq(d => d.FullDocument.Prop1, "value") - /// The max number of entities to receive for a single event occurence - /// Set to false if you'd like to skip the changes that happened while the watching was stopped. This will also make you unable to retrieve a ResumeToken. - /// A cancellation token for ending the watching/change stream - public void Start( - EventType eventTypes, - Expression> projection, - Func>, FilterDefinition>> filter, - int batchSize = 25, - bool autoResume = true, - CancellationToken cancellation = default) - => Init(null, eventTypes, filter(Builders>.Filter), projection, batchSize, false, autoResume, cancellation); - - /// - /// Starts the watcher instance with the supplied configuration - /// - /// A resume token to start receiving changes after some point back in time - /// Type of event to watch for. Specify multiple like: EventType.Created | EventType.Updated | EventType.Deleted - /// x => x.FullDocument.Prop1 == "SomeValue" - /// The max number of entities to receive for a single event occurence - /// Set to true if you don't want the complete entity details. All properties except the ID will then be null. - /// A cancellation token for ending the watching/change stream - public void StartWithToken( - BsonDocument resumeToken, - EventType eventTypes, - Expression, bool>>? filter = null, - int batchSize = 25, - bool onlyGetIDs = false, - CancellationToken cancellation = default) - => Init(resumeToken, eventTypes, filter, null, batchSize, onlyGetIDs, true, cancellation); - - /// - /// Starts the watcher instance with the supplied configuration. Supports projection. - /// - /// A resume token to start receiving changes after some point back in time - /// Type of event to watch for. Specify multiple like: EventType.Created | EventType.Updated | EventType.Deleted - /// A projection expression for the entity - /// x => x.FullDocument.Prop1 == "SomeValue" - /// The max number of entities to receive for a single event occurence - /// A cancellation token for ending the watching/change stream - public void StartWithToken( - BsonDocument resumeToken, - EventType eventTypes, - Expression> projection, - Expression, bool>>? filter = null, - int batchSize = 25, - CancellationToken cancellation = default) - => Init(resumeToken, eventTypes, filter, projection, batchSize, false, true, cancellation); - - /// - /// Starts the watcher instance with the supplied configuration - /// - /// A resume token to start receiving changes after some point back in time - /// Type of event to watch for. Specify multiple like: EventType.Created | EventType.Updated | EventType.Deleted - /// b => b.Eq(d => d.FullDocument.Prop1, "value") - /// The max number of entities to receive for a single event occurence - /// Set to true if you don't want the complete entity details. All properties except the ID will then be null. - /// A cancellation token for ending the watching/change stream - public void StartWithToken( - BsonDocument resumeToken, - EventType eventTypes, - Func>, FilterDefinition>> filter, - int batchSize = 25, - bool onlyGetIDs = false, - CancellationToken cancellation = default) - => Init(resumeToken, eventTypes, filter(Builders>.Filter), null, batchSize, onlyGetIDs, true, cancellation); - - /// - /// Starts the watcher instance with the supplied configuration. Supports projection. - /// - /// A resume token to start receiving changes after some point back in time - /// Type of event to watch for. Specify multiple like: EventType.Created | EventType.Updated | EventType.Deleted - /// A projection expression for the entity - /// b => b.Eq(d => d.FullDocument.Prop1, "value") - /// The max number of entities to receive for a single event occurence - /// A cancellation token for ending the watching/change stream - public void StartWithToken( - BsonDocument resumeToken, - EventType eventTypes, - Expression> projection, - Func>, FilterDefinition>> filter, - int batchSize = 25, - CancellationToken cancellation = default) - => Init(resumeToken, eventTypes, filter(Builders>.Filter), projection, batchSize, false, true, cancellation); - - private void Init( - BsonDocument? resumeToken, - EventType eventTypes, - FilterDefinition> filter, - Expression>? projection, - int batchSize, - bool onlyGetIDs, - bool autoResume, - CancellationToken cancellation) - { - if (_initialized) - throw new InvalidOperationException("This watcher has already been initialized!"); + _resume = autoResume; + _cancelToken = cancellation; - _resume = autoResume; - _cancelToken = cancellation; + var ops = new List(3) { ChangeStreamOperationType.Invalidate }; - var ops = new List(3) { ChangeStreamOperationType.Invalidate }; + if ((eventTypes & EventType.Created) != 0) + ops.Add(ChangeStreamOperationType.Insert); - if ((eventTypes & EventType.Created) != 0) - ops.Add(ChangeStreamOperationType.Insert); + if ((eventTypes & EventType.Updated) != 0) + { + ops.Add(ChangeStreamOperationType.Update); + ops.Add(ChangeStreamOperationType.Replace); + } - if ((eventTypes & EventType.Updated) != 0) + if ((eventTypes & EventType.Deleted) != 0) + ops.Add(ChangeStreamOperationType.Delete); + + if (ops.Contains(ChangeStreamOperationType.Delete)) + { + if (filter != null) { - ops.Add(ChangeStreamOperationType.Update); - ops.Add(ChangeStreamOperationType.Replace); + throw new ArgumentException( + "Filtering is not supported when watching for deletions " + + "as the entity data no longer exists in the db " + + "at the time of receiving the event."); } - if ((eventTypes & EventType.Deleted) != 0) - ops.Add(ChangeStreamOperationType.Delete); - - if (ops.Contains(ChangeStreamOperationType.Delete)) + if (projection != null) { - if (filter != null) - { - throw new ArgumentException( - "Filtering is not supported when watching for deletions " + - "as the entity data no longer exists in the db " + - "at the time of receiving the event."); - } - - if (projection != null) - { - throw new ArgumentException( - "Projecting is not supported when watching for deletions " + - "as the entity data no longer exists in the db " + - "at the time of receiving the event."); - } + throw new ArgumentException( + "Projecting is not supported when watching for deletions " + + "as the entity data no longer exists in the db " + + "at the time of receiving the event."); } + } - var filters = Builders>.Filter.Where(x => ops.Contains(x.OperationType)); + var filters = Builders>.Filter.Where(x => ops.Contains(x.OperationType)); - if (filter != null) - filters &= filter; + if (filter != null) + filters &= filter; - var stages = new List(3) { + var stages = new List(3) { PipelineStageDefinitionBuilder.Match(filters), PipelineStageDefinitionBuilder.Project,ChangeStreamDocument>(@" { @@ -299,175 +289,174 @@ private void Init( }") }; - if (projection != null) - stages.Add(PipelineStageDefinitionBuilder.Project(BuildProjection(projection))); - - _pipeline = stages; + if (projection != null) + stages.Add(PipelineStageDefinitionBuilder.Project(BuildProjection(projection))); - _options = new ChangeStreamOptions - { - StartAfter = resumeToken, - BatchSize = batchSize, - FullDocument = onlyGetIDs ? ChangeStreamFullDocumentOption.Default : ChangeStreamFullDocumentOption.UpdateLookup, - MaxAwaitTime = TimeSpan.FromSeconds(10) - }; + _pipeline = stages; - _initialized = true; + _options = new ChangeStreamOptions + { + StartAfter = resumeToken, + BatchSize = batchSize, + FullDocument = onlyGetIDs ? ChangeStreamFullDocumentOption.Default : ChangeStreamFullDocumentOption.UpdateLookup, + MaxAwaitTime = TimeSpan.FromSeconds(10) + }; - StartWatching(); - } + _initialized = true; - private static ProjectionDefinition, ChangeStreamDocument> BuildProjection(Expression> projection) - { - var rendered = Builders.Projection - .Expression(projection) - .Render(BsonSerializer.SerializerRegistry.GetSerializer(), - BsonSerializer.SerializerRegistry); + StartWatching(); + } - BsonDocument doc = new() - { - { "_id", 1 }, - { "operationType", 1 }, - { "documentKey", 1 }, - { "updateDescription", 1 }, - { "fullDocument._id", 1 } - }; + private static ProjectionDefinition, ChangeStreamDocument> BuildProjection(Expression> projection) + { + var rendered = Builders.Projection + .Expression(projection) + .Render(BsonSerializer.SerializerRegistry.GetSerializer(), + BsonSerializer.SerializerRegistry); - foreach (var element in rendered.Document.Elements) + BsonDocument doc = new() + { + { "_id", 1 }, + { "operationType", 1 }, + { "documentKey", 1 }, + { "updateDescription", 1 }, + { "fullDocument._id", 1 } + }; + + foreach (var element in rendered.Document.Elements) + { + if (element.Name != "_id") { - if (element.Name != "_id") - { - doc["fullDocument." + element.Name] = element.Value; - } + doc["fullDocument." + element.Name] = element.Value; } - - return doc; } - /// - /// If the watcher stopped due to an error or invalidate event, you can try to restart the watching again with this method. - /// - /// An optional resume token to restart watching with - public void ReStart(BsonDocument? resumeToken = null) + return doc; + } + + /// + /// If the watcher stopped due to an error or invalidate event, you can try to restart the watching again with this method. + /// + /// An optional resume token to restart watching with + public void ReStart(BsonDocument? resumeToken = null) + { + if (!CanRestart) { - if (!CanRestart) - { - throw new InvalidOperationException( - "This watcher has been aborted/cancelled. " + - "The subscribers have already been purged. " + - "Please instantiate a new watcher and subscribe to the events again."); - } + throw new InvalidOperationException( + "This watcher has been aborted/cancelled. " + + "The subscribers have already been purged. " + + "Please instantiate a new watcher and subscribe to the events again."); + } - if (!_initialized) - throw new InvalidOperationException("This watcher was never started. Please use .Start() first!"); + if (!_initialized) + throw new InvalidOperationException("This watcher was never started. Please use .Start() first!"); - if (_cancelToken.IsCancellationRequested) - throw new InvalidOperationException("This watcher cannot be restarted as it has been aborted/cancelled!"); + if (_cancelToken.IsCancellationRequested) + throw new InvalidOperationException("This watcher cannot be restarted as it has been aborted/cancelled!"); - if (resumeToken != null && _options is not null) - _options.StartAfter = resumeToken; + if (resumeToken != null && _options is not null) + _options.StartAfter = resumeToken; - StartWatching(); - } + StartWatching(); + } - private void StartWatching() - { - //note : don't use Task.Factory.StartNew with long running option - //reason: http://blog.i3arnon.com/2015/07/02/task-run-long-running/ - // StartNew creates an unnecessary dedicated thread which gets released upon reaching first await. - // continuations will be run on differnt threadpool threads upon re-entry. - // i.e. long running thread creation is useless/wasteful for async delegates. + private void StartWatching() + { + //note : don't use Task.Factory.StartNew with long running option + //reason: http://blog.i3arnon.com/2015/07/02/task-run-long-running/ + // StartNew creates an unnecessary dedicated thread which gets released upon reaching first await. + // continuations will be run on differnt threadpool threads upon re-entry. + // i.e. long running thread creation is useless/wasteful for async delegates. - _ = IterateCursorAsync(); - async Task IterateCursorAsync() + _ = IterateCursorAsync(); + async Task IterateCursorAsync() + { + try { - try - { - using var cursor = await _collection.WatchAsync(_pipeline, _options, _cancelToken).ConfigureAwait(false); - while (!_cancelToken.IsCancellationRequested && await cursor.MoveNextAsync(_cancelToken).ConfigureAwait(false)) + using var cursor = await _collection.WatchAsync(_pipeline, _options, _cancelToken).ConfigureAwait(false); + while (!_cancelToken.IsCancellationRequested && await cursor.MoveNextAsync(_cancelToken).ConfigureAwait(false)) + { + if (cursor.Current.Any()) { - if (cursor.Current.Any()) + if (_resume && _options is not null) + _options.StartAfter = cursor.Current.Last().ResumeToken; + + if (OnChangesAsync != null) { - if (_resume && _options is not null) - _options.StartAfter = cursor.Current.Last().ResumeToken; - - if (OnChangesAsync != null) - { - await OnChangesAsync.InvokeAllAsync( - cursor.Current - .Where(d => d.OperationType != ChangeStreamOperationType.Invalidate) - .Select(d => d.FullDocument) - ).ConfigureAwait(false); - } - - OnChanges?.Invoke( - cursor.Current - .Where(d => d.OperationType != ChangeStreamOperationType.Invalidate) - .Select(d => d.FullDocument)); - - if (OnChangesCSDAsync != null) - await OnChangesCSDAsync.InvokeAllAsync(cursor.Current).ConfigureAwait(false); - - OnChangesCSD?.Invoke(cursor.Current); + await OnChangesAsync.InvokeAllAsync( + cursor.Current + .Where(d => d.OperationType != ChangeStreamOperationType.Invalidate) + .Select(d => d.FullDocument) + ).ConfigureAwait(false); } + + OnChanges?.Invoke( + cursor.Current + .Where(d => d.OperationType != ChangeStreamOperationType.Invalidate) + .Select(d => d.FullDocument)); + + if (OnChangesCSDAsync != null) + await OnChangesCSDAsync.InvokeAllAsync(cursor.Current).ConfigureAwait(false); + + OnChangesCSD?.Invoke(cursor.Current); } + } - OnStop?.Invoke(); + OnStop?.Invoke(); - if (_cancelToken.IsCancellationRequested) + if (_cancelToken.IsCancellationRequested) + { + if (OnChangesAsync != null) { - if (OnChangesAsync != null) - { - foreach (var h in OnChangesAsync.GetHandlers()) - OnChangesAsync -= h; - } + foreach (var h in OnChangesAsync.GetHandlers()) + OnChangesAsync -= h; + } - if (OnChangesCSDAsync != null) - { - foreach (var h in OnChangesCSDAsync.GetHandlers()) - OnChangesCSDAsync -= h; - } + if (OnChangesCSDAsync != null) + { + foreach (var h in OnChangesCSDAsync.GetHandlers()) + OnChangesCSDAsync -= h; + } - if (OnChanges != null) - { - foreach (Action> a in OnChanges.GetInvocationList()) - OnChanges -= a; - } + if (OnChanges != null) + { + foreach (Action> a in OnChanges.GetInvocationList()) + OnChanges -= a; + } - if (OnChangesCSD != null) - { - foreach (Action>> a in OnChangesCSD.GetInvocationList()) - OnChangesCSD -= a; - } + if (OnChangesCSD != null) + { + foreach (Action>> a in OnChangesCSD.GetInvocationList()) + OnChangesCSD -= a; + } - if (OnError != null) - { - foreach (Action a in OnError.GetInvocationList()) - OnError -= a; - } + if (OnError != null) + { + foreach (Action a in OnError.GetInvocationList()) + OnError -= a; + } - if (OnStop != null) - { - foreach (Action a in OnStop.GetInvocationList()) - OnStop -= a; - } + if (OnStop != null) + { + foreach (Action a in OnStop.GetInvocationList()) + OnStop -= a; } } - catch (Exception x) - { - OnError?.Invoke(x); - } + } + catch (Exception x) + { + OnError?.Invoke(x); } } } +} - [Flags] - public enum EventType - { - Created = 1 << 1, - Updated = 1 << 2, - Deleted = 1 << 3 - } +[Flags] +public enum EventType +{ + Created = 1 << 1, + Updated = 1 << 2, + Deleted = 1 << 3 } diff --git a/MongoDB.Entities/DB/DB.Collection.cs b/MongoDB.Entities/DB/DB.Collection.cs index 6272f4958..b0b98539d 100644 --- a/MongoDB.Entities/DB/DB.Collection.cs +++ b/MongoDB.Entities/DB/DB.Collection.cs @@ -8,9 +8,9 @@ namespace MongoDB.Entities { public static partial class DB { - internal static IMongoCollection GetRefCollection(string name) where T : IEntity + internal static IMongoCollection> GetRefCollection(string name) where T : IEntity { - return Context.GetCollection(name); + return Context.GetCollection>(name); } /// diff --git a/MongoDB.Entities/DBContext/DBContext.Collection.cs b/MongoDB.Entities/DBContext/DBContext.Collection.cs index ee05df23f..ffa8f4f68 100644 --- a/MongoDB.Entities/DBContext/DBContext.Collection.cs +++ b/MongoDB.Entities/DBContext/DBContext.Collection.cs @@ -15,7 +15,7 @@ public partial class DBContext /// The options to use for collection creation /// An optional cancellation token /// - public Task CreateCollectionAsync(Action> options, CancellationToken cancellation = default, string? collectionName = null) where T : IEntity + public Task CreateCollectionAsync(Action> options, CancellationToken cancellation = default, string? collectionName = null) { var opts = new CreateCollectionOptions(); options(opts); @@ -29,7 +29,7 @@ public Task CreateCollectionAsync(Action> options, /// TIP: When deleting a collection, all relationships associated with that entity type is also deleted. /// /// The entity type to drop the collection of - public async Task DropCollectionAsync(string? collectionName = null) where T : IEntity + public async Task DropCollectionAsync(string? collectionName = null) { var tasks = new List(); var db = Database; diff --git a/MongoDB.Entities/DBContext/DBContext.Count.cs b/MongoDB.Entities/DBContext/DBContext.Count.cs index 76dc3d07f..7d628765f 100644 --- a/MongoDB.Entities/DBContext/DBContext.Count.cs +++ b/MongoDB.Entities/DBContext/DBContext.Count.cs @@ -16,7 +16,7 @@ public partial class DBContext /// To override the default collection name /// To override the default collection /// An optional cancellation token - public Task CountEstimatedAsync(CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + public Task CountEstimatedAsync(CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) { return Collection(collectionName, collection).EstimatedDocumentCountAsync(cancellationToken: cancellation); } @@ -31,7 +31,7 @@ public Task CountEstimatedAsync(CancellationToken cancellation = defaul /// Set to true if you'd like to ignore any global filters for this operation /// /// - public Task CountAsync(Expression> expression, CancellationToken cancellation = default, CountOptions? options = null, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + public Task CountAsync(Expression> expression, CancellationToken cancellation = default, CountOptions? options = null, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) { return CountAsync((FilterDefinition)expression, cancellation, options, ignoreGlobalFilters, collection: collection, collectionName: collectionName); } @@ -43,9 +43,9 @@ public Task CountAsync(Expression> expression, Cancellati /// An optional cancellation token /// /// - public Task CountAsync(CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + public Task CountAsync(CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) { - return CountAsync(_ => true, cancellation, collectionName: collectionName, collection: collection); + return CountAsync(_ => true, cancellation, collectionName: collectionName, collection: collection); } @@ -59,7 +59,7 @@ public Task CountAsync(CancellationToken cancellation = default, string /// Set to true if you'd like to ignore any global filters for this operation /// /// - public Task CountAsync(FilterDefinition filter, CancellationToken cancellation = default, CountOptions? options = null, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + public Task CountAsync(FilterDefinition filter, CancellationToken cancellation = default, CountOptions? options = null, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) { filter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, filter); return @@ -78,7 +78,7 @@ public Task CountAsync(FilterDefinition filter, CancellationToken ca /// Set to true if you'd like to ignore any global filters for this operation /// /// - public Task CountAsync(Func, FilterDefinition> filter, CancellationToken cancellation = default, CountOptions? options = null, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + public Task CountAsync(Func, FilterDefinition> filter, CancellationToken cancellation = default, CountOptions? options = null, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) { return CountAsync(filter(Builders.Filter), cancellation, options, ignoreGlobalFilters, collectionName: collectionName, collection: collection); } diff --git a/MongoDB.Entities/DBContext/DBContext.Delete.cs b/MongoDB.Entities/DBContext/DBContext.Delete.cs index b1ea05eb5..614e3b196 100644 --- a/MongoDB.Entities/DBContext/DBContext.Delete.cs +++ b/MongoDB.Entities/DBContext/DBContext.Delete.cs @@ -17,7 +17,9 @@ private void ThrowIfCancellationNotSupported(CancellationToken cancellation = de throw new NotSupportedException("Cancellation is only supported within transactions for delete operations!"); } - private async Task DeleteCascadingAsync(IEnumerable IDs, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + private async Task DeleteCascadingAsync(IEnumerable IDs, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) + where TId : IComparable, IEquatable + where T : IEntity { // note: cancellation should not be enabled outside of transactions because multiple collections are involved // and premature cancellation could cause data inconsistencies. @@ -34,13 +36,14 @@ private async Task DeleteCascadingAsync(IEnumerable IDs // note: db.listCollections() mongo command does not support transactions. // so don't add session support here. var collNamesCursor = await ListCollectionNamesAsync(options, cancellation).ConfigureAwait(false); - + IDs = IDs.OfType(); + var casted = IDs.Cast(); foreach (var cName in await collNamesCursor.ToListAsync(cancellation).ConfigureAwait(false)) { tasks.Add( Session is null - ? Collection(cName).DeleteManyAsync(r => IDs.Contains(r.ChildID) || IDs.Contains(r.ParentID)) - : Collection(cName).DeleteManyAsync(Session, r => IDs.Contains(r.ChildID) || IDs.Contains(r.ParentID), null, cancellation)); + ? Collection>(cName).DeleteManyAsync(r => casted.Contains(r.ID.ChildID) || casted.Contains(r.ID.ParentID)) + : Collection>(cName).DeleteManyAsync(Session, r => casted.Contains(r.ID.ChildID) || casted.Contains(r.ID.ParentID), null, cancellation)); } var delResTask = @@ -54,8 +57,8 @@ Session is null { tasks.Add( Session is null - ? Collection().DeleteManyAsync(x => IDs.Contains(x.FileID)) - : Collection().DeleteManyAsync(Session, x => IDs.Contains(x.FileID), null, cancellation)); + ? Collection>().DeleteManyAsync(x => IDs.Contains(x.FileID)) + : Collection>().DeleteManyAsync(Session, x => IDs.Contains(x.FileID), null, cancellation)); } await Task.WhenAll(tasks).ConfigureAwait(false); @@ -69,14 +72,17 @@ Session is null /// HINT: If this entity is referenced by one-to-many/many-to-many relationships, those references are also deleted. /// /// The type of entity + /// ID type /// The Id of the entity to delete /// An optional cancellation token /// Set to true if you'd like to ignore any global filters for this operation /// /// - public Task DeleteAsync(string ID, CancellationToken cancellation = default, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + public Task DeleteAsync(TId ID, CancellationToken cancellation = default, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) + where TId : IComparable, IEquatable + where T : IEntity { - return DeleteAsync(Builders.Filter.Eq(e => e.ID, ID), cancellation, ignoreGlobalFilters: ignoreGlobalFilters, collection: collection, collectionName: collectionName); + return DeleteAsync(Builders.Filter.Eq(e => e.ID, ID), cancellation, ignoreGlobalFilters: ignoreGlobalFilters, collection: collection, collectionName: collectionName); } /// @@ -85,14 +91,17 @@ public Task DeleteAsync(string ID, CancellationToken cancellati /// TIP: Try to keep the number of entities to delete under 100 in a single call /// /// The type of entity + /// ID type /// An IEnumerable of entity IDs /// An optional cancellation token /// Set to true if you'd like to ignore any global filters for this operation /// /// - public Task DeleteAsync(IEnumerable IDs, CancellationToken cancellation = default, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + public Task DeleteAsync(IEnumerable IDs, CancellationToken cancellation = default, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) + where TId : IComparable, IEquatable + where T : IEntity { - return DeleteAsync(Builders.Filter.In(e => e.ID, IDs), cancellation, ignoreGlobalFilters: ignoreGlobalFilters, collection: collection, collectionName: collectionName); + return DeleteAsync(Builders.Filter.In(e => e.ID, IDs), cancellation, ignoreGlobalFilters: ignoreGlobalFilters, collection: collection, collectionName: collectionName); } /// @@ -101,15 +110,18 @@ public Task DeleteAsync(IEnumerable IDs, CancellationTo /// TIP: Try to keep the number of entities to delete under 100 in a single call /// /// The type of entity + /// ID type /// A lambda expression for matching entities to delete. /// An optional cancellation token /// An optional collation object /// Set to true if you'd like to ignore any global filters for this operation /// /// - public Task DeleteAsync(Expression> expression, CancellationToken cancellation = default, Collation? collation = null, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + public Task DeleteAsync(Expression> expression, CancellationToken cancellation = default, Collation? collation = null, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) + where TId : IComparable, IEquatable + where T : IEntity { - return DeleteAsync(Builders.Filter.Where(expression), cancellation, collation, ignoreGlobalFilters, collection: collection, collectionName: collectionName); + return DeleteAsync(Builders.Filter.Where(expression), cancellation, collation, ignoreGlobalFilters, collection: collection, collectionName: collectionName); } /// @@ -118,15 +130,18 @@ public Task DeleteAsync(Expression> expression, C /// HINT: If these entities are referenced by one-to-many/many-to-many relationships, those references are also deleted. /// /// Any class that implements IEntity + /// ID type /// f => f.Eq(x => x.Prop, Value) & f.Gt(x => x.Prop, Value) /// An optional cancellation token /// An optional collation object /// Set to true if you'd like to ignore any global filters for this operation /// /// - public Task DeleteAsync(Func, FilterDefinition> filter, CancellationToken cancellation = default, Collation? collation = null, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + public Task DeleteAsync(Func, FilterDefinition> filter, CancellationToken cancellation = default, Collation? collation = null, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) + where TId : IComparable, IEquatable + where T : IEntity { - return DeleteAsync(filter(Builders.Filter), cancellation, collation, ignoreGlobalFilters, collection: collection, collectionName: collectionName); + return DeleteAsync(filter(Builders.Filter), cancellation, collation, ignoreGlobalFilters, collection: collection, collectionName: collectionName); } /// @@ -135,18 +150,21 @@ public Task DeleteAsync(Func, Filter /// HINT: If these entities are referenced by one-to-many/many-to-many relationships, those references are also deleted. /// /// Any class that implements IEntity + /// ID type /// A filter definition for matching entities to delete. /// An optional cancellation token /// An optional collation object /// Set to true if you'd like to ignore any global filters for this operation /// /// - public async Task DeleteAsync(FilterDefinition filter, CancellationToken cancellation = default, Collation? collation = null, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + public async Task DeleteAsync(FilterDefinition filter, CancellationToken cancellation = default, Collation? collation = null, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) + where TId : IComparable, IEquatable + where T : IEntity { ThrowIfCancellationNotSupported(cancellation); var filterDef = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, filter); - var cursor = await new Find(this, Collection(collectionName, collection)) + var cursor = await new Find(this, Collection(collectionName, collection)) .Match(filter) .Project(e => e.ID) .Option(o => o.BatchSize = _deleteBatchSize) @@ -163,7 +181,7 @@ public async Task DeleteAsync(FilterDefinition filter, Cance { if (cursor.Current.Any()) { - res = await DeleteCascadingAsync(cursor.Current, cancellation).ConfigureAwait(false); + res = await DeleteCascadingAsync(cursor.Current, cancellation).ConfigureAwait(false); deletedCount += res.DeletedCount; } } diff --git a/MongoDB.Entities/DBContext/DBContext.Distinct.cs b/MongoDB.Entities/DBContext/DBContext.Distinct.cs index 52b65b5b5..d68d71cd8 100644 --- a/MongoDB.Entities/DBContext/DBContext.Distinct.cs +++ b/MongoDB.Entities/DBContext/DBContext.Distinct.cs @@ -8,9 +8,12 @@ public partial class DBContext /// Represents a MongoDB Distinct command where you can get back distinct values for a given property of a given Entity /// /// Any Entity that implements IEntity interface + /// Id type /// The type of the property of the entity you'd like to get unique values for - public Distinct Distinct(string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + public Distinct Distinct(string? collectionName = null, IMongoCollection? collection = null) + where TId : IComparable, IEquatable + where T : IEntity { - return new Distinct(this, Collection(collectionName, collection)); + return new Distinct(this, Collection(collectionName, collection)); } } diff --git a/MongoDB.Entities/DBContext/DBContext.File.cs b/MongoDB.Entities/DBContext/DBContext.File.cs index 0c0c38598..662b877df 100644 --- a/MongoDB.Entities/DBContext/DBContext.File.cs +++ b/MongoDB.Entities/DBContext/DBContext.File.cs @@ -10,14 +10,14 @@ public partial class DBContext /// Returns a DataStreamer object to enable uploading/downloading file data directly by supplying the ID of the file entity /// /// The file entity type + /// ID type /// The ID of the file entity /// /// - public DataStreamer File(string ID, string? collectionName = null, IMongoCollection? collection = null) where T : FileEntity, new() + public DataStreamer File(TId ID, string? collectionName = null, IMongoCollection? collection = null) + where TId : IComparable, IEquatable + where T : FileEntity, new() { - if (!ObjectId.TryParse(ID, out _)) - throw new ArgumentException("The ID passed in is not of the correct format!"); - - return new DataStreamer(new T() { ID = ID, UploadSuccessful = true }, this, Collection(collectionName, collection)); + return new DataStreamer(new T() { ID = ID, UploadSuccessful = true }, this, Collection(collectionName, collection)); } } diff --git a/MongoDB.Entities/DBContext/DBContext.Find.cs b/MongoDB.Entities/DBContext/DBContext.Find.cs index 85d10b264..e8cedbc27 100644 --- a/MongoDB.Entities/DBContext/DBContext.Find.cs +++ b/MongoDB.Entities/DBContext/DBContext.Find.cs @@ -8,18 +8,24 @@ public partial class DBContext /// Starts a find command for the given entity type /// /// The type of entity - public Find Find(string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + /// ID type + public Find Find(string? collectionName = null, IMongoCollection? collection = null) + where TId : IComparable, IEquatable + where T : IEntity { - return new Find(this, Collection(collectionName, collection)); + return new Find(this, Collection(collectionName, collection)); } /// /// Starts a find command with projection support for the given entity type /// /// The type of entity + /// ID type /// The type of the end result - public Find Find(string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + public Find Find(string? collectionName = null, IMongoCollection? collection = null) + where TId : IComparable, IEquatable + where T : IEntity { - return new Find(this, Collection(collectionName, collection)); + return new Find(this, Collection(collectionName, collection)); } } diff --git a/MongoDB.Entities/DBContext/DBContext.Fluent.cs b/MongoDB.Entities/DBContext/DBContext.Fluent.cs index d6f3f05cf..5717efca1 100644 --- a/MongoDB.Entities/DBContext/DBContext.Fluent.cs +++ b/MongoDB.Entities/DBContext/DBContext.Fluent.cs @@ -12,7 +12,7 @@ public partial class DBContext /// Set to true if you'd like to ignore any global filters for this operation /// /// - public IAggregateFluent Fluent(AggregateOptions? options = null, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + public IAggregateFluent Fluent(AggregateOptions? options = null, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) { var globalFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, Builders.Filter.Empty); @@ -40,7 +40,7 @@ public IAggregateFluent Fluent(AggregateOptions? options = null, bool igno /// Set to true if you'd like to ignore any global filters for this operation /// /// - public IAggregateFluent FluentTextSearch(Search searchType, string searchTerm, bool caseSensitive = false, bool diacriticSensitive = false, string? language = null, AggregateOptions? options = null, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + public IAggregateFluent FluentTextSearch(Search searchType, string searchTerm, bool caseSensitive = false, bool diacriticSensitive = false, string? language = null, AggregateOptions? options = null, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) { var globalFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, Builders.Filter.Empty); diff --git a/MongoDB.Entities/DBContext/DBContext.GeoNear.cs b/MongoDB.Entities/DBContext/DBContext.GeoNear.cs index bc1274aa9..2226e9d7b 100644 --- a/MongoDB.Entities/DBContext/DBContext.GeoNear.cs +++ b/MongoDB.Entities/DBContext/DBContext.GeoNear.cs @@ -25,7 +25,7 @@ public partial class DBContext /// Set to true if you'd like to ignore any global filters for this operation /// /// - public IAggregateFluent GeoNear(Coordinates2D NearCoordinates, Expression>? DistanceField, bool Spherical = true, double? MaxDistance = null, double? MinDistance = null, int? Limit = null, BsonDocument? Query = null, double? DistanceMultiplier = null, Expression>? IncludeLocations = null, string? IndexKey = null, AggregateOptions? options = null, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + public IAggregateFluent GeoNear(Coordinates2D NearCoordinates, Expression>? DistanceField, bool Spherical = true, double? MaxDistance = null, double? MinDistance = null, int? Limit = null, BsonDocument? Query = null, double? DistanceMultiplier = null, Expression>? IncludeLocations = null, string? IndexKey = null, AggregateOptions? options = null, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) { var globalFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, Builders.Filter.Empty); diff --git a/MongoDB.Entities/DBContext/DBContext.Index.cs b/MongoDB.Entities/DBContext/DBContext.Index.cs index 8f500cd67..dcc8488ae 100644 --- a/MongoDB.Entities/DBContext/DBContext.Index.cs +++ b/MongoDB.Entities/DBContext/DBContext.Index.cs @@ -9,9 +9,12 @@ public partial class DBContext /// TIP: Define the keys first with .Key() method and finally call the .Create() method. /// /// Any class that implements IEntity - public Index Index(string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + /// ID type + public Index Index(string? collectionName = null, IMongoCollection? collection = null) + where TId : IComparable, IEquatable + where T : IEntity { - return new Index(this, Collection(collectionName, collection)); + return new Index(this, Collection(collectionName, collection)); } } } diff --git a/MongoDB.Entities/DBContext/DBContext.Insert.cs b/MongoDB.Entities/DBContext/DBContext.Insert.cs index 4e66f8163..3617b3eb2 100644 --- a/MongoDB.Entities/DBContext/DBContext.Insert.cs +++ b/MongoDB.Entities/DBContext/DBContext.Insert.cs @@ -12,23 +12,29 @@ public partial class DBContext { private static readonly BulkWriteOptions _unOrdBlkOpts = new() { IsOrdered = false }; private static readonly UpdateOptions _updateOptions = new() { IsUpsert = true }; - private Task SavePartial(T entity, Expression> members, CancellationToken cancellation, bool excludeMode = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + private Task SavePartial(T entity, Expression> members, CancellationToken cancellation, bool excludeMode = false, string? collectionName = null, IMongoCollection? collection = null) + where TId : IComparable, IEquatable + where T : IEntity { - PrepAndCheckIfInsert(entity); //just prep. we don't care about inserts here + PrepAndCheckIfInsert(entity); //just prep. we don't care about inserts here + var filter = Builders.Filter.Eq(e => e.ID, entity.ID); + var update = Builders.Update.Combine(Logic.BuildUpdateDefs(entity, members, this, excludeMode)); return Session == null - ? Collection(collectionName, collection).UpdateOneAsync(e => e.ID == entity.ID, Builders.Update.Combine(Logic.BuildUpdateDefs(entity, members, excludeMode)), _updateOptions, cancellation) - : Collection(collectionName, collection).UpdateOneAsync(Session, e => e.ID == entity.ID, Builders.Update.Combine(Logic.BuildUpdateDefs(entity, members, excludeMode)), _updateOptions, cancellation); + ? Collection(collectionName, collection).UpdateOneAsync(filter, update, _updateOptions, cancellation) + : Collection(collectionName, collection).UpdateOneAsync(Session, filter, update, _updateOptions, cancellation); } - private Task> SavePartial(IEnumerable entities, Expression> members, CancellationToken cancellation, bool excludeMode = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + private Task> SavePartial(IEnumerable entities, Expression> members, CancellationToken cancellation, bool excludeMode = false, string? collectionName = null, IMongoCollection? collection = null) + where TId : IComparable, IEquatable + where T : IEntity { var models = entities.Select(ent => { - PrepAndCheckIfInsert(ent); //just prep. we don't care about inserts here + PrepAndCheckIfInsert(ent); //just prep. we don't care about inserts here return new UpdateOneModel( filter: Builders.Filter.Eq(e => e.ID, ent.ID), - update: Builders.Update.Combine(Logic.BuildUpdateDefs(ent, members, excludeMode))) + update: Builders.Update.Combine(Logic.BuildUpdateDefs(ent, members, this, excludeMode))) { IsUpsert = true }; }).ToList(); return Session == null @@ -36,10 +42,12 @@ private Task> SavePartial(IEnumerable entities, Express : Collection(collectionName, collection).BulkWriteAsync(Session, models, _unOrdBlkOpts, cancellation); } - private bool PrepAndCheckIfInsert(T entity) where T : IEntity + private bool PrepAndCheckIfInsert(T entity) + where TId : IComparable, IEquatable + where T : IEntity { var cache = Cache(); - if (string.IsNullOrEmpty(entity.ID)) + if (EqualityComparer.Default.Equals(entity.ID, default)) { entity.ID = entity.GenerateNewID(); if (cache.HasCreatedOn) ((ICreatedOn)entity).CreatedOn = DateTime.UtcNow; @@ -56,15 +64,18 @@ private bool PrepAndCheckIfInsert(T entity) where T : IEntity /// If ID value is null, a new entity is created. If ID has a value, then existing entity is replaced. /// /// The type of entity + /// ID type /// The instance to persist /// And optional cancellation token /// /// - public Task InsertAsync(T entity, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + public Task InsertAsync(T entity, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) + where TId : IComparable, IEquatable + where T : IEntity { SetModifiedBySingle(entity); OnBeforeSave(entity); - PrepAndCheckIfInsert(entity); + PrepAndCheckIfInsert(entity); return Session == null ? Collection(collectionName, collection).InsertOneAsync(entity, null, cancellation) : Collection(collectionName, collection).InsertOneAsync(Session, entity, null, cancellation); @@ -75,18 +86,21 @@ public Task InsertAsync(T entity, CancellationToken cancellation = default, s /// If ID value is null, a new entity is created. If ID has a value, then existing entity is replaced. /// /// The type of entity + /// ID type /// The entities to persist /// And optional cancellation token /// /// - public Task> InsertAsync(IEnumerable entities, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + public Task> InsertAsync(IEnumerable entities, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) + where TId : IComparable, IEquatable + where T : IEntity { SetModifiedByMultiple(entities); foreach (var ent in entities) OnBeforeSave(ent); var models = entities.Select(ent => { - PrepAndCheckIfInsert(ent); + PrepAndCheckIfInsert(ent); return new InsertOneModel(ent); }).ToList(); diff --git a/MongoDB.Entities/DBContext/DBContext.PagedSearch.cs b/MongoDB.Entities/DBContext/DBContext.PagedSearch.cs index 891df99cd..56e9d1412 100644 --- a/MongoDB.Entities/DBContext/DBContext.PagedSearch.cs +++ b/MongoDB.Entities/DBContext/DBContext.PagedSearch.cs @@ -1,26 +1,31 @@ using MongoDB.Driver; -namespace MongoDB.Entities +namespace MongoDB.Entities; + +public partial class DBContext { - public partial class DBContext + /// + /// Represents an aggregation query that retrieves results with easy paging support. + /// + /// Any class that implements IEntity + /// ID type + public PagedSearch PagedSearch(string? collectionName = null, IMongoCollection? collection = null) + where TId : IComparable, IEquatable + where T : IEntity { - /// - /// Represents an aggregation query that retrieves results with easy paging support. - /// - /// Any class that implements IEntity - public PagedSearch PagedSearch(string? collectionName = null, IMongoCollection? collection = null) where T : IEntity - { - return new PagedSearch(this, Collection(collectionName, collection)); - } + return new PagedSearch(this, Collection(collectionName, collection)); + } - /// - /// Represents an aggregation query that retrieves results with easy paging support. - /// - /// Any class that implements IEntity - /// The type you'd like to project the results to. - public PagedSearch PagedSearch(string? collectionName = null, IMongoCollection? collection = null) where T : IEntity - { - return new PagedSearch(this, Collection(collectionName, collection)); - } + /// + /// Represents an aggregation query that retrieves results with easy paging support. + /// + /// Any class that implements IEntity + /// ID type + /// The type you'd like to project the results to. + public PagedSearch PagedSearch(string? collectionName = null, IMongoCollection? collection = null) + where TId : IComparable, IEquatable + where T : IEntity + { + return new PagedSearch(this, Collection(collectionName, collection)); } } diff --git a/MongoDB.Entities/DBContext/DBContext.Pipeline.cs b/MongoDB.Entities/DBContext/DBContext.Pipeline.cs index cd35efeed..48f74517c 100644 --- a/MongoDB.Entities/DBContext/DBContext.Pipeline.cs +++ b/MongoDB.Entities/DBContext/DBContext.Pipeline.cs @@ -22,7 +22,7 @@ public partial class DBContext /// Set to true if you'd like to ignore any global filters for this operation /// /// - public Task> PipelineCursorAsync(Template template, AggregateOptions? options = null, CancellationToken cancellation = default, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + public Task> PipelineCursorAsync(Template template, AggregateOptions? options = null, CancellationToken cancellation = default, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) { template = MergeTemplateGlobalFilter(template, ignoreGlobalFilters); return Session == null @@ -42,7 +42,7 @@ public Task> PipelineCursorAsync(TemplateSet to true if you'd like to ignore any global filters for this operation /// /// - public async Task> PipelineAsync(Template template, AggregateOptions? options = null, CancellationToken cancellation = default, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + public async Task> PipelineAsync(Template template, AggregateOptions? options = null, CancellationToken cancellation = default, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) { var list = new List(); using (var cursor = await PipelineCursorAsync(template, options, cancellation, ignoreGlobalFilters: ignoreGlobalFilters, collectionName: collectionName, collection: collection).ConfigureAwait(false)) @@ -67,7 +67,7 @@ public async Task> PipelineAsync(Template /// Set to true if you'd like to ignore any global filters for this operation /// /// - public async Task PipelineSingleAsync(Template template, AggregateOptions? options = null, CancellationToken cancellation = default, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + public async Task PipelineSingleAsync(Template template, AggregateOptions? options = null, CancellationToken cancellation = default, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) { AggregateOptions opts = options ?? new AggregateOptions(); @@ -88,7 +88,9 @@ public async Task PipelineSingleAsync(Template /// The options for the aggregation. This is not required. /// An optional cancellation token /// Set to true if you'd like to ignore any global filters for this operation - public async Task PipelineFirstAsync(Template template, AggregateOptions? options = null, CancellationToken cancellation = default, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + /// + /// + public async Task PipelineFirstAsync(Template template, AggregateOptions? options = null, CancellationToken cancellation = default, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) { var opts = options ?? new AggregateOptions(); @@ -99,7 +101,7 @@ public async Task PipelineFirstAsync(Template t return cursor.Current.SingleOrDefault(); } - private Template MergeTemplateGlobalFilter(Template template, bool ignoreGlobalFilters) where T : IEntity + private Template MergeTemplateGlobalFilter(Template template, bool ignoreGlobalFilters) { //WARNING: this has to do the same thing as Logic.MergeGlobalFilter method // if the following logic changes, update the other method also diff --git a/MongoDB.Entities/DBContext/DBContext.Queryable.cs b/MongoDB.Entities/DBContext/DBContext.Queryable.cs index cb79264ca..62e37d4ed 100644 --- a/MongoDB.Entities/DBContext/DBContext.Queryable.cs +++ b/MongoDB.Entities/DBContext/DBContext.Queryable.cs @@ -13,7 +13,7 @@ public partial class DBContext /// Set to true if you'd like to ignore any global filters for this operation /// /// - public IMongoQueryable Queryable(AggregateOptions? options = null, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + public IMongoQueryable Queryable(AggregateOptions? options = null, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) { var globalFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, Builders.Filter.Empty); collection = Collection(collectionName, collection); diff --git a/MongoDB.Entities/DBContext/DBContext.Replace.cs b/MongoDB.Entities/DBContext/DBContext.Replace.cs index b831a2f90..e3a8712b8 100644 --- a/MongoDB.Entities/DBContext/DBContext.Replace.cs +++ b/MongoDB.Entities/DBContext/DBContext.Replace.cs @@ -9,10 +9,13 @@ public partial class DBContext /// TIP: Only the first matched entity will be replaced /// /// The type of entity - public Replace Replace(string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + /// ID type + public Replace Replace(string? collectionName = null, IMongoCollection? collection = null) + where TId : IComparable, IEquatable + where T : IEntity { ThrowIfModifiedByIsEmpty(); - return new Replace(this, Collection(collectionName, collection), OnBeforeSave); + return new Replace(this, Collection(collectionName, collection), OnBeforeSave); } } } diff --git a/MongoDB.Entities/DBContext/DBContext.Save.cs b/MongoDB.Entities/DBContext/DBContext.Save.cs index bde3f9848..7c6182a70 100644 --- a/MongoDB.Entities/DBContext/DBContext.Save.cs +++ b/MongoDB.Entities/DBContext/DBContext.Save.cs @@ -17,25 +17,28 @@ public partial class DBContext /// If ID value is null, a new entity is created. If ID has a value, then existing entity is replaced. /// /// The type of entity + /// ID type /// The instance to persist /// And optional cancellation token /// /// - public Task SaveAsync(T entity, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + public Task SaveAsync(T entity, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) + where TId : IComparable, IEquatable + where T : IEntity { SetModifiedBySingle(entity); OnBeforeSave(entity); collection = Collection(collectionName, collection); - if (PrepAndCheckIfInsert(entity)) + if (PrepAndCheckIfInsert(entity)) { return Session is null ? collection.InsertOneAsync(entity, null, cancellation) : collection.InsertOneAsync(Session, entity, null, cancellation); } - + var filter = Builders.Filter.Eq(x => x.ID, entity.ID); return Session == null - ? collection.ReplaceOneAsync(x => x.ID == entity.ID, entity, new ReplaceOptions { IsUpsert = true }, cancellation) - : collection.ReplaceOneAsync(Session, x => x.ID == entity.ID, entity, new ReplaceOptions { IsUpsert = true }, cancellation); + ? collection.ReplaceOneAsync(filter, entity, new ReplaceOptions { IsUpsert = true }, cancellation) + : collection.ReplaceOneAsync(Session, filter, entity, new ReplaceOptions { IsUpsert = true }, cancellation); } /// @@ -43,11 +46,14 @@ public Task SaveAsync(T entity, CancellationToken cancellation = default, str /// If ID value is null, a new entity is created. If ID has a value, then existing entity is replaced. /// /// The type of entity + /// ID type /// The entities to persist /// And optional cancellation token /// /// - public Task> SaveAsync(IEnumerable entities, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + public Task> SaveAsync(IEnumerable entities, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) + where TId : IComparable, IEquatable + where T : IEntity { SetModifiedByMultiple(entities); foreach (var ent in entities) OnBeforeSave(ent); @@ -55,7 +61,7 @@ public Task> SaveAsync(IEnumerable entities, Cancellati var models = entities.Select>(ent => { - if (PrepAndCheckIfInsert(ent)) + if (PrepAndCheckIfInsert(ent)) { return new InsertOneModel(ent); } @@ -80,16 +86,19 @@ public Task> SaveAsync(IEnumerable entities, Cancellati /// You can only specify root level properties with the expression. /// /// Any class that implements IEntity + /// ID type /// The entity to save /// x => new { x.PropOne, x.PropTwo } /// An optional cancellation token /// /// - public Task SaveOnlyAsync(T entity, Expression> members, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + public Task SaveOnlyAsync(T entity, Expression> members, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) + where TId : IComparable, IEquatable + where T : IEntity { SetModifiedBySingle(entity); OnBeforeSave(entity); - return SavePartial(entity, members, cancellation, collectionName: collectionName, collection: collection); + return SavePartial(entity, members, cancellation, collectionName: collectionName, collection: collection); } @@ -100,16 +109,19 @@ public Task SaveOnlyAsync(T entity, Expression> /// You can only specify root level properties with the expression. /// /// Any class that implements IEntity + /// ID type /// The batch of entities to save /// x => new { x.PropOne, x.PropTwo } /// An optional cancellation token /// /// - public Task> SaveOnlyAsync(IEnumerable entities, Expression> members, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + public Task> SaveOnlyAsync(IEnumerable entities, Expression> members, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) + where TId : IComparable, IEquatable + where T : IEntity { SetModifiedByMultiple(entities); foreach (var ent in entities) OnBeforeSave(ent); - return SavePartial(entities, members, cancellation, collectionName: collectionName, collection: collection); + return SavePartial(entities, members, cancellation, collectionName: collectionName, collection: collection); } /// @@ -119,16 +131,19 @@ public Task> SaveOnlyAsync(IEnumerable entities, Expres /// You can only specify root level properties with the expression. /// /// Any class that implements IEntity + /// ID type /// The entity to save /// x => new { x.PropOne, x.PropTwo } /// An optional cancellation token /// /// - public Task SaveExceptAsync(T entity, Expression> members, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + public Task SaveExceptAsync(T entity, Expression> members, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) + where TId : IComparable, IEquatable + where T : IEntity { SetModifiedBySingle(entity); OnBeforeSave(entity); - return SavePartial(entity, members, cancellation, true, collectionName: collectionName, collection: collection); + return SavePartial(entity, members, cancellation, true, collectionName: collectionName, collection: collection); } /// @@ -138,16 +153,19 @@ public Task SaveExceptAsync(T entity, Expression /// /// Any class that implements IEntity + /// ID type /// The batch of entities to save /// x => new { x.PropOne, x.PropTwo } /// An optional cancellation token /// /// - public Task> SaveExceptAsync(IEnumerable entities, Expression> members, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + public Task> SaveExceptAsync(IEnumerable entities, Expression> members, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) + where TId : IComparable, IEquatable + where T : IEntity { SetModifiedByMultiple(entities); foreach (var ent in entities) OnBeforeSave(ent); - return SavePartial(entities, members, cancellation, true, collectionName: collectionName, collection: collection); + return SavePartial(entities, members, cancellation, true, collectionName: collectionName, collection: collection); } /// @@ -155,11 +173,14 @@ public Task> SaveExceptAsync(IEnumerable entities, Expr /// The properties to be excluded can be specified using the [Preserve] or [DontPreserve] attributes. /// /// The type of entity + /// ID type /// The entity to save /// An optional cancellation token /// /// - public Task SavePreservingAsync(T entity, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + public Task SavePreservingAsync(T entity, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) + where TId : IComparable, IEquatable + where T : IEntity { SetModifiedBySingle(entity); OnBeforeSave(entity); @@ -201,16 +222,18 @@ public Task SavePreservingAsync(T entity, CancellationToken can defs.Add(Builders.Update.Set(p.Name, p.GetValue(entity))); } + var filter = Builders.Filter.Eq(e => e.ID, entity.ID); + var update = Builders.Update.Combine(defs); return Session == null - ? Collection(collectionName, collection).UpdateOneAsync(e => e.ID == entity.ID, Builders.Update.Combine(defs), _updateOptions, cancellation) - : Collection(collectionName, collection).UpdateOneAsync(Session, e => e.ID == entity.ID, Builders.Update.Combine(defs), _updateOptions, cancellation); + ? Collection(collectionName, collection).UpdateOneAsync(filter, update, _updateOptions, cancellation) + : Collection(collectionName, collection).UpdateOneAsync(Session, filter, update, _updateOptions, cancellation); } - private void SetModifiedBySingle(T entity) where T : IEntity + private void SetModifiedBySingle(T entity) { var cache = Cache(); - if (cache.ModifiedByProp is null) + if (cache.ModifiedByProp is null) return; ThrowIfModifiedByIsEmpty(); @@ -221,7 +244,7 @@ private void SetModifiedBySingle(T entity) where T : IEntity // to be able to correctly deserialize a user supplied derived/sub class of ModifiedOn. } - private void SetModifiedByMultiple(IEnumerable entities) where T : IEntity + private void SetModifiedByMultiple(IEnumerable entities) { var cache = Cache(); if (cache.ModifiedByProp is null) diff --git a/MongoDB.Entities/DBContext/DBContext.Sequence.cs b/MongoDB.Entities/DBContext/DBContext.Sequence.cs index 3d4f8f454..8ce89c474 100644 --- a/MongoDB.Entities/DBContext/DBContext.Sequence.cs +++ b/MongoDB.Entities/DBContext/DBContext.Sequence.cs @@ -11,7 +11,7 @@ public partial class DBContext /// The type of entity to get the next sequential number for /// An optional cancellation token /// transaction support will not be added due to unpredictability with concurrency. - public Task NextSequentialNumberAsync(CancellationToken cancellation = default) where T : IEntity + public Task NextSequentialNumberAsync(CancellationToken cancellation = default) { return NextSequentialNumberAsync(CollectionName(), cancellation); } @@ -24,7 +24,7 @@ public Task NextSequentialNumberAsync(CancellationToken cancellation = /// transaction support will not be added due to unpredictability with concurrency. public Task NextSequentialNumberAsync(string sequenceName, CancellationToken cancellation = default) { - return new UpdateAndGet(this, Collection(), onUpdateAction: null, defs: null) + return new UpdateAndGet(this, Collection(), onUpdateAction: null, defs: null) .Match(s => s.ID == sequenceName) .Modify(b => b.Inc(s => s.Count, 1ul)) .Option(o => o.IsUpsert = true) diff --git a/MongoDB.Entities/DBContext/DBContext.Transaction.cs b/MongoDB.Entities/DBContext/DBContext.Transaction.cs index ed40c8710..8eb40b0a9 100644 --- a/MongoDB.Entities/DBContext/DBContext.Transaction.cs +++ b/MongoDB.Entities/DBContext/DBContext.Transaction.cs @@ -20,14 +20,12 @@ public IClientSessionHandle Transaction(ClientSessionOptions? options = null) } /// - /// Creates a new DBContext and a new MongoServerContext and Starts a transaction on the new instance. + /// Creates a new Transaction and a new MongoServerContext and Starts a transaction on the new instance. /// /// Client session options for this transaction - public DBContext TransactionCopy(ClientSessionOptions? options = null) + public Transaction TransactionCopy(ClientSessionOptions? options = null) { - var server = new MongoServerContext(MongoServerContext); - server.Transaction(options); - return new DBContext(server, Database, Options); + return new Transaction(MongoServerContext, Database.DatabaseNamespace.DatabaseName, options); } /// diff --git a/MongoDB.Entities/DBContext/DBContext.Update.cs b/MongoDB.Entities/DBContext/DBContext.Update.cs index 1e0bc984e..06114ed3a 100644 --- a/MongoDB.Entities/DBContext/DBContext.Update.cs +++ b/MongoDB.Entities/DBContext/DBContext.Update.cs @@ -1,48 +1,53 @@ -using MongoDB.Driver; -using System.Reflection; +namespace MongoDB.Entities; -namespace MongoDB.Entities +public partial class DBContext { - public partial class DBContext + /// + /// Starts an update command for the given entity type + /// + /// The type of entity + /// ID type + public Update Update(string? collectionName = null, IMongoCollection? collection = null) + where TId : IComparable, IEquatable + where T : IEntity { - /// - /// Starts an update command for the given entity type - /// - /// The type of entity - public Update Update(string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + var cmd = new Update(this, Collection(collectionName, collection), OnBeforeUpdate>); + if (Cache().ModifiedByProp is PropertyInfo ModifiedByProp) { - var cmd = new Update(this, Collection(collectionName, collection), OnBeforeUpdate>); - if (Cache().ModifiedByProp is PropertyInfo ModifiedByProp) - { - ThrowIfModifiedByIsEmpty(); - cmd.Modify(b => b.Set(ModifiedByProp.Name, ModifiedBy)); - } - return cmd; + ThrowIfModifiedByIsEmpty(); + cmd.Modify(b => b.Set(ModifiedByProp.Name, ModifiedBy)); } + return cmd; + } - /// - /// Starts an update-and-get command for the given entity type - /// - /// The type of entity - public UpdateAndGet UpdateAndGet(string? collectionName = null, IMongoCollection? collection = null) where T : IEntity - { - return UpdateAndGet(collectionName, collection); - } + /// + /// Starts an update-and-get command for the given entity type + /// + /// The type of entity + /// ID type + public UpdateAndGet UpdateAndGet(string? collectionName = null, IMongoCollection? collection = null) + where TId : IComparable, IEquatable + where T : IEntity + { + return UpdateAndGet(collectionName, collection); + } - /// - /// Starts an update-and-get command with projection support for the given entity type - /// - /// The type of entity - /// The type of the end result - public UpdateAndGet UpdateAndGet(string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + /// + /// Starts an update-and-get command with projection support for the given entity type + /// + /// The type of entity + /// The type of the end result + /// ID type + public UpdateAndGet UpdateAndGet(string? collectionName = null, IMongoCollection? collection = null) + where TId : IComparable, IEquatable + where T : IEntity + { + var cmd = new UpdateAndGet(this, Collection(collectionName, collection), OnBeforeUpdate>); + if (Cache().ModifiedByProp is PropertyInfo ModifiedByProp) { - var cmd = new UpdateAndGet(this, Collection(collectionName, collection), OnBeforeUpdate>); - if (Cache().ModifiedByProp is PropertyInfo ModifiedByProp) - { - ThrowIfModifiedByIsEmpty(); - cmd.Modify(b => b.Set(ModifiedByProp.Name, ModifiedBy)); - } - return cmd; + ThrowIfModifiedByIsEmpty(); + cmd.Modify(b => b.Set(ModifiedByProp.Name, ModifiedBy)); } + return cmd; } } diff --git a/MongoDB.Entities/DBContext/DBContext.Watcher.cs b/MongoDB.Entities/DBContext/DBContext.Watcher.cs index 451c0821f..0895c1147 100644 --- a/MongoDB.Entities/DBContext/DBContext.Watcher.cs +++ b/MongoDB.Entities/DBContext/DBContext.Watcher.cs @@ -1,34 +1,30 @@ -using MongoDB.Driver; -using System.Collections.Generic; +namespace MongoDB.Entities; -namespace MongoDB.Entities +public partial class DBContext { - public partial class DBContext + /// + /// Retrieves the 'change-stream' watcher instance for a given unique name. + /// If an instance for the name does not exist, it will return a new instance. + /// If an instance already exists, that instance will be returned. + /// + /// The entity type to get a watcher for + /// A unique name for the watcher of this entity type. Names can be duplicate among different entity types. + /// + /// + public Watcher Watcher(string name, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity { - /// - /// Retrieves the 'change-stream' watcher instance for a given unique name. - /// If an instance for the name does not exist, it will return a new instance. - /// If an instance already exists, that instance will be returned. - /// - /// The entity type to get a watcher for - /// A unique name for the watcher of this entity type. Names can be duplicate among different entity types. - /// - /// - public Watcher Watcher(string name, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity - { - var cache = Cache(); - if (cache.Watchers.TryGetValue(name.ToLowerInvariant().Trim(), out Watcher watcher)) - return watcher; - - watcher = new Watcher(name.ToLowerInvariant().Trim(), this, Collection(collectionName, collection)); - cache.Watchers.TryAdd(name, watcher); + var cache = Cache(); + if (cache.Watchers.TryGetValue(name.ToLowerInvariant().Trim(), out Watcher watcher)) return watcher; - } - /// - /// Returns all the watchers for a given entity type - /// - /// The entity type to get the watcher of - public IEnumerable> Watchers() where T : IEntity => Cache().Watchers.Values; + watcher = new Watcher(name.ToLowerInvariant().Trim(), Collection(collectionName, collection)); + cache.Watchers.TryAdd(name, watcher); + return watcher; } + + /// + /// Returns all the watchers for a given entity type + /// + /// The entity type to get the watcher of + public IEnumerable> Watchers() where T : IEntity => Cache().Watchers.Values; } diff --git a/MongoDB.Entities/DBContext/DBContext.cs b/MongoDB.Entities/DBContext/DBContext.cs index 9a428e96e..f7866a585 100644 --- a/MongoDB.Entities/DBContext/DBContext.cs +++ b/MongoDB.Entities/DBContext/DBContext.cs @@ -169,8 +169,8 @@ public DBContext(ModifiedBy? modifiedBy = null) : this("default", modifiedBy: mo /// /// This event hook will be trigged right before an entity is persisted /// - /// Any entity that implements IEntity - protected virtual void OnBeforeSave(T entity) where T : IEntity + /// Any entity + protected virtual void OnBeforeSave(T entity) { } @@ -179,7 +179,11 @@ protected virtual void OnBeforeSave(T entity) where T : IEntity /// /// Any entity that implements IEntity /// Any entity that implements IEntity - protected virtual void OnBeforeUpdate(UpdateBase updateBase) where T : IEntity where TSelf : UpdateBase + /// ID type + protected virtual void OnBeforeUpdate(UpdateBase updateBase) + where T : IEntity + where TId : IComparable, IEquatable + where TSelf : UpdateBase { } @@ -191,7 +195,7 @@ protected virtual void OnBeforeUpdate(UpdateBase updateBase) /// The type of Entity this global filter should be applied to /// x => x.Prop1 == "some value" /// Set to true if you want to prepend this global filter to your operation filters instead of being appended - protected void SetGlobalFilter(Expression> filter, bool prepend = false) where T : IEntity + protected void SetGlobalFilter(Expression> filter, bool prepend = false) { SetGlobalFilter(Builders.Filter.Where(filter), prepend); } @@ -202,7 +206,7 @@ protected void SetGlobalFilter(Expression> filter, bool prepend /// The type of Entity this global filter should be applied to /// b => b.Eq(x => x.Prop1, "some value") /// Set to true if you want to prepend this global filter to your operation filters instead of being appended - protected void SetGlobalFilter(Func, FilterDefinition> filter, bool prepend = false) where T : IEntity + protected void SetGlobalFilter(Func, FilterDefinition> filter, bool prepend = false) { SetGlobalFilter(filter(Builders.Filter), prepend); } @@ -213,7 +217,7 @@ protected void SetGlobalFilter(Func, FilterDefinit /// The type of Entity this global filter should be applied to /// A filter definition to be applied /// Set to true if you want to prepend this global filter to your operation filters instead of being appended - protected void SetGlobalFilter(FilterDefinition filter, bool prepend = false) where T : IEntity + protected void SetGlobalFilter(FilterDefinition filter, bool prepend = false) { AddFilter(typeof(T), (filter, prepend)); } @@ -237,7 +241,7 @@ protected void SetGlobalFilter(Type type, string jsonString, bool prepend = fals /// The type of the base class /// b => b.Eq(x => x.Prop1, "some value") /// Set to true if you want to prepend this global filter to your operation filters instead of being appended - protected void SetGlobalFilterForBaseClass(Expression> filter, bool prepend = false) where TBase : IEntity + protected void SetGlobalFilterForBaseClass(Expression> filter, bool prepend = false) { SetGlobalFilterForBaseClass(Builders.Filter.Where(filter), prepend); } @@ -248,7 +252,7 @@ protected void SetGlobalFilterForBaseClass(Expression> /// The type of the base class /// b => b.Eq(x => x.Prop1, "some value") /// Set to true if you want to prepend this global filter to your operation filters instead of being appended - protected void SetGlobalFilterForBaseClass(Func, FilterDefinition> filter, bool prepend = false) where TBase : IEntity + protected void SetGlobalFilterForBaseClass(Func, FilterDefinition> filter, bool prepend = false) { SetGlobalFilterForBaseClass(filter(Builders.Filter), prepend); } @@ -259,7 +263,7 @@ protected void SetGlobalFilterForBaseClass(FuncThe type of the base class /// A filter definition to be applied /// Set to true if you want to prepend this global filter to your operation filters instead of being appended - protected void SetGlobalFilterForBaseClass(FilterDefinition filter, bool prepend = false) where TBase : IEntity + protected void SetGlobalFilterForBaseClass(FilterDefinition filter, bool prepend = false) { foreach (var entType in MongoServerContext.AllEntitiyTypes.Where(t => t.IsSubclassOf(typeof(TBase)))) { @@ -293,7 +297,7 @@ protected void SetGlobalFilterForInterface(string jsonString, bool p - private void ThrowIfModifiedByIsEmpty() where T : IEntity + private void ThrowIfModifiedByIsEmpty() { var cache = Cache(); if (cache.ModifiedByProp is not null && ModifiedBy is null) @@ -310,20 +314,23 @@ private void AddFilter(Type type, (object filterDef, bool prepend) filter) private readonly ConcurrentDictionary _cache = new(); - internal EntityCache Cache() where T : IEntity + + internal EntityCache Cache() { - if (!_cache.TryGetValue(typeof(T), out var c)) + var type = typeof(T); + if (!_cache.TryGetValue(type, out var c)) { - c = new EntityCache(); + _cache[type] = c = EntityCache.Default; } return (EntityCache)c; } - public virtual string CollectionName() where T : IEntity + + public virtual string CollectionName() { return Cache().CollectionName; } - public virtual IMongoCollection Collection(string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + public virtual IMongoCollection Collection(string? collectionName = null, IMongoCollection? collection = null) { return collection ?? GetCollection(collectionName ?? CollectionName()); } diff --git a/MongoDB.Entities/Extensions/Entity.cs b/MongoDB.Entities/Extensions/Entity.cs index c0ddf3fb2..388b0dc09 100644 --- a/MongoDB.Entities/Extensions/Entity.cs +++ b/MongoDB.Entities/Extensions/Entity.cs @@ -30,13 +30,12 @@ private static T Duplicate(this T source) ).Data; } - internal static void ThrowIfUnsaved(this string? entityID) + internal static void ThrowIfUnsaved(this TId? id) where TId : IComparable, IEquatable { - if (string.IsNullOrWhiteSpace(entityID)) + if ((id is string strId && string.IsNullOrWhiteSpace(strId)) || EqualityComparer.Default.Equals(id, default)) throw new InvalidOperationException("Please save the entity before performing this operation!"); } - - internal static void ThrowIfUnsaved(this IEntity entity) + internal static void ThrowIfUnsaved(this IEntity entity) where TId : IComparable, IEquatable { ThrowIfUnsaved(entity.ID); } @@ -78,7 +77,7 @@ public static string FullPath(this Expression> expression) /// public static IMongoQueryable Queryable(this T _, AggregateOptions? options = null, string tenantPrefix = null) where T : IEntity { - return DB.Queryable(options, tenantPrefix: tenantPrefix); + return DB.Queryable(options); } /// diff --git a/MongoDB.Entities/GlobalUsing.cs b/MongoDB.Entities/GlobalUsing.cs new file mode 100644 index 000000000..31b77f63a --- /dev/null +++ b/MongoDB.Entities/GlobalUsing.cs @@ -0,0 +1,8 @@ +global using MongoDB.Driver; +global using System.Linq.Expressions; +global using MongoDB.Bson; +global using MongoDB.Bson.Serialization; +global using MongoDB.Bson.Serialization.Attributes; +global using MongoDB.Bson.Serialization.Serializers; +global using MongoDB.Driver.Linq; +global using System.Reflection; diff --git a/MongoDB.Entities/MongoDB.Entities.csproj b/MongoDB.Entities/MongoDB.Entities.csproj index fe0c9639b..50395ab3c 100644 --- a/MongoDB.Entities/MongoDB.Entities.csproj +++ b/MongoDB.Entities/MongoDB.Entities.csproj @@ -27,7 +27,10 @@ README.md mongodb mongodb-orm mongodb-repo mongodb-repository entities nosql orm linq netcore repository aspnetcore netcore2 netcore3 dotnetstandard database persistance dal repo true + + latest + enable diff --git a/MongoDB.Entities/Relationships/JoinRecord.cs b/MongoDB.Entities/Relationships/JoinRecord.cs index 2611c256b..2f253f1a4 100644 --- a/MongoDB.Entities/Relationships/JoinRecord.cs +++ b/MongoDB.Entities/Relationships/JoinRecord.cs @@ -1,21 +1,30 @@ -namespace MongoDB.Entities +using MongoDB.Bson.Serialization.Attributes; +using System; + +namespace MongoDB.Entities; + +/// +/// Represents a parent-child relationship between two entities. +/// TIP: The ParentID and ChildID switches around for many-to-many relationships depending on the side of the relationship you're accessing. +/// +public class JoinRecord : Entity<(TId1 ParentID, TId2 ChildID)> { - /// - /// Represents a parent-child relationship between two entities. - /// TIP: The ParentID and ChildID switches around for many-to-many relationships depending on the side of the relationship you're accessing. - /// - public class JoinRecord : Entity - { - /// - /// The ID of the parent IEntity for both one-to-many and the owner side of many-to-many relationships. - /// - [AsObjectId] - public string ParentID { get; set; } = null!; + ///// + ///// The ID of the parent IEntity for both one-to-many and the owner side of many-to-many relationships. + ///// + //[BsonIgnore] + //public string ParentID { get; set; } = null!; - /// - /// The ID of the child IEntity in one-to-many relationships and the ID of the inverse side IEntity in many-to-many relationships. - /// - [AsObjectId] - public string ChildID { get; set; } = null!; + ///// + ///// The ID of the child IEntity in one-to-many relationships and the ID of the inverse side IEntity in many-to-many relationships. + ///// + //[AsObjectId] + //public string ChildID { get; set; } = null!; + + + public override (TId1 ParentID, TId2 ChildID) GenerateNewID() + { + //the caller is responsible for assigning ids + throw new NotImplementedException(); } } diff --git a/MongoDB.Entities/Relationships/Many.Queryable.cs b/MongoDB.Entities/Relationships/Many.Queryable.cs index c7c28fb7f..26b0f392e 100644 --- a/MongoDB.Entities/Relationships/Many.Queryable.cs +++ b/MongoDB.Entities/Relationships/Many.Queryable.cs @@ -5,148 +5,147 @@ using System.Collections.Generic; using System.Linq; -namespace MongoDB.Entities +namespace MongoDB.Entities; + +public sealed partial class Many : IEnumerable where TChild : IEntity { - public sealed partial class Many : IEnumerable where TChild : IEntity + /// + /// An IQueryable of JoinRecords for this relationship + /// + /// An optional session if using within a transaction + /// An optional AggregateOptions object + public IMongoQueryable JoinQueryable(IClientSessionHandle session = null, AggregateOptions options = null) + { + return session == null + ? JoinCollection.AsQueryable(options) + : JoinCollection.AsQueryable(session, options); + } + + /// + /// Get an IQueryable of parents matching a single child ID for this relationship. + /// + /// The type of the parent IEntity + /// A child ID + /// An optional session if using within a transaction + /// An optional AggregateOptions object + public IMongoQueryable ParentsQueryable(string childID, IClientSessionHandle session = null, AggregateOptions options = null) where TParent : IEntity + { + return ParentsQueryable(new[] { childID }, session, options); + } + + /// + /// Get an IQueryable of parents matching multiple child IDs for this relationship. + /// + /// The type of the parent IEntity + /// An IEnumerable of child IDs + /// An optional session if using within a transaction + /// An optional AggregateOptions object + public IMongoQueryable ParentsQueryable(IEnumerable childIDs, IClientSessionHandle session = null, AggregateOptions options = null) where TParent : IEntity { - /// - /// An IQueryable of JoinRecords for this relationship - /// - /// An optional session if using within a transaction - /// An optional AggregateOptions object - public IMongoQueryable JoinQueryable(IClientSessionHandle session = null, AggregateOptions options = null) + if (typeof(TParent) == typeof(TChild)) throw new InvalidOperationException("Both parent and child types cannot be the same"); + + if (isInverse) { - return session == null - ? JoinCollection.AsQueryable(options) - : JoinCollection.AsQueryable(session, options); + return JoinQueryable(session, options) + .Where(j => childIDs.Contains(j.ParentID)) + .Join( + DB.Collection(null), + j => j.ChildID, + p => p.ID, + (_, p) => p) + .Distinct(); } - - /// - /// Get an IQueryable of parents matching a single child ID for this relationship. - /// - /// The type of the parent IEntity - /// A child ID - /// An optional session if using within a transaction - /// An optional AggregateOptions object - public IMongoQueryable ParentsQueryable(string childID, IClientSessionHandle session = null, AggregateOptions options = null) where TParent : IEntity + else { - return ParentsQueryable(new[] { childID }, session, options); + return JoinQueryable(session, options) + .Where(j => childIDs.Contains(j.ChildID)) + .Join( + DB.Collection(null), + j => j.ParentID, + p => p.ID, + (_, p) => p) + .Distinct(); } + } - /// - /// Get an IQueryable of parents matching multiple child IDs for this relationship. - /// - /// The type of the parent IEntity - /// An IEnumerable of child IDs - /// An optional session if using within a transaction - /// An optional AggregateOptions object - public IMongoQueryable ParentsQueryable(IEnumerable childIDs, IClientSessionHandle session = null, AggregateOptions options = null) where TParent : IEntity - { - if (typeof(TParent) == typeof(TChild)) throw new InvalidOperationException("Both parent and child types cannot be the same"); + /// + /// Get an IQueryable of parents matching a supplied IQueryable of children for this relationship. + /// + /// The type of the parent IEntity + /// An IQueryable of children + /// An optional session if using within a transaction + /// An optional AggregateOptions object + public IMongoQueryable ParentsQueryable(IMongoQueryable children, IClientSessionHandle session = null, AggregateOptions options = null) where TParent : IEntity + { + if (typeof(TParent) == typeof(TChild)) throw new InvalidOperationException("Both parent and child types cannot be the same"); - if (isInverse) - { - return JoinQueryable(session, options) - .Where(j => childIDs.Contains(j.ParentID)) - .Join( - DB.Collection(null), - j => j.ChildID, - p => p.ID, - (_, p) => p) - .Distinct(); - } - else - { - return JoinQueryable(session, options) - .Where(j => childIDs.Contains(j.ChildID)) - .Join( - DB.Collection(null), - j => j.ParentID, - p => p.ID, - (_, p) => p) - .Distinct(); - } + if (isInverse) + { + return children + .Join( + JoinQueryable(session, options), + c => c.ID, + j => j.ParentID, + (_, j) => j) + .Join( + DB.Collection(null), + j => j.ChildID, + p => p.ID, + (_, p) => p) + .Distinct(); } - - /// - /// Get an IQueryable of parents matching a supplied IQueryable of children for this relationship. - /// - /// The type of the parent IEntity - /// An IQueryable of children - /// An optional session if using within a transaction - /// An optional AggregateOptions object - public IMongoQueryable ParentsQueryable(IMongoQueryable children, IClientSessionHandle session = null, AggregateOptions options = null) where TParent : IEntity + else { - if (typeof(TParent) == typeof(TChild)) throw new InvalidOperationException("Both parent and child types cannot be the same"); - - if (isInverse) - { - return children - .Join( - JoinQueryable(session, options), - c => c.ID, - j => j.ParentID, - (_, j) => j) - .Join( - DB.Collection(null), - j => j.ChildID, - p => p.ID, - (_, p) => p) - .Distinct(); - } - else - { - return children - .Join( - JoinQueryable(session, options), - c => c.ID, - j => j.ChildID, - (_, j) => j) - .Join( - DB.Collection(null), - j => j.ParentID, - p => p.ID, - (_, p) => p) - .Distinct(); - } + return children + .Join( + JoinQueryable(session, options), + c => c.ID, + j => j.ChildID, + (_, j) => j) + .Join( + DB.Collection(null), + j => j.ParentID, + p => p.ID, + (_, p) => p) + .Distinct(); } + } - /// - /// An IQueryable of child Entities for the parent. - /// - /// An optional session if using within a transaction - /// An optional AggregateOptions object - public IMongoQueryable ChildrenQueryable(IClientSessionHandle session = null, AggregateOptions options = null) - { - parent.ThrowIfUnsaved(); + /// + /// An IQueryable of child Entities for the parent. + /// + /// An optional session if using within a transaction + /// An optional AggregateOptions object + public IMongoQueryable ChildrenQueryable(IClientSessionHandle session = null, AggregateOptions options = null) + { + parent.ThrowIfUnsaved(); - if (isInverse) - { - return JoinQueryable(session, options) - .Where(j => j.ChildID == parent.ID) - .Join( - DB.Collection(null), - j => j.ParentID, - c => c.ID, - (_, c) => c); - } - else - { - return JoinQueryable(session, options) - .Where(j => j.ParentID == parent.ID) - .Join( - DB.Collection(null), - j => j.ChildID, - c => c.ID, - (_, c) => c); - } + if (isInverse) + { + return JoinQueryable(session, options) + .Where(j => j.ChildID == parent.ID) + .Join( + DB.Collection(null), + j => j.ParentID, + c => c.ID, + (_, c) => c); + } + else + { + return JoinQueryable(session, options) + .Where(j => j.ParentID == parent.ID) + .Join( + DB.Collection(null), + j => j.ChildID, + c => c.ID, + (_, c) => c); } + } - /// - public IEnumerator GetEnumerator() => ChildrenQueryable().GetEnumerator(); + /// + public IEnumerator GetEnumerator() => ChildrenQueryable().GetEnumerator(); - /// - IEnumerator IEnumerable.GetEnumerator() => ChildrenQueryable().GetEnumerator(); + /// + IEnumerator IEnumerable.GetEnumerator() => ChildrenQueryable().GetEnumerator(); - } } diff --git a/MongoDB.Entities/Relationships/Many.cs b/MongoDB.Entities/Relationships/Many.cs index ee5493d96..9d49933d1 100644 --- a/MongoDB.Entities/Relationships/Many.cs +++ b/MongoDB.Entities/Relationships/Many.cs @@ -27,7 +27,10 @@ public abstract class ManyBase /// /// /// Type of the child IEntity. -public sealed partial class Many : ManyBase where TChild : IEntity +/// Child Id type. +public sealed partial class Many : ManyBase + where TChildId : IComparable, IEquatable + where TChild : IEntity { private static readonly BulkWriteOptions unOrdBlkOpts = new() { IsOrdered = false }; private bool isInverse; diff --git a/MongoDB.Entities/Relationships/NewMany.cs b/MongoDB.Entities/Relationships/NewMany.cs index e2a98d641..b757feb51 100644 --- a/MongoDB.Entities/Relationships/NewMany.cs +++ b/MongoDB.Entities/Relationships/NewMany.cs @@ -1,13 +1,4 @@ -using MongoDB.Driver; -using MongoDB.Driver.Linq; -using System; -using System.Collections.Generic; -using System.Linq.Expressions; -using System.Reflection; -using System.Text; - -namespace MongoDB.Entities.NewMany; - +namespace MongoDB.Entities.NewMany; /* results in the following: @@ -45,43 +36,94 @@ namespace MongoDB.Entities.NewMany; * Add C to the Many list in B * Add an arbitrary join record */ -class A : Entity +//class A : Entity +//{ +// public One? SingleB1 { get; set; } +// public One? SingleB2 { get; set; } +//} +//class B : Entity +//{ +// public IManyS ManyAVia1 { get; set; } +// public IManyS ManyAVia2 { get; set; } + +// [OwnerSide] +// public IMany ManyC { get; set; } + +// public B() +// { +// ManyAVia1 = (IManyS)this.InitManyToOne(x => x.ManyAVia1, x => x.SingleB1); +// ManyAVia2 = (IManyS)this.InitManyToOne(x => x.ManyAVia2, x => x.SingleB2); +// ManyC = this.InitManyToMany(x => x.ManyC, x => x.ManyB); +// } + +// public override ObjectId GenerateNewID() +// { +// return ObjectId.GenerateNewId(); +// } +//} +//class C : Entity +//{ +// [InverseSide] +// public IMany ManyB { get; set; } + +// public C() +// { +// ManyB = this.InitManyToMany(c => c.ManyB, b => b.ManyC); +// } + +// public override Guid GenerateNewID() => Guid.NewGuid(); +//} + +public interface IManyS : IMany + where TChild : IEntity { - public One? SingleB1 { get; set; } - public One? SingleB2 { get; set; } + } -class B : Entity +public interface IMany + where TChild : IEntity + where TChildId : IComparable, IEquatable { - public Many ManyAVia1 { get; set; } - public Many ManyAVia2 { get; set; } + Find GetChildrenFind(DBContext context, string? childCollectionName = null, IMongoCollection? collection = null); + Find GetChildrenFind(DBContext context, string? childCollectionName = null, IMongoCollection? collection = null); + IMongoQueryable GetChildrenQuery(DBContext context, string? childCollectionName = null, IMongoCollection? collection = null); +} - [OwnerSide] - public Many ManyC { get; set; } +public interface IMany : IMany + where TParent : IEntity + where TParentId : IComparable, IEquatable + where TChild : IEntity + where TChildId : IComparable, IEquatable +{ + TParent Parent { get; } - public B() - { - ManyAVia1 = this.InitManyToOne(x => x.ManyAVia1, x => x.SingleB1); - ManyAVia2 = this.InitManyToOne(x => x.ManyAVia2, x => x.SingleB2); + FilterDefinition GetFilterForSingleDocument(); +} - ManyC = this.InitManyToMany(x => x.ManyC, x => x.ManyB); - } +public interface IManyToMany : IMany + where TParent : IEntity + where TParentId : IComparable, IEquatable + where TChild : IEntity + where TChildId : IComparable, IEquatable +{ + bool IsParentOwner { get; } } -class C : Entity +public interface IManyToOne : IMany + where TParent : IEntity + where TParentId : IComparable, IEquatable + where TChild : IEntity + where TChildId : IComparable, IEquatable { - [InverseSide] - public Many ManyB { get; set; } - - public C() - { - ManyB = this.InitManyToMany(x => x.ManyB, x => x.ManyC); - } } + /// /// Marker class /// /// -public abstract class Many where TChild : IEntity +/// +public abstract class Many : IMany + where TChildId : IComparable, IEquatable + where TChild : IEntity { protected Many(PropertyInfo parentProperty, PropertyInfo childProperty) { @@ -93,10 +135,14 @@ protected Many(PropertyInfo parentProperty, PropertyInfo childProperty) internal PropertyInfo ChildProperty { get; } public abstract IMongoQueryable GetChildrenQuery(DBContext context, string? childCollectionName = null, IMongoCollection? collection = null); - public Find GetChildrenFind(DBContext context, string? childCollectionName = null, IMongoCollection? collection = null) => GetChildrenFind(context, childCollectionName, collection); - public abstract Find GetChildrenFind(DBContext context, string? childCollectionName = null, IMongoCollection? collection = null); + public Find GetChildrenFind(DBContext context, string? childCollectionName = null, IMongoCollection? collection = null) => GetChildrenFind(context, childCollectionName, collection); + public abstract Find GetChildrenFind(DBContext context, string? childCollectionName = null, IMongoCollection? collection = null); } -public abstract class Many : Many where TParent : IEntity where TChild : IEntity +public abstract class Many : Many, IMany + where TParent : IEntity + where TParentId : IComparable, IEquatable + where TChild : IEntity + where TChildId : IComparable, IEquatable { public TParent Parent { get; } protected Many(TParent parent, PropertyInfo parentProperty, PropertyInfo childProperty) : base(parentProperty, childProperty) @@ -107,7 +153,12 @@ protected Many(TParent parent, PropertyInfo parentProperty, PropertyInfo childPr } -public sealed class ManyToMany : Many where TParent : IEntity where TChild : IEntity + +public sealed class ManyToMany : Many, IManyToMany + where TParent : IEntity + where TParentId : IComparable, IEquatable + where TChild : IEntity + where TChildId : IComparable, IEquatable { public ManyToMany(bool isParentOwner, TParent parent, PropertyInfo parentProperty, PropertyInfo childProperty) : base(parent, parentProperty, childProperty) { @@ -116,7 +167,7 @@ public ManyToMany(bool isParentOwner, TParent parent, PropertyInfo parentPropert public bool IsParentOwner { get; } - public override Find GetChildrenFind(DBContext context, string? childCollectionName = null, IMongoCollection? collection = null) + public override Find GetChildrenFind(DBContext context, string? childCollectionName = null, IMongoCollection? collection = null) { throw new NotImplementedException(); } @@ -127,15 +178,19 @@ public override IMongoQueryable GetChildrenQuery(DBContext context, stri } } -public sealed class ManyToOne : Many where TParent : IEntity where TChild : IEntity +public sealed class ManyToOne : Many, IManyToOne + where TParent : IEntity + where TParentId : IComparable, IEquatable + where TChild : IEntity + where TChildId : IComparable, IEquatable { public ManyToOne(TParent parent, PropertyInfo parentProperty, PropertyInfo childProperty) : base(parent, parentProperty, childProperty) { } - public override Find GetChildrenFind(DBContext context, string? childCollectionName = null, IMongoCollection? childCollection = null) + public override Find GetChildrenFind(DBContext context, string? childCollectionName = null, IMongoCollection? childCollection = null) { - return context.Find(childCollectionName, childCollection) + return context.Find(childCollectionName, childCollection) .Match(GetFilterForSingleDocument()); //BRef==Parent.Id } @@ -144,7 +199,6 @@ public override IMongoQueryable GetChildrenQuery(DBContext context, stri return context.Queryable(collectionName: childCollectionName, collection: childCollection) .Where(_ => GetFilterForSingleDocument().Inject()); } - } @@ -175,7 +229,11 @@ public static PropertyInfo GetPropertyInfo(this Expression InitManyToMany(this TParent parent, Expression>> propertyExpression, Expression>> propertyOtherSide) where TParent : IEntity where TChild : IEntity + public static IManyToMany InitManyToMany(this TParent parent, Expression>> propertyExpression, Expression>> propertyOtherSide) + where TParent : IEntity + where TParentId : IComparable, IEquatable + where TChild : IEntity + where TChildId : IComparable, IEquatable { var property = propertyExpression.GetPropertyInfo(); var hasOwnerAttrib = property.IsDefined(typeof(OwnerSideAttribute), false); @@ -190,17 +248,20 @@ public static ManyToMany InitManyToMany(this T if (!osHasOwnerAttrib && !osHasInverseAttrib) throw new InvalidOperationException("Missing attribute for determining relationship side of a many-to-many relationship"); if ((hasOwnerAttrib == osHasOwnerAttrib) || (hasInverseAttrib == osHasInverseAttrib)) throw new InvalidOperationException("Both sides of the relationship cannot have the same attribute"); - var res = new ManyToMany(hasInverseAttrib, parent, property, osProperty); + var res = new ManyToMany(hasInverseAttrib, parent, property, osProperty); //should we set the property ourself or let the user handle it ? //property.SetValue(parent, res); return res; } - public static ManyToOne InitManyToOne(this TParent parent, Expression>> propertyExpression, Expression?>> propertyOtherSide) where TParent : IEntity where TChild : IEntity + public static IManyToOne InitManyToOne(this TParent parent, Expression>> propertyExpression, Expression?>> propertyOtherSide) + where TParent : IEntity + where TParentId : IComparable, IEquatable + where TChild : IEntity + where TChildId : IComparable, IEquatable { var property = propertyExpression.GetPropertyInfo(); var osProperty = propertyOtherSide.GetPropertyInfo(); - return new ManyToOne(parent, property, osProperty); - + return new ManyToOne(parent, property, osProperty); } } \ No newline at end of file diff --git a/MongoDB.Entities/Relationships/One.cs b/MongoDB.Entities/Relationships/One.cs index 99f4aa7f2..3e9544e3b 100644 --- a/MongoDB.Entities/Relationships/One.cs +++ b/MongoDB.Entities/Relationships/One.cs @@ -11,7 +11,10 @@ namespace MongoDB.Entities /// Represents a one-to-one relationship with an IEntity. /// /// Any type that implements IEntity - public class One where T : IEntity + /// Any type that implements IEntity + public class One + where TId : IComparable, IEquatable + where T : IEntity { private T? _cache; @@ -19,7 +22,7 @@ public class One where T : IEntity /// The Id of the entity referenced by this instance. /// [AsObjectId] - public string? ID { get; set; } + public TId? ID { get; set; } public T? Cache { @@ -28,9 +31,16 @@ public T? Cache { value?.ThrowIfUnsaved(); _cache = value; - if (ID != value?.ID) + if (value is not null) { - ID = value?.ID!; + if (!EqualityComparer.Default.Equals(ID, value.ID)) + { + ID = value.ID!; + } + } + else + { + ID = default; } } } @@ -52,18 +62,18 @@ internal One(T entity) /// Operator for returning a new One<T> object from a string ID /// /// The ID to create a new One<T> with - public static implicit operator One(string id) + public static implicit operator One(TId id) { - return new One() { ID = id }; + return new One() { ID = id }; } /// /// Operator for returning a new One<T> object from an entity /// /// The entity to make a reference to - public static implicit operator One(T entity) + public static implicit operator One(T entity) { - return new One(entity); + return new One(entity); } /// @@ -81,7 +91,7 @@ public static implicit operator One(T entity) return default; } - return Cache = await new Find(context, collection ?? context.Collection(collectionName)).OneAsync(ID, cancellation); + return Cache = await new Find(context, collection ?? context.Collection(collectionName)).OneAsync(ID, cancellation); } /// @@ -93,14 +103,14 @@ public static implicit operator One(T entity) /// /// /// A Task containing the actual projected entity - public async Task ToEntityAsync(DBContext context, Expression> projection, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) where TFrom : IEntity + public async Task ToEntityAsync(DBContext context, Expression> projection, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) { if (ID is null) { return default; } - return Cache = (await new Find(context, collection ?? context.Collection(collectionName)) + return (await new Find(context, collection ?? context.Collection(collectionName)) .Match(ID) .Project(projection) .ExecuteAsync(cancellation).ConfigureAwait(false)) @@ -116,17 +126,22 @@ public static implicit operator One(T entity) /// /// /// A Task containing the actual projected entity - public async Task ToEntityAsync(DBContext context, Func, ProjectionDefinition> projection, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) where TFrom : IEntity + public async Task ToEntityAsync(DBContext context, Func, ProjectionDefinition> projection, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) { if (ID is null) { return default; } - return Cache = (await new Find(context, collection ?? context.Collection(collectionName)) + return (await new Find(context, collection ?? context.Collection(collectionName)) .Match(ID) .Project(projection) .ExecuteAsync(cancellation).ConfigureAwait(false)) .SingleOrDefault(); } } + + public class One : One where T : IEntity + { + + } } From 7f6a31250bb60aa45bedb06b41aaa3351bd6d991 Mon Sep 17 00:00:00 2001 From: Ahmed Fwela Date: Thu, 11 Nov 2021 23:19:51 +0200 Subject: [PATCH 22/26] WIP JoinRecord --- MongoDB.Entities/Core/IEntity.cs | 18 ------- .../Core/IgnoreManyPropsConvention.cs | 3 +- MongoDB.Entities/Core/JoinRecordSerializer.cs | 27 ++++++++++ MongoDB.Entities/DBContext/DBContext.cs | 2 +- MongoDB.Entities/Relationships/JoinRecord.cs | 50 +++++++++++++------ 5 files changed, 64 insertions(+), 36 deletions(-) create mode 100644 MongoDB.Entities/Core/JoinRecordSerializer.cs diff --git a/MongoDB.Entities/Core/IEntity.cs b/MongoDB.Entities/Core/IEntity.cs index cc9399b8d..f9beb1b1c 100644 --- a/MongoDB.Entities/Core/IEntity.cs +++ b/MongoDB.Entities/Core/IEntity.cs @@ -27,21 +27,3 @@ public interface IEntity /// TId GenerateNewID(); } - -//public class MultipleIdEntity : IEntity> -//{ -// public (Guid, ObjectId) ID { get; set; } -// [BsonIgnore] -// public Guid Id1 -// { -// get => ID.Item1; -// set => ID = (value, ID.Item2); -// } -// [BsonIgnore] -// public ObjectId Id2 -// { -// get => ID.Item2; -// set => ID = (ID.Item1, value); -// } -// public (Guid, ObjectId) GenerateNewID() => (Guid.NewGuid(), ObjectId.GenerateNewId()); -//} \ No newline at end of file diff --git a/MongoDB.Entities/Core/IgnoreManyPropsConvention.cs b/MongoDB.Entities/Core/IgnoreManyPropsConvention.cs index 859ab0c03..1fb0e1042 100644 --- a/MongoDB.Entities/Core/IgnoreManyPropsConvention.cs +++ b/MongoDB.Entities/Core/IgnoreManyPropsConvention.cs @@ -1,4 +1,5 @@ using MongoDB.Bson.Serialization.Conventions; +using MongoDB.Entities.NewMany; namespace MongoDB.Entities; @@ -6,7 +7,7 @@ internal class IgnoreManyPropsConvention : ConventionBase, IMemberMapConvention { public void Apply(BsonMemberMap mMap) { - if (mMap.MemberType.Name == ManyBase.PropTypeName) + if (typeof(IMany<,>).IsAssignableFrom(mMap.MemberType)) { _ = mMap.SetShouldSerializeMethod(_ => false); } diff --git a/MongoDB.Entities/Core/JoinRecordSerializer.cs b/MongoDB.Entities/Core/JoinRecordSerializer.cs new file mode 100644 index 000000000..1483929c8 --- /dev/null +++ b/MongoDB.Entities/Core/JoinRecordSerializer.cs @@ -0,0 +1,27 @@ +namespace MongoDB.Entities; + +internal class JoinRecordSerializer : SerializerBase>, IBsonDocumentSerializer +{ + public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, JoinRecord value) + { + //documents will exist in 2 formats + + } + public override JoinRecord Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + { + + } + + public bool TryGetMemberSerializationInfo(string memberName, out BsonSerializationInfo? serializationInfo) + { + switch (memberName) + { + case nameof(JoinRecord.ID): + serializationInfo = null; + return false; + default: + serializationInfo = null; + return false; + } + } +} \ No newline at end of file diff --git a/MongoDB.Entities/DBContext/DBContext.cs b/MongoDB.Entities/DBContext/DBContext.cs index f7866a585..b7a4198e4 100644 --- a/MongoDB.Entities/DBContext/DBContext.cs +++ b/MongoDB.Entities/DBContext/DBContext.cs @@ -139,7 +139,7 @@ public static void InitStatic() BsonSerializer.RegisterSerializer(new FuzzyStringSerializer()); BsonSerializer.RegisterSerializer(typeof(decimal), new DecimalSerializer(BsonType.Decimal128)); BsonSerializer.RegisterSerializer(typeof(decimal?), new NullableSerializer(new DecimalSerializer(BsonType.Decimal128))); - + BsonSerializer.RegisterGenericSerializerDefinition(typeof(JoinRecord<,>), typeof(JoinRecordSerializer<,>)); ConventionRegistry.Register( "DefaultConventions", new ConventionPack diff --git a/MongoDB.Entities/Relationships/JoinRecord.cs b/MongoDB.Entities/Relationships/JoinRecord.cs index 2f253f1a4..d11aa463d 100644 --- a/MongoDB.Entities/Relationships/JoinRecord.cs +++ b/MongoDB.Entities/Relationships/JoinRecord.cs @@ -1,30 +1,48 @@ -using MongoDB.Bson.Serialization.Attributes; -using System; +namespace MongoDB.Entities; -namespace MongoDB.Entities; /// /// Represents a parent-child relationship between two entities. /// TIP: The ParentID and ChildID switches around for many-to-many relationships depending on the side of the relationship you're accessing. /// -public class JoinRecord : Entity<(TId1 ParentID, TId2 ChildID)> +public class JoinRecord : Entity { - ///// - ///// The ID of the parent IEntity for both one-to-many and the owner side of many-to-many relationships. - ///// - //[BsonIgnore] - //public string ParentID { get; set; } = null!; + /// + /// The ID of the parent IEntity for both one-to-many and the owner side of many-to-many relationships. + /// + public string ParentID { get; set; } = null!; - ///// - ///// The ID of the child IEntity in one-to-many relationships and the ID of the inverse side IEntity in many-to-many relationships. - ///// - //[AsObjectId] - //public string ChildID { get; set; } = null!; + /// + /// The ID of the child IEntity in one-to-many relationships and the ID of the inverse side IEntity in many-to-many relationships. + /// + public string ChildID { get; set; } = null!; +} + +/// +/// Represents a parent-child relationship between two entities. +/// TIP: The ParentID and ChildID switches around for many-to-many relationships depending on the side of the relationship you're accessing. +/// +public class JoinRecord : Entity<(TParentId ParentId, TChildId ChildId)> +{ + public JoinRecord(TParentId parentID, TChildId childID) + { + ID = (parentID, childID); + } + + /// + /// The ID of the parent IEntity for both one-to-many and the owner side of many-to-many relationships. + /// + [BsonIgnore] + public TParentId ParentID => ID.ParentId; + /// + /// The ID of the child IEntity in one-to-many relationships and the ID of the inverse side IEntity in many-to-many relationships. + /// + [BsonIgnore] + public TChildId ChildID => ID.ChildId; - public override (TId1 ParentID, TId2 ChildID) GenerateNewID() + public override (TParentId ParentId, TChildId ChildId) GenerateNewID() { - //the caller is responsible for assigning ids throw new NotImplementedException(); } } From 84d9bbfb4df25f2538d4b0ff6f0a846e914498c5 Mon Sep 17 00:00:00 2001 From: Ahmed Fwela Date: Tue, 7 Dec 2021 10:26:14 +0200 Subject: [PATCH 23/26] WIP attempt to remove dependency on IEntity --- MongoDB.Entities/Builders/Distinct.Base.cs | 52 ++++ .../Builders/Distinct.Interface.cs | 25 ++ MongoDB.Entities/Builders/Distinct.cs | 60 +--- .../Builders/FilterQuery.Extensions.cs | 41 +++ .../Builders/FilterQuery.Interface.cs | 78 ++++++ MongoDB.Entities/Builders/FilterQuery.cs | 104 +++++++ MongoDB.Entities/Builders/FilterQueryBase.cs | 167 ----------- MongoDB.Entities/Builders/Find.Base.cs | 114 ++++++++ MongoDB.Entities/Builders/Find.Interface.cs | 68 +++++ MongoDB.Entities/Builders/Find.cs | 219 +++------------ .../Builders/ICollectionRelated.cs | 1 + MongoDB.Entities/Builders/Index.Interface.cs | 22 ++ MongoDB.Entities/Builders/Index.cs | 32 +-- MongoDB.Entities/Builders/PagedSearch.cs | 28 +- MongoDB.Entities/Builders/Replace.cs | 2 +- .../Builders/SortFilterQueryBase.Interface.cs | 20 ++ .../Builders/SortFilterQueryBase.cs | 14 +- MongoDB.Entities/Core/EntityCache.cs | 10 +- MongoDB.Entities/Core/Prop.cs | 16 +- MongoDB.Entities/DBContext/DBContext.Find.cs | 16 +- MongoDB.Entities/Extensions/Entity.cs | 8 +- MongoDB.Entities/GlobalUsing.cs | 2 + MongoDB.Entities/MongoDB.Entities.csproj | 2 +- MongoDB.Entities/Relationships/NewMany.cs | 75 ++--- MongoDB.Entities/Relationships/One.cs | 262 ++++++++++-------- Tests/TestModifiedBy.cs | 2 +- 26 files changed, 800 insertions(+), 640 deletions(-) create mode 100644 MongoDB.Entities/Builders/Distinct.Base.cs create mode 100644 MongoDB.Entities/Builders/Distinct.Interface.cs create mode 100644 MongoDB.Entities/Builders/FilterQuery.Extensions.cs create mode 100644 MongoDB.Entities/Builders/FilterQuery.Interface.cs create mode 100644 MongoDB.Entities/Builders/FilterQuery.cs delete mode 100644 MongoDB.Entities/Builders/FilterQueryBase.cs create mode 100644 MongoDB.Entities/Builders/Find.Base.cs create mode 100644 MongoDB.Entities/Builders/Find.Interface.cs create mode 100644 MongoDB.Entities/Builders/Index.Interface.cs create mode 100644 MongoDB.Entities/Builders/SortFilterQueryBase.Interface.cs diff --git a/MongoDB.Entities/Builders/Distinct.Base.cs b/MongoDB.Entities/Builders/Distinct.Base.cs new file mode 100644 index 000000000..4b791d1a2 --- /dev/null +++ b/MongoDB.Entities/Builders/Distinct.Base.cs @@ -0,0 +1,52 @@ +namespace MongoDB.Entities; + +public class DistinctBase : FilterQueryBase, IDistinct + where TSelf : DistinctBase +{ + internal DistinctOptions _options = new(); + internal FieldDefinition? _field; + + internal DistinctBase(DistinctBase other) : base(other) + { + _options = other._options; + _field = other._field; + } + internal DistinctBase(Dictionary globalFilters) : base(globalFilters) + { + _globalFilters = globalFilters; + } + + + private TSelf This => (TSelf)this; + + + /// + /// Specify an option for this find command (use multiple times if needed) + /// + /// x => x.OptionName = OptionValue + public TSelf Option(Action option) + { + option(_options); + return This; + } + + /// + /// Specify the property you want to get the unique values for (as a string path) + /// + /// ex: "Address.Street" + public TSelf Property(string property) + { + _field = property; + return This; + } + + /// + /// Specify the property you want to get the unique values for (as a member expression) + /// + /// x => x.Address.Street + public TSelf Property(Expression> property) + { + _field = property.FullPath(); + return This; + } +} diff --git a/MongoDB.Entities/Builders/Distinct.Interface.cs b/MongoDB.Entities/Builders/Distinct.Interface.cs new file mode 100644 index 000000000..a766d6789 --- /dev/null +++ b/MongoDB.Entities/Builders/Distinct.Interface.cs @@ -0,0 +1,25 @@ +namespace MongoDB.Entities; + +public interface IDistinct + where TSelf : IDistinct +{ + /// + /// Specify an option for this find command (use multiple times if needed) + /// + /// x => x.OptionName = OptionValue + public TSelf Option(Action option); + + /// + /// Specify the property you want to get the unique values for (as a string path) + /// + /// ex: "Address.Street" + public TSelf Property(string property); + + /// + /// Specify the property you want to get the unique values for (as a member expression) + /// + /// x => x.Address.Street + public TSelf Property(Expression> property); + + +} diff --git a/MongoDB.Entities/Builders/Distinct.cs b/MongoDB.Entities/Builders/Distinct.cs index f2bffb03f..1b6e59604 100644 --- a/MongoDB.Entities/Builders/Distinct.cs +++ b/MongoDB.Entities/Builders/Distinct.cs @@ -1,67 +1,11 @@ namespace MongoDB.Entities; -public class DistinctBase : FilterQueryBase - where TId : IComparable, IEquatable - where T : IEntity - where TSelf : DistinctBase -{ - internal DistinctOptions _options = new(); - internal FieldDefinition? _field; - - internal DistinctBase(DistinctBase other) : base(other) - { - _options = other._options; - _field = other._field; - } - internal DistinctBase(Dictionary globalFilters) : base(globalFilters) - { - _globalFilters = globalFilters; - } - - - private TSelf This => (TSelf)this; - - - /// - /// Specify an option for this find command (use multiple times if needed) - /// - /// x => x.OptionName = OptionValue - public TSelf Option(Action option) - { - option(_options); - return This; - } - - /// - /// Specify the property you want to get the unique values for (as a string path) - /// - /// ex: "Address.Street" - public TSelf Property(string property) - { - _field = property; - return This; - } - - /// - /// Specify the property you want to get the unique values for (as a member expression) - /// - /// x => x.Address.Street - public TSelf Property(Expression> property) - { - _field = property.FullPath(); - return This; - } -} - /// /// Represents a MongoDB Distinct command where you can get back distinct values for a given property of a given Entity. /// /// Any Entity that implements IEntity interface /// The type of the property of the entity you'd like to get unique values for -/// The type of the id -public class Distinct : DistinctBase>, ICollectionRelated - where TId : IComparable, IEquatable - where T : IEntity +public class Distinct : DistinctBase>, ICollectionRelated { public DBContext Context { get; } public IMongoCollection Collection { get; } @@ -69,7 +13,7 @@ public class Distinct : DistinctBase collection, - DistinctBase> other) : base(other) + DistinctBase> other) : base(other) { Context = context; Collection = collection; diff --git a/MongoDB.Entities/Builders/FilterQuery.Extensions.cs b/MongoDB.Entities/Builders/FilterQuery.Extensions.cs new file mode 100644 index 000000000..b2be6b40b --- /dev/null +++ b/MongoDB.Entities/Builders/FilterQuery.Extensions.cs @@ -0,0 +1,41 @@ +namespace MongoDB.Entities; + +public static class FilterExt +{ + //protected FilterDefinition MergedFilter => Logic.MergeWithGlobalFilter(_ignoreGlobalFilters, _globalFilters, _filter); + /// + /// Specify an IEntity ID as the matching criteria + /// + /// the query + internal static FilterDefinition MergedFilter(this TSelf self) + where TSelf : IFilterBuilder + { + return Logic.MergeWithGlobalFilter(self.IsIgnoreGlobalFilters, self.GlobalFilters, self.Filter); + } + + /// + /// Specify an IEntity ID as the matching criteria + /// + /// the query + /// A unique IEntity ID + public static TSelf MatchID(this TSelf self, TId id) + where TId : IComparable, IEquatable + where TEntity : IEntity + where TSelf : IFilterBuilder + { + return self.Match(f => f.Eq(t => t.ID, id)); + } + + /// + /// Specify an IEntity ID as the matching criteria + /// + /// the query + /// A unique IEntity ID + public static TSelf Match(this TSelf self, TId id) + where TId : IComparable, IEquatable + where TEntity : IEntity + where TSelf : IFilterBuilder + { + return self.MatchID(id); + } +} \ No newline at end of file diff --git a/MongoDB.Entities/Builders/FilterQuery.Interface.cs b/MongoDB.Entities/Builders/FilterQuery.Interface.cs new file mode 100644 index 000000000..ed628ad29 --- /dev/null +++ b/MongoDB.Entities/Builders/FilterQuery.Interface.cs @@ -0,0 +1,78 @@ +namespace MongoDB.Entities; + +public interface IFilterBuilder + where TSelf : IFilterBuilder +{ + internal bool IsIgnoreGlobalFilters { get; } + internal FilterDefinition Filter { get; } + internal Dictionary GlobalFilters { get; } + + /// + /// Specify that this operation should ignore any global filters + /// + TSelf IgnoreGlobalFilters(bool IgnoreGlobalFilters = true); + + /// + /// Specify the matching criteria with a lambda expression + /// + /// x => x.Property == Value + TSelf Match(Expression> expression); + + /// + /// Specify the matching criteria with a filter expression + /// + /// f => f.Eq(x => x.Prop, Value) & f.Gt(x => x.Prop, Value) + TSelf Match(Func, FilterDefinition> filter); + + /// + /// Specify the matching criteria with a filter definition + /// + /// A filter definition + TSelf Match(FilterDefinition filterDefinition); + + /// + /// Specify the matching criteria with a template + /// + /// A Template with a find query + TSelf Match(Template template); + + /// + /// Specify a search term to find results from the text index of this particular collection. + /// TIP: Make sure to define a text index with DB.Index<T>() before searching + /// + /// The type of text matching to do + /// The search term + /// Case sensitivity of the search (optional) + /// Diacritic sensitivity of the search (optional) + /// The language for the search (optional) + TSelf Match(Search searchType, string searchTerm, bool caseSensitive = false, bool diacriticSensitive = false, string? language = null); + + /// + /// Specify criteria for matching entities based on GeoSpatial data (longitude & latitude) + /// TIP: Make sure to define a Geo2DSphere index with DB.Index<T>() before searching + /// Note: DB.FluentGeoNear() supports more advanced options + /// + /// The property where 2DCoordinates are stored + /// The search point + /// Maximum distance in meters from the search point + /// Minimum distance in meters from the search point + TSelf Match(Expression> coordinatesProperty, Coordinates2D nearCoordinates, double? maxDistance = null, double? minDistance = null); + + /// + /// Specify the matching criteria with an aggregation expression (i.e. $expr) + /// + /// { $gt: ['$Property1', '$Property2'] } + TSelf MatchExpression(string expression); + + /// + /// Specify the matching criteria with a Template + /// + /// A Template object + TSelf MatchExpression(Template template); + + /// + /// Specify the matching criteria with a JSON string + /// + /// { Title : 'The Power Of Now' } + TSelf MatchString(string jsonString); +} diff --git a/MongoDB.Entities/Builders/FilterQuery.cs b/MongoDB.Entities/Builders/FilterQuery.cs new file mode 100644 index 000000000..e99552848 --- /dev/null +++ b/MongoDB.Entities/Builders/FilterQuery.cs @@ -0,0 +1,104 @@ +namespace MongoDB.Entities; + + +public abstract class FilterQueryBase : IFilterBuilder + where TSelf : FilterQueryBase + +{ + internal FilterDefinition _filter = Builders.Filter.Empty; + internal Dictionary _globalFilters; + internal bool _ignoreGlobalFilters; + + internal FilterQueryBase(IFilterBuilder other) : this(globalFilters: other.GlobalFilters) + { + _filter = other.Filter; + _ignoreGlobalFilters = other.IsIgnoreGlobalFilters; + } + + internal FilterQueryBase(Dictionary globalFilters) + { + _globalFilters = globalFilters; + } + + bool IFilterBuilder.IsIgnoreGlobalFilters => _ignoreGlobalFilters; + FilterDefinition IFilterBuilder.Filter => _filter; + Dictionary IFilterBuilder.GlobalFilters => _globalFilters; + + private TSelf This => (TSelf)this; + + /// + /// Specify that this operation should ignore any global filters + /// + public TSelf IgnoreGlobalFilters(bool IgnoreGlobalFilters = true) + { + _ignoreGlobalFilters = IgnoreGlobalFilters; + return This; + } + + public TSelf Match(Expression> expression) + { + return Match(f => f.Where(expression)); + } + + public TSelf Match(Func, FilterDefinition> filter) + { + _filter &= filter(Builders.Filter); + return This; + } + + public TSelf Match(FilterDefinition filterDefinition) + { + _filter &= filterDefinition; + return This; + } + + public TSelf Match(Template template) + { + _filter &= template.RenderToString(); + return This; + } + + public TSelf Match(Search searchType, string searchTerm, bool caseSensitive = false, bool diacriticSensitive = false, string? language = null) + { + if (searchType == Search.Fuzzy) + { + searchTerm = searchTerm.ToDoubleMetaphoneHash(); + caseSensitive = false; + diacriticSensitive = false; + language = null; + } + + return Match( + f => f.Text( + searchTerm, + new TextSearchOptions + { + CaseSensitive = caseSensitive, + DiacriticSensitive = diacriticSensitive, + Language = language + })); + } + + public TSelf Match(Expression> coordinatesProperty, Coordinates2D nearCoordinates, double? maxDistance = null, double? minDistance = null) + { + return Match(f => f.Near(coordinatesProperty, nearCoordinates.ToGeoJsonPoint(), maxDistance, minDistance)); + } + + public TSelf MatchExpression(string expression) + { + _filter &= "{$expr:" + expression + "}"; + return This; + } + + public TSelf MatchExpression(Template template) + { + return MatchExpression(template.RenderToString()); + } + + + public TSelf MatchString(string jsonString) + { + _filter &= jsonString; + return This; + } +} diff --git a/MongoDB.Entities/Builders/FilterQueryBase.cs b/MongoDB.Entities/Builders/FilterQueryBase.cs deleted file mode 100644 index eaa32de9a..000000000 --- a/MongoDB.Entities/Builders/FilterQueryBase.cs +++ /dev/null @@ -1,167 +0,0 @@ -namespace MongoDB.Entities; - -public abstract class FilterQueryBase - where TId : IComparable, IEquatable - where T : IEntity - where TSelf : FilterQueryBase - -{ - internal FilterDefinition _filter = Builders.Filter.Empty; - internal Dictionary _globalFilters; - internal bool _ignoreGlobalFilters; - - internal FilterQueryBase(FilterQueryBase other) : this(globalFilters: other._globalFilters) - { - _filter = other._filter; - _ignoreGlobalFilters = other._ignoreGlobalFilters; - } - internal FilterQueryBase(Dictionary globalFilters) - { - _globalFilters = globalFilters; - } - - protected FilterDefinition MergedFilter => Logic.MergeWithGlobalFilter(_ignoreGlobalFilters, _globalFilters, _filter); - private TSelf This => (TSelf)this; - - - /// - /// Specify that this operation should ignore any global filters - /// - public TSelf IgnoreGlobalFilters() - { - _ignoreGlobalFilters = true; - return This; - } - - - - /// - /// Specify an IEntity ID as the matching criteria - /// - /// A unique IEntity ID - public TSelf Match(TId ID) - { - return Match(f => f.Eq(t => t.ID, ID)); - } - - /// - /// Specify the matching criteria with a lambda expression - /// - /// x => x.Property == Value - public TSelf Match(Expression> expression) - { - return Match(f => f.Where(expression)); - } - - /// - /// Specify the matching criteria with a filter expression - /// - /// f => f.Eq(x => x.Prop, Value) & f.Gt(x => x.Prop, Value) - public TSelf Match(Func, FilterDefinition> filter) - { - _filter &= filter(Builders.Filter); - return This; - } - - /// - /// Specify the matching criteria with a filter definition - /// - /// A filter definition - public TSelf Match(FilterDefinition filterDefinition) - { - _filter &= filterDefinition; - return This; - } - - /// - /// Specify the matching criteria with a template - /// - /// A Template with a find query - public TSelf Match(Template template) - { - _filter &= template.RenderToString(); - return This; - } - - /// - /// Specify a search term to find results from the text index of this particular collection. - /// TIP: Make sure to define a text index with DB.Index<T>() before searching - /// - /// The type of text matching to do - /// The search term - /// Case sensitivity of the search (optional) - /// Diacritic sensitivity of the search (optional) - /// The language for the search (optional) - public TSelf Match(Search searchType, string searchTerm, bool caseSensitive = false, bool diacriticSensitive = false, string? language = null) - { - if (searchType == Search.Fuzzy) - { - searchTerm = searchTerm.ToDoubleMetaphoneHash(); - caseSensitive = false; - diacriticSensitive = false; - language = null; - } - - return Match( - f => f.Text( - searchTerm, - new TextSearchOptions - { - CaseSensitive = caseSensitive, - DiacriticSensitive = diacriticSensitive, - Language = language - })); - } - - /// - /// Specify criteria for matching entities based on GeoSpatial data (longitude & latitude) - /// TIP: Make sure to define a Geo2DSphere index with DB.Index<T>() before searching - /// Note: DB.FluentGeoNear() supports more advanced options - /// - /// The property where 2DCoordinates are stored - /// The search point - /// Maximum distance in meters from the search point - /// Minimum distance in meters from the search point - public TSelf Match(Expression> coordinatesProperty, Coordinates2D nearCoordinates, double? maxDistance = null, double? minDistance = null) - { - return Match(f => f.Near(coordinatesProperty, nearCoordinates.ToGeoJsonPoint(), maxDistance, minDistance)); - } - - /// - /// Specify the matching criteria with an aggregation expression (i.e. $expr) - /// - /// { $gt: ['$Property1', '$Property2'] } - public TSelf MatchExpression(string expression) - { - _filter &= "{$expr:" + expression + "}"; - return This; - } - - /// - /// Specify the matching criteria with a Template - /// - /// A Template object - public TSelf MatchExpression(Template template) - { - return MatchExpression(template.RenderToString()); - } - - /// - /// Specify an IEntity ID as the matching criteria - /// - /// A unique IEntity ID - public TSelf MatchID(TId ID) - { - return Match(f => f.Eq(t => t.ID, ID)); - } - - /// - /// Specify the matching criteria with a JSON string - /// - /// { Title : 'The Power Of Now' } - public TSelf MatchString(string jsonString) - { - _filter &= jsonString; - return This; - } -} diff --git a/MongoDB.Entities/Builders/Find.Base.cs b/MongoDB.Entities/Builders/Find.Base.cs new file mode 100644 index 000000000..703a38675 --- /dev/null +++ b/MongoDB.Entities/Builders/Find.Base.cs @@ -0,0 +1,114 @@ +namespace MongoDB.Entities; + +public abstract class FindBase : + SortFilterQueryBase, IFindBuilder + where TSelf : FindBase +{ + internal FindOptions _options = new(); + + internal FindBase(FindBase other) : base(other) + { + _options = other._options; + } + internal FindBase(Dictionary globalFilters) : base(globalFilters: globalFilters) + { + _globalFilters = globalFilters; + } + public abstract DBContext Context { get; } + private TSelf This => (TSelf)this; + + + public TSelf Skip(int skipCount) + { + _options.Skip = skipCount; + return This; + } + + + public TSelf Limit(int takeCount) + { + _options.Limit = takeCount; + return This; + } + + + public TSelf Project(Expression> expression) + { + return Project(p => p.Expression(expression)); + } + + public TSelf Project(Func, ProjectionDefinition> projection) + { + _options.Projection = projection(Builders.Projection); + return This; + } + + + public TSelf IncludeRequiredProps() + { + if (typeof(T) != typeof(TProjection)) + throw new InvalidOperationException("IncludeRequiredProps() cannot be used when projecting to a different type."); + + _options.Projection = Context.Cache().CombineWithRequiredProps(_options.Projection); + return This; + } + + + public TSelf ProjectExcluding(Expression> exclusion) + { + var props = (exclusion.Body as NewExpression)?.Arguments + .Select(a => a.ToString().Split('.')[1]); + + if (props == null || !props.Any()) + throw new ArgumentException("Unable to get any properties from the exclusion expression!"); + + var defs = new List>(props.Count()); + + foreach (var prop in props) + { + defs.Add(Builders.Projection.Exclude(prop)); + } + + _options.Projection = Builders.Projection.Combine(defs); + + return This; + } + + + public TSelf Option(Action> option) + { + option(_options); + return This; + } + + private void AddTxtScoreToProjection(string propName) + { + if (_options.Projection == null) _options.Projection = "{}"; + + _options.Projection = + _options.Projection + .Render(BsonSerializer.SerializerRegistry.GetSerializer(), BsonSerializer.SerializerRegistry) + .Document.Add(propName, new BsonDocument { { "$meta", "textScore" } }); + } + + + public TSelf SortByTextScore() + { + return SortByTextScore(null); + } + + + public TSelf SortByTextScore(Expression>? scoreProperty) + { + switch (scoreProperty) + { + case null: + AddTxtScoreToProjection("_Text_Match_Score_"); + return Sort(s => s.MetaTextScore("_Text_Match_Score_")); + + default: + AddTxtScoreToProjection(Prop.Path(scoreProperty)); + return Sort(s => s.MetaTextScore(Prop.Path(scoreProperty))); + } + } +} diff --git a/MongoDB.Entities/Builders/Find.Interface.cs b/MongoDB.Entities/Builders/Find.Interface.cs new file mode 100644 index 000000000..72996d442 --- /dev/null +++ b/MongoDB.Entities/Builders/Find.Interface.cs @@ -0,0 +1,68 @@ + +namespace MongoDB.Entities +{ + public interface IProjectionBuilder + { + ///// + ///// Specify to automatically include all properties marked with [BsonRequired] attribute on the entity in the final projection. + ///// HINT: this method should only be called after the .Project() method. + ///// + //TSelf IncludeRequiredProps(); + + /// + /// Specify how to project the results using a lambda expression + /// + /// x => new Test { PropName = x.Prop } + TSelf Project(Expression> expression); + + /// + /// Specify how to project the results using a projection expression + /// + /// p => p.Include("Prop1").Exclude("Prop2") + TSelf Project(Func, ProjectionDefinition> projection); + + /// + /// Specify how to project the results using an exclusion projection expression. + /// + /// x => new { x.PropToExclude, x.AnotherPropToExclude } + TSelf ProjectExcluding(Expression> exclusion); + + + } + public interface IFindBuilder + where TSelf : IFindBuilder + { + + + /// + /// Specify how many entities to Take/Limit + /// + /// The number to limit/take + TSelf Limit(int takeCount); + + /// + /// Specify an option for this find command (use multiple times if needed) + /// + /// x => x.OptionName = OptionValue + TSelf Option(Action> option); + + /// + /// Specify how many entities to skip + /// + /// The number to skip + TSelf Skip(int skipCount); + + /// + /// Sort the results of a text search by the MetaTextScore + /// TIP: Use this method after .Project() if you need to do a projection also + /// + TSelf SortByTextScore(); + + /// + /// Sort the results of a text search by the MetaTextScore and get back the score as well + /// TIP: Use this method after .Project() if you need to do a projection also + /// + /// x => x.TextScoreProp + TSelf SortByTextScore(Expression>? scoreProperty); + } +} \ No newline at end of file diff --git a/MongoDB.Entities/Builders/Find.cs b/MongoDB.Entities/Builders/Find.cs index f77d73f67..261ee0ccc 100644 --- a/MongoDB.Entities/Builders/Find.cs +++ b/MongoDB.Entities/Builders/Find.cs @@ -1,163 +1,17 @@ namespace MongoDB.Entities; -public abstract class FindBase : SortFilterQueryBase - where TId : IComparable, IEquatable - where T : IEntity - where TSelf : FindBase -{ - internal FindOptions _options = new(); - - internal FindBase(FindBase other) : base(other) - { - _options = other._options; - } - internal FindBase(Dictionary globalFilters) : base(globalFilters: globalFilters) - { - _globalFilters = globalFilters; - } - public abstract DBContext Context { get; } - private TSelf This => (TSelf)this; - - /// - /// Specify how many entities to skip - /// - /// The number to skip - public TSelf Skip(int skipCount) - { - _options.Skip = skipCount; - return This; - } - - /// - /// Specify how many entities to Take/Limit - /// - /// The number to limit/take - public TSelf Limit(int takeCount) - { - _options.Limit = takeCount; - return This; - } - - /// - /// Specify how to project the results using a lambda expression - /// - /// x => new Test { PropName = x.Prop } - public TSelf Project(Expression> expression) - { - return Project(p => p.Expression(expression)); - } - - /// - /// Specify how to project the results using a projection expression - /// - /// p => p.Include("Prop1").Exclude("Prop2") - public TSelf Project(Func, ProjectionDefinition> projection) - { - _options.Projection = projection(Builders.Projection); - return This; - } - - /// - /// Specify to automatically include all properties marked with [BsonRequired] attribute on the entity in the final projection. - /// HINT: this method should only be called after the .Project() method. - /// - public TSelf IncludeRequiredProps() - { - if (typeof(T) != typeof(TProjection)) - throw new InvalidOperationException("IncludeRequiredProps() cannot be used when projecting to a different type."); - - _options.Projection = Context.Cache().CombineWithRequiredProps(_options.Projection); - return This; - } - - /// - /// Specify how to project the results using an exclusion projection expression. - /// - /// x => new { x.PropToExclude, x.AnotherPropToExclude } - public TSelf ProjectExcluding(Expression> exclusion) - { - var props = (exclusion.Body as NewExpression)?.Arguments - .Select(a => a.ToString().Split('.')[1]); - - if (props == null || !props.Any()) - throw new ArgumentException("Unable to get any properties from the exclusion expression!"); - - var defs = new List>(props.Count()); - - foreach (var prop in props) - { - defs.Add(Builders.Projection.Exclude(prop)); - } - - _options.Projection = Builders.Projection.Combine(defs); - - return This; - } - - /// - /// Specify an option for this find command (use multiple times if needed) - /// - /// x => x.OptionName = OptionValue - public TSelf Option(Action> option) - { - option(_options); - return This; - } - - private void AddTxtScoreToProjection(string propName) - { - if (_options.Projection == null) _options.Projection = "{}"; - - _options.Projection = - _options.Projection - .Render(BsonSerializer.SerializerRegistry.GetSerializer(), BsonSerializer.SerializerRegistry) - .Document.Add(propName, new BsonDocument { { "$meta", "textScore" } }); - } - - /// - /// Sort the results of a text search by the MetaTextScore - /// TIP: Use this method after .Project() if you need to do a projection also - /// - public TSelf SortByTextScore() - { - return SortByTextScore(null); - } - - /// - /// Sort the results of a text search by the MetaTextScore and get back the score as well - /// TIP: Use this method after .Project() if you need to do a projection also - /// - /// x => x.TextScoreProp - public TSelf SortByTextScore(Expression>? scoreProperty) - { - switch (scoreProperty) - { - case null: - AddTxtScoreToProjection("_Text_Match_Score_"); - return Sort(s => s.MetaTextScore("_Text_Match_Score_")); - - default: - AddTxtScoreToProjection(Prop.Path(scoreProperty)); - return Sort(s => s.MetaTextScore(Prop.Path(scoreProperty))); - } - } -} - /// /// Represents a MongoDB Find command. /// TIP: Specify your criteria using .Match() .Sort() .Skip() .Take() .Project() .Option() methods and finally call .Execute() /// Note: For building queries, use the DB.Fluent* interfaces /// /// Any class that implements IEntity -/// Id type -public class Find : Find - where TId : IComparable, IEquatable - where T : IEntity +public class Find : Find { internal Find(DBContext context, IMongoCollection collection) : base(context, collection) { } - internal Find(DBContext context, IMongoCollection collection, FindBase> baseQuery) + internal Find(DBContext context, IMongoCollection collection, FindBase> baseQuery) : base(context, collection, baseQuery) { } } @@ -167,11 +21,9 @@ internal Find(DBContext context, IMongoCollection collection, FindBaseTIP: Specify your criteria using .Match() .Sort() .Skip() .Take() .Project() .Option() methods and finally call .Execute() /// /// Any class that implements IEntity -/// ID type /// The type you'd like to project the results to. -public class Find : FindBase>, ICollectionRelated - where TId : IComparable, IEquatable - where T : IEntity +public class Find : + FindBase>, ICollectionRelated { /// @@ -180,7 +32,7 @@ public class Find : FindBase /// /// - internal Find(DBContext context, IMongoCollection collection, FindBase> other) : base(other) + internal Find(DBContext context, IMongoCollection collection, FindBase> other) : base(other) { Context = context; Collection = collection; @@ -196,17 +48,7 @@ internal Find(DBContext context, IMongoCollection collection) : base(context. - /// - /// Find a single IEntity by ID - /// - /// The unique ID of an IEntity - /// An optional cancellation token - /// A single entity or null if not found - public Task OneAsync(TId ID, CancellationToken cancellation = default) - { - Match(ID); - return ExecuteSingleAsync(cancellation); - } + /// /// Find entities by supplying a lambda expression @@ -274,18 +116,7 @@ public async Task ExecuteFirstAsync(CancellationToken cancellation return cursor.Current.SingleOrDefault(); //because we're limiting to 1 } - /// - /// Run the Find command and get back a bool indicating whether any entities matched the query - /// - /// An optional cancellation token - public async Task ExecuteAnyAsync(CancellationToken cancellation = default) - { - Project(b => b.Include(x => x.ID)); - Limit(1); - using var cursor = await ExecuteCursorAsync(cancellation).ConfigureAwait(false); - await cursor.MoveNextAsync(cancellation).ConfigureAwait(false); - return cursor.Current.Any(); - } + /// /// Run the Find command in MongoDB server and get a cursor instead of materialized results @@ -302,8 +133,44 @@ public Task> ExecuteCursorAsync(CancellationToken canc Collection.FindAsync(mergedFilter, _options, cancellation) : Collection.FindAsync(session, mergedFilter, _options, cancellation); } + + /// + /// Run the Find command and get back a bool indicating whether any entities matched the query + /// + /// An optional cancellation token + public async Task ExecuteAnyAsync(CancellationToken cancellation = default) + { + if (Context.Cache().IsEntity) + { + Project(b => b.Include(nameof(IEntity.ID))); + } + Limit(1); + using var cursor = await ExecuteCursorAsync(cancellation).ConfigureAwait(false); + await cursor.MoveNextAsync(cancellation).ConfigureAwait(false); + return cursor.Current.Any(); + } + + } +public static class FindExt +{ + /// + /// Find a single IEntity by ID + /// + /// + /// The unique ID of an IEntity + /// An optional cancellation token + /// A single entity or null if not found + public static Task OneAsync(this TSelf self, TId ID, CancellationToken cancellation = default) + where TId : IComparable, IEquatable + where TEntity : IEntity + where TSelf : Find, IFilterBuilder + { + self.MatchID(ID); + return self.ExecuteSingleAsync(cancellation); + } +} public enum Order { Ascending, diff --git a/MongoDB.Entities/Builders/ICollectionRelated.cs b/MongoDB.Entities/Builders/ICollectionRelated.cs index 4a31b2ce5..3b28b998c 100644 --- a/MongoDB.Entities/Builders/ICollectionRelated.cs +++ b/MongoDB.Entities/Builders/ICollectionRelated.cs @@ -7,6 +7,7 @@ internal interface ICollectionRelated } internal static class ICollectionRelatedExt { + public static EntityCache Cache(this ICollectionRelated c) => c.Context.Cache(); public static IClientSessionHandle? Session(this ICollectionRelated collectionRelated) { return collectionRelated.Context.Session; diff --git a/MongoDB.Entities/Builders/Index.Interface.cs b/MongoDB.Entities/Builders/Index.Interface.cs new file mode 100644 index 000000000..32681a425 --- /dev/null +++ b/MongoDB.Entities/Builders/Index.Interface.cs @@ -0,0 +1,22 @@ + +namespace MongoDB.Entities +{ + public interface IIndexBuilder + where TSelf : IIndexBuilder + { + /// + /// Adds a key definition to the index + /// TIP: At least one key definition is required + /// + /// x => x.PropertyName + /// The type of the key + TSelf Key(Expression> propertyToIndex, KeyType type); + + /// + /// Set the options for this index definition + /// TIP: Setting options is not required. + /// + /// x => x.OptionName = OptionValue + TSelf Option(Action> option); + } +} \ No newline at end of file diff --git a/MongoDB.Entities/Builders/Index.cs b/MongoDB.Entities/Builders/Index.cs index 0e1fbf1ca..67610a180 100644 --- a/MongoDB.Entities/Builders/Index.cs +++ b/MongoDB.Entities/Builders/Index.cs @@ -4,13 +4,10 @@ /// Represents an index creation command /// TIP: Define the keys first with .Key() method and finally call the .Create() method. /// -/// Any class that implements IEntity -/// Id type -public class Index : ICollectionRelated - where TId : IComparable, IEquatable - where T : IEntity +/// Any class +public class Index : ICollectionRelated, IIndexBuilder> { - internal List> Keys { get; set; } = new(); + internal List> Keys { get; set; } = new(); public DBContext Context { get; } public IMongoCollection Collection { get; } @@ -98,26 +95,17 @@ public async Task CreateAsync(CancellationToken cancellation = default) return _options.Name; } - /// - /// Set the options for this index definition - /// TIP: Setting options is not required. - /// - /// x => x.OptionName = OptionValue - public Index Option(Action> option) + + public Index Option(Action> option) { option(_options); return this; } - /// - /// Adds a key definition to the index - /// TIP: At least one key definition is required - /// - /// x => x.PropertyName - /// The type of the key - public Index Key(Expression> propertyToIndex, KeyType type) + + public Index Key(Expression> propertyToIndex, KeyType type) { - Keys.Add(new Key(propertyToIndex, type)); + Keys.Add(new Key(propertyToIndex, type)); return this; } @@ -146,9 +134,7 @@ private Task CreateAsync(CreateIndexModel model, CancellationToken cancellati } } -internal class Key - where TId : IComparable, IEquatable - where T : IEntity +internal class Key { internal string PropertyName { get; set; } internal KeyType Type { get; set; } diff --git a/MongoDB.Entities/Builders/PagedSearch.cs b/MongoDB.Entities/Builders/PagedSearch.cs index 73a7f0bb5..99f4941c7 100644 --- a/MongoDB.Entities/Builders/PagedSearch.cs +++ b/MongoDB.Entities/Builders/PagedSearch.cs @@ -1,11 +1,14 @@ namespace MongoDB.Entities; -public class PagedSearchBase : SortFilterQueryBase - where TId : IComparable, IEquatable - where T : IEntity - where TSelf : PagedSearchBase +public interface IPagedSearchBuilder : IProjectionBuilder + where TSelf : IPagedSearchBuilder { - internal PagedSearchBase(PagedSearchBase other) : base(other) + +} +public abstract class PagedSearchBase : SortFilterQueryBase, IPagedSearchBuilder + where TSelf : PagedSearchBase +{ + internal PagedSearchBase(PagedSearchBase other) : base(other) { } @@ -163,10 +166,7 @@ public TSelf Option(Action option) /// Represents an aggregation query that retrieves results with easy paging support. /// /// Any class that implements IEntity -/// Id type -public class PagedSearch : PagedSearch - where TId : IComparable, IEquatable - where T : IEntity +public class PagedSearch : PagedSearch { internal PagedSearch( DBContext context, IMongoCollection collection) @@ -177,11 +177,8 @@ internal PagedSearch( /// Represents an aggregation query that retrieves results with easy paging support. /// /// Any class that implements IEntity -/// Id type /// The type you'd like to project the results to. -public class PagedSearch : PagedSearchBase> - where TId : IComparable, IEquatable - where T : IEntity +public class PagedSearch : PagedSearchBase>, ICollectionRelated { public DBContext Context { get; set; } @@ -197,6 +194,11 @@ internal PagedSearch(DBContext context, IMongoCollection collection) : base(c } + public PagedSearch IncludeRequiredProps() + { + _projectionStage = PipelineStageDefinitionBuilder.Project(this.Cache().CombineWithRequiredProps(_projectionStage?.ToString())); + return this; + } /// /// Run the aggregation search command in MongoDB server and get a page of results and total + page count diff --git a/MongoDB.Entities/Builders/Replace.cs b/MongoDB.Entities/Builders/Replace.cs index a47d203d0..fa9b34e73 100644 --- a/MongoDB.Entities/Builders/Replace.cs +++ b/MongoDB.Entities/Builders/Replace.cs @@ -6,7 +6,7 @@ /// /// Any class that implements IEntity /// ID type -public class Replace : FilterQueryBase>, ICollectionRelated +public class Replace : FilterQueryBase>, ICollectionRelated where TId : IComparable, IEquatable where T : IEntity { diff --git a/MongoDB.Entities/Builders/SortFilterQueryBase.Interface.cs b/MongoDB.Entities/Builders/SortFilterQueryBase.Interface.cs new file mode 100644 index 000000000..a3d6c8b14 --- /dev/null +++ b/MongoDB.Entities/Builders/SortFilterQueryBase.Interface.cs @@ -0,0 +1,20 @@ +namespace MongoDB.Entities; + +public interface ISortBuilder +{ + internal List> Sorts { get; } + + /// + /// Specify which property and order to use for sorting (use multiple times if needed) + /// + /// x => x.Prop + /// The sort order + public TSelf Sort(Expression> propertyToSortBy, Order sortOrder); + + /// + /// Specify how to sort using a sort expression + /// + /// s => s.Ascending("Prop1").MetaTextScore("Prop2") + /// + public TSelf Sort(Func, SortDefinition> sortFunction); +} diff --git a/MongoDB.Entities/Builders/SortFilterQueryBase.cs b/MongoDB.Entities/Builders/SortFilterQueryBase.cs index 0a1e97d64..44ab11a56 100644 --- a/MongoDB.Entities/Builders/SortFilterQueryBase.cs +++ b/MongoDB.Entities/Builders/SortFilterQueryBase.cs @@ -1,14 +1,14 @@ namespace MongoDB.Entities; - -public abstract class SortFilterQueryBase : FilterQueryBase - where TId : IComparable, IEquatable - where T : IEntity - where TSelf : SortFilterQueryBase +public abstract class SortFilterQueryBase : FilterQueryBase, + ISortBuilder + where TSelf : SortFilterQueryBase { internal List> _sorts = new(); private TSelf This => (TSelf)this; - internal SortFilterQueryBase(SortFilterQueryBase other) : base(other) + List> ISortBuilder.Sorts => _sorts; + + internal SortFilterQueryBase(SortFilterQueryBase other) : base(other) { _sorts = other._sorts; } @@ -42,4 +42,6 @@ public TSelf Sort(Func, SortDefinition> sortFunction _sorts.Add(sortFunction(Builders.Sort)); return This; } + + } diff --git a/MongoDB.Entities/Core/EntityCache.cs b/MongoDB.Entities/Core/EntityCache.cs index d230f4f2b..7b1d59987 100644 --- a/MongoDB.Entities/Core/EntityCache.cs +++ b/MongoDB.Entities/Core/EntityCache.cs @@ -13,6 +13,8 @@ internal class Cache public bool HasIgnoreIfDefaultProps { get; protected set; } public string CollectionName { get; protected set; } = null!; public bool IsFileEntity { get; protected set; } + public bool IsEntity { get; protected set; } + public Type? IdType { get; set; } protected Cache(Type type) { var interfaces = type.GetInterfaces(); @@ -24,6 +26,9 @@ protected Cache(Type type) if (string.IsNullOrWhiteSpace(CollectionName) || CollectionName.Contains("~")) throw new ArgumentException($"{CollectionName} is an illegal name for a collection!"); + var ientityType = interfaces.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IEntity<>)); + IsEntity = ientityType != null; + IdType = ientityType?.GenericTypeArguments.First(); HasCreatedOn = interfaces.Any(i => i == typeof(ICreatedOn)); HasModifiedOn = interfaces.Any(i => i == typeof(IModifiedOn)); @@ -86,7 +91,10 @@ public ProjectionDefinition CombineWithRequiredProps(Expression> expression) + private static void ThrowIfInvalid(Expression> expression) { if (expression == null) throw new ArgumentNullException(nameof(expression), "The supplied expression is null!"); @@ -35,7 +35,7 @@ private static void ThrowIfInvalid(Expression> expression) throw new ArgumentException("Cannot generate property path from lambda parameter!"); } - private static string GetPath(Expression> expression) + private static string GetPath(Expression> expression) { ThrowIfInvalid(expression); @@ -70,7 +70,7 @@ public static string Property(Expression> expression) /// EX: Authors[0].Books[0].Title > Authors.Books.Title /// /// x => x.SomeList[0].SomeProp - public static string Path(Expression> expression) + public static string Path(Expression> expression) { return _rxThree.Replace(GetPath(expression), ""); } @@ -83,7 +83,7 @@ public static string Path(Expression> expression) /// TIP: Index positions start from [0] which is converted to $[a] and so on. /// /// x => x.SomeList[0].SomeProp - public static string PosFiltered(Expression> expression) + public static string PosFiltered(Expression> expression) { return _rxFour.Replace( GetPath(expression), @@ -95,7 +95,7 @@ public static string PosFiltered(Expression> expression) /// EX: Authors[0].Name > Authors.$[].Name /// /// x => x.SomeList[0].SomeProp - public static string PosAll(Expression> expression) + public static string PosAll(Expression> expression) { return _rxThree.Replace(GetPath(expression), ".$[]"); } @@ -105,7 +105,7 @@ public static string PosAll(Expression> expression) /// EX: Authors[0].Name > Authors.$.Name /// /// x => x.SomeList[0].SomeProp - public static string PosFirst(Expression> expression) + public static string PosFirst(Expression> expression) { return _rxThree.Replace(GetPath(expression), ".$"); } @@ -115,7 +115,7 @@ public static string PosFirst(Expression> expression) /// EX: b => b.Tags > Tags /// /// x => x.SomeProp - public static string Elements(Expression> expression) + public static string Elements(Expression> expression) { return Path(expression); } @@ -128,7 +128,7 @@ public static string Elements(Expression> expression) /// /// 0=a 1=b 2=c 3=d and so on... /// x => x.SomeProp - public static string Elements(int index, Expression> expression) + public static string Elements(int index, Expression> expression) { return $"{ToLowerCaseLetter(index)}.{Path(expression)}"; } diff --git a/MongoDB.Entities/DBContext/DBContext.Find.cs b/MongoDB.Entities/DBContext/DBContext.Find.cs index e8cedbc27..3eac92376 100644 --- a/MongoDB.Entities/DBContext/DBContext.Find.cs +++ b/MongoDB.Entities/DBContext/DBContext.Find.cs @@ -8,24 +8,18 @@ public partial class DBContext /// Starts a find command for the given entity type /// /// The type of entity - /// ID type - public Find Find(string? collectionName = null, IMongoCollection? collection = null) - where TId : IComparable, IEquatable - where T : IEntity + public Find Find(string? collectionName = null, IMongoCollection? collection = null) { - return new Find(this, Collection(collectionName, collection)); + return new Find(this, Collection(collectionName, collection)); } /// /// Starts a find command with projection support for the given entity type /// - /// The type of entity - /// ID type + /// The type of entity /// The type of the end result - public Find Find(string? collectionName = null, IMongoCollection? collection = null) - where TId : IComparable, IEquatable - where T : IEntity + public Find Find(string? collectionName = null, IMongoCollection? collection = null) { - return new Find(this, Collection(collectionName, collection)); + return new Find(this, Collection(collectionName, collection)); } } diff --git a/MongoDB.Entities/Extensions/Entity.cs b/MongoDB.Entities/Extensions/Entity.cs index 388b0dc09..4c26ec4e2 100644 --- a/MongoDB.Entities/Extensions/Entity.cs +++ b/MongoDB.Entities/Extensions/Entity.cs @@ -35,6 +35,7 @@ internal static void ThrowIfUnsaved(this TId? id) where TId : IComparable.Default.Equals(id, default)) throw new InvalidOperationException("Please save the entity before performing this operation!"); } + internal static void ThrowIfUnsaved(this IEntity entity) where TId : IComparable, IEquatable { ThrowIfUnsaved(entity.ID); @@ -66,8 +67,9 @@ public static IEnumerable> ToBatches(this IEnumerable colle /// /// Returns the full dotted path of a property for the given expression /// - /// Any class that implements IEntity - public static string FullPath(this Expression> expression) + /// Any class + /// Property type + public static string FullPath(this Expression> expression) { return Prop.Path(expression); } @@ -75,7 +77,7 @@ public static string FullPath(this Expression> expression) /// /// An IQueryable collection of sibling Entities. /// - public static IMongoQueryable Queryable(this T _, AggregateOptions? options = null, string tenantPrefix = null) where T : IEntity + public static IMongoQueryable Queryable(this T _, AggregateOptions? options = null, string? tenantPrefix = null) where T : IEntity { return DB.Queryable(options); } diff --git a/MongoDB.Entities/GlobalUsing.cs b/MongoDB.Entities/GlobalUsing.cs index 31b77f63a..a343aca18 100644 --- a/MongoDB.Entities/GlobalUsing.cs +++ b/MongoDB.Entities/GlobalUsing.cs @@ -6,3 +6,5 @@ global using MongoDB.Bson.Serialization.Serializers; global using MongoDB.Driver.Linq; global using System.Reflection; + +global using System.Diagnostics.CodeAnalysis; diff --git a/MongoDB.Entities/MongoDB.Entities.csproj b/MongoDB.Entities/MongoDB.Entities.csproj index 50395ab3c..02926f487 100644 --- a/MongoDB.Entities/MongoDB.Entities.csproj +++ b/MongoDB.Entities/MongoDB.Entities.csproj @@ -11,7 +11,7 @@ - upgrade dependencies - netstandard2.0 + netstandard2.1 MongoDB.Entities MongoDB.Entities Đĵ ΝιΓΞΗΛψΚ diff --git a/MongoDB.Entities/Relationships/NewMany.cs b/MongoDB.Entities/Relationships/NewMany.cs index b757feb51..dc4a91654 100644 --- a/MongoDB.Entities/Relationships/NewMany.cs +++ b/MongoDB.Entities/Relationships/NewMany.cs @@ -74,44 +74,25 @@ // public override Guid GenerateNewID() => Guid.NewGuid(); //} -public interface IManyS : IMany - where TChild : IEntity +public interface IMany { - -} -public interface IMany - where TChild : IEntity - where TChildId : IComparable, IEquatable -{ - Find GetChildrenFind(DBContext context, string? childCollectionName = null, IMongoCollection? collection = null); - Find GetChildrenFind(DBContext context, string? childCollectionName = null, IMongoCollection? collection = null); + Find GetChildrenFind(DBContext context, string? childCollectionName = null, IMongoCollection? collection = null); + Find GetChildrenFind(DBContext context, string? childCollectionName = null, IMongoCollection? collection = null); IMongoQueryable GetChildrenQuery(DBContext context, string? childCollectionName = null, IMongoCollection? collection = null); } -public interface IMany : IMany - where TParent : IEntity - where TParentId : IComparable, IEquatable - where TChild : IEntity - where TChildId : IComparable, IEquatable +public interface IMany : IMany { TParent Parent { get; } FilterDefinition GetFilterForSingleDocument(); } -public interface IManyToMany : IMany - where TParent : IEntity - where TParentId : IComparable, IEquatable - where TChild : IEntity - where TChildId : IComparable, IEquatable +public interface IManyToMany : IMany { bool IsParentOwner { get; } } -public interface IManyToOne : IMany - where TParent : IEntity - where TParentId : IComparable, IEquatable - where TChild : IEntity - where TChildId : IComparable, IEquatable +public interface IManyToOne : IMany { } @@ -121,9 +102,7 @@ public interface IManyToOne : IMany /// /// -public abstract class Many : IMany - where TChildId : IComparable, IEquatable - where TChild : IEntity +public abstract class Many : IMany { protected Many(PropertyInfo parentProperty, PropertyInfo childProperty) { @@ -135,14 +114,10 @@ protected Many(PropertyInfo parentProperty, PropertyInfo childProperty) internal PropertyInfo ChildProperty { get; } public abstract IMongoQueryable GetChildrenQuery(DBContext context, string? childCollectionName = null, IMongoCollection? collection = null); - public Find GetChildrenFind(DBContext context, string? childCollectionName = null, IMongoCollection? collection = null) => GetChildrenFind(context, childCollectionName, collection); - public abstract Find GetChildrenFind(DBContext context, string? childCollectionName = null, IMongoCollection? collection = null); + public Find GetChildrenFind(DBContext context, string? childCollectionName = null, IMongoCollection? collection = null) => GetChildrenFind(context, childCollectionName, collection); + public abstract Find GetChildrenFind(DBContext context, string? childCollectionName = null, IMongoCollection? collection = null); } -public abstract class Many : Many, IMany - where TParent : IEntity - where TParentId : IComparable, IEquatable - where TChild : IEntity - where TChildId : IComparable, IEquatable +public abstract class Many : Many, IMany { public TParent Parent { get; } protected Many(TParent parent, PropertyInfo parentProperty, PropertyInfo childProperty) : base(parentProperty, childProperty) @@ -150,15 +125,9 @@ protected Many(TParent parent, PropertyInfo parentProperty, PropertyInfo childPr Parent = parent; } public FilterDefinition GetFilterForSingleDocument() => Builders.Filter.Eq(ChildProperty.Name, Parent.ID); - - } -public sealed class ManyToMany : Many, IManyToMany - where TParent : IEntity - where TParentId : IComparable, IEquatable - where TChild : IEntity - where TChildId : IComparable, IEquatable +public sealed class ManyToMany : Many, IManyToMany { public ManyToMany(bool isParentOwner, TParent parent, PropertyInfo parentProperty, PropertyInfo childProperty) : base(parent, parentProperty, childProperty) { @@ -167,7 +136,7 @@ public ManyToMany(bool isParentOwner, TParent parent, PropertyInfo parentPropert public bool IsParentOwner { get; } - public override Find GetChildrenFind(DBContext context, string? childCollectionName = null, IMongoCollection? collection = null) + public override Find GetChildrenFind(DBContext context, string? childCollectionName = null, IMongoCollection? collection = null) { throw new NotImplementedException(); } @@ -178,19 +147,15 @@ public override IMongoQueryable GetChildrenQuery(DBContext context, stri } } -public sealed class ManyToOne : Many, IManyToOne - where TParent : IEntity - where TParentId : IComparable, IEquatable - where TChild : IEntity - where TChildId : IComparable, IEquatable +public sealed class ManyToOne : Many, IManyToOne { public ManyToOne(TParent parent, PropertyInfo parentProperty, PropertyInfo childProperty) : base(parent, parentProperty, childProperty) { } - public override Find GetChildrenFind(DBContext context, string? childCollectionName = null, IMongoCollection? childCollection = null) + public override Find GetChildrenFind(DBContext context, string? childCollectionName = null, IMongoCollection? childCollection = null) { - return context.Find(childCollectionName, childCollection) + return context.Find(childCollectionName, childCollection) .Match(GetFilterForSingleDocument()); //BRef==Parent.Id } @@ -229,7 +194,7 @@ public static PropertyInfo GetPropertyInfo(this Expression InitManyToMany(this TParent parent, Expression>> propertyExpression, Expression>> propertyOtherSide) + public static IManyToMany InitManyToMany(this TParent parent, Expression>> propertyExpression, Expression>> propertyOtherSide) where TParent : IEntity where TParentId : IComparable, IEquatable where TChild : IEntity @@ -248,13 +213,13 @@ public static IManyToMany InitManyToMany(hasInverseAttrib, parent, property, osProperty); + var res = new ManyToMany(hasInverseAttrib, parent, property, osProperty); //should we set the property ourself or let the user handle it ? //property.SetValue(parent, res); return res; } - public static IManyToOne InitManyToOne(this TParent parent, Expression>> propertyExpression, Expression?>> propertyOtherSide) - where TParent : IEntity + public static IManyToOne InitManyToOne(this TParent parent, Expression>> propertyExpression, Expression?>> propertyOtherSide) + where TParent : IEntity where TParentId : IComparable, IEquatable where TChild : IEntity where TChildId : IComparable, IEquatable @@ -262,6 +227,6 @@ public static IManyToOne InitManyToOne(parent, property, osProperty); + return new ManyToOne(parent, property, osProperty); } } \ No newline at end of file diff --git a/MongoDB.Entities/Relationships/One.cs b/MongoDB.Entities/Relationships/One.cs index 3e9544e3b..df2f4b3fb 100644 --- a/MongoDB.Entities/Relationships/One.cs +++ b/MongoDB.Entities/Relationships/One.cs @@ -1,147 +1,177 @@ using MongoDB.Driver; using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; -namespace MongoDB.Entities +namespace MongoDB.Entities; +public interface IOneRelation { + T? Cache { get; set; } +} +/// +/// Represents a one-to-x relationship. +/// Note: this doesn't get serialized nor store any information, it just marks a related entity +/// +/// Any type +public class One : IOneRelation +{ + [BsonIgnore] + public T? Cache { get; set; } + + public One() + { + } + /// - /// Represents a one-to-one relationship with an IEntity. + /// Initializes a reference to an entity in MongoDB. /// - /// Any type that implements IEntity - /// Any type that implements IEntity - public class One - where TId : IComparable, IEquatable - where T : IEntity + /// The actual entity this reference represents. + internal One(T entity) { - private T? _cache; + Cache = entity; + } - /// - /// The Id of the entity referenced by this instance. - /// - [AsObjectId] - public TId? ID { get; set; } - public T? Cache - { - get => _cache; - set - { - value?.ThrowIfUnsaved(); - _cache = value; - if (value is not null) - { - if (!EqualityComparer.Default.Equals(ID, value.ID)) - { - ID = value.ID!; - } - } - else - { - ID = default; - } - } - } + /// + /// Operator for returning a new MarkerOne<T> object from an entity + /// + /// The entity to make a reference to + public static implicit operator One(T entity) + { + return new One(entity); + } - public One() - { - } + /// + /// Returns a find for the related entity + /// + /// + /// + /// + /// + /// + public Find ToFind(DBContext context, FilterDefinition filter, string? collectionName = null, IMongoCollection? collection = null) + { + return context.Find(collectionName, collection).Match(filter); + } - /// - /// Initializes a reference to an entity in MongoDB. - /// - /// The actual entity this reference represents. - internal One(T entity) - { - Cache = entity; - } + ///// + ///// Fetches the actual entity this reference represents from the database. + ///// + ///// + ///// An optional cancellation token + ///// + ///// + ///// A Task containing the actual entity + //public async Task ToEntityAsync(DBContext context, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) + //{ + // if (ID is null) + // { + // return default; + // } - /// - /// Operator for returning a new One<T> object from a string ID - /// - /// The ID to create a new One<T> with - public static implicit operator One(TId id) - { - return new One() { ID = id }; - } + // return Cache = context.Find(collectionName, collection).OneAsync(ID, cancellation); + //} - /// - /// Operator for returning a new One<T> object from an entity - /// - /// The entity to make a reference to - public static implicit operator One(T entity) - { - return new One(entity); - } + ///// + ///// Fetches the actual entity this reference represents from the database with a projection. + ///// + ///// + ///// x => new Test { PropName = x.Prop } + ///// An optional cancellation token + ///// + ///// + ///// A Task containing the actual projected entity + //public async Task ToEntityAsync(DBContext context, Expression> projection, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) + //{ + // if (ID is null) + // { + // return default; + // } - /// - /// Fetches the actual entity this reference represents from the database. - /// - /// - /// An optional cancellation token - /// - /// - /// A Task containing the actual entity - public async Task ToEntityAsync(DBContext context, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) - { - if (ID is null) - { - return default; - } + // return (await context.Find(collectionName, collection) + // .MatchID(ID) + // .Project(projection) + // .ExecuteAsync(cancellation).ConfigureAwait(false)) + // .SingleOrDefault(); + //} - return Cache = await new Find(context, collection ?? context.Collection(collectionName)).OneAsync(ID, cancellation); - } + ///// + ///// Fetches the actual entity this reference represents from the database with a projection. + ///// + ///// + ///// p=> p.Include("Prop1").Exclude("Prop2") + ///// An optional cancellation token + ///// + ///// + ///// A Task containing the actual projected entity + //public async Task ToEntityAsync(DBContext context, Func, ProjectionDefinition> projection, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) + //{ + // if (ID is null) + // { + // return default; + // } + // return (await new Find(context, collection ?? context.Collection(collectionName)) + // .Match(ID) + // .Project(projection) + // .ExecuteAsync(cancellation).ConfigureAwait(false)) + // .SingleOrDefault(); + //} +} - /// - /// Fetches the actual entity this reference represents from the database with a projection. - /// - /// - /// x => new Test { PropName = x.Prop } - /// An optional cancellation token - /// - /// - /// A Task containing the actual projected entity - public async Task ToEntityAsync(DBContext context, Expression> projection, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) +/// +/// Represents a one-to-x relationship with a known Id. +/// This DOES get serialized to +/// { +/// "ID" : "xxxxxxxx" +/// } +/// +/// +/// +public class One : IOneRelation + where T : IEntity + where TId : IComparable, IEquatable +{ + private T? _cache; + + [BsonIgnore] + public T? Cache + { + get => _cache; + set { - if (ID is null) + value?.ThrowIfUnsaved(); + _cache = value; + if (value is not null) { - return default; + if (!EqualityComparer.Default.Equals(ID, value.ID)) + { + ID = value.ID!; + } } - - return (await new Find(context, collection ?? context.Collection(collectionName)) - .Match(ID) - .Project(projection) - .ExecuteAsync(cancellation).ConfigureAwait(false)) - .SingleOrDefault(); - } - - /// - /// Fetches the actual entity this reference represents from the database with a projection. - /// - /// - /// p=> p.Include("Prop1").Exclude("Prop2") - /// An optional cancellation token - /// - /// - /// A Task containing the actual projected entity - public async Task ToEntityAsync(DBContext context, Func, ProjectionDefinition> projection, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) - { - if (ID is null) + else { - return default; + ID = default; } - return (await new Find(context, collection ?? context.Collection(collectionName)) - .Match(ID) - .Project(projection) - .ExecuteAsync(cancellation).ConfigureAwait(false)) - .SingleOrDefault(); } } - public class One : One where T : IEntity + public TId? ID { get; set; } + + public One() { } -} + public One(TId id) + { + ID = id; + } + public One(T entity) + { + entity.ThrowIfUnsaved(); + _cache = entity; + ID = entity.ID; + } +} \ No newline at end of file diff --git a/Tests/TestModifiedBy.cs b/Tests/TestModifiedBy.cs index 33ca4b3ba..1f5844e8b 100644 --- a/Tests/TestModifiedBy.cs +++ b/Tests/TestModifiedBy.cs @@ -13,7 +13,7 @@ public async Task throw_if_mod_by_not_supplied() { var db = new DBContext(); await Assert.ThrowsExceptionAsync( - async () => await db.SaveAsync(new Author())); + async () => await db.SaveAsync(new Author())); } [TestMethod] From 44678156ec35671830d572f8a3feb6ebfb809e03 Mon Sep 17 00:00:00 2001 From: Ahmed Fwela Date: Tue, 28 Dec 2021 05:56:46 +0200 Subject: [PATCH 24/26] remove static API --- MongoDB.Entities/DB/DB.Collection.cs | 56 ------------- MongoDB.Entities/DB/DB.Count.cs | 68 --------------- MongoDB.Entities/DB/DB.Delete.cs | 81 ------------------ MongoDB.Entities/DB/DB.Distinct.cs | 16 ---- MongoDB.Entities/DB/DB.File.cs | 18 ---- MongoDB.Entities/DB/DB.Find.cs | 24 ------ MongoDB.Entities/DB/DB.Fluent.cs | 32 -------- MongoDB.Entities/DB/DB.GeoNear.cs | 29 ------- MongoDB.Entities/DB/DB.Index.cs | 17 ---- MongoDB.Entities/DB/DB.Insert.cs | 36 -------- MongoDB.Entities/DB/DB.Migrate.cs | 25 +++--- MongoDB.Entities/DB/DB.PagedSearch.cs | 26 ------ MongoDB.Entities/DB/DB.Pipeline.cs | 67 --------------- MongoDB.Entities/DB/DB.Queryable.cs | 18 ---- MongoDB.Entities/DB/DB.Replace.cs | 17 ---- MongoDB.Entities/DB/DB.Save.cs | 114 -------------------------- MongoDB.Entities/DB/DB.Sequence.cs | 30 ------- MongoDB.Entities/DB/DB.Transaction.cs | 30 ------- MongoDB.Entities/DB/DB.Update.cs | 32 -------- MongoDB.Entities/DB/DB.Watcher.cs | 25 ------ 20 files changed, 11 insertions(+), 750 deletions(-) delete mode 100644 MongoDB.Entities/DB/DB.Collection.cs delete mode 100644 MongoDB.Entities/DB/DB.Count.cs delete mode 100644 MongoDB.Entities/DB/DB.Delete.cs delete mode 100644 MongoDB.Entities/DB/DB.Distinct.cs delete mode 100644 MongoDB.Entities/DB/DB.File.cs delete mode 100644 MongoDB.Entities/DB/DB.Find.cs delete mode 100644 MongoDB.Entities/DB/DB.Fluent.cs delete mode 100644 MongoDB.Entities/DB/DB.GeoNear.cs delete mode 100644 MongoDB.Entities/DB/DB.Index.cs delete mode 100644 MongoDB.Entities/DB/DB.Insert.cs delete mode 100644 MongoDB.Entities/DB/DB.PagedSearch.cs delete mode 100644 MongoDB.Entities/DB/DB.Pipeline.cs delete mode 100644 MongoDB.Entities/DB/DB.Queryable.cs delete mode 100644 MongoDB.Entities/DB/DB.Replace.cs delete mode 100644 MongoDB.Entities/DB/DB.Save.cs delete mode 100644 MongoDB.Entities/DB/DB.Sequence.cs delete mode 100644 MongoDB.Entities/DB/DB.Transaction.cs delete mode 100644 MongoDB.Entities/DB/DB.Update.cs delete mode 100644 MongoDB.Entities/DB/DB.Watcher.cs diff --git a/MongoDB.Entities/DB/DB.Collection.cs b/MongoDB.Entities/DB/DB.Collection.cs deleted file mode 100644 index b0b98539d..000000000 --- a/MongoDB.Entities/DB/DB.Collection.cs +++ /dev/null @@ -1,56 +0,0 @@ -using MongoDB.Driver; -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace MongoDB.Entities -{ - public static partial class DB - { - internal static IMongoCollection> GetRefCollection(string name) where T : IEntity - { - return Context.GetCollection>(name); - } - - /// - /// Gets the IMongoCollection for a given IEntity type. - /// - /// Any class that implements IEntity - /// Optionally use a specific collection name - public static IMongoCollection Collection(string? collectionName = null) where T : IEntity - { - return Context.Collection(collectionName); - } - - /// - /// Gets the collection name for a given entity type - /// - /// The type of entity to get the collection name for - public static string CollectionName() where T : IEntity - { - return Context.CollectionName(); - } - - /// - /// Creates a collection for an Entity type explicitly using the given options - /// - /// The type of entity that will be stored in the created collection - /// The options to use for collection creation - /// An optional cancellation token - public static Task CreateCollectionAsync(Action> options, CancellationToken cancellation = default) where T : IEntity - { - return Context.CreateCollectionAsync(options, cancellation); - } - - /// - /// Deletes the collection of a given entity type as well as the join collections for that entity. - /// TIP: When deleting a collection, all relationships associated with that entity type is also deleted. - /// - /// The entity type to drop the collection of - public static async Task DropCollectionAsync() where T : IEntity - { - await Context.DropCollectionAsync(); - } - } -} diff --git a/MongoDB.Entities/DB/DB.Count.cs b/MongoDB.Entities/DB/DB.Count.cs deleted file mode 100644 index 56aa6c5fb..000000000 --- a/MongoDB.Entities/DB/DB.Count.cs +++ /dev/null @@ -1,68 +0,0 @@ -using MongoDB.Driver; -using System; -using System.Linq.Expressions; -using System.Threading; -using System.Threading.Tasks; - -namespace MongoDB.Entities -{ - public static partial class DB - { - /// - /// Gets a fast estimation of how many documents are in the collection using metadata. - /// HINT: The estimation may not be exactly accurate. - /// - /// The entity type to get the count for - /// An optional cancellation token - public static Task CountEstimatedAsync(CancellationToken cancellation = default) where T : IEntity - { - return Context.CountEstimatedAsync(cancellation); - } - - /// - /// Gets an accurate count of how many entities are matched for a given expression/filter - /// - /// The entity type to get the count for - /// A lambda expression for getting the count for a subset of the data - /// An optional cancellation token - /// An optional CountOptions object - public static Task CountAsync(Expression> expression, CancellationToken cancellation = default, CountOptions? options = null) where T : IEntity - { - return Context.CountAsync(expression, cancellation, options); - } - - /// - /// Gets an accurate count of how many total entities are in the collection for a given entity type - /// - /// The entity type to get the count for - /// A filter definition - /// An optional cancellation token - /// An optional CountOptions object - public static Task CountAsync(FilterDefinition filter, CancellationToken cancellation = default, CountOptions? options = null) where T : IEntity - { - return Context.CountAsync(filter, cancellation, options); - } - - /// - /// Gets an accurate count of how many total entities are in the collection for a given entity type - /// - /// The entity type to get the count for - /// f => f.Eq(x => x.Prop, Value) & f.Gt(x => x.Prop, Value) - /// An optional cancellation token - /// An optional CountOptions object - public static Task CountAsync(Func, FilterDefinition> filter, CancellationToken cancellation = default, CountOptions? options = null) where T : IEntity - { - return Context.CountAsync(filter, cancellation, options); - } - - /// - /// Gets an accurate count of how many total entities are in the collection for a given entity type - /// - /// The entity type to get the count for - /// An optional cancellation token - public static Task CountAsync(CancellationToken cancellation = default) where T : IEntity - { - return Context.CountAsync(cancellation); - } - } -} diff --git a/MongoDB.Entities/DB/DB.Delete.cs b/MongoDB.Entities/DB/DB.Delete.cs deleted file mode 100644 index 98b085483..000000000 --- a/MongoDB.Entities/DB/DB.Delete.cs +++ /dev/null @@ -1,81 +0,0 @@ -using MongoDB.Driver; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Threading; -using System.Threading.Tasks; - -namespace MongoDB.Entities -{ - public static partial class DB - { - - /// - /// Deletes a single entity from MongoDB. - /// HINT: If this entity is referenced by one-to-many/many-to-many relationships, those references are also deleted. - /// - /// Any class that implements IEntity - /// The Id of the entity to delete - /// An optional cancellation token - public static Task DeleteAsync(string ID, CancellationToken cancellation = default) where T : IEntity - { - return Context.DeleteAsync(ID, cancellation); - } - - /// - /// Deletes entities using a collection of IDs - /// HINT: If more than 100,000 IDs are passed in, they will be processed in batches of 100k. - /// HINT: If these entities are referenced by one-to-many/many-to-many relationships, those references are also deleted. - /// - /// Any class that implements IEntity - /// An IEnumerable of entity IDs - /// An optional cancellation token - public static Task DeleteAsync(IEnumerable IDs, CancellationToken cancellation = default) where T : IEntity - { - return Context.DeleteAsync(IDs, cancellation); - } - - /// - /// Deletes matching entities with an expression - /// HINT: If the expression matches more than 100,000 entities, they will be deleted in batches of 100k. - /// HINT: If these entities are referenced by one-to-many/many-to-many relationships, those references are also deleted. - /// - /// Any class that implements IEntity - /// A lambda expression for matching entities to delete. - /// An optional cancellation token - /// An optional collation object - public static Task DeleteAsync(Expression> expression, CancellationToken cancellation = default, Collation? collation = null) where T : IEntity - { - return Context.DeleteAsync(expression, collation: collation, cancellation: cancellation); - } - - /// - /// Deletes matching entities with a filter expression - /// HINT: If the expression matches more than 100,000 entities, they will be deleted in batches of 100k. - /// HINT: If these entities are referenced by one-to-many/many-to-many relationships, those references are also deleted. - /// - /// Any class that implements IEntity - /// f => f.Eq(x => x.Prop, Value) & f.Gt(x => x.Prop, Value) - /// An optional cancellation token - /// An optional collation object - public static Task DeleteAsync(Func, FilterDefinition> filter, CancellationToken cancellation = default, Collation? collation = null) where T : IEntity - { - return Context.DeleteAsync(filter, collation: collation, cancellation: cancellation); - } - - /// - /// Deletes matching entities with a filter definition - /// HINT: If the expression matches more than 100,000 entities, they will be deleted in batches of 100k. - /// HINT: If these entities are referenced by one-to-many/many-to-many relationships, those references are also deleted. - /// - /// Any class that implements IEntity - /// A filter definition for matching entities to delete. - /// An optional cancellation token - /// An optional collation object - public static Task DeleteAsync(FilterDefinition filter, CancellationToken cancellation = default, Collation? collation = null) where T : IEntity - { - return Context.DeleteAsync(filter, collation: collation, cancellation: cancellation); - } - } -} diff --git a/MongoDB.Entities/DB/DB.Distinct.cs b/MongoDB.Entities/DB/DB.Distinct.cs deleted file mode 100644 index b5170053f..000000000 --- a/MongoDB.Entities/DB/DB.Distinct.cs +++ /dev/null @@ -1,16 +0,0 @@ -using MongoDB.Driver; - -namespace MongoDB.Entities -{ - public static partial class DB - { - /// - /// Represents a MongoDB Distinct command where you can get back distinct values for a given property of a given Entity. - /// - /// Any Entity that implements IEntity interface - /// The type of the property of the entity you'd like to get unique values for - /// Specifiy to override the collection name - public static Distinct Distinct(string? collectionName = null) where T : IEntity - => new(Context, Collection(collectionName)); - } -} diff --git a/MongoDB.Entities/DB/DB.File.cs b/MongoDB.Entities/DB/DB.File.cs deleted file mode 100644 index 0aef3d0b9..000000000 --- a/MongoDB.Entities/DB/DB.File.cs +++ /dev/null @@ -1,18 +0,0 @@ -using MongoDB.Bson; -using System; - -namespace MongoDB.Entities -{ - public static partial class DB - { - /// - /// Returns a DataStreamer object to enable uploading/downloading file data directly by supplying the ID of the file entity - /// - /// The file entity type - /// The ID of the file entity - public static DataStreamer File(string ID) where T : FileEntity, new() - { - return Context.File(ID); - } - } -} diff --git a/MongoDB.Entities/DB/DB.Find.cs b/MongoDB.Entities/DB/DB.Find.cs deleted file mode 100644 index 02fda51c7..000000000 --- a/MongoDB.Entities/DB/DB.Find.cs +++ /dev/null @@ -1,24 +0,0 @@ -using MongoDB.Driver; - -namespace MongoDB.Entities -{ - public static partial class DB - { - /// - /// Represents a MongoDB Find command - /// TIP: Specify your criteria using .Match() .Sort() .Skip() .Take() .Project() .Option() methods and finally call .Execute() - /// - /// Any class that implements IEntity - public static Find Find(string? collectionName = null, IMongoCollection? collection = null) where T : IEntity - => new(Context, collection ?? Collection(collectionName)); - - /// - /// Represents a MongoDB Find command - /// TIP: Specify your criteria using .Match() .Sort() .Skip() .Take() .Project() .Option() methods and finally call .Execute() - /// - /// Any class that implements IEntity - /// The type that is returned by projection - public static Find Find(string? collectionName = null, IMongoCollection? collection = null) where T : IEntity - => new(Context, collection ?? Collection(collectionName)); - } -} diff --git a/MongoDB.Entities/DB/DB.Fluent.cs b/MongoDB.Entities/DB/DB.Fluent.cs deleted file mode 100644 index 1cd48f3ad..000000000 --- a/MongoDB.Entities/DB/DB.Fluent.cs +++ /dev/null @@ -1,32 +0,0 @@ -using MongoDB.Driver; - -namespace MongoDB.Entities -{ - public static partial class DB - { - /// - /// Exposes the MongoDB collection for the given IEntity as an IAggregateFluent in order to facilitate Fluent queries. - /// - /// Any class that implements IEntity - /// The options for the aggregation. This is not required. - public static IAggregateFluent Fluent(AggregateOptions? options = null) where T : IEntity - { - return Context.Fluent(options); - } - - /// - /// Start a fluent aggregation pipeline with a $text stage with the supplied parameters. - /// TIP: Make sure to define a text index with DB.Index<T>() before searching - /// - /// The type of text matching to do - /// The search term - /// Case sensitivity of the search (optional) - /// Diacritic sensitivity of the search (optional) - /// The language for the search (optional) - /// Options for finding documents (not required) - public static IAggregateFluent FluentTextSearch(Search searchType, string searchTerm, bool caseSensitive = false, bool diacriticSensitive = false, string? language = null, AggregateOptions? options = null) where T : IEntity - { - return Context.FluentTextSearch(searchType: searchType, searchTerm: searchTerm, caseSensitive: caseSensitive, diacriticSensitive: diacriticSensitive, language: language, options: options); - } - } -} diff --git a/MongoDB.Entities/DB/DB.GeoNear.cs b/MongoDB.Entities/DB/DB.GeoNear.cs deleted file mode 100644 index 7f51fd763..000000000 --- a/MongoDB.Entities/DB/DB.GeoNear.cs +++ /dev/null @@ -1,29 +0,0 @@ -using MongoDB.Bson; -using MongoDB.Driver; -using System; -using System.Linq.Expressions; - -namespace MongoDB.Entities -{ - public static partial class DB - { - /// - /// Start a fluent aggregation pipeline with a $GeoNear stage with the supplied parameters. - /// - /// The coordinates from which to find documents from - /// x => x.Distance - /// Calculate distances using spherical geometry or not - /// The maximum distance in meters from the center point that the documents can be - /// The minimum distance in meters from the center point that the documents can be - /// The maximum number of documents to return - /// Limits the results to the documents that match the query - /// The factor to multiply all distances returned by the query - /// Specify the output field to store the point used to calculate the distance - /// - /// The options for the aggregation. This is not required. - public static IAggregateFluent FluentGeoNear(Coordinates2D NearCoordinates, Expression>? DistanceField, bool Spherical = true, double? MaxDistance = null, double? MinDistance = null, int? Limit = null, BsonDocument? Query = null, double? DistanceMultiplier = null, Expression>? IncludeLocations = null, string? IndexKey = null, AggregateOptions? options = null) where T : IEntity - { - return Context.GeoNear(NearCoordinates: NearCoordinates, DistanceField: DistanceField, Spherical: Spherical, MaxDistance: MaxDistance, MinDistance: MinDistance, Limit: Limit, Query: Query, DistanceMultiplier: DistanceMultiplier, IncludeLocations: IncludeLocations, IndexKey: IndexKey, options: options); - } - } -} diff --git a/MongoDB.Entities/DB/DB.Index.cs b/MongoDB.Entities/DB/DB.Index.cs deleted file mode 100644 index 1598df601..000000000 --- a/MongoDB.Entities/DB/DB.Index.cs +++ /dev/null @@ -1,17 +0,0 @@ -using MongoDB.Driver; - -namespace MongoDB.Entities -{ - public static partial class DB - { - /// - /// Represents an index for a given IEntity - /// TIP: Define the keys first with .Key() method and finally call the .Create() method. - /// - /// Any class that implements IEntity - public static Index Index(string? collectionName = null, IMongoCollection? collection = null) where T : IEntity - { - return new Index(Context, collection ?? Collection(collectionName)); - } - } -} diff --git a/MongoDB.Entities/DB/DB.Insert.cs b/MongoDB.Entities/DB/DB.Insert.cs deleted file mode 100644 index 024fbf410..000000000 --- a/MongoDB.Entities/DB/DB.Insert.cs +++ /dev/null @@ -1,36 +0,0 @@ -using MongoDB.Driver; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace MongoDB.Entities -{ - public static partial class DB - { - - /// - /// Inserts a new entity into the colleciton. - /// - /// Any class that implements IEntity - /// The instance to persist - /// And optional cancellation token - public static Task InsertAsync(T entity, CancellationToken cancellation = default) where T : IEntity - { - return Context.InsertAsync(entity, cancellation); - } - - /// - /// Inserts a batch of new entities into the collection. - /// - /// Any class that implements IEntity - /// The entities to persist - /// An optional session if using within a transaction - /// And optional cancellation token - /// Optional tenant prefix if using multi-tenancy - public static Task> InsertAsync(IEnumerable entities, CancellationToken cancellation = default) where T : IEntity - { - return Context.InsertAsync(entities, cancellation); - } - } -} diff --git a/MongoDB.Entities/DB/DB.Migrate.cs b/MongoDB.Entities/DB/DB.Migrate.cs index 021e8d188..7672be7c6 100644 --- a/MongoDB.Entities/DB/DB.Migrate.cs +++ b/MongoDB.Entities/DB/DB.Migrate.cs @@ -13,10 +13,9 @@ public static partial class DB /// Discover and run migrations from the same assembly as the specified type. /// /// A type that is from the same assembly as the migrations you want to run - /// Optional tenant prefix if using multi-tenancy - public static Task MigrateAsync(string tenantPrefix = null) where T : class + public static Task MigrateAsync() where T : class { - return Migrate(typeof(T), tenantPrefix); + return Migrate(typeof(T)); } /// @@ -25,23 +24,21 @@ public static Task MigrateAsync(string tenantPrefix = null) where T : class /// and implement IMigration interface on them. /// Call this method at the startup of the application in order to run the migrations. /// - /// Optional tenant prefix if using multi-tenancy - public static Task MigrateAsync(string tenantPrefix = null) + public static Task MigrateAsync() { - return Migrate(null, tenantPrefix); + return Migrate(null); } /// /// Executes the given collection of IMigrations in the correct order to transform the database. /// /// The collection of migrations to execute - /// Optional tenant prefix if using multi-tenancy - public static Task MigrationsAsync(IEnumerable migrations, string tenantPrefix = null) + public static Task MigrationsAsync(IEnumerable migrations) { - return Execute(migrations, tenantPrefix); + return Execute(migrations); } - private static Task Migrate(Type targetType, string tenantPrefix = null) + private static Task Migrate(Type? targetType) { IEnumerable assemblies; @@ -77,13 +74,13 @@ private static Task Migrate(Type targetType, string tenantPrefix = null) if (!types.Any()) throw new InvalidOperationException("Didn't find any classes that implement IMigrate interface."); - return Execute(types.Select(t => (IMigration)Activator.CreateInstance(t)), tenantPrefix); + return Execute(types.Select(t => (IMigration)Activator.CreateInstance(t))); } - private static async Task Execute(IEnumerable migrations, string tenantPrefix = null) + private static async Task Execute(IEnumerable migrations) { var lastMigNum = await - Find(tenantPrefix: tenantPrefix) + Context.Find() .Sort(m => m.Number, Order.Descending) .Project(m => m.Number) .ExecuteFirstAsync() @@ -119,7 +116,7 @@ private static async Task Execute(IEnumerable migrations, string ten Name = migration.Value.name, TimeTakenSeconds = sw.Elapsed.TotalSeconds }; - await SaveAsync(mig, tenantPrefix: tenantPrefix).ConfigureAwait(false); + await Context.SaveAsync(mig).ConfigureAwait(false); sw.Stop(); sw.Reset(); } diff --git a/MongoDB.Entities/DB/DB.PagedSearch.cs b/MongoDB.Entities/DB/DB.PagedSearch.cs deleted file mode 100644 index a1697f34e..000000000 --- a/MongoDB.Entities/DB/DB.PagedSearch.cs +++ /dev/null @@ -1,26 +0,0 @@ -using MongoDB.Driver; - -namespace MongoDB.Entities -{ - public static partial class DB - { - /// - /// Represents an aggregation query that retrieves results with easy paging support. - /// - /// Any class that implements IEntity - /// An optional session if using within a transaction - /// Optional tenant prefix if using multi-tenancy - public static PagedSearch PagedSearch(IClientSessionHandle session = null, string tenantPrefix = null) where T : IEntity - => new(session, null, tenantPrefix); - - /// - /// Represents an aggregation query that retrieves results with easy paging support. - /// - /// Any class that implements IEntity - /// An optional session if using within a transaction - /// The type you'd like to project the results to. - /// Optional tenant prefix if using multi-tenancy - public static PagedSearch PagedSearch(IClientSessionHandle session = null, string tenantPrefix = null) where T : IEntity - => new(session, null, tenantPrefix); - } -} diff --git a/MongoDB.Entities/DB/DB.Pipeline.cs b/MongoDB.Entities/DB/DB.Pipeline.cs deleted file mode 100644 index cf6d94fab..000000000 --- a/MongoDB.Entities/DB/DB.Pipeline.cs +++ /dev/null @@ -1,67 +0,0 @@ -using MongoDB.Driver; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace MongoDB.Entities -{ - public static partial class DB - { - /// - /// Executes an aggregation pipeline by supplying a 'Template' object and returns a cursor - /// - /// Any class that implements IEntity - /// The type of the resulting objects - /// A 'Template' object with tags replaced - /// The options for the aggregation. This is not required. - /// An optional session if using within a transaction - /// An optional cancellation token - /// Optional tenant prefix if using multi-tenancy - public static Task> PipelineCursorAsync(Template template, AggregateOptions? options = null, CancellationToken cancellation = default) where T : IEntity - { - return Context.PipelineCursorAsync(template, options, cancellation); - } - - /// - /// Executes an aggregation pipeline by supplying a 'Template' object and get a list of results - /// - /// Any class that implements IEntity - /// The type of the resulting objects - /// A 'Template' object with tags replaced - /// The options for the aggregation. This is not required. - /// An optional cancellation token - public static Task> PipelineAsync(Template template, AggregateOptions? options = null, CancellationToken cancellation = default) where T : IEntity - { - return Context.PipelineAsync(template, options, cancellation); - } - - /// - /// Executes an aggregation pipeline by supplying a 'Template' object and get a single result or default value if not found. - /// If more than one entity is found, it will throw an exception. - /// - /// Any class that implements IEntity - /// The type of the resulting object - /// A 'Template' object with tags replaced - /// The options for the aggregation. This is not required. - /// An optional cancellation token - public static Task PipelineSingleAsync(Template template, AggregateOptions? options = null, CancellationToken cancellation = default) where T : IEntity - { - return Context.PipelineSingleAsync(template, options, cancellation); - } - - /// - /// Executes an aggregation pipeline by supplying a 'Template' object and get the first result or default value if not found. - /// - /// Any class that implements IEntity - /// The type of the resulting object - /// A 'Template' object with tags replaced - /// The options for the aggregation. This is not required. - /// An optional cancellation token - public static Task PipelineFirstAsync(Template template, AggregateOptions? options = null, CancellationToken cancellation = default) where T : IEntity - { - return Context.PipelineFirstAsync(template, options, cancellation); - - } - } -} diff --git a/MongoDB.Entities/DB/DB.Queryable.cs b/MongoDB.Entities/DB/DB.Queryable.cs deleted file mode 100644 index f75126adf..000000000 --- a/MongoDB.Entities/DB/DB.Queryable.cs +++ /dev/null @@ -1,18 +0,0 @@ -using MongoDB.Driver; -using MongoDB.Driver.Linq; - -namespace MongoDB.Entities -{ - public static partial class DB - { - /// - /// Exposes the MongoDB collection for the given IEntity as an IQueryable in order to facilitate LINQ queries. - /// - /// The aggregate options - /// Any class that implements IEntity - public static IMongoQueryable Queryable(AggregateOptions? options = null) where T : IEntity - { - return Context.Queryable(options); - } - } -} diff --git a/MongoDB.Entities/DB/DB.Replace.cs b/MongoDB.Entities/DB/DB.Replace.cs deleted file mode 100644 index debec4caa..000000000 --- a/MongoDB.Entities/DB/DB.Replace.cs +++ /dev/null @@ -1,17 +0,0 @@ -using MongoDB.Driver; - -namespace MongoDB.Entities -{ - public static partial class DB - { - /// - /// Represents a ReplaceOne command, which can replace the first matched document with a given entity - /// TIP: Specify a filter first with the .Match(). Then set entity with .WithEntity() and finally call .Execute() to run the command. - /// - /// Any class that implements IEntity - /// An optional session if using within a transaction - /// Optional tenant prefix if using multi-tenancy - public static Replace Replace(IClientSessionHandle session = null, string tenantPrefix = null) where T : IEntity - => new(session, null, null, null, tenantPrefix); - } -} diff --git a/MongoDB.Entities/DB/DB.Save.cs b/MongoDB.Entities/DB/DB.Save.cs deleted file mode 100644 index f5a562ec7..000000000 --- a/MongoDB.Entities/DB/DB.Save.cs +++ /dev/null @@ -1,114 +0,0 @@ -using MongoDB.Driver; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Threading; -using System.Threading.Tasks; - -namespace MongoDB.Entities -{ - public static partial class DB - { - - - /// - /// Saves a complete entity replacing an existing entity or creating a new one if it does not exist. - /// If ID value is null, a new entity is created. If ID has a value, then existing entity is replaced. - /// - /// Any class that implements IEntity - /// The instance to persist - /// And optional cancellation token - public static Task SaveAsync(T entity, CancellationToken cancellation = default) where T : IEntity - { - return Context.SaveAsync(entity, cancellation); - } - - /// - /// Saves a batch of complete entities replacing existing ones or creating new ones if they do not exist. - /// If ID value is null, a new entity is created. If ID has a value, then existing entity is replaced. - /// - /// Any class that implements IEntity - /// The entities to persist - /// And optional cancellation token - public static Task> SaveAsync(IEnumerable entities, CancellationToken cancellation = default) where T : IEntity - { - return Context.SaveAsync(entities, cancellation); - } - - /// - /// Saves an entity partially with only the specified subset of properties. - /// If ID value is null, a new entity is created. If ID has a value, then existing entity is updated. - /// TIP: The properties to be saved can be specified with a 'New' expression. - /// You can only specify root level properties with the expression. - /// - /// Any class that implements IEntity - /// The entity to save - /// x => new { x.PropOne, x.PropTwo } - /// An optional cancellation token - public static Task SaveOnlyAsync(T entity, Expression> members, CancellationToken cancellation = default) where T : IEntity - { - return Context.SaveOnlyAsync(entity, members, cancellation); - - } - - /// - /// Saves a batch of entities partially with only the specified subset of properties. - /// If ID value is null, a new entity is created. If ID has a value, then existing entity is updated. - /// TIP: The properties to be saved can be specified with a 'New' expression. - /// You can only specify root level properties with the expression. - /// - /// Any class that implements IEntity - /// The batch of entities to save - /// x => new { x.PropOne, x.PropTwo } - /// An optional cancellation token - public static Task> SaveOnlyAsync(IEnumerable entities, Expression> members, CancellationToken cancellation = default) where T : IEntity - { - return Context.SaveOnlyAsync(entities, members, cancellation); - } - - /// - /// Saves an entity partially excluding the specified subset of properties. - /// If ID value is null, a new entity is created. If ID has a value, then existing entity is updated. - /// TIP: The properties to be excluded can be specified with a 'New' expression. - /// You can only specify root level properties with the expression. - /// - /// Any class that implements IEntity - /// The entity to save - /// x => new { x.PropOne, x.PropTwo } - /// An optional cancellation token - public static Task SaveExceptAsync(T entity, Expression> members, CancellationToken cancellation = default) where T : IEntity - { - return Context.SaveExceptAsync(entity, members, cancellation); - } - - /// - /// Saves a batch of entities partially excluding the specified subset of properties. - /// If ID value is null, a new entity is created. If ID has a value, then existing entity is updated. - /// TIP: The properties to be excluded can be specified with a 'New' expression. - /// You can only specify root level properties with the expression. - /// - /// Any class that implements IEntity - /// The batch of entities to save - /// x => new { x.PropOne, x.PropTwo } - /// An optional cancellation token - public static Task> SaveExceptAsync(IEnumerable entities, Expression> members, CancellationToken cancellation = default) where T : IEntity - { - return Context.SaveExceptAsync(entities, members, cancellation); - } - - /// - /// Saves an entity partially while excluding some properties. - /// The properties to be excluded can be specified using the [Preserve] or [DontPreserve] attributes. - /// - /// Any class that implements IEntity - /// The entity to save - /// An optional cancellation token - public static Task SavePreservingAsync(T entity, CancellationToken cancellation = default) where T : IEntity - { - return Context.SavePreservingAsync(entity, cancellation); - } - - - } -} diff --git a/MongoDB.Entities/DB/DB.Sequence.cs b/MongoDB.Entities/DB/DB.Sequence.cs deleted file mode 100644 index 05bd537bb..000000000 --- a/MongoDB.Entities/DB/DB.Sequence.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace MongoDB.Entities -{ - public static partial class DB - { - //NOTE: transaction support will not be added due to unpredictability with concurrency. - - /// - /// Returns an atomically generated sequential number for the given Entity type everytime the method is called - /// - /// The type of entity to get the next sequential number for - /// An optional cancellation token - public static Task NextSequentialNumberAsync(CancellationToken cancellation = default) where T : IEntity - { - return Context.NextSequentialNumberAsync(cancellation); - } - - /// - /// Returns an atomically generated sequential number for the given sequence name everytime the method is called - /// - /// The name of the sequence to get the next number for - /// An optional cancellation token - public static Task NextSequentialNumberAsync(string sequenceName, CancellationToken cancellation = default) - { - return Context.NextSequentialNumberAsync(sequenceName, cancellation); - } - } -} diff --git a/MongoDB.Entities/DB/DB.Transaction.cs b/MongoDB.Entities/DB/DB.Transaction.cs deleted file mode 100644 index 0d746e449..000000000 --- a/MongoDB.Entities/DB/DB.Transaction.cs +++ /dev/null @@ -1,30 +0,0 @@ -using MongoDB.Driver; - -namespace MongoDB.Entities -{ - public static partial class DB - { - /// - /// Gets a transaction context/scope for a given database or the default database if not specified. - /// - /// The name of the database which this transaction is for (not required) - /// Client session options (not required) - /// - public static Transaction Transaction(string database = default, ClientSessionOptions options = null, ModifiedBy modifiedBy = null) - { - return new Transaction(database, options, modifiedBy); - } - - /// - /// Gets a transaction context/scope for a given entity type's database - /// - /// The entity type to determine the database from for the transaction - /// Client session options (not required) - /// - /// Optional tenant prefix if using multi-tenancy - public static Transaction Transaction(ClientSessionOptions options = null, ModifiedBy modifiedBy = null, string tenantPrefix = null) where T : IEntity - { - return new Transaction(DatabaseName(tenantPrefix), options, modifiedBy); - } - } -} diff --git a/MongoDB.Entities/DB/DB.Update.cs b/MongoDB.Entities/DB/DB.Update.cs deleted file mode 100644 index 06570c5fe..000000000 --- a/MongoDB.Entities/DB/DB.Update.cs +++ /dev/null @@ -1,32 +0,0 @@ -using MongoDB.Driver; - -namespace MongoDB.Entities -{ - public static partial class DB - { - /// - /// Represents an update command - /// TIP: Specify a filter first with the .Match() method. Then set property values with .Modify() and finally call .Execute() to run the command. - /// - /// Any class that implements IEntity - public static Update Update() where T : IEntity - => Context.Update(); - - /// - /// Update and retrieve the first document that was updated. - /// TIP: Specify a filter first with the .Match(). Then set property values with .Modify() and finally call .Execute() to run the command. - /// - /// Any class that implements IEntity - /// The type to project to - public static UpdateAndGet UpdateAndGet() where T : IEntity - => Context.UpdateAndGet(); - - /// - /// Update and retrieve the first document that was updated. - /// TIP: Specify a filter first with the .Match(). Then set property values with .Modify() and finally call .Execute() to run the command. - /// - /// Any class that implements IEntity - public static UpdateAndGet UpdateAndGet() where T : IEntity - => Context.UpdateAndGet(); - } -} diff --git a/MongoDB.Entities/DB/DB.Watcher.cs b/MongoDB.Entities/DB/DB.Watcher.cs deleted file mode 100644 index 0776c751a..000000000 --- a/MongoDB.Entities/DB/DB.Watcher.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Collections.Generic; - -namespace MongoDB.Entities -{ - public static partial class DB - { - /// - /// Retrieves the 'change-stream' watcher instance for a given unique name. - /// If an instance for the name does not exist, it will return a new instance. - /// If an instance already exists, that instance will be returned. - /// - /// The entity type to get a watcher for - /// A unique name for the watcher of this entity type. Names can be duplicate among different entity types. - public static Watcher Watcher(string name) where T : IEntity - { - return Context.Watcher(name); - } - - /// - /// Returns all the watchers for a given entity type - /// - /// The entity type to get the watcher of - public static IEnumerable> Watchers() where T : IEntity => Context.Cache().Watchers.Values; - } -} From e1aba55d87f3d58f97c5dd4bfa13f6907703e0cc Mon Sep 17 00:00:00 2001 From: Ahmed Fwela Date: Tue, 28 Dec 2021 06:25:59 +0200 Subject: [PATCH 25/26] WIP --- Benchmark/Benchmarks/Update.cs | 11 +++-- .../Builders/FilterQuery.Extensions.cs | 43 ++++++++++++------- .../Builders/FilterQuery.Interface.cs | 5 +++ MongoDB.Entities/Builders/FilterQuery.cs | 2 + MongoDB.Entities/Builders/Find.cs | 6 +-- MongoDB.Entities/Builders/Update.cs | 2 +- MongoDB.Entities/DBContext/DBContext.Index.cs | 10 ++--- .../DBContext/DBContext.Replace.cs | 12 +++++- MongoDB.Entities/DBContext/DBContext.Save.cs | 16 ++++++- .../DBContext/DBContext.Update.cs | 12 +++++- MongoDB.Entities/Relationships/NewMany.cs | 2 +- 11 files changed, 85 insertions(+), 36 deletions(-) diff --git a/Benchmark/Benchmarks/Update.cs b/Benchmark/Benchmarks/Update.cs index 5dedb32f8..a65cd9aa9 100644 --- a/Benchmark/Benchmarks/Update.cs +++ b/Benchmark/Benchmarks/Update.cs @@ -15,13 +15,15 @@ public class UpdateOne : BenchBase public UpdateOne() { - DB.SaveAsync(new Author { ID = id, FirstName = "initial" }).GetAwaiter().GetResult(); + DB.Context.SaveAsync(new Author { ID = id, FirstName = "initial" }).GetAwaiter().GetResult(); } [Benchmark] public override Task MongoDB_Entities() { - return DB.Update() + var update = DB.Context.Update(); + update.MatchID(id); + return DB.Context.Update() .MatchID(id) .Modify(a => a.FirstName, "updated") .ExecuteAsync(); @@ -44,7 +46,8 @@ public class Update100 : BenchBase public Update100() { - DB.Index() + DB.Context + .Index() .Key(a => a.FirstName, KeyType.Ascending) .Option(o => o.Background = false) .CreateAsync() @@ -64,7 +67,7 @@ public Update100() [Benchmark] public override Task MongoDB_Entities() { - return DB + return DB.Context .Update() .Match(x => x.FirstName == guid) .Modify(x => x.FirstName, "updated") diff --git a/MongoDB.Entities/Builders/FilterQuery.Extensions.cs b/MongoDB.Entities/Builders/FilterQuery.Extensions.cs index b2be6b40b..2c2b0622f 100644 --- a/MongoDB.Entities/Builders/FilterQuery.Extensions.cs +++ b/MongoDB.Entities/Builders/FilterQuery.Extensions.cs @@ -1,41 +1,54 @@ namespace MongoDB.Entities; +//i dream to live in a world where c# can infer generic arguments on its own public static class FilterExt { - //protected FilterDefinition MergedFilter => Logic.MergeWithGlobalFilter(_ignoreGlobalFilters, _globalFilters, _filter); + /// /// Specify an IEntity ID as the matching criteria /// /// the query - internal static FilterDefinition MergedFilter(this TSelf self) + /// A unique IEntity ID + public static TSelf MatchID(this IFilterBuilder self, TId id) + where TId : IComparable, IEquatable + where TEntity : IEntity where TSelf : IFilterBuilder - { - return Logic.MergeWithGlobalFilter(self.IsIgnoreGlobalFilters, self.GlobalFilters, self.Filter); - } + => self.Match(f => f.Eq(t => t.ID, id)); + /// /// Specify an IEntity ID as the matching criteria /// /// the query /// A unique IEntity ID - public static TSelf MatchID(this TSelf self, TId id) - where TId : IComparable, IEquatable - where TEntity : IEntity + public static TSelf MatchID(this IFilterBuilder self, string id) + where TEntity : IEntity where TSelf : IFilterBuilder - { - return self.Match(f => f.Eq(t => t.ID, id)); - } + => MatchID(self, id); + + + + /// /// Specify an IEntity ID as the matching criteria /// /// the query /// A unique IEntity ID - public static TSelf Match(this TSelf self, TId id) + public static TSelf Match(this IFilterBuilder self, TId id) where TId : IComparable, IEquatable where TEntity : IEntity where TSelf : IFilterBuilder - { - return self.MatchID(id); - } + => self.MatchID(id); + + + /// + /// Specify an IEntity ID as the matching criteria + /// + /// the query + /// A unique IEntity ID + public static TSelf Match(this IFilterBuilder self, string id) + where TEntity : IEntity + where TSelf : IFilterBuilder + => self.MatchID(id); } \ No newline at end of file diff --git a/MongoDB.Entities/Builders/FilterQuery.Interface.cs b/MongoDB.Entities/Builders/FilterQuery.Interface.cs index ed628ad29..cd389872b 100644 --- a/MongoDB.Entities/Builders/FilterQuery.Interface.cs +++ b/MongoDB.Entities/Builders/FilterQuery.Interface.cs @@ -7,6 +7,11 @@ public interface IFilterBuilder internal FilterDefinition Filter { get; } internal Dictionary GlobalFilters { get; } + internal FilterDefinition MergedFilter => Logic.MergeWithGlobalFilter(IsIgnoreGlobalFilters, GlobalFilters, Filter); + + + + /// /// Specify that this operation should ignore any global filters /// diff --git a/MongoDB.Entities/Builders/FilterQuery.cs b/MongoDB.Entities/Builders/FilterQuery.cs index e99552848..a6c147a8e 100644 --- a/MongoDB.Entities/Builders/FilterQuery.cs +++ b/MongoDB.Entities/Builders/FilterQuery.cs @@ -23,9 +23,11 @@ internal FilterQueryBase(Dictionary glob bool IFilterBuilder.IsIgnoreGlobalFilters => _ignoreGlobalFilters; FilterDefinition IFilterBuilder.Filter => _filter; Dictionary IFilterBuilder.GlobalFilters => _globalFilters; + internal FilterDefinition MergedFilter => (this as IFilterBuilder).MergedFilter; private TSelf This => (TSelf)this; + /// /// Specify that this operation should ignore any global filters /// diff --git a/MongoDB.Entities/Builders/Find.cs b/MongoDB.Entities/Builders/Find.cs index 261ee0ccc..64cc6cb9b 100644 --- a/MongoDB.Entities/Builders/Find.cs +++ b/MongoDB.Entities/Builders/Find.cs @@ -155,6 +155,7 @@ public async Task ExecuteAnyAsync(CancellationToken cancellation = default public static class FindExt { + /// /// Find a single IEntity by ID /// @@ -162,12 +163,11 @@ public static class FindExt /// The unique ID of an IEntity /// An optional cancellation token /// A single entity or null if not found - public static Task OneAsync(this TSelf self, TId ID, CancellationToken cancellation = default) + public static Task OneAsync(this Find self, TId ID, CancellationToken cancellation = default) where TId : IComparable, IEquatable where TEntity : IEntity - where TSelf : Find, IFilterBuilder { - self.MatchID(ID); + self.MatchID(ID); return self.ExecuteSingleAsync(cancellation); } } diff --git a/MongoDB.Entities/Builders/Update.cs b/MongoDB.Entities/Builders/Update.cs index e9fb8891e..df85864b7 100644 --- a/MongoDB.Entities/Builders/Update.cs +++ b/MongoDB.Entities/Builders/Update.cs @@ -1,6 +1,6 @@ namespace MongoDB.Entities; -public abstract class UpdateBase : FilterQueryBase +public abstract class UpdateBase : FilterQueryBase where TId : IComparable, IEquatable where T : IEntity where TSelf : UpdateBase diff --git a/MongoDB.Entities/DBContext/DBContext.Index.cs b/MongoDB.Entities/DBContext/DBContext.Index.cs index dcc8488ae..0ed0f8744 100644 --- a/MongoDB.Entities/DBContext/DBContext.Index.cs +++ b/MongoDB.Entities/DBContext/DBContext.Index.cs @@ -8,13 +8,11 @@ public partial class DBContext /// Represents an index for a given IEntity /// TIP: Define the keys first with .Key() method and finally call the .Create() method. /// - /// Any class that implements IEntity - /// ID type - public Index Index(string? collectionName = null, IMongoCollection? collection = null) - where TId : IComparable, IEquatable - where T : IEntity + /// Any class + public Index Index(string? collectionName = null, IMongoCollection? collection = null) { - return new Index(this, Collection(collectionName, collection)); + return new Index(this, Collection(collectionName, collection)); } + } } diff --git a/MongoDB.Entities/DBContext/DBContext.Replace.cs b/MongoDB.Entities/DBContext/DBContext.Replace.cs index e3a8712b8..97135fc6e 100644 --- a/MongoDB.Entities/DBContext/DBContext.Replace.cs +++ b/MongoDB.Entities/DBContext/DBContext.Replace.cs @@ -4,6 +4,14 @@ namespace MongoDB.Entities { public partial class DBContext { + /// + /// Starts a replace command for the given entity type + /// TIP: Only the first matched entity will be replaced + /// + /// The type of entity + public Replace Replace(string? collectionName = null, IMongoCollection? collection = null) + where T : IEntity => Replace(collectionName, collection); + /// /// Starts a replace command for the given entity type /// TIP: Only the first matched entity will be replaced @@ -11,8 +19,8 @@ public partial class DBContext /// The type of entity /// ID type public Replace Replace(string? collectionName = null, IMongoCollection? collection = null) - where TId : IComparable, IEquatable - where T : IEntity + where TId : IComparable, IEquatable + where T : IEntity { ThrowIfModifiedByIsEmpty(); return new Replace(this, Collection(collectionName, collection), OnBeforeSave); diff --git a/MongoDB.Entities/DBContext/DBContext.Save.cs b/MongoDB.Entities/DBContext/DBContext.Save.cs index 7c6182a70..3e2aca79d 100644 --- a/MongoDB.Entities/DBContext/DBContext.Save.cs +++ b/MongoDB.Entities/DBContext/DBContext.Save.cs @@ -12,6 +12,18 @@ namespace MongoDB.Entities { public partial class DBContext { + /// + /// Saves a complete entity replacing an existing entity or creating a new one if it does not exist. + /// If ID value is null, a new entity is created. If ID has a value, then existing entity is replaced. + /// + /// The type of entity + /// The instance to persist + /// And optional cancellation token + /// + /// + public Task SaveAsync(T entity, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) + where T : IEntity => SaveAsync(entity, cancellation, collectionName, collection); + /// /// Saves a complete entity replacing an existing entity or creating a new one if it does not exist. /// If ID value is null, a new entity is created. If ID has a value, then existing entity is replaced. @@ -23,8 +35,8 @@ public partial class DBContext /// /// public Task SaveAsync(T entity, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) - where TId : IComparable, IEquatable - where T : IEntity + where TId : IComparable, IEquatable + where T : IEntity { SetModifiedBySingle(entity); OnBeforeSave(entity); diff --git a/MongoDB.Entities/DBContext/DBContext.Update.cs b/MongoDB.Entities/DBContext/DBContext.Update.cs index 06114ed3a..d5b02b2c2 100644 --- a/MongoDB.Entities/DBContext/DBContext.Update.cs +++ b/MongoDB.Entities/DBContext/DBContext.Update.cs @@ -2,14 +2,22 @@ public partial class DBContext { + /// + /// Starts an update command for the given entity type + /// + /// The type of entity + public Update Update(string? collectionName = null, IMongoCollection? collection = null) where T : IEntity + => Update(collectionName: collectionName, collection: collection); + + /// /// Starts an update command for the given entity type /// /// The type of entity /// ID type public Update Update(string? collectionName = null, IMongoCollection? collection = null) - where TId : IComparable, IEquatable - where T : IEntity + where TId : IComparable, IEquatable + where T : IEntity { var cmd = new Update(this, Collection(collectionName, collection), OnBeforeUpdate>); if (Cache().ModifiedByProp is PropertyInfo ModifiedByProp) diff --git a/MongoDB.Entities/Relationships/NewMany.cs b/MongoDB.Entities/Relationships/NewMany.cs index dc4a91654..6292646d6 100644 --- a/MongoDB.Entities/Relationships/NewMany.cs +++ b/MongoDB.Entities/Relationships/NewMany.cs @@ -124,7 +124,7 @@ protected Many(TParent parent, PropertyInfo parentProperty, PropertyInfo childPr { Parent = parent; } - public FilterDefinition GetFilterForSingleDocument() => Builders.Filter.Eq(ChildProperty.Name, Parent.ID); + public FilterDefinition GetFilterForSingleDocument() => Builders.Filter.Eq(ChildProperty.Name, /*TODO: uncomment me Parent.ID*/ ""); } public sealed class ManyToMany : Many, IManyToMany From 899df7abaee8b18e3905419bc0aa9a9eb10a0684 Mon Sep 17 00:00:00 2001 From: Ahmed Fwela Date: Tue, 28 Dec 2021 07:15:59 +0200 Subject: [PATCH 26/26] WIP extensions --- MongoDB.Entities/Builders/Distinct.cs | 2 +- MongoDB.Entities/Builders/PagedSearch.cs | 2 +- MongoDB.Entities/Core/JoinRecordSerializer.cs | 2 +- MongoDB.Entities/Core/SequenceCounter.cs | 2 +- MongoDB.Entities/DB/DB.cs | 4 +- .../DBContext/DBContext.Delete.cs | 11 ++-- .../DBContext/DBContext.Distinct.cs | 6 +- .../DBContext/DBContext.Insert.cs | 37 ++++++++++-- .../DBContext/DBContext.PagedSearch.cs | 14 ++--- MongoDB.Entities/DBContext/DBContext.Save.cs | 46 ++++++++++----- MongoDB.Entities/Extensions/Collection.cs | 13 +++-- MongoDB.Entities/Extensions/Database.cs | 2 +- MongoDB.Entities/Extensions/Delete.cs | 56 ++++++++++++++++--- MongoDB.Entities/Extensions/Entity.cs | 9 +-- MongoDB.Entities/Extensions/Fluent.cs | 9 +-- MongoDB.Entities/Extensions/Insert.cs | 18 +++--- MongoDB.Entities/Extensions/PagedSearch.cs | 8 +-- MongoDB.Entities/Extensions/Save.cs | 18 +++--- .../Relationships/Many.Queryable.cs | 8 +-- 19 files changed, 177 insertions(+), 90 deletions(-) diff --git a/MongoDB.Entities/Builders/Distinct.cs b/MongoDB.Entities/Builders/Distinct.cs index 1b6e59604..560d05d4b 100644 --- a/MongoDB.Entities/Builders/Distinct.cs +++ b/MongoDB.Entities/Builders/Distinct.cs @@ -35,7 +35,7 @@ public Task> ExecuteCursorAsync(CancellationToken cancel if (_field == null) throw new InvalidOperationException("Please use the .Property() method to specify the field to use for obtaining unique values for!"); - var mergedFilter = Logic.MergeWithGlobalFilter(_ignoreGlobalFilters, _globalFilters, _filter); + var mergedFilter = MergedFilter; return Context.Session is IClientSessionHandle session ? Collection.DistinctAsync(session, _field, mergedFilter, _options, cancellation) diff --git a/MongoDB.Entities/Builders/PagedSearch.cs b/MongoDB.Entities/Builders/PagedSearch.cs index 99f4941c7..b6ebb795a 100644 --- a/MongoDB.Entities/Builders/PagedSearch.cs +++ b/MongoDB.Entities/Builders/PagedSearch.cs @@ -30,7 +30,7 @@ internal PagedSearchBase(Dictionary glob /// The input IAggregateFluent pipeline public TSelf WithFluent(TFluent fluentPipeline) where TFluent : IAggregateFluent { - this._fluentPipeline = fluentPipeline; + _fluentPipeline = fluentPipeline; return This; } diff --git a/MongoDB.Entities/Core/JoinRecordSerializer.cs b/MongoDB.Entities/Core/JoinRecordSerializer.cs index 1483929c8..b0ed79dd5 100644 --- a/MongoDB.Entities/Core/JoinRecordSerializer.cs +++ b/MongoDB.Entities/Core/JoinRecordSerializer.cs @@ -9,7 +9,7 @@ public override void Serialize(BsonSerializationContext context, BsonSerializati } public override JoinRecord Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) { - + return new JoinRecord(default, default); } public bool TryGetMemberSerializationInfo(string memberName, out BsonSerializationInfo? serializationInfo) diff --git a/MongoDB.Entities/Core/SequenceCounter.cs b/MongoDB.Entities/Core/SequenceCounter.cs index 53528d9f1..1d8d62c9f 100644 --- a/MongoDB.Entities/Core/SequenceCounter.cs +++ b/MongoDB.Entities/Core/SequenceCounter.cs @@ -4,7 +4,7 @@ internal class SequenceCounter : IEntity { [BsonId] - public string ID { get; set; } = null!; + public string? ID { get; set; } [BsonRepresentation(BsonType.Int64)] public ulong Count { get; set; } diff --git a/MongoDB.Entities/DB/DB.cs b/MongoDB.Entities/DB/DB.cs index fc6b72146..a40930309 100644 --- a/MongoDB.Entities/DB/DB.cs +++ b/MongoDB.Entities/DB/DB.cs @@ -160,8 +160,8 @@ public static IMongoDatabase Database(string? name) /// Gets the name of the database a given entity type is attached to. Returns name of default database if not specifically attached. /// /// Any class that implements IEntity - [Obsolete("This method returns the current DatabaseName in the Context")] - public static string DatabaseName() where T : IEntity + [Obsolete("This method always returns the current DatabaseName in the Context")] + public static string DatabaseName() { return Context.DatabaseNamespace.DatabaseName; } diff --git a/MongoDB.Entities/DBContext/DBContext.Delete.cs b/MongoDB.Entities/DBContext/DBContext.Delete.cs index 614e3b196..65887a016 100644 --- a/MongoDB.Entities/DBContext/DBContext.Delete.cs +++ b/MongoDB.Entities/DBContext/DBContext.Delete.cs @@ -40,10 +40,11 @@ private async Task DeleteCascadingAsync(IEnumerable var casted = IDs.Cast(); foreach (var cName in await collNamesCursor.ToListAsync(cancellation).ConfigureAwait(false)) { - tasks.Add( - Session is null - ? Collection>(cName).DeleteManyAsync(r => casted.Contains(r.ID.ChildID) || casted.Contains(r.ID.ParentID)) - : Collection>(cName).DeleteManyAsync(Session, r => casted.Contains(r.ID.ChildID) || casted.Contains(r.ID.ParentID), null, cancellation)); + //TODO(@ahmednfwela): check if this is needed + //tasks.Add( + // Session is null + // ? Collection>(cName).DeleteManyAsync(r => casted.Contains(r.ID.ChildID) || casted.Contains(r.ID.ParentID)) + // : Collection>(cName).DeleteManyAsync(Session, r => casted.Contains(r.ID.ChildID) || casted.Contains(r.ID.ParentID), null, cancellation)); } var delResTask = @@ -164,7 +165,7 @@ public async Task DeleteAsync(FilterDefinition filter, ThrowIfCancellationNotSupported(cancellation); var filterDef = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, filter); - var cursor = await new Find(this, Collection(collectionName, collection)) + var cursor = await new Find(this, Collection(collectionName, collection)) .Match(filter) .Project(e => e.ID) .Option(o => o.BatchSize = _deleteBatchSize) diff --git a/MongoDB.Entities/DBContext/DBContext.Distinct.cs b/MongoDB.Entities/DBContext/DBContext.Distinct.cs index d68d71cd8..de7a5a9f9 100644 --- a/MongoDB.Entities/DBContext/DBContext.Distinct.cs +++ b/MongoDB.Entities/DBContext/DBContext.Distinct.cs @@ -10,10 +10,8 @@ public partial class DBContext /// Any Entity that implements IEntity interface /// Id type /// The type of the property of the entity you'd like to get unique values for - public Distinct Distinct(string? collectionName = null, IMongoCollection? collection = null) - where TId : IComparable, IEquatable - where T : IEntity + public Distinct Distinct(string? collectionName = null, IMongoCollection? collection = null) { - return new Distinct(this, Collection(collectionName, collection)); + return new Distinct(this, Collection(collectionName, collection)); } } diff --git a/MongoDB.Entities/DBContext/DBContext.Insert.cs b/MongoDB.Entities/DBContext/DBContext.Insert.cs index 3617b3eb2..2f0b45681 100644 --- a/MongoDB.Entities/DBContext/DBContext.Insert.cs +++ b/MongoDB.Entities/DBContext/DBContext.Insert.cs @@ -18,7 +18,7 @@ private Task SavePartial(T entity, Expression(entity); //just prep. we don't care about inserts here var filter = Builders.Filter.Eq(e => e.ID, entity.ID); - var update = Builders.Update.Combine(Logic.BuildUpdateDefs(entity, members, this, excludeMode)); + var update = Builders.Update.Combine(Logic.BuildUpdateDefs(entity, members, this, excludeMode)); return Session == null ? Collection(collectionName, collection).UpdateOneAsync(filter, update, _updateOptions, cancellation) @@ -34,7 +34,7 @@ private Task> SavePartial(IEnumerable entities, Ex PrepAndCheckIfInsert(ent); //just prep. we don't care about inserts here return new UpdateOneModel( filter: Builders.Filter.Eq(e => e.ID, ent.ID), - update: Builders.Update.Combine(Logic.BuildUpdateDefs(ent, members, this, excludeMode))) + update: Builders.Update.Combine(Logic.BuildUpdateDefs(ent, members, this, excludeMode))) { IsUpsert = true }; }).ToList(); return Session == null @@ -59,6 +59,7 @@ private bool PrepAndCheckIfInsert(T entity) return false; } + /// /// Saves a complete entity replacing an existing entity or creating a new one if it does not exist. /// If ID value is null, a new entity is created. If ID has a value, then existing entity is replaced. @@ -70,7 +71,7 @@ private bool PrepAndCheckIfInsert(T entity) /// /// public Task InsertAsync(T entity, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) - where TId : IComparable, IEquatable + where TId : IComparable, IEquatable where T : IEntity { SetModifiedBySingle(entity); @@ -81,6 +82,19 @@ public Task InsertAsync(T entity, CancellationToken cancellation = defau : Collection(collectionName, collection).InsertOneAsync(Session, entity, null, cancellation); } + /// + /// Saves a complete entity replacing an existing entity or creating a new one if it does not exist. + /// If ID value is null, a new entity is created. If ID has a value, then existing entity is replaced. + /// + /// The type of entity + /// The instance to persist + /// And optional cancellation token + /// + /// + public Task InsertAsync(T entity, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) + where T : IEntity + => InsertAsync(entity, cancellation, collectionName, collection); + /// /// Saves a batch of complete entities replacing an existing entities or creating a new ones if they do not exist. /// If ID value is null, a new entity is created. If ID has a value, then existing entity is replaced. @@ -92,7 +106,7 @@ public Task InsertAsync(T entity, CancellationToken cancellation = defau /// /// public Task> InsertAsync(IEnumerable entities, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) - where TId : IComparable, IEquatable + where TId : IComparable, IEquatable where T : IEntity { SetModifiedByMultiple(entities); @@ -108,5 +122,20 @@ public Task> InsertAsync(IEnumerable entities, Can ? Collection(collectionName, collection).BulkWriteAsync(models, _unOrdBlkOpts, cancellation) : Collection(collectionName, collection).BulkWriteAsync(Session, models, _unOrdBlkOpts, cancellation); } + + /// + /// Saves a batch of complete entities replacing an existing entities or creating a new ones if they do not exist. + /// If ID value is null, a new entity is created. If ID has a value, then existing entity is replaced. + /// + /// The type of entity + /// The entities to persist + /// And optional cancellation token + /// + /// + public Task> InsertAsync(IEnumerable entities, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) + where T : IEntity + { + return InsertAsync(entities, cancellation, collectionName, collection); + } } } diff --git a/MongoDB.Entities/DBContext/DBContext.PagedSearch.cs b/MongoDB.Entities/DBContext/DBContext.PagedSearch.cs index 56e9d1412..625d124b4 100644 --- a/MongoDB.Entities/DBContext/DBContext.PagedSearch.cs +++ b/MongoDB.Entities/DBContext/DBContext.PagedSearch.cs @@ -8,24 +8,18 @@ public partial class DBContext /// Represents an aggregation query that retrieves results with easy paging support. /// /// Any class that implements IEntity - /// ID type - public PagedSearch PagedSearch(string? collectionName = null, IMongoCollection? collection = null) - where TId : IComparable, IEquatable - where T : IEntity + public PagedSearch PagedSearch(string? collectionName = null, IMongoCollection? collection = null) { - return new PagedSearch(this, Collection(collectionName, collection)); + return new PagedSearch(this, Collection(collectionName, collection)); } /// /// Represents an aggregation query that retrieves results with easy paging support. /// /// Any class that implements IEntity - /// ID type /// The type you'd like to project the results to. - public PagedSearch PagedSearch(string? collectionName = null, IMongoCollection? collection = null) - where TId : IComparable, IEquatable - where T : IEntity + public PagedSearch PagedSearch(string? collectionName = null, IMongoCollection? collection = null) { - return new PagedSearch(this, Collection(collectionName, collection)); + return new PagedSearch(this, Collection(collectionName, collection)); } } diff --git a/MongoDB.Entities/DBContext/DBContext.Save.cs b/MongoDB.Entities/DBContext/DBContext.Save.cs index 3e2aca79d..0c6a27b60 100644 --- a/MongoDB.Entities/DBContext/DBContext.Save.cs +++ b/MongoDB.Entities/DBContext/DBContext.Save.cs @@ -12,17 +12,6 @@ namespace MongoDB.Entities { public partial class DBContext { - /// - /// Saves a complete entity replacing an existing entity or creating a new one if it does not exist. - /// If ID value is null, a new entity is created. If ID has a value, then existing entity is replaced. - /// - /// The type of entity - /// The instance to persist - /// And optional cancellation token - /// - /// - public Task SaveAsync(T entity, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) - where T : IEntity => SaveAsync(entity, cancellation, collectionName, collection); /// /// Saves a complete entity replacing an existing entity or creating a new one if it does not exist. @@ -35,8 +24,8 @@ public Task SaveAsync(T entity, CancellationToken cancellation = default, str /// /// public Task SaveAsync(T entity, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) - where TId : IComparable, IEquatable - where T : IEntity + where TId : IComparable, IEquatable + where T : IEntity { SetModifiedBySingle(entity); OnBeforeSave(entity); @@ -53,6 +42,19 @@ public Task SaveAsync(T entity, CancellationToken cancellation = default : collection.ReplaceOneAsync(Session, filter, entity, new ReplaceOptions { IsUpsert = true }, cancellation); } + /// + /// Saves a complete entity replacing an existing entity or creating a new one if it does not exist. + /// If ID value is null, a new entity is created. If ID has a value, then existing entity is replaced. + /// + /// The type of entity + /// The instance to persist + /// And optional cancellation token + /// + /// + public Task SaveAsync(T entity, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) + where T : IEntity => SaveAsync(entity, cancellation, collectionName, collection); + + /// /// Saves a batch of complete entities replacing an existing entities or creating a new ones if they do not exist. /// If ID value is null, a new entity is created. If ID has a value, then existing entity is replaced. @@ -64,7 +66,7 @@ public Task SaveAsync(T entity, CancellationToken cancellation = default /// /// public Task> SaveAsync(IEnumerable entities, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) - where TId : IComparable, IEquatable + where TId : IComparable, IEquatable where T : IEntity { SetModifiedByMultiple(entities); @@ -91,6 +93,22 @@ public Task> SaveAsync(IEnumerable entities, Cance : Collection(collectionName, collection).BulkWriteAsync(Session, models, _unOrdBlkOpts, cancellation); } + + /// + /// Saves a batch of complete entities replacing an existing entities or creating a new ones if they do not exist. + /// If ID value is null, a new entity is created. If ID has a value, then existing entity is replaced. + /// + /// The type of entity + /// The entities to persist + /// And optional cancellation token + /// + /// + public Task> SaveAsync(IEnumerable entities, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) + where T : IEntity + { + return SaveAsync(entities: entities, cancellation: cancellation, collectionName: collectionName, collection: collection); + } + /// /// Saves an entity partially with only the specified subset of properties. /// If ID value is null, a new entity is created. If ID has a value, then existing entity is updated. diff --git a/MongoDB.Entities/Extensions/Collection.cs b/MongoDB.Entities/Extensions/Collection.cs index 1597a3a36..32a67e7be 100644 --- a/MongoDB.Entities/Extensions/Collection.cs +++ b/MongoDB.Entities/Extensions/Collection.cs @@ -9,16 +9,19 @@ public static partial class Extensions /// Gets the IMongoCollection for a given IEntity type. /// TIP: Try never to use this unless really neccessary. /// - /// Any class that implements IEntity - /// Optional tenant prefix if using multi-tenancy - public static IMongoCollection Collection(this T _, string tenantPrefix) where T : IEntity => DB.Collection(tenantPrefix); + /// Any class + /// + /// + /// + public static IMongoCollection Collection(this T _, string? collectionName = null, IMongoCollection? collection = null) + => DB.Context.Collection(collectionName: collectionName, collection: collection); /// /// Gets the collection name for this entity /// - public static string CollectionName(this T _) where T : IEntity + public static string CollectionName(this T _) { - return DB.CollectionName(); + return DB.Context.CollectionName(); } /// diff --git a/MongoDB.Entities/Extensions/Database.cs b/MongoDB.Entities/Extensions/Database.cs index 6e3883e03..849c764d6 100644 --- a/MongoDB.Entities/Extensions/Database.cs +++ b/MongoDB.Entities/Extensions/Database.cs @@ -19,7 +19,7 @@ public static partial class Extensions /// Gets the name of the database this entity is attached to. Returns name of default database if not specifically attached. /// [Obsolete("This method returns the current DatabaseName in the Context")] - public static string DatabaseName(this T _, string tenantPrefix) where T : IEntity => DB.DatabaseName(); + public static string DatabaseName(this T _, string tenantPrefix) => DB.DatabaseName(); /// /// Pings the mongodb server to check if it's still connectable diff --git a/MongoDB.Entities/Extensions/Delete.cs b/MongoDB.Entities/Extensions/Delete.cs index 8e221aa82..a47ee31f4 100644 --- a/MongoDB.Entities/Extensions/Delete.cs +++ b/MongoDB.Entities/Extensions/Delete.cs @@ -13,25 +13,63 @@ public static partial class Extensions /// HINT: If this entity is referenced by one-to-many/many-to-many relationships, those references are also deleted. /// /// - /// /// An optional cancellation token - /// Optional tenant prefix if using multi-tenancy - public static Task DeleteAsync(this T entity, IClientSessionHandle session = null, CancellationToken cancellation = default, string tenantPrefix = null) where T : IEntity + /// + /// + /// + public static Task DeleteAsync(this T entity, CancellationToken cancellation = default, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) + where TId : IEquatable, IComparable + where T : IEntity { - return DB.DeleteAsync(entity.ID, session, cancellation, tenantPrefix); + entity.ThrowIfUnsaved(); + return DB.Context.DeleteAsync(entity.ID!, cancellation, ignoreGlobalFilters: ignoreGlobalFilters, collectionName: collectionName, collection: collection); + } + + /// + /// Deletes a single entity from MongoDB. + /// HINT: If this entity is referenced by one-to-many/many-to-many relationships, those references are also deleted. + /// + /// + /// An optional cancellation token + /// + /// + /// + public static Task DeleteAsync(this T entity, CancellationToken cancellation = default, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) + where T : IEntity + { + return DeleteAsync(entity, cancellation, ignoreGlobalFilters: ignoreGlobalFilters, collectionName: collectionName, collection: collection); + } + + + /// + /// Deletes multiple entities from the database + /// HINT: If these entities are referenced by one-to-many/many-to-many relationships, those references are also deleted. + /// + /// + /// An optional cancellation token + /// + /// + /// + public static Task DeleteAllAsync(this IEnumerable entities, CancellationToken cancellation = default, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) + where T : IEntity + { + return DeleteAllAsync(entities, cancellation: cancellation, ignoreGlobalFilters: ignoreGlobalFilters, collectionName: collectionName, collection: collection); } /// /// Deletes multiple entities from the database /// HINT: If these entities are referenced by one-to-many/many-to-many relationships, those references are also deleted. /// - /// - /// + /// /// An optional cancellation token - /// Optional tenant prefix if using multi-tenancy - public static Task DeleteAllAsync(this IEnumerable entities, IClientSessionHandle session = null, CancellationToken cancellation = default, string tenantPrefix = null) where T : IEntity + /// + /// + /// + public static Task DeleteAllAsync(this IEnumerable entities, CancellationToken cancellation = default, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) + where TId : IEquatable, IComparable + where T : IEntity { - return DB.DeleteAsync(entities.Select(e => e.ID), session, cancellation, tenantPrefix); + return DB.Context.DeleteAsync(IDs: entities.Select(e => e.ID!), cancellation: cancellation, ignoreGlobalFilters: ignoreGlobalFilters, collectionName: collectionName, collection: collection); } } diff --git a/MongoDB.Entities/Extensions/Entity.cs b/MongoDB.Entities/Extensions/Entity.cs index 4c26ec4e2..d6bec53b3 100644 --- a/MongoDB.Entities/Extensions/Entity.cs +++ b/MongoDB.Entities/Extensions/Entity.cs @@ -36,6 +36,7 @@ internal static void ThrowIfUnsaved(this TId? id) where TId : IComparable(this IEntity entity) where TId : IComparable, IEquatable { ThrowIfUnsaved(entity.ID); @@ -77,9 +78,9 @@ public static string FullPath(this Expression> expressi /// /// An IQueryable collection of sibling Entities. /// - public static IMongoQueryable Queryable(this T _, AggregateOptions? options = null, string? tenantPrefix = null) where T : IEntity + public static IMongoQueryable Queryable(this T _, AggregateOptions? options = null, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity { - return DB.Queryable(options); + return DB.Context.Queryable(options, collectionName: collectionName, collection: collection, ignoreGlobalFilters: ignoreGlobalFilters); } /// @@ -167,9 +168,9 @@ string RemoveDiacritics(string text) /// /// An optional cancellation token /// Optional tenant prefix if using multi-tenancy - public static Task NextSequentialNumberAsync(this T _, CancellationToken cancellation = default, string tenantPrefix = null) where T : IEntity + public static Task NextSequentialNumberAsync(this T _, CancellationToken cancellation = default) where T : IEntity { - return DB.NextSequentialNumberAsync(cancellation, tenantPrefix); + return DB.Context.NextSequentialNumberAsync(cancellation); } } } diff --git a/MongoDB.Entities/Extensions/Fluent.cs b/MongoDB.Entities/Extensions/Fluent.cs index 0ea4af8d4..1715d87ec 100644 --- a/MongoDB.Entities/Extensions/Fluent.cs +++ b/MongoDB.Entities/Extensions/Fluent.cs @@ -10,12 +10,13 @@ public static partial class Extensions /// /// /// - /// An optional session if using within a transaction /// The options for the aggregation. This is not required. - /// Optional tenant prefix if using multi-tenancy - public static IAggregateFluent Fluent(this T _, IClientSessionHandle session = null, AggregateOptions options = null, string tenantPrefix = null) where T : IEntity + /// + /// + /// + public static IAggregateFluent Fluent(this T _, AggregateOptions? options = null, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity { - return DB.Fluent(options, session, tenantPrefix); + return DB.Context.Fluent(options, ignoreGlobalFilters: ignoreGlobalFilters, collection: collection, collectionName: collectionName); } /// diff --git a/MongoDB.Entities/Extensions/Insert.cs b/MongoDB.Entities/Extensions/Insert.cs index 089ed32f1..483a88686 100644 --- a/MongoDB.Entities/Extensions/Insert.cs +++ b/MongoDB.Entities/Extensions/Insert.cs @@ -11,24 +11,26 @@ public static partial class Extensions /// Inserts a new entity into the colleciton. /// /// - /// Optional tenant prefix if using multi-tenancy - /// An optional session if using within a transaction /// An optional cancellation token - public static Task InsertAsync(this T entity, string tenantPrefix, IClientSessionHandle session = null, CancellationToken cancellation = default) where T : IEntity + /// + /// + public static Task InsertAsync(this T entity, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) + where T : IEntity { - return DB.InsertAsync(entity, session, cancellation, tenantPrefix); + return DB.Context.InsertAsync(entity, cancellation, collectionName, collection); } /// /// Inserts a batch of new entities into the collection. /// /// - /// Optional tenant prefix if using multi-tenancy - /// An optional session if using within a transaction /// An optional cancellation token - public static Task> InsertAsync(this IEnumerable entities, string tenantPrefix, IClientSessionHandle session = null, CancellationToken cancellation = default) where T : IEntity + /// + /// + public static Task> InsertAsync(this IEnumerable entities, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) + where T : IEntity { - return DB.InsertAsync(entities, session, cancellation, tenantPrefix); + return DB.Context.InsertAsync(entities, cancellation, collectionName: collectionName, collection: collection); } } diff --git a/MongoDB.Entities/Extensions/PagedSearch.cs b/MongoDB.Entities/Extensions/PagedSearch.cs index 6c8f09513..0a8b50745 100644 --- a/MongoDB.Entities/Extensions/PagedSearch.cs +++ b/MongoDB.Entities/Extensions/PagedSearch.cs @@ -9,18 +9,18 @@ public static partial class Extensions /// /// Any class that implements IEntity /// The type of the resulting projection - public static PagedSearch PagedSearch(this IAggregateFluent aggregate, string tenantPrefix) where T : IEntity + public static PagedSearch PagedSearch(this IAggregateFluent aggregate, string? collectionName = null, IMongoCollection? collection = null) { - return DB.PagedSearch(tenantPrefix: tenantPrefix).WithFluent(aggregate); + return DB.Context.PagedSearch(collectionName: collectionName, collection: collection).WithFluent(aggregate); } /// /// Starts a paged search pipeline for this fluent pipeline /// /// Any class that implements IEntity - public static PagedSearch PagedSearch(this IAggregateFluent aggregate, string tenantPrefix) where T : IEntity + public static PagedSearch PagedSearch(this IAggregateFluent aggregate, string? collectionName = null, IMongoCollection? collection = null) { - return DB.PagedSearch(tenantPrefix: tenantPrefix).WithFluent(aggregate); + return DB.Context.PagedSearch(collectionName: collectionName, collection: collection).WithFluent(aggregate); } } } diff --git a/MongoDB.Entities/Extensions/Save.cs b/MongoDB.Entities/Extensions/Save.cs index 5ef05d47f..6d9aa5417 100644 --- a/MongoDB.Entities/Extensions/Save.cs +++ b/MongoDB.Entities/Extensions/Save.cs @@ -14,12 +14,13 @@ public static partial class Extensions /// If ID value is null, a new entity is created. If ID has a value, then existing entity is replaced. /// /// - /// An optional session if using within a transaction /// An optional cancellation token - /// Optional tenant prefix if using multi-tenancy - public static Task SaveAsync(this T entity, IClientSessionHandle session = null, CancellationToken cancellation = default, string tenantPrefix = null) where T : IEntity + /// + /// + public static Task SaveAsync(this T entity, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) + where T : IEntity { - return DB.SaveAsync(entity, session, cancellation, tenantPrefix); + return DB.Context.SaveAsync(entity, cancellation: cancellation, collectionName: collectionName, collection: collection); } /// @@ -27,12 +28,13 @@ public static Task SaveAsync(this T entity, IClientSessionHandle session = nu /// If ID value is null, a new entity is created. If ID has a value, then existing entity is replaced. /// /// - /// An optional session if using within a transaction /// An optional cancellation token - /// Optional tenant prefix if using multi-tenancy - public static Task> SaveAsync(this IEnumerable entities, IClientSessionHandle session = null, CancellationToken cancellation = default, string tenantPrefix = null) where T : IEntity + /// + /// + public static Task> SaveAsync(this IEnumerable entities, CancellationToken cancellation = default, string? collectionName = null, IMongoCollection? collection = null) + where T : IEntity { - return DB.SaveAsync(entities, session, cancellation, tenantPrefix); + return DB.Context.SaveAsync(entities: entities, cancellation, collectionName: collectionName, collection: collection); } /// diff --git a/MongoDB.Entities/Relationships/Many.Queryable.cs b/MongoDB.Entities/Relationships/Many.Queryable.cs index 26b0f392e..b1834440e 100644 --- a/MongoDB.Entities/Relationships/Many.Queryable.cs +++ b/MongoDB.Entities/Relationships/Many.Queryable.cs @@ -49,7 +49,7 @@ public IMongoQueryable ParentsQueryable(IEnumerable ch return JoinQueryable(session, options) .Where(j => childIDs.Contains(j.ParentID)) .Join( - DB.Collection(null), + DB.Context.Collection(null), j => j.ChildID, p => p.ID, (_, p) => p) @@ -60,7 +60,7 @@ public IMongoQueryable ParentsQueryable(IEnumerable ch return JoinQueryable(session, options) .Where(j => childIDs.Contains(j.ChildID)) .Join( - DB.Collection(null), + DB.Context.Collection(null), j => j.ParentID, p => p.ID, (_, p) => p) @@ -125,7 +125,7 @@ public IMongoQueryable ChildrenQueryable(IClientSessionHandle session = return JoinQueryable(session, options) .Where(j => j.ChildID == parent.ID) .Join( - DB.Collection(null), + DB.Context.Collection(null), j => j.ParentID, c => c.ID, (_, c) => c); @@ -135,7 +135,7 @@ public IMongoQueryable ChildrenQueryable(IClientSessionHandle session = return JoinQueryable(session, options) .Where(j => j.ParentID == parent.ID) .Join( - DB.Collection(null), + DB.Context.Collection(null), j => j.ChildID, c => c.ID, (_, c) => c);