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/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 8fb4e563c..560d05d4b 100644 --- a/MongoDB.Entities/Builders/Distinct.cs +++ b/MongoDB.Entities/Builders/Distinct.cs @@ -1,209 +1,61 @@ -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 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 { + public DBContext Context { get; } + public IMongoCollection Collection { get; } + + 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. + /// Run the Distinct command in MongoDB server and get a cursor instead of materialized results /// - /// 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 + /// An optional cancellation token + public Task> ExecuteCursorAsync(CancellationToken cancellation = default) { - 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; - } - - /// - /// 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) - { - this.filter &= filter(Builders.Filter); - return this; - } - - /// - /// Specify the matching criteria with a lambda expression - /// - /// x => x.Property == Value - public Distinct Match(Expression> expression) - { - return Match(f => f.Where(expression)); - } - - /// - /// 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; - } - - 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 Distinct 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 Distinct MatchString(string jsonString) - { - filter &= jsonString; - return this; - } - - /// - /// Specify the matching criteria with an aggregation expression (i.e. $expr) - /// - /// { $gt: ['$Property1', '$Property2'] } - public Distinct MatchExpression(string expression) - { - filter &= "{$expr:" + expression + "}"; - return this; - } - - /// - /// Specify the matching criteria with a Template - /// - /// A Template object - public Distinct MatchExpression(Template template) - { - filter &= "{$expr:" + template.RenderToString() + "}"; - return this; - } - - /// - /// Specify an option for this find command (use multiple times if needed) - /// - /// x => x.OptionName = OptionValue - public Distinct Option(Action option) - { - option(options); - return this; - } - - /// - /// Specify that this operation should ignore any global filters - /// - public Distinct IgnoreGlobalFilters() - { - ignoreGlobalFilters = true; - return this; - } + if (_field == null) + throw new InvalidOperationException("Please use the .Property() method to specify the field to use for obtaining unique values for!"); - /// - /// 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!"); - - var mergedFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, filter); + var mergedFilter = MergedFilter; - 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); + } - /// - /// 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) + /// + /// 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/FilterQuery.Extensions.cs b/MongoDB.Entities/Builders/FilterQuery.Extensions.cs new file mode 100644 index 000000000..2c2b0622f --- /dev/null +++ b/MongoDB.Entities/Builders/FilterQuery.Extensions.cs @@ -0,0 +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 +{ + + /// + /// Specify an IEntity ID as the matching criteria + /// + /// the query + /// A unique IEntity ID + public static TSelf MatchID(this IFilterBuilder self, TId id) + where TId : IComparable, IEquatable + where TEntity : IEntity + where TSelf : IFilterBuilder + => 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 IFilterBuilder self, string id) + where TEntity : IEntity + where TSelf : IFilterBuilder + => MatchID(self, id); + + + + + + /// + /// Specify an IEntity ID as the matching criteria + /// + /// the query + /// A unique IEntity ID + public static TSelf Match(this IFilterBuilder self, TId id) + where TId : IComparable, IEquatable + where TEntity : IEntity + where TSelf : IFilterBuilder + => 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 new file mode 100644 index 000000000..cd389872b --- /dev/null +++ b/MongoDB.Entities/Builders/FilterQuery.Interface.cs @@ -0,0 +1,83 @@ +namespace MongoDB.Entities; + +public interface IFilterBuilder + where TSelf : IFilterBuilder +{ + internal bool IsIgnoreGlobalFilters { get; } + 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 + /// + 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..a6c147a8e --- /dev/null +++ b/MongoDB.Entities/Builders/FilterQuery.cs @@ -0,0 +1,106 @@ +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; + internal FilterDefinition MergedFilter => (this as IFilterBuilder).MergedFilter; + + 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/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 6405f7c39..64cc6cb9b 100644 --- a/MongoDB.Entities/Builders/Find.cs +++ b/MongoDB.Entities/Builders/Find.cs @@ -1,461 +1,184 @@ -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; + +/// +/// 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 +{ + internal Find(DBContext context, IMongoCollection collection) + : base(context, collection) { } + + internal Find(DBContext context, IMongoCollection collection, FindBase> baseQuery) + : base(context, collection, baseQuery) { } +} -namespace MongoDB.Entities + +/// +/// 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>, ICollectionRelated { + /// - /// 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 + /// copy constructor /// - /// Any class that implements IEntity - public class Find : Find where T : IEntity + /// + /// + /// + internal Find(DBContext context, IMongoCollection collection, FindBase> other) : base(other) { - internal Find( - IClientSessionHandle session, - Dictionary globalFilters, string tenantPrefix) - : base(session, globalFilters, tenantPrefix) { } + Context = context; + Collection = 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 where T : IEntity + internal Find(DBContext context, IMongoCollection collection) : base(context.GlobalFilters) { - 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 Find( - IClientSessionHandle session, - Dictionary globalFilters, string tenantPrefix) - { - this.session = session; - this.globalFilters = globalFilters; - this.tenantPrefix = tenantPrefix; - } - - /// - /// 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); - } - - /// - /// Specify an IEntity ID as the matching criteria - /// - /// A unique IEntity ID - public Find 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 Find 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 Find 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 Find Match(Func, FilterDefinition> filter) - { - this.filter &= filter(Builders.Filter); - return this; - } - - /// - /// Specify the matching criteria with a filter definition - /// - /// A filter definition - public Find Match(FilterDefinition filterDefinition) - { - filter &= filterDefinition; - return this; - } - - /// - /// Specify the matching criteria with a template - /// - /// A Template with a find query - public Find 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 Find 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 Find 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 Find MatchString(string jsonString) - { - filter &= jsonString; - return this; - } - - /// - /// Specify the matching criteria with an aggregation expression (i.e. $expr) - /// - /// { $gt: ['$Property1', '$Property2'] } - public Find MatchExpression(string expression) - { - filter &= "{$expr:" + expression + "}"; - return this; - } - - /// - /// Specify the matching criteria with a Template - /// - /// A Template object - public Find 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 Find 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 Find SortByTextScore() - { - return SortByTextScore(null); - } + Context = context; + Collection = collection; + } - /// - /// 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 Find SortByTextScore(Expression> scoreProperty) - { - switch (scoreProperty) - { - case null: - AddTxtScoreToProjection("_Text_Match_Score_"); - return Sort(s => s.MetaTextScore("_Text_Match_Score_")); + public override DBContext Context { get; } + public IMongoCollection Collection { get; private set; } - 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 Find Sort(Func, SortDefinition> sortFunction) - { - sorts.Add(sortFunction(Builders.Sort)); - return this; - } - /// - /// Specify how many entities to skip - /// - /// The number to skip - public Find Skip(int skipCount) - { - options.Skip = skipCount; - return this; - } - /// - /// Specify how many entities to Take/Limit - /// - /// The number to limit/take - public Find 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 Find Project(Expression> expression) - { - return Project(p => p.Expression(expression)); - } + /// + /// 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); + } - /// - /// Specify how to project the results using a projection expression - /// - /// p => p.Include("Prop1").Exclude("Prop2") - public Find Project(Func, ProjectionDefinition> projection) - { - options.Projection = projection(Builders.Projection); - return this; - } + /// + /// 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); + } - /// - /// Specify how to project the results using an exclusion projection expression. - /// - /// x => new { x.PropToExclude, x.AnotherPropToExclude } - public Find ProjectExcluding(Expression> exclusion) + /// + /// 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)) { - var props = (exclusion.Body as NewExpression)?.Arguments - .Select(a => a.ToString().Split('.')[1]); - - if (!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) + while (await cursor.MoveNextAsync(cancellation).ConfigureAwait(false)) { - defs.Add(Builders.Projection.Exclude(prop)); + list.AddRange(cursor.Current); } - - options.Projection = Builders.Projection.Combine(defs); - - return this; } + return list; + } - /// - /// 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() - { - if (typeof(T) != typeof(TProjection)) - throw new InvalidOperationException("IncludeRequiredProps() cannot be used when projecting to a different type."); + /// + /// 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(); + } - options.Projection = Cache.CombineWithRequiredProps(options.Projection); - return this; - } + /// + /// 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 + } - /// - /// Specify an option for this find command (use multiple times if needed) - /// - /// x => x.OptionName = OptionValue - public Find Option(Action> option) - { - option(options); - return this; - } - /// - /// Specify that this operation should ignore any global filters - /// - public Find IgnoreGlobalFilters() - { - ignoreGlobalFilters = true; - return this; - } - /// - /// 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 cursor instead of materialized results + /// + /// An optional cancellation token + public Task> ExecuteCursorAsync(CancellationToken cancellation = default) + { + if (_sorts.Count > 0) + _options.Sort = Builders.Sort.Combine(_sorts); - /// - /// 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(); - } + var mergedFilter = Logic.MergeWithGlobalFilter(_ignoreGlobalFilters, _globalFilters, _filter); - /// - /// 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 - } + return this.Session() is not IClientSessionHandle session ? + 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) + /// + /// 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(x => x.ID)); - Limit(1); - using var cursor = await ExecuteCursorAsync(cancellation).ConfigureAwait(false); - await cursor.MoveNextAsync(cancellation).ConfigureAwait(false); - return cursor.Current.Any(); + 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(); + } - /// - /// 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) - { - if (sorts.Count > 0) - options.Sort = Builders.Sort.Combine(sorts); - - 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" } }); - } - } +public static class FindExt +{ - public enum Order + /// + /// 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 Find self, TId ID, CancellationToken cancellation = default) + where TId : IComparable, IEquatable + where TEntity : IEntity { - Ascending, - Descending + self.MatchID(ID); + return self.ExecuteSingleAsync(cancellation); } +} +public enum Order +{ + Ascending, + Descending +} - public enum Search - { - Fuzzy, - Full - } +public enum Search +{ + Fuzzy, + Full } diff --git a/MongoDB.Entities/Builders/ICollectionRelated.cs b/MongoDB.Entities/Builders/ICollectionRelated.cs new file mode 100644 index 000000000..3b28b998c --- /dev/null +++ b/MongoDB.Entities/Builders/ICollectionRelated.cs @@ -0,0 +1,16 @@ +namespace MongoDB.Entities; + +internal interface ICollectionRelated +{ + public DBContext Context { get; } + public IMongoCollection Collection { get; } +} +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 fab18e129..67610a180 100644 --- a/MongoDB.Entities/Builders/Index.cs +++ b/MongoDB.Entities/Builders/Index.cs @@ -1,188 +1,174 @@ -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 +public class Index : ICollectionRelated, IIndexBuilder> { + 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 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>(); - private readonly CreateIndexOptions options = new() { Background = true }; - private readonly string tenantPrefix; + if (Keys.Count == 0) throw new ArgumentException("Please define keys before calling this method."); - internal Index(string tenantPrefix) - { - this.tenantPrefix = tenantPrefix; - } + 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 DB.Collection(tenantPrefix).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 DB.Collection(tenantPrefix).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 DB.Collection(tenantPrefix).Indexes.CreateOneAsync(model, cancellationToken: cancellation); - } + return _options.Name; } - internal class Key where T : IEntity + + 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; - if (expression.Body.NodeType == ExpressionType.Parameter && type == KeyType.Text) - { - PropertyName = "$**"; - return; - } + public Index Key(Expression> propertyToIndex, KeyType type) + { + Keys.Add(new Key(propertyToIndex, type)); + return this; + } - 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 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); + } - PropertyName = expression.FullPath(); - } + /// + /// 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); + } + + private Task CreateAsync(CreateIndexModel model, CancellationToken cancellation = default) + { + return Collection.Indexes.CreateOneAsync(model, cancellationToken: cancellation); } +} + +internal class Key +{ + 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 cb41fbfde..b6ebb795a 100644 --- a/MongoDB.Entities/Builders/PagedSearch.cs +++ b/MongoDB.Entities/Builders/PagedSearch.cs @@ -1,408 +1,269 @@ -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 interface IPagedSearchBuilder : IProjectionBuilder + where TSelf : IPagedSearchBuilder { - /// - /// Represents an aggregation query that retrieves results with easy paging support. - /// - /// Any class that implements IEntity - public class PagedSearch : PagedSearch where T : IEntity + +} +public abstract class PagedSearchBase : SortFilterQueryBase, IPagedSearchBuilder + where TSelf : PagedSearchBase +{ + internal PagedSearchBase(PagedSearchBase other) : base(other) { - internal PagedSearch( - IClientSessionHandle session, - Dictionary globalFilters, - string tenantPrefix) - : base(session, globalFilters, 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; + /// - /// Represents an aggregation query that retrieves results with easy paging support. + /// 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() /// - /// Any class that implements IEntity - /// The type you'd like to project the results to. - public class PagedSearch where T : IEntity + /// The type of the input pipeline + /// The input IAggregateFluent pipeline + public TSelf WithFluent(TFluent fluentPipeline) where TFluent : IAggregateFluent { - 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) - { - var type = typeof(TProjection); - if (type.IsPrimitive || type.IsValueType || (type == typeof(string))) - throw new NotSupportedException("Projecting to primitive types is not supported!"); + _fluentPipeline = fluentPipeline; + return This; + } - this.session = session; - this.globalFilters = globalFilters; - this.tenantPrefix = tenantPrefix; - } - /// - /// 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 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; - } + /// + /// 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 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) + /// + /// 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) { - 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 - })); - } + case null: + AddTxtScoreToProjection("_Text_Match_Score_"); + return Sort(s => s.MetaTextScore("_Text_Match_Score_")); - /// - /// 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) - { - return Match(f => f.Near(coordinatesProperty, nearCoordinates.ToGeoJsonPoint(), maxDistance, minDistance)); + default: + AddTxtScoreToProjection(Prop.Path(scoreProperty)); + return Sort(s => s.MetaTextScore(Prop.Path(scoreProperty))); } + } - /// - /// Specify the matching criteria with a JSON string - /// - /// { Title : 'The Power Of Now' } - public PagedSearch MatchString(string jsonString) + private void AddTxtScoreToProjection(string fieldName) + { + if (_projectionStage == null) { - filter &= jsonString; - return this; + _projectionStage = $"{{ $set : {{ {fieldName} : {{ $meta : 'textScore' }} }} }}"; + return; } - /// - /// Specify the matching criteria with an aggregation expression (i.e. $expr) - /// - /// { $gt: ['$Property1', '$Property2'] } - public PagedSearch MatchExpression(string expression) - { - filter &= "{$expr:" + expression + "}"; - return this; - } + var renderedStage = _projectionStage.Render( + BsonSerializer.SerializerRegistry.GetSerializer(), + BsonSerializer.SerializerRegistry); - /// - /// Specify the matching criteria with a Template - /// - /// A Template object - public PagedSearch MatchExpression(Template template) - { - filter &= "{$expr:" + template.RenderToString() + "}"; - return this; - } + renderedStage.Document["$project"][fieldName] = new BsonDocument { { "$meta", "textScore" } }; - /// - /// 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)); + _projectionStage = renderedStage.Document; + } - 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() - { - return SortByTextScore(null); - } + /// + /// Specify the page number to get + /// + /// The page number + public TSelf PageNumber(int pageNumber) + { + this._pageNumber = pageNumber; + 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 PagedSearch 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 the number of items per page + /// + /// The size of a page + public TSelf PageSize(int pageSize) + { + this._pageSize = pageSize; + return This; + } - private void AddTxtScoreToProjection(string fieldName) - { - if (projectionStage == null) - { - projectionStage = $"{{ $set : {{ {fieldName} : {{ $meta : 'textScore' }} }} }}"; - return; - } + /// + /// 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 renderedStage = projectionStage.Render( - BsonSerializer.SerializerRegistry.GetSerializer(), - BsonSerializer.SerializerRegistry); + /// + /// 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; + } - renderedStage.Document["$project"][fieldName] = new BsonDocument { { "$meta", "textScore" } }; + /// + /// 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 = renderedStage.Document; - } + if (props == null || !props.Any()) + throw new ArgumentException("Unable to get any properties from the exclusion expression!"); - /// - /// 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; - } + var defs = new List>(props.Count()); - /// - /// Specify the page number to get - /// - /// The page number - public PagedSearch PageNumber(int pageNumber) + foreach (var prop in props) { - this.pageNumber = pageNumber; - return this; + defs.Add(Builders.Projection.Exclude(prop)); } - /// - /// Specify the number of items per page - /// - /// The size of a page - public PagedSearch PageSize(int pageSize) - { - this.pageSize = pageSize; - return this; - } + _projectionStage = PipelineStageDefinitionBuilder.Project(Builders.Projection.Combine(defs)); - /// - /// Specify how to project the results using a lambda expression - /// - /// x => new Test { PropName = x.Prop } - public PagedSearch Project(Expression> expression) - { - projectionStage = PipelineStageDefinitionBuilder.Project(expression); - return this; - } + return This; + } - /// - /// Specify how to project the results using a projection expression - /// - /// p => p.Include("Prop1").Exclude("Prop2") - public PagedSearch Project(Func, ProjectionDefinition> projection) - { - projectionStage = PipelineStageDefinitionBuilder.Project(projection(Builders.Projection)); - 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 how to project the results using an exclusion projection expression. - /// - /// x => new { x.PropToExclude, x.AnotherPropToExclude } - public PagedSearch ProjectExcluding(Expression> exclusion) - { - var props = (exclusion.Body as NewExpression)?.Arguments - .Select(a => a.ToString().Split('.')[1]); - if (!props.Any()) - throw new ArgumentException("Unable to get any properties from the exclusion expression!"); +} - var defs = new List>(props.Count()); +/// +/// Represents an aggregation query that retrieves results with easy paging support. +/// +/// Any class that implements IEntity +public class PagedSearch : PagedSearch +{ + internal PagedSearch( + DBContext context, IMongoCollection collection) + : base(context, collection) { } +} - foreach (var prop in props) - { - defs.Add(Builders.Projection.Exclude(prop)); - } +/// +/// 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>, ICollectionRelated +{ - projectionStage = PipelineStageDefinitionBuilder.Project(Builders.Projection.Combine(defs)); + public DBContext Context { get; set; } + public IMongoCollection Collection { get; set; } - return this; - } + 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; + } - /// - /// Specify an option for this find command (use multiple times if needed) - /// - /// x => x.OptionName = OptionValue - public PagedSearch Option(Action option) - { - option(options); - return this; - } - /// - /// Specify that this operation should ignore any global filters - /// - public PagedSearch IgnoreGlobalFilters() - { - ignoreGlobalFilters = true; - return this; - } + 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 - /// - /// 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!"); + /// + /// 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 pipelineStages = new List(4); + var pipelineStages = new List(4); - if (sorts.Count == 0) - throw new InvalidOperationException("Paging without sorting is a sin!"); - else - pipelineStages.Add(PipelineStageDefinitionBuilder.Sort(Builders.Sort.Combine(sorts))); + 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)); + 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); + var resultsFacet = AggregateFacet.Create("_results", pipelineStages); - var countFacet = AggregateFacet.Create("_count", - PipelineDefinition.Create(new[] - { + var countFacet = AggregateFacet.Create("_count", + PipelineDefinition.Create(new[] + { PipelineStageDefinitionBuilder.Count() - })); + })); - AggregateFacetResults facetResult; + AggregateFacetResults facetResult; - if (fluentPipeline == null) //.Match() used - { - 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); - } - 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); + 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); } } diff --git a/MongoDB.Entities/Builders/Replace.cs b/MongoDB.Entities/Builders/Replace.cs index b05408e34..fa9b34e73 100644 --- a/MongoDB.Entities/Builders/Replace.cs +++ b/MongoDB.Entities/Builders/Replace.cs @@ -1,272 +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 { - /// - /// 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 - public class Replace 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) { - 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; - - internal Replace( - IClientSessionHandle session, - 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; - } + Context = context; + Collection = collection; + _modifiedBy = context.ModifiedBy; + _onSaveAction = onSaveAction; + } - /// - /// 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) - { - if (searchType == Search.Fuzzy) - { - searchTerm = searchTerm.ToDoubleMetaphoneHash(); - caseSensitive = false; - diacriticSensitive = false; - language = null; - } + /// + /// 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 (entity.ID is null) + throw new InvalidOperationException("Cannot replace an entity with an empty ID value!"); - return Match( - f => f.Text( - searchTerm, - new TextSearchOptions - { - CaseSensitive = caseSensitive, - DiacriticSensitive = diacriticSensitive, - Language = language - })); - } + _onSaveAction?.Invoke(entity); - /// - /// 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)); - } + _entity = entity; + return this; + } - /// - /// Specify the matching criteria with a JSON string - /// - /// { Title : 'The Power Of Now' } - public Replace MatchString(string jsonString) - { - filter &= jsonString; - 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) + { + option(_options); + 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; - } + /// + /// 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) + { + Collation = _options.Collation, + Hint = _options.Hint, + IsUpsert = _options.IsUpsert + }); + _filter = Builders.Filter.Empty; + _entity = default; + _options = new ReplaceOptions(); + return this; + } - /// - /// 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) + /// + /// Run the replace command in MongoDB. + /// + /// An optional cancellation token + public async Task ExecuteAsync(CancellationToken cancellation = default) + { + if (_models.Count > 0) { - if (string.IsNullOrEmpty(entity.ID)) - throw new InvalidOperationException("Cannot replace an entity with an empty ID value!"); + var bulkWriteResult = await ( + this.Session() is not IClientSessionHandle session + ? Collection.BulkWriteAsync(_models, null, cancellation) + : Collection.BulkWriteAsync(session, _models, null, cancellation) + ).ConfigureAwait(false); - onSaveAction?.Invoke(entity); + _models.Clear(); - this.entity = entity; + if (!bulkWriteResult.IsAcknowledged) + return ReplaceOneResult.Unacknowledged.Instance; - this.entity.SetTenantPrefixOnFileEntity(tenantPrefix); - - return this; + return new ReplaceOneResult.Acknowledged(bulkWriteResult.MatchedCount, bulkWriteResult.ModifiedCount, null); } - - /// - /// 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) + else { - 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 = 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(); - 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 ( - session == null - ? DB.Collection(tenantPrefix).BulkWriteAsync(models, null, cancellation) - : DB.Collection(tenantPrefix).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 = 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(); - - 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() + private void SetModOnAndByValues() + { + var cache = Context.Cache(); + if (cache.HasModifiedOn && _entity is IModifiedOn _entityModifiedOn) _entityModifiedOn.ModifiedOn = DateTime.UtcNow; + if (cache.ModifiedByProp != null && _modifiedBy != null) { - if (Cache.HasModifiedOn) ((IModifiedOn)entity).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.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 new file mode 100644 index 000000000..44ab11a56 --- /dev/null +++ b/MongoDB.Entities/Builders/SortFilterQueryBase.cs @@ -0,0 +1,47 @@ +namespace MongoDB.Entities; +public abstract class SortFilterQueryBase : FilterQueryBase, + ISortBuilder + where TSelf : SortFilterQueryBase +{ + internal List> _sorts = new(); + private TSelf This => (TSelf)this; + + List> ISortBuilder.Sorts => _sorts; + + 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) + { + 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 6291c3ada..29c74ddbc 100644 --- a/MongoDB.Entities/Builders/Update.cs +++ b/MongoDB.Entities/Builders/Update.cs @@ -1,491 +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 +namespace MongoDB.Entities; + +public abstract class UpdateBase : FilterQueryBase + where TId : IComparable, IEquatable + where T : IEntity + where TSelf : UpdateBase { - public abstract class UpdateBase where T : IEntity + protected readonly List> defs; + protected readonly Action? onUpdateAction; + + public abstract DBContext Context { get; } + private EntityCache? _cache; + internal EntityCache Cache() => _cache ??= Context.Cache(); + + internal UpdateBase(UpdateBase other) : base(other) { - //note: this base class exists for facilating the OnBeforeUpdate custom hook of DBContext class - // there's no other purpose for this. + 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(); + } - protected readonly List> defs = new(); + /// + /// 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 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 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 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 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 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 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()); - } - protected void SetTenantDbOnFileEntities(string tenantPrefix) - { - if (Cache.IsFileEntity) - { - defs.Add(Builders.Update.Set( - nameof(FileEntity.TenantPrefix), - Cache.Collection(tenantPrefix).Database.DatabaseNamespace.DatabaseName)); - } - } + /// + /// 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; } /// - /// 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. + /// Specify the update definition builder operation to modify the Entities (use multiple times if needed) /// - /// Any class that implements IEntity - public class Update : UpdateBase where T : IEntity + /// b => b.Inc(x => x.PropName, Value) + /// + public TSelf Modify(Func, UpdateDefinition> operation) { - 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; - - internal Update( - IClientSessionHandle session, - Dictionary globalFilters, - Action> onUpdateAction, - string tenantPrefix) - { - this.session = session; - this.globalFilters = globalFilters; - this.onUpdateAction = onUpdateAction; - this.tenantPrefix = tenantPrefix; - } + AddModification(operation); + return This; + } - /// - /// 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 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 matching criteria with a lambda expression - /// - /// x => x.Property == Value - public Update Match(Expression> expression) - { - return Match(f => f.Where(expression)); - } + /// + /// 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 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; - } + /// + /// 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 the matching criteria with a filter definition - /// - /// A filter definition - public Update Match(FilterDefinition filterDefinition) - { - filter &= filterDefinition; - return this; - } + if (Cache().HasModifiedOn) ((IModifiedOn)entity).ModifiedOn = DateTime.UtcNow; + defs.AddRange(Logic.BuildUpdateDefs(entity, Context)); + 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; - } + /// + /// 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, Context)); + 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 - })); - } + /// + /// 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; + } +} - /// - /// 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)); - } +/// +/// 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(); - /// - /// Specify the matching criteria with a JSON string - /// - /// { Title : 'The Power Of Now' } - public Update MatchString(string jsonString) - { - filter &= jsonString; - return this; - } + internal Update(DBContext context, IMongoCollection collection, UpdateBase> other) : base(other) + { + Context = context; + Collection = collection; + } - /// - /// Specify the matching criteria with an aggregation expression (i.e. $expr) - /// - /// { $gt: ['$Property1', '$Property2'] } - public Update MatchExpression(string expression) - { - filter &= "{$expr:" + expression + "}"; - return this; - } + internal Update(DBContext context, IMongoCollection collection, Action>? onUpdateAction, List>? defs = null) : base(context.GlobalFilters, onUpdateAction, defs) + { + Context = context; + Collection = collection; + } - /// - /// Specify the matching criteria with a Template - /// - /// A Template object - public Update MatchExpression(Template template) - { - filter &= "{$expr:" + template.RenderToString() + "}"; - return this; - } + public override DBContext Context { get; } + public IMongoCollection Collection { get; } - /// - /// 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 Update 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 Update Modify(Func, UpdateDefinition> operation) + /// + /// 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()) { - AddModification(operation); - return this; + _stages.Add(stage); } - /// - /// Specify an update (json string) to modify the Entities (use multiple times if needed) - /// - /// { $set: { 'RootProp.$[x].SubProp' : 321 } } - public Update 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) - { - AddModification(template.RenderToString()); - 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) + { + _stages.Add(stage); + 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) - { - if (Cache.HasModifiedOn) ((IModifiedOn)entity).ModifiedOn = DateTime.UtcNow; - defs.AddRange(Logic.BuildUpdateDefs(entity)); - 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()); + } - /// - /// 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 Update ModifyOnly(Expression> members, T entity) - { - if (Cache.HasModifiedOn) ((IModifiedOn)entity).ModifiedOn = DateTime.UtcNow; - defs.AddRange(Logic.BuildUpdateDefs(entity, members)); - 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; - /// - /// 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 Update ModifyExcept(Expression> members, T entity) - { - if (Cache.HasModifiedOn) ((IModifiedOn)entity).ModifiedOn = DateTime.UtcNow; - defs.AddRange(Logic.BuildUpdateDefs(entity, members, excludeMode: true)); - return this; - } + _options.ArrayFilters = + _options.ArrayFilters == null + ? new[] { def } + : _options.ArrayFilters.Concat(new[] { def }); - /// - /// 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; + } - return this; - } + /// + /// Specify a single array filter using a Template to target nested entities for updates + /// + /// + public Update WithArrayFilter(Template template) + { + WithArrayFilter(template.RenderToString()); + 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) - { - stages.Add(stage); - 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 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()); - } + _options.ArrayFilters = + _options.ArrayFilters == null + ? defs + : _options.ArrayFilters.Concat(defs); - /// - /// 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; + return this; + } - options.ArrayFilters = - options.ArrayFilters == null - ? new[] { def } - : options.ArrayFilters.Concat(new[] { def }); + /// + /// 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; + } - return this; - } - /// - /// Specify a single array filter using a Template to target nested entities for updates - /// - /// - public Update WithArrayFilter(Template template) - { - WithArrayFilter(template.RenderToString()); - 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(); + /// + /// 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 (ShouldSetModDate()) 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; + } - options.ArrayFilters = - options.ArrayFilters == null - ? defs - : options.ArrayFilters.Concat(defs); + /// + /// Run the update 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); - return this; - } + _models.Clear(); - /// - /// 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; - } + if (!bulkWriteResult.IsAcknowledged) + return UpdateResult.Unacknowledged.Instance; - /// - /// Specify that this operation should ignore any global filters - /// - public Update IgnoreGlobalFilters() - { - ignoreGlobalFilters = true; - return this; + return new UpdateResult.Acknowledged(bulkWriteResult.MatchedCount, bulkWriteResult.ModifiedCount, null); } - - /// - /// Queue up an update command for bulk execution later. - /// - public Update AddToQueue() + 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 Modify() method first!"); - if (ShouldSetModDate()) Modify(b => b.CurrentDate(Cache.ModifiedOnPropName)); - SetTenantDbOnFileEntities(tenantPrefix); - 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; - } + 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)); - /// - /// Run the update command in MongoDB. - /// - /// An optional cancellation token - public async Task ExecuteAsync(CancellationToken cancellation = default) - { - if (models.Count > 0) - { - var bulkWriteResult = await ( - session == null - ? DB.Collection(tenantPrefix).BulkWriteAsync(models, null, cancellation) - : DB.Collection(tenantPrefix).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 = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, filter); - 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); - } + onUpdateAction?.Invoke(this); + return await UpdateAsync(mergedFilter, Builders.Update.Combine(defs), _options, cancellation).ConfigureAwait(false); } + } - /// - /// Run the update command with pipeline stages - /// - /// An optional cancellation token - public Task ExecutePipelineAsync(CancellationToken cancellation = default) - { - var mergedFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, filter); - 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() }} }}"); - SetTenantDbOnFileEntities(tenantPrefix); - - return UpdateAsync( - mergedFilter, - Builders.Update.Pipeline(stages.ToArray()), - options, - session, - 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 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, 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); - } + 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 fc42f67c4..6bea924c3 100644 --- a/MongoDB.Entities/Builders/UpdateAndGet.cs +++ b/MongoDB.Entities/Builders/UpdateAndGet.cs @@ -14,14 +14,18 @@ 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( - 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, Action>? onUpdateAction, List>? defs) : base(context, collection, onUpdateAction, defs) + { + } } /// @@ -30,237 +34,41 @@ internal UpdateAndGet( /// /// Any class that implements IEntity /// The type to project to - public class UpdateAndGet : UpdateBase where T : IEntity + /// ID type + public class UpdateAndGet : UpdateBase>, ICollectionRelated + where TId : IComparable, IEquatable + 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, - 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 override 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, Action>? onUpdateAction = null, List>? defs = null) : base(context.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. /// 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()) { - stages.Add(stage); + _stages.Add(stage); } return this; @@ -271,9 +79,9 @@ 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); + _stages.Add(stage); return this; } @@ -282,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()); } @@ -291,14 +99,14 @@ 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; - options.ArrayFilters = - options.ArrayFilters == null + _options.ArrayFilters = + _options.ArrayFilters == null ? new[] { def } - : options.ArrayFilters.Concat(new[] { def }); + : _options.ArrayFilters.Concat(new[] { def }); return this; } @@ -307,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; @@ -317,14 +125,14 @@ 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(); - options.ArrayFilters = - options.ArrayFilters == null + _options.ArrayFilters = + _options.ArrayFilters == null ? defs - : options.ArrayFilters.Concat(defs); + : _options.ArrayFilters.Concat(defs); return this; } @@ -334,9 +142,9 @@ 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); + option(_options); return this; } @@ -344,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)); } @@ -353,9 +161,9 @@ 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); + _options.Projection = projection(Builders.Projection); return this; } @@ -363,21 +171,12 @@ 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."); - 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().CombineWithRequiredProps(_options.Projection); return this; } @@ -387,14 +186,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().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, cancellation).ConfigureAwait(false); } /// @@ -403,14 +201,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().ModifiedOnPropName}': new Date() }} }}"); + - return UpdateAndGetAsync(mergedFilter, Builders.Update.Pipeline(stages.ToArray()), options, session, cancellation); + return UpdateAndGetAsync(mergedFilter, Builders.Update.Pipeline(_stages.ToArray()), _options, cancellation); } private bool ShouldSetModDate() @@ -418,18 +216,19 @@ private bool ShouldSetModDate() //only set mod date by library if user hasn't done anything with the ModifiedOn property return - Cache.HasModifiedOn && + Cache().HasModifiedOn && !defs.Any(d => d .Render(BsonSerializer.SerializerRegistry.GetSerializer(), BsonSerializer.SerializerRegistry) .ToString() - .Contains($"\"{Cache.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 - ? DB.Collection(tenantPrefix).FindOneAndUpdateAsync(filter, definition, options, cancellation) - : DB.Collection(tenantPrefix).FindOneAndUpdateAsync(session, filter, definition, options, cancellation); + 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/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/Cache.cs b/MongoDB.Entities/Core/Cache.cs deleted file mode 100644 index d7c293bac..000000000 --- a/MongoDB.Entities/Core/Cache.cs +++ /dev/null @@ -1,166 +0,0 @@ -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; - -namespace MongoDB.Entities -{ - internal abstract class Cache - { - //key: entity type - //val: collection name - protected static 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(); - - internal static string CollectionNameFor(Type entityType) - => typeToCollectionNameMap[entityType]; - - internal static void MapTypeToDbNameWithoutTenantPrefix(string dbNameWithoutTenantPrefix) where T : IEntity - => typeToDbNameWithoutTenantPrefixMap[typeof(T)] = dbNameWithoutTenantPrefix; - - internal static 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 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; } - - //key: TenantPrefix:CollectionName - //val: IMongoCollection - private static readonly ConcurrentDictionary> cache = new(); - private static readonly PropertyInfo[] updatableProps; - private static ProjectionDefinition requiredPropsProjection; - - static Cache() - { - 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; - - HasCreatedOn = interfaces.Any(i => i == typeof(ICreatedOn)); - HasModifiedOn = interfaces.Any(i => i == typeof(IModifiedOn)); - ModifiedOnPropName = nameof(IModifiedOn.ModifiedOn); - IsFileEntity = type.BaseType == typeof(FileEntity); - - 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!"); - } - } - - internal static IMongoCollection Collection(string tenantPrefix) - { - return cache.GetOrAdd( - key: $"{tenantPrefix}:{CollectionName}", - valueFactory: _ => DB.Database(GetFullDbName(typeof(T), tenantPrefix)).GetCollection(CollectionName)); - } - - internal static 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; - } - - internal static 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/DBContextOptions.cs b/MongoDB.Entities/Core/DBContextOptions.cs new file mode 100644 index 000000000..a289b1ae9 --- /dev/null +++ b/MongoDB.Entities/Core/DBContextOptions.cs @@ -0,0 +1,7 @@ +namespace MongoDB.Entities; + +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 bc1975b14..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 new file mode 100644 index 000000000..7b1d59987 --- /dev/null +++ b/MongoDB.Entities/Core/EntityCache.cs @@ -0,0 +1,130 @@ +using System.Collections.Concurrent; + +namespace MongoDB.Entities; + +internal 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; } + public bool IsEntity { get; protected set; } + public Type? IdType { get; set; } + protected Cache(Type type) + { + 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!"); + + 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)); + 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!"); + } + } + + + + +} + +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) + { + return _updatableProps.Where(p => + !(p.IsDefined(typeof(BsonIgnoreIfDefaultAttribute), false) && p.GetValue(entity) == default) && + !(p.IsDefined(typeof(BsonIgnoreIfNullAttribute), false) && p.GetValue(entity) == null)); + } + return _updatableProps; + } + + private ProjectionDefinition? _requiredPropsProjection; + + public ProjectionDefinition CombineWithRequiredProps(ProjectionDefinition userProjection) + { + if (userProjection == null) + throw new InvalidOperationException("Please use .Project() method before .IncludeRequiredProps()"); + + if (_requiredPropsProjection is null) + { + if (IsEntity) + { + _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 + }); + } + + +} diff --git a/MongoDB.Entities/Core/FileEntity.cs b/MongoDB.Entities/Core/FileEntity.cs index 47103c57c..6ab699647 100644 --- a/MongoDB.Entities/Core/FileEntity.cs +++ b/MongoDB.Entities/Core/FileEntity.cs @@ -1,294 +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() { - private DataStreamer streamer; - - /// - /// 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; } - - [IgnoreDefault] - public string TenantPrefix { get; set; } - - /// - /// Access the DataStreamer class for uploading and downloading data - /// - public DataStreamer Data => streamer ??= new DataStreamer(this, TenantPrefix); + 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; } + /// + /// 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; } + public byte[] Data { get; set; } = Array.Empty(); - public byte[] Data { get; set; } + 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 + /// 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 FileEntity parent; - private readonly Type parentType; - private readonly IMongoDatabase 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, string tenantPrefix) - { - this.parent = parent; - parentType = parent.GetType(); - db = DB.Database(Cache.GetFullDbName(parentType, tenantPrefix)); - chunkCollection = db.GetCollection(DB.CollectionName()); + return DownloadAsync(stream, batchSize, new CancellationTokenSource(timeOutSeconds * 1000).Token); + } - 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)" })); - } - } + /// + /// 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 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, IClientSessionHandle session = null) + var filter = Builders>.Filter.Eq(c => c.FileID, _parent.ID); + var options = new FindOptions, byte[]> { - return DownloadAsync(stream, batchSize, new CancellationTokenSource(timeOutSeconds * 1000).Token, session); - } + BatchSize = batchSize, + Sort = Builders>.Sort.Ascending(c => c.ID), + Projection = Builders>.Projection.Expression(c => c.Data) + }; - /// - /// 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. - /// An optional session if using within a transaction - public async Task DownloadAsync(Stream stream, int batchSize = 1, CancellationToken cancellation = default, IClientSessionHandle session = null) - { - 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 findTask = + _db.Session is not IClientSessionHandle session + ? _chunkCollection.FindAsync(filter, options, cancellation) + : _chunkCollection.FindAsync(session, filter, options, cancellation); - 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) - }; + using var cursor = await findTask.ConfigureAwait(false); + var hasChunks = false; - var findTask = - session == null - ? chunkCollection.FindAsync(filter, options, cancellation) - : chunkCollection.FindAsync(session, filter, options, cancellation); - - 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 - /// An optional session if using within a transaction - public Task UploadWithTimeoutAsync(Stream stream, int timeOutSeconds, int chunkSizeKB = 256, IClientSessionHandle session = null) - { - return UploadAsync(stream, chunkSizeKB, new CancellationTokenSource(timeOutSeconds * 1000).Token, session); - } + 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. - /// An optional session if using within a transaction - public async Task UploadAsync(Stream stream, int chunkSizeKB = 256, CancellationToken cancellation = default, IClientSessionHandle session = null) - { - 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); + /// + /// 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(session, 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(session, 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(session).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(session).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 session if using within a transaction - /// An optional cancellation token. - public Task DeleteBinaryChunks(IClientSessionHandle session = null, CancellationToken cancellation = default) + catch (Exception) { - parent.ThrowIfUnsaved(); - - if (cancellation != default && session == null) - throw new NotSupportedException("Cancellation is only supported within transactions for deleting binary chunks!"); - - return CleanUpAsync(session, cancellation); + await CleanUpAsync().ConfigureAwait(false); + throw; } - - private Task CleanUpAsync(IClientSessionHandle session, CancellationToken cancellation = default) + finally { - 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); + await UpdateMetaDataAsync().ConfigureAwait(false); + _doc = null; + _buffer = null; + _dataChunk = null; + _md5?.Dispose(); + _md5 = null; } + } - private Task FlushToDBAsync(IClientSessionHandle session, bool isLastChunk = false, CancellationToken cancellation = default) - { - if (!isLastChunk) - { - dataChunk.AddRange(new ArraySegment(buffer, 0, readCount)); - parent.FileSize += readCount; - } + /// + /// 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(); - if (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); - } + 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(IClientSessionHandle session) + if (_dataChunk is not null && (_dataChunk.Count >= _chunkSize || isLastChunk)) { - 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 - ? collection.UpdateOneAsync(filter, update) - : collection.UpdateOneAsync(session, filter, update); + + _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() + { + 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 20e37b383..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; } - 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(string tenantPrefix, AggregateOptions options = null, IClientSessionHandle session = 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 session == null - ? DB.Collection(tenantPrefix).Aggregate(options).AppendStage(stage) - : DB.Collection(tenantPrefix).Aggregate(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 90e799495..f9beb1b1c 100644 --- a/MongoDB.Entities/Core/IEntity.cs +++ b/MongoDB.Entities/Core/IEntity.cs @@ -1,21 +1,29 @@ -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 /// - 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? 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() - /// - string GenerateNewID(); - } -} \ No newline at end of file + /// + /// 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() + /// + TId GenerateNewID(); +} 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..1fb0e1042 100644 --- a/MongoDB.Entities/Core/IgnoreManyPropsConvention.cs +++ b/MongoDB.Entities/Core/IgnoreManyPropsConvention.cs @@ -1,16 +1,15 @@ -using MongoDB.Bson.Serialization; -using MongoDB.Bson.Serialization.Conventions; +using MongoDB.Bson.Serialization.Conventions; +using MongoDB.Entities.NewMany; -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 (typeof(IMany<,>).IsAssignableFrom(mMap.MemberType)) { - if (mMap.MemberType.Name == ManyBase.PropTypeName) - { - _ = mMap.SetShouldSerializeMethod(_ => false); - } + _ = mMap.SetShouldSerializeMethod(_ => false); } } -} \ No newline at end of file +} diff --git a/MongoDB.Entities/Core/JoinRecordSerializer.cs b/MongoDB.Entities/Core/JoinRecordSerializer.cs new file mode 100644 index 000000000..b0ed79dd5 --- /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) + { + return new JoinRecord(default, default); + } + + 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/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 eb5747e56..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) 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 = Cache.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) 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 = Cache.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?.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/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/Core/Prop.cs b/MongoDB.Entities/Core/Prop.cs index fcf8a551b..eee90367a 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 Cache.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 e2f10dbb3..1d8d62c9f 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; } + [BsonId] + public string? ID { get; set; } - [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 7ffff7dff..da17ce42f 100644 --- a/MongoDB.Entities/Core/Watcher.cs +++ b/MongoDB.Entities/Core/Watcher.cs @@ -1,294 +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; - public static Task InvokeAllAsync(this AsyncEventHandler handler, TEventArgs args) - => Task.WhenAll(handler.GetHandlers().Select(h => h(args))); + /// + /// 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, 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. /// - /// 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 + /// 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) { - /// - /// 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 string tenantPrefix; - - internal Watcher(string name, string tenantPrefix) - { - Name = name; - this.tenantPrefix = tenantPrefix; - } + 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; + + var ops = new List(3) { ChangeStreamOperationType.Invalidate }; - resume = autoResume; - cancelToken = cancellation; + if ((eventTypes & EventType.Created) != 0) + ops.Add(ChangeStreamOperationType.Insert); - var ops = new List(3) { ChangeStreamOperationType.Invalidate }; + if ((eventTypes & EventType.Updated) != 0) + { + ops.Add(ChangeStreamOperationType.Update); + ops.Add(ChangeStreamOperationType.Replace); + } - if ((eventTypes & EventType.Created) != 0) - ops.Add(ChangeStreamOperationType.Insert); + if ((eventTypes & EventType.Deleted) != 0) + ops.Add(ChangeStreamOperationType.Delete); - if ((eventTypes & EventType.Updated) != 0) + 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>(@" { @@ -300,176 +289,174 @@ private void Init( }") }; - if (projection != null) - stages.Add(PipelineStageDefinitionBuilder.Project(BuildProjection(projection))); + if (projection != null) + stages.Add(PipelineStageDefinitionBuilder.Project(BuildProjection(projection))); - pipeline = stages; + _pipeline = stages; - options = new ChangeStreamOptions - { - StartAfter = resumeToken, - BatchSize = batchSize, - FullDocument = onlyGetIDs ? ChangeStreamFullDocumentOption.Default : ChangeStreamFullDocumentOption.UpdateLookup, - MaxAwaitTime = TimeSpan.FromSeconds(10) - }; - - _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.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 DB.Collection(tenantPrefix).WatchAsync(pipeline, options, cancelToken).ConfigureAwait(false)) + if (cursor.Current.Any()) { - while (!cancelToken.IsCancellationRequested && await cursor.MoveNextAsync(cancelToken).ConfigureAwait(false)) + if (_resume && _options is not null) + _options.StartAfter = cursor.Current.Last().ResumeToken; + + if (OnChangesAsync != null) { - if (cursor.Current.Any()) - { - 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); } - OnStop?.Invoke(); + OnChanges?.Invoke( + cursor.Current + .Where(d => d.OperationType != ChangeStreamOperationType.Invalidate) + .Select(d => d.FullDocument)); - if (cancelToken.IsCancellationRequested) - { - if (OnChangesAsync != null) - { - foreach (var h in OnChangesAsync.GetHandlers()) - OnChangesAsync -= h; - } - - if (OnChangesCSDAsync != null) - { - foreach (var h in OnChangesCSDAsync.GetHandlers()) - OnChangesCSDAsync -= h; - } - - if (OnChanges != null) - { - foreach (Action> a in OnChanges.GetInvocationList()) - OnChanges -= a; - } - - if (OnChangesCSD != null) - { - foreach (Action>> a in OnChangesCSD.GetInvocationList()) - OnChangesCSD -= a; - } - - if (OnError != null) - { - foreach (Action a in OnError.GetInvocationList()) - OnError -= a; - } - - if (OnStop != null) - { - foreach (Action a in OnStop.GetInvocationList()) - OnStop -= a; - } - } + if (OnChangesCSDAsync != null) + await OnChangesCSDAsync.InvokeAllAsync(cursor.Current).ConfigureAwait(false); + + OnChangesCSD?.Invoke(cursor.Current); } } - catch (Exception x) + + OnStop?.Invoke(); + + if (_cancelToken.IsCancellationRequested) { - OnError?.Invoke(x); + if (OnChangesAsync != null) + { + foreach (var h in OnChangesAsync.GetHandlers()) + OnChangesAsync -= h; + } + + if (OnChangesCSDAsync != null) + { + foreach (var h in OnChangesCSDAsync.GetHandlers()) + OnChangesCSDAsync -= h; + } + + if (OnChanges != null) + { + foreach (Action> a in OnChanges.GetInvocationList()) + OnChanges -= a; + } + + if (OnChangesCSD != null) + { + foreach (Action>> a in OnChangesCSD.GetInvocationList()) + OnChangesCSD -= a; + } + + if (OnError != null) + { + foreach (Action a in OnError.GetInvocationList()) + OnError -= a; + } + + if (OnStop != null) + { + foreach (Action a in OnStop.GetInvocationList()) + OnStop -= a; + } } } + 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 deleted file mode 100644 index c26147333..000000000 --- a/MongoDB.Entities/DB/DB.Collection.cs +++ /dev/null @@ -1,86 +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 - { - //no support for multi-tenancy :-( - return Database(null).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 - { - return Cache.Collection(tenantPrefix); - } - - /// - /// 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 Cache.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 - /// 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 - { - 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); - } - - /// - /// 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 - /// 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 - { - 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); - } - } -} diff --git a/MongoDB.Entities/DB/DB.Count.cs b/MongoDB.Entities/DB/DB.Count.cs deleted file mode 100644 index 367fed665..000000000 --- a/MongoDB.Entities/DB/DB.Count.cs +++ /dev/null @@ -1,86 +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 - /// Optional tenant prefix if using multi-tenancy - public static Task CountEstimatedAsync(CancellationToken cancellation = default, string tenantPrefix = null) where T : IEntity - { - return Collection(tenantPrefix).EstimatedDocumentCountAsync(null, 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 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 - { - return - session == null - ? Collection(tenantPrefix).CountDocumentsAsync(expression, options, cancellation) - : Collection(tenantPrefix).CountDocumentsAsync(session, expression, 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 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 - { - return - session == null - ? Collection(tenantPrefix).CountDocumentsAsync(filter, options, cancellation) - : Collection(tenantPrefix).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 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, IClientSessionHandle session = null, CancellationToken cancellation = default, CountOptions options = null, string tenantPrefix = null) where T : IEntity - { - return - session == null - ? Collection(tenantPrefix).CountDocumentsAsync(filter(Builders.Filter), options, cancellation) - : Collection(tenantPrefix).CountDocumentsAsync(session, filter(Builders.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 - /// 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 - { - return CountAsync(_ => true, session, cancellation, tenantPrefix: tenantPrefix); - } - } -} diff --git a/MongoDB.Entities/DB/DB.Delete.cs b/MongoDB.Entities/DB/DB.Delete.cs deleted file mode 100644 index 1f1c51373..000000000 --- a/MongoDB.Entities/DB/DB.Delete.cs +++ /dev/null @@ -1,194 +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 - { - 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. - /// 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 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 - { - ThrowIfCancellationNotSupported(session, cancellation); - return DeleteCascadingAsync(tenantPrefix, new[] { ID }, session, 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 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 - { - 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); - } - - /// - /// 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 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 - { - return DeleteAsync(Builders.Filter.Where(expression), session, cancellation, collation, tenantPrefix); - } - - /// - /// 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 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 - { - return DeleteAsync(filter(Builders.Filter), session, cancellation, collation, tenantPrefix); - } - - /// - /// 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 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, string tenantPrefix = 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) - { - if (cancellation != default && session == null) - throw new NotSupportedException("Cancellation is only supported within transactions for delete operations!"); - } - } -} diff --git a/MongoDB.Entities/DB/DB.Distinct.cs b/MongoDB.Entities/DB/DB.Distinct.cs deleted file mode 100644 index 973355a7f..000000000 --- a/MongoDB.Entities/DB/DB.Distinct.cs +++ /dev/null @@ -1,17 +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 - /// 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); - } -} diff --git a/MongoDB.Entities/DB/DB.File.cs b/MongoDB.Entities/DB/DB.File.cs deleted file mode 100644 index 778a872cc..000000000 --- a/MongoDB.Entities/DB/DB.File.cs +++ /dev/null @@ -1,24 +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 - /// Optional tenant prefix if using multi-tenancy - public static DataStreamer File(string ID, string tenantPrefix = null) 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); - } - } -} diff --git a/MongoDB.Entities/DB/DB.Find.cs b/MongoDB.Entities/DB/DB.Find.cs deleted file mode 100644 index 1860cd108..000000000 --- a/MongoDB.Entities/DB/DB.Find.cs +++ /dev/null @@ -1,28 +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 - /// 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); - - /// - /// 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 - /// 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); - } -} diff --git a/MongoDB.Entities/DB/DB.Fluent.cs b/MongoDB.Entities/DB/DB.Fluent.cs deleted file mode 100644 index 423afcfdd..000000000 --- a/MongoDB.Entities/DB/DB.Fluent.cs +++ /dev/null @@ -1,57 +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. - /// 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 - { - return session == null - ? Collection(tenantPrefix).Aggregate(options) - : Collection(tenantPrefix).Aggregate(session, 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) - /// 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 - { - 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); - } - } -} diff --git a/MongoDB.Entities/DB/DB.GeoNear.cs b/MongoDB.Entities/DB/DB.GeoNear.cs deleted file mode 100644 index a8e52e7c2..000000000 --- a/MongoDB.Entities/DB/DB.GeoNear.cs +++ /dev/null @@ -1,44 +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. - /// 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 - { - 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); - } - } -} diff --git a/MongoDB.Entities/DB/DB.Index.cs b/MongoDB.Entities/DB/DB.Index.cs deleted file mode 100644 index 1a441866f..000000000 --- a/MongoDB.Entities/DB/DB.Index.cs +++ /dev/null @@ -1,16 +0,0 @@ -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 - /// Optional tenant prefix if using multi-tenancy - public static Index Index(string tenantPrefix = null) where T : IEntity - { - return new Index(tenantPrefix); - } - } -} diff --git a/MongoDB.Entities/DB/DB.Insert.cs b/MongoDB.Entities/DB/DB.Insert.cs deleted file mode 100644 index 083465294..000000000 --- a/MongoDB.Entities/DB/DB.Insert.cs +++ /dev/null @@ -1,51 +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 - /// 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 - { - PrepAndCheckIfInsert(entity); - return session == null - ? Collection(tenantPrefix).InsertOneAsync(entity, null, cancellation) - : Collection(tenantPrefix).InsertOneAsync(session, entity, null, 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, IClientSessionHandle session = null, CancellationToken cancellation = default, string tenantPrefix = null) 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); - } - } -} 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 28786e875..000000000 --- a/MongoDB.Entities/DB/DB.Pipeline.cs +++ /dev/null @@ -1,96 +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, IClientSessionHandle session = null, CancellationToken cancellation = default, string tenantPrefix = null) where T : IEntity - { - return session == null - ? Collection(tenantPrefix).AggregateAsync(template.ToPipeline(), options, cancellation) - : Collection(tenantPrefix).AggregateAsync(session, template.ToPipeline(), 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 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 - { - 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; - } - - /// - /// 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 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 - { - 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(); - } - } - - /// - /// 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 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 - { - AggregateOptions opts = options ?? new AggregateOptions(); - opts.BatchSize = 1; - - 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.Queryable.cs b/MongoDB.Entities/DB/DB.Queryable.cs deleted file mode 100644 index 13e2cfca5..000000000 --- a/MongoDB.Entities/DB/DB.Queryable.cs +++ /dev/null @@ -1,22 +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 - /// 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 - { - return session == null - ? Collection(tenantPrefix).AsQueryable(options) - : Collection(tenantPrefix).AsQueryable(session, 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 68dbe0919..000000000 --- a/MongoDB.Entities/DB/DB.Save.cs +++ /dev/null @@ -1,236 +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 - { - 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. - /// 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 - /// 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 - { - 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); - } - - /// - /// 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 - /// 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 - { - 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); - } - - /// - /// 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 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 - { - return SavePartial(entity, members, tenantPrefix, session, 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 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 - { - return SavePartial(entities, members, tenantPrefix, session, 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 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 - { - return SavePartial(entity, members, tenantPrefix, session, cancellation, true); - } - - /// - /// 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 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 - { - return SavePartial(entities, members, tenantPrefix, session, cancellation, true); - } - - /// - /// 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 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 - { - 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); - } - - 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 deleted file mode 100644 index 739a0c14e..000000000 --- a/MongoDB.Entities/DB/DB.Sequence.cs +++ /dev/null @@ -1,37 +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 - /// Optional tenant prefix if using multi-tenancy - public static Task NextSequentialNumberAsync(CancellationToken cancellation = default, string tenantPrefix = null) where T : IEntity - { - return NextSequentialNumberAsync(CollectionName(), cancellation, tenantPrefix); - } - - /// - /// 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 - /// Optional tenant prefix if using multi-tenancy - public static Task NextSequentialNumberAsync(string sequenceName, CancellationToken cancellation = default, string tenantPrefix = null) - { - 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); - } - } -} 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 a0aeaa63a..000000000 --- a/MongoDB.Entities/DB/DB.Update.cs +++ /dev/null @@ -1,38 +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 - /// 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); - - /// - /// 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 - /// 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); - - /// - /// 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); - } -} diff --git a/MongoDB.Entities/DB/DB.Watcher.cs b/MongoDB.Entities/DB/DB.Watcher.cs deleted file mode 100644 index 6758c6540..000000000 --- a/MongoDB.Entities/DB/DB.Watcher.cs +++ /dev/null @@ -1,31 +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. - /// Optional tenant prefix if using multi-tenancy - public static Watcher Watcher(string name, string tenantPrefix = null) 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; - } - - /// - /// 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; - } -} diff --git a/MongoDB.Entities/DB/DB.cs b/MongoDB.Entities/DB/DB.cs index a37b098b6..a40930309 100644 --- a/MongoDB.Entities/DB/DB.cs +++ b/MongoDB.Entities/DB/DB.cs @@ -13,28 +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(); } - //key: FullDBName(including tenant prefix - ex: TenantX~DBName) - private static readonly ConcurrentDictionary dbs = new(); - private static IMongoDatabase defaultDb; + /// + /// 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. @@ -43,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; + } } /// @@ -115,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; } /// @@ -135,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 always returns the current DatabaseName in the Context")] + public static string DatabaseName() { - 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 916db93bc..ffa8f4f68 100644 --- a/MongoDB.Entities/DBContext/DBContext.Collection.cs +++ b/MongoDB.Entities/DBContext/DBContext.Collection.cs @@ -1,31 +1,57 @@ using MongoDB.Driver; using System; +using System.Collections.Generic; 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) + { + 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(string? collectionName = null) { - /// - /// 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 tasks = new List(); + var db = Database; + var collName = collectionName ?? CollectionName(); + var options = new ListCollectionNamesOptions { - return DB.CreateCollectionAsync(options, cancellation, Session, tenantPrefix); - } + Filter = "{$and:[{name:/~/},{name:/" + collName + "/}]}" + }; - /// - /// 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 Task DropCollectionAsync() where T : IEntity + foreach (var cName in await db.ListCollectionNames(options).ToListAsync().ConfigureAwait(false)) { - return DB.DropCollectionAsync(Session, tenantPrefix); + 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..7d628765f 100644 --- a/MongoDB.Entities/DBContext/DBContext.Count.cs +++ b/MongoDB.Entities/DBContext/DBContext.Count.cs @@ -4,83 +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) { - /// - /// 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 DB.CountEstimatedAsync(cancellation, tenantPrefix); - } + 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, string? collectionName = null, IMongoCollection? collection = null) + { + return CountAsync((FilterDefinition)expression, cancellation, options, ignoreGlobalFilters, collection: collection, collectionName: collectionName); + } - /// - /// 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 DB.CountAsync( - Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, expression), - Session, - cancellation, - options, - tenantPrefix); - } + /// + /// 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) + { + 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 - /// An optional cancellation token - public Task CountAsync(CancellationToken cancellation = default) where T : IEntity - { - return DB.CountAsync(Session, cancellation, tenantPrefix); - } + } - /// - /// 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 - { - return DB.CountAsync( - Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, filter), - Session, - cancellation, - options, - tenantPrefix); - } + /// + /// 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) + { + 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 DB.CountAsync( - Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, filter(Builders.Filter)), - Session, - cancellation, - options, - tenantPrefix); - } + /// + /// 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) + { + 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 edaa9062a..65887a016 100644 --- a/MongoDB.Entities/DBContext/DBContext.Delete.cs +++ b/MongoDB.Entities/DBContext/DBContext.Delete.cs @@ -1,107 +1,196 @@ 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 partial class DBContext { - public partial class DBContext + private static readonly int _deleteBatchSize = 100000; + private void ThrowIfCancellationNotSupported(CancellationToken cancellation = default) { - /// - /// 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 DB.DeleteAsync( - Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, Builders.Filter.Eq(e => e.ID, ID)), - Session, - cancellation, - tenantPrefix: tenantPrefix); - } + if (cancellation != default && Session is null) + throw new NotSupportedException("Cancellation is only supported within transactions for delete operations!"); + } - /// - /// 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 + 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. + // 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 options = new ListCollectionNamesOptions { - return DB.DeleteAsync( - Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, Builders.Filter.In(e => e.ID, IDs)), - Session, - cancellation, - tenantPrefix: tenantPrefix); - } + Filter = "{$and:[{name:/~/},{name:/" + collectionName ?? Cache().CollectionName + "/}]}" + }; + + var tasks = new List(); - /// - /// 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 + // 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)) { - return DB.DeleteAsync( - Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, Builders.Filter.Where(expression)), - Session, - cancellation, - collation, - tenantPrefix); + //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)); } - /// - /// 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 + 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); + + if (typeof(FileEntity).IsAssignableFrom(typeof(T))) { - return DB.DeleteAsync( - Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, filter(Builders.Filter)), - Session, - cancellation, - collation, - tenantPrefix); + tasks.Add( + Session is null + ? Collection>().DeleteManyAsync(x => IDs.Contains(x.FileID)) + : Collection>().DeleteManyAsync(Session, x => IDs.Contains(x.FileID), null, 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 Task DeleteAsync(FilterDefinition filter, CancellationToken cancellation = default, Collation collation = null, bool ignoreGlobalFilters = false) where T : IEntity + 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. + /// + /// 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(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); + } + + /// + /// 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 + /// 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 TId : IComparable, IEquatable + 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 + /// 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 TId : IComparable, IEquatable + 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 + /// 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 TId : IComparable, IEquatable + 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 + /// 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 TId : IComparable, IEquatable + where T : IEntity + { + ThrowIfCancellationNotSupported(cancellation); + + 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; + + using (cursor) { - return DB.DeleteAsync( - Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, filter), - Session, - cancellation, - collation, - tenantPrefix); + 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); } } diff --git a/MongoDB.Entities/DBContext/DBContext.Distinct.cs b/MongoDB.Entities/DBContext/DBContext.Distinct.cs index 6507371f1..de7a5a9f9 100644 --- a/MongoDB.Entities/DBContext/DBContext.Distinct.cs +++ b/MongoDB.Entities/DBContext/DBContext.Distinct.cs @@ -1,15 +1,17 @@ -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 + /// 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) { - /// - /// 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(Session, globalFilters, tenantPrefix); - } + return new Distinct(this, Collection(collectionName, collection)); } } diff --git a/MongoDB.Entities/DBContext/DBContext.File.cs b/MongoDB.Entities/DBContext/DBContext.File.cs index 34b3ec23f..662b877df 100644 --- a/MongoDB.Entities/DBContext/DBContext.File.cs +++ b/MongoDB.Entities/DBContext/DBContext.File.cs @@ -1,15 +1,23 @@ -namespace MongoDB.Entities +using MongoDB.Bson; +using MongoDB.Driver; +using System; + +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 + /// ID type + /// The ID of the file entity + /// + /// + public DataStreamer File(TId ID, string? collectionName = null, IMongoCollection? collection = null) + where TId : IComparable, IEquatable + 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() - { - return DB.File(ID, tenantPrefix); - } + 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 a3d7d957b..3eac92376 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) { - /// - /// Starts a find command for the given entity type - /// - /// The type of entity - public Find Find() where T : IEntity - { - return new Find(Session, globalFilters, tenantPrefix); - } + 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(Session, globalFilters, tenantPrefix); - } + /// + /// 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) + { + return new Find(this, Collection(collectionName, collection)); } } diff --git a/MongoDB.Entities/DBContext/DBContext.Fluent.cs b/MongoDB.Entities/DBContext/DBContext.Fluent.cs index 9b4f9a86b..5717efca1 100644 --- a/MongoDB.Entities/DBContext/DBContext.Fluent.cs +++ b/MongoDB.Entities/DBContext/DBContext.Fluent.cs @@ -10,18 +10,21 @@ 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) { - var globalFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, Builders.Filter.Empty); + var globalFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, Builders.Filter.Empty); + + var aggregate = Session is not IClientSessionHandle session + ? Collection(collectionName, collection).Aggregate(options) + : Collection(collectionName, collection).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 +38,40 @@ 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, string? collectionName = null, IMongoCollection? collection = null) { - var globalFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, Builders.Filter.Empty); + 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 + ? Collection(collectionName, collection).Aggregate(options).Match(filter) + : Collection(collectionName, collection).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 d944fba74..2226e9d7b 100644 --- a/MongoDB.Entities/DBContext/DBContext.GeoNear.cs +++ b/MongoDB.Entities/DBContext/DBContext.GeoNear.cs @@ -23,18 +23,33 @@ 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, string? collectionName = null, IMongoCollection? collection = null) { - var globalFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, Builders.Filter.Empty); + 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, collectionName: collectionName, collection: collection); 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.IMongoDatabase.cs b/MongoDB.Entities/DBContext/DBContext.IMongoDatabase.cs new file mode 100644 index 000000000..20663f796 --- /dev/null +++ b/MongoDB.Entities/DBContext/DBContext.IMongoDatabase.cs @@ -0,0 +1,238 @@ +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 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); + } + + 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); + } + + public Task> ListCollectionNamesAsync(ListCollectionNamesOptions options, CancellationToken cancellationToken) + { + return Database.ListCollectionNamesAsync(options, cancellationToken); + } + + public Task> 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.Index.cs b/MongoDB.Entities/DBContext/DBContext.Index.cs index c7b1eff3b..0ed0f8744 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 { @@ -6,10 +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 - public Index Index() where T : IEntity + /// Any class + public Index Index(string? collectionName = null, IMongoCollection? collection = null) { - return new Index(tenantPrefix); + return new Index(this, Collection(collectionName, collection)); } + } } diff --git a/MongoDB.Entities/DBContext/DBContext.Insert.cs b/MongoDB.Entities/DBContext/DBContext.Insert.cs index b40985b8f..2f0b45681 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,34 +10,132 @@ 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 TId : IComparable, IEquatable + where T : IEntity + { + 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(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 TId : IComparable, IEquatable + 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, this, 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 TId : IComparable, IEquatable + where T : IEntity + { + var cache = Cache(); + if (EqualityComparer.Default.Equals(entity.ID, default)) + { + 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. /// /// The type of entity + /// ID type /// 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 TId : IComparable, IEquatable + where T : IEntity { SetModifiedBySingle(entity); - entity.SetTenantPrefixOnFileEntity(tenantPrefix); - OnBeforeSave()?.Invoke(entity); - return DB.InsertAsync(entity, Session, cancellation, tenantPrefix); + OnBeforeSave(entity); + PrepAndCheckIfInsert(entity); + return Session == null + ? Collection(collectionName, collection).InsertOneAsync(entity, null, cancellation) + : 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. /// /// The type of entity + /// ID type /// 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 TId : IComparable, IEquatable + where T : IEntity { SetModifiedByMultiple(entities); - entities.SetTenantDbOnFileEntities(tenantPrefix); - foreach (var ent in entities) OnBeforeSave()?.Invoke(ent); - return DB.InsertAsync(entities, Session, cancellation, tenantPrefix); + 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); + } + + /// + /// 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 ced6c1e30..625d124b4 100644 --- a/MongoDB.Entities/DBContext/DBContext.PagedSearch.cs +++ b/MongoDB.Entities/DBContext/DBContext.PagedSearch.cs @@ -1,24 +1,25 @@ -namespace MongoDB.Entities +using MongoDB.Driver; + +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 + public PagedSearch PagedSearch(string? collectionName = null, IMongoCollection? collection = null) { - /// - /// Represents an aggregation query that retrieves results with easy paging support. - /// - /// Any class that implements IEntity - public PagedSearch PagedSearch() where T : IEntity - { - return new PagedSearch(Session, globalFilters, tenantPrefix); - } + 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() where T : IEntity - { - return new PagedSearch(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 PagedSearch PagedSearch(string? collectionName = null, IMongoCollection? collection = null) + { + return new PagedSearch(this, Collection(collectionName, collection)); } } diff --git a/MongoDB.Entities/DBContext/DBContext.Pipeline.cs b/MongoDB.Entities/DBContext/DBContext.Pipeline.cs index c160b2351..48f74517c 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) { - return DB.PipelineCursorAsync(MergeGlobalFilter(template, ignoreGlobalFilters), options, Session, cancellation, tenantPrefix); + 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) { - return DB.PipelineAsync(MergeGlobalFilter(template, ignoreGlobalFilters), options, Session, cancellation, tenantPrefix); + 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) { - return DB.PipelineSingleAsync(MergeGlobalFilter(template, ignoreGlobalFilters), options, Session, cancellation, tenantPrefix); + + 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,27 @@ 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) { - return DB.PipelineFirstAsync(MergeGlobalFilter(template, ignoreGlobalFilters), options, Session, cancellation, tenantPrefix); + + 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) { //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 74a88a6a6..62e37d4ed 100644 --- a/MongoDB.Entities/DBContext/DBContext.Queryable.cs +++ b/MongoDB.Entities/DBContext/DBContext.Queryable.cs @@ -11,17 +11,22 @@ 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) { - var globalFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, globalFilters, Builders.Filter.Empty); + var globalFilter = Logic.MergeWithGlobalFilter(ignoreGlobalFilters, _globalFilters, Builders.Filter.Empty); + collection = Collection(collectionName, collection); + var q = Session == null + ? collection.AsQueryable(options) + : collection.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.Replace.cs b/MongoDB.Entities/DBContext/DBContext.Replace.cs index 9ef9d9511..97135fc6e 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,21 @@ 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 => Replace(collectionName, collection); + + /// + /// Starts a replace command for the given entity type + /// TIP: Only the first matched entity will be replaced + /// + /// The type of entity + /// ID type + public Replace Replace(string? collectionName = null, IMongoCollection? collection = null) + where TId : IComparable, IEquatable + 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 b0f8b8a0a..0c6a27b60 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; @@ -11,34 +12,101 @@ 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 + /// ID type /// 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 TId : IComparable, IEquatable + where T : IEntity { SetModifiedBySingle(entity); - entity.SetTenantPrefixOnFileEntity(tenantPrefix); - OnBeforeSave()?.Invoke(entity); - return DB.SaveAsync(entity, Session, cancellation, tenantPrefix); + OnBeforeSave(entity); + collection = Collection(collectionName, collection); + 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(filter, entity, new ReplaceOptions { IsUpsert = true }, cancellation) + : 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. /// /// The type of entity + /// ID type /// 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 TId : IComparable, IEquatable + where T : IEntity { SetModifiedByMultiple(entities); - entities.SetTenantDbOnFileEntities(tenantPrefix); - foreach (var ent in entities) OnBeforeSave()?.Invoke(ent); - return DB.SaveAsync(entities, Session, cancellation, tenantPrefix); + 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); + } + + + /// + /// 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); } /// @@ -48,15 +116,20 @@ 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) 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); - entity.SetTenantPrefixOnFileEntity(tenantPrefix); - OnBeforeSave()?.Invoke(entity); - return DB.SaveOnlyAsync(entity, members, Session, cancellation, tenantPrefix); + OnBeforeSave(entity); + return SavePartial(entity, members, cancellation, collectionName: collectionName, collection: collection); + } /// @@ -66,15 +139,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) 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); - entities.SetTenantDbOnFileEntities(tenantPrefix); - foreach (var ent in entities) OnBeforeSave()?.Invoke(ent); - return DB.SaveOnlyAsync(entities, members, Session, cancellation, tenantPrefix); + foreach (var ent in entities) OnBeforeSave(ent); + return SavePartial(entities, members, cancellation, collectionName: collectionName, collection: collection); } /// @@ -84,15 +161,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) 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); - entity.SetTenantPrefixOnFileEntity(tenantPrefix); - OnBeforeSave()?.Invoke(entity); - return DB.SaveExceptAsync(entity, members, Session, cancellation, tenantPrefix); + OnBeforeSave(entity); + return SavePartial(entity, members, cancellation, true, collectionName: collectionName, collection: collection); } /// @@ -102,15 +183,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) 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); - entities.SetTenantDbOnFileEntities(tenantPrefix); - foreach (var ent in entities) OnBeforeSave()?.Invoke(ent); - return DB.SaveExceptAsync(entities, members, Session, cancellation, tenantPrefix); + foreach (var ent in entities) OnBeforeSave(ent); + return SavePartial(entities, members, cancellation, true, collectionName: collectionName, collection: collection); } /// @@ -118,37 +203,89 @@ 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) 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); - entity.SetTenantPrefixOnFileEntity(tenantPrefix); - OnBeforeSave()?.Invoke(entity); - return DB.SavePreservingAsync(entity, Session, cancellation, tenantPrefix); + 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))); + } + + var filter = Builders.Filter.Eq(e => e.ID, entity.ID); + var update = Builders.Update.Combine(defs); + return + Session == null + ? 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) + return; ThrowIfModifiedByIsEmpty(); - Cache.ModifiedByProp?.SetValue( + + 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 + private void SetModifiedByMultiple(IEnumerable entities) { - 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.Sequence.cs b/MongoDB.Entities/DBContext/DBContext.Sequence.cs index 1f3ac8307..8ce89c474 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 - public Task NextSequentialNumberAsync(CancellationToken cancellation = default) where T : IEntity + /// transaction support will not be added due to unpredictability with concurrency. + public Task NextSequentialNumberAsync(CancellationToken cancellation = default) { - 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..8eb40b0a9 100644 --- a/MongoDB.Entities/DBContext/DBContext.Transaction.cs +++ b/MongoDB.Entities/DBContext/DBContext.Transaction.cs @@ -7,51 +7,43 @@ 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.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); } /// - /// 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. + /// Creates a new Transaction and a new MongoServerContext and Starts a transaction on the new instance. /// - /// 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 + /// Client session options for this transaction + public Transaction TransactionCopy(ClientSessionOptions? options = null) { - return Transaction(DB.DatabaseName(tenantPrefix), options); + return new Transaction(MongoServerContext, Database.DatabaseNamespace.DatabaseName, 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) + { + return MongoServerContext.CommitAsync(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) + { + return MongoServerContext.AbortAsync(cancellation); + } } } diff --git a/MongoDB.Entities/DBContext/DBContext.Update.cs b/MongoDB.Entities/DBContext/DBContext.Update.cs index 785d4ed7d..d5b02b2c2 100644 --- a/MongoDB.Entities/DBContext/DBContext.Update.cs +++ b/MongoDB.Entities/DBContext/DBContext.Update.cs @@ -1,45 +1,61 @@ -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 + 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 { - /// - /// Starts an update command for the given entity type - /// - /// The type of entity - public Update Update() where T : IEntity + var cmd = new Update(this, Collection(collectionName, collection), OnBeforeUpdate>); + if (Cache().ModifiedByProp is PropertyInfo ModifiedByProp) { - var cmd = new Update(Session, globalFilters, OnBeforeUpdate(), tenantPrefix); - if (Cache.ModifiedByProp != null) - { - ThrowIfModifiedByIsEmpty(); - cmd.Modify(b => b.Set(Cache.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() where T : IEntity - { - return UpdateAndGet(); - } + /// + /// 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() 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(Session, globalFilters, OnBeforeUpdate(), tenantPrefix); - if (Cache.ModifiedByProp != null) - { - ThrowIfModifiedByIsEmpty(); - cmd.Modify(b => b.Set(Cache.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 b1801837b..0895c1147 100644 --- a/MongoDB.Entities/DBContext/DBContext.Watcher.cs +++ b/MongoDB.Entities/DBContext/DBContext.Watcher.cs @@ -1,25 +1,30 @@ -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) where T : IEntity - { - return DB.Watcher(name, tenantPrefix); - } + 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 => DB.Watchers(); + 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 6339f0f93..b7a4198e4 100644 --- a/MongoDB.Entities/DBContext/DBContext.cs +++ b/MongoDB.Entities/DBContext/DBContext.cs @@ -1,254 +1,352 @@ -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; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; - -namespace MongoDB.Entities +using System.Threading; +using System.Threading.Tasks; +#nullable enable +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. - /// - public partial class DBContext - { - /// - /// Returns the session object used for transactions - /// - public IClientSessionHandle Session { get; protected set; } - - /// - /// 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; } - - protected string tenantPrefix; - private static Type[] allEntitiyTypes; - private Dictionary globalFilters; - - /// - /// 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) + /// Returns the session object used for transactions + /// + public IClientSessionHandle? Session => MongoServerContext.Session; + + + 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 + { + get { - DB.Initialize( - new MongoClientSettings { Server = new MongoServerAddress(host, port) }, - database, - true) - .GetAwaiter() - .GetResult(); - - ModifiedBy = modifiedBy; + return MongoServerContext.ModifiedBy; } - - /// - /// 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) + [Obsolete("Use MongoContext.Options.ModifiedBy = value instead")] + set { - DB.Initialize(settings, database, true) - .GetAwaiter() - .GetResult(); - - ModifiedBy = modifiedBy; + MongoServerContext.Options.ModifiedBy = value; } + } - /// - /// 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. - public DBContext(ModifiedBy modifiedBy = null) - => ModifiedBy = modifiedBy; + public IMongoClient Client => MongoServerContext; + public DatabaseNamespace DatabaseNamespace => Database.DatabaseNamespace; + public MongoDatabaseSettings Settings => Database.Settings; - /// - /// 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; - } + private Dictionary? _globalFilters; + internal Dictionary GlobalFilters => _globalFilters ??= new(); - /// - /// 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 - { - return null; - } + /// + /// 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() + { + 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.Client.GetDatabase(database); + } - /// - /// 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); - } + 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))); + BsonSerializer.RegisterGenericSerializerDefinition(typeof(JoinRecord<,>), typeof(JoinRecordSerializer<,>)); + ConventionRegistry.Register( + "DefaultConventions", + new ConventionPack + { + new IgnoreExtraElementsConvention(true), + new IgnoreManyPropsConvention() + }, + _ => true); + } - /// - /// 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)); - } + /// + /// 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 + protected virtual void OnBeforeSave(T entity) + { + } - /// - /// 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); - } + /// + /// This event hook will be triggered right before an update/replace command is executed + /// + /// Any entity that implements IEntity + /// Any entity that implements IEntity + /// ID type + protected virtual void OnBeforeUpdate(UpdateBase updateBase) + where T : IEntity + where TId : IComparable, IEquatable + 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) + { + 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) + { + 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) + { + 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(Func, 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 + /// 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) + { + 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) + { + 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) + { + foreach (var entType in MongoServerContext.AllEntitiyTypes.Where(t => t.IsSubclassOf(typeof(TBase)))) { - SetGlobalFilterForBaseClass(filter(Builders.Filter), prepend); + var bsonDoc = filter.Render( + BsonSerializer.SerializerRegistry.GetSerializer(), + BsonSerializer.SerializerRegistry); + + AddFilter(entType, (bsonDoc, 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 - { - if (allEntitiyTypes is null) allEntitiyTypes = GetAllEntityTypes(); + /// + /// 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); - foreach (var entType in allEntitiyTypes.Where(t => t.IsSubclassOf(typeof(TBase)))) - { - var bsonDoc = filter.Render( - BsonSerializer.SerializerRegistry.GetSerializer(), - BsonSerializer.SerializerRegistry); + if (!targetType.IsInterface) throw new ArgumentException("Only interfaces are allowed!", "TInterface"); - 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) + foreach (var entType in MongoServerContext.AllEntitiyTypes.Where(t => targetType.IsAssignableFrom(t))) { - var targetType = typeof(TInterface); + AddFilter(entType, (jsonString, prepend)); + } + } - 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))) - { - AddFilter(entType, (jsonString, prepend)); - } + + private void ThrowIfModifiedByIsEmpty() + { + var cache = Cache(); + if (cache.ModifiedByProp is not null && ModifiedBy is null) + { + 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 readonly ConcurrentDictionary _cache = new(); - private static Type[] GetAllEntityTypes() + internal EntityCache Cache() + { + var type = typeof(T); + if (!_cache.TryGetValue(type, out var c)) { - 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(); + _cache[type] = c = EntityCache.Default; } + return (EntityCache)c; + } + + public virtual string CollectionName() + { + return Cache().CollectionName; + } + + public virtual IMongoCollection Collection(string? collectionName = null, IMongoCollection? collection = null) + { + return collection ?? GetCollection(collectionName ?? CollectionName()); + } - private void ThrowIfModifiedByIsEmpty() where T : IEntity + public async Task PingNetwork() + { + try { - 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}]"); - } + await Database.RunCommandAsync((Command)"{ping:1}").ConfigureAwait(false); + return true; } - - private void AddFilter(Type type, (object filterDef, bool prepend) filter) + catch (Exception) { - if (globalFilters is null) globalFilters = new Dictionary(); - - globalFilters[type] = filter; + 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/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 331ba7197..849c764d6 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) => 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/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 bfb3dca39..d6bec53b3 100644 --- a/MongoDB.Entities/Extensions/Entity.cs +++ b/MongoDB.Entities/Extensions/Entity.cs @@ -30,13 +30,14 @@ 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) + //TODO(@ahmednfwela): add static analysis attributes + internal static void ThrowIfUnsaved(this IEntity entity) where TId : IComparable, IEquatable { ThrowIfUnsaved(entity.ID); } @@ -67,8 +68,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); } @@ -76,9 +78,9 @@ 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, bool ignoreGlobalFilters = false, string? collectionName = null, IMongoCollection? collection = null) where T : IEntity { - return DB.Queryable(options, tenantPrefix: tenantPrefix); + return DB.Context.Queryable(options, collectionName: collectionName, collection: collection, ignoreGlobalFilters: ignoreGlobalFilters); } /// @@ -166,25 +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); - } - - 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); - } + 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/GlobalUsing.cs b/MongoDB.Entities/GlobalUsing.cs new file mode 100644 index 000000000..a343aca18 --- /dev/null +++ b/MongoDB.Entities/GlobalUsing.cs @@ -0,0 +1,10 @@ +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; + +global using System.Diagnostics.CodeAnalysis; diff --git a/MongoDB.Entities/MongoContext/MongoContext.IMongoClient.cs b/MongoDB.Entities/MongoContext/MongoContext.IMongoClient.cs new file mode 100644 index 000000000..bcc830df9 --- /dev/null +++ b/MongoDB.Entities/MongoContext/MongoContext.IMongoClient.cs @@ -0,0 +1,170 @@ +using MongoDB.Bson; +using MongoDB.Driver; +using MongoDB.Driver.Core.Clusters; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace MongoDB.Entities +{ + //Make these interface implmentation explicit, so we can fine-tune the api return result + public partial class MongoServerContext + { + public ICluster Cluster => Client.Cluster; + + public MongoClientSettings Settings => Client.Settings; + + void IMongoClient.DropDatabase(string name, CancellationToken cancellationToken) + { + Client.DropDatabase(name, cancellationToken); + } + + void IMongoClient.DropDatabase(IClientSessionHandle session, string name, CancellationToken cancellationToken) + { + Client.DropDatabase(session, name, cancellationToken); + } + + Task IMongoClient.DropDatabaseAsync(string name, CancellationToken cancellationToken) + { + return Client.DropDatabaseAsync(name, cancellationToken); + } + + Task IMongoClient.DropDatabaseAsync(IClientSessionHandle session, string name, CancellationToken cancellationToken) + { + return Client.DropDatabaseAsync(session, name, cancellationToken); + } + + + IMongoDatabase IMongoClient.GetDatabase(string name, MongoDatabaseSettings settings) + { + return Client.GetDatabase(name, settings); + } + + IAsyncCursor IMongoClient.ListDatabaseNames(CancellationToken cancellationToken) + { + return Client.ListDatabaseNames(cancellationToken); + } + + IAsyncCursor IMongoClient.ListDatabaseNames(ListDatabaseNamesOptions options, CancellationToken cancellationToken) + { + return Client.ListDatabaseNames(options, cancellationToken); + } + + IAsyncCursor IMongoClient.ListDatabaseNames(IClientSessionHandle session, CancellationToken cancellationToken) + { + return Client.ListDatabaseNames(session, cancellationToken); + } + + IAsyncCursor IMongoClient.ListDatabaseNames(IClientSessionHandle session, ListDatabaseNamesOptions options, CancellationToken cancellationToken) + { + return Client.ListDatabaseNames(session, options, cancellationToken); + } + + Task> IMongoClient.ListDatabaseNamesAsync(CancellationToken cancellationToken) + { + return Client.ListDatabaseNamesAsync(cancellationToken); + } + + Task> IMongoClient.ListDatabaseNamesAsync(ListDatabaseNamesOptions options, CancellationToken cancellationToken) + { + return Client.ListDatabaseNamesAsync(options, cancellationToken); + } + + Task> IMongoClient.ListDatabaseNamesAsync(IClientSessionHandle session, CancellationToken cancellationToken) + { + return Client.ListDatabaseNamesAsync(session, cancellationToken); + } + + Task> IMongoClient.ListDatabaseNamesAsync(IClientSessionHandle session, ListDatabaseNamesOptions options, CancellationToken cancellationToken) + { + return Client.ListDatabaseNamesAsync(session, options, cancellationToken); + } + + IAsyncCursor IMongoClient.ListDatabases(CancellationToken cancellationToken) + { + return Client.ListDatabases(cancellationToken); + } + + IAsyncCursor IMongoClient.ListDatabases(ListDatabasesOptions options, CancellationToken cancellationToken) + { + return Client.ListDatabases(options, cancellationToken); + } + + IAsyncCursor IMongoClient.ListDatabases(IClientSessionHandle session, CancellationToken cancellationToken) + { + return Client.ListDatabases(session, cancellationToken); + } + + IAsyncCursor IMongoClient.ListDatabases(IClientSessionHandle session, ListDatabasesOptions options, CancellationToken cancellationToken) + { + return Client.ListDatabases(session, options, cancellationToken); + } + + Task> IMongoClient.ListDatabasesAsync(CancellationToken cancellationToken) + { + return Client.ListDatabasesAsync(cancellationToken); + } + + Task> IMongoClient.ListDatabasesAsync(ListDatabasesOptions options, CancellationToken cancellationToken) + { + return Client.ListDatabasesAsync(options, cancellationToken); + } + + Task> IMongoClient.ListDatabasesAsync(IClientSessionHandle session, CancellationToken cancellationToken) + { + return Client.ListDatabasesAsync(session, cancellationToken); + } + + Task> IMongoClient.ListDatabasesAsync(IClientSessionHandle session, ListDatabasesOptions options, CancellationToken cancellationToken) + { + return Client.ListDatabasesAsync(session, options, cancellationToken); + } + + IClientSessionHandle IMongoClient.StartSession(ClientSessionOptions options, CancellationToken cancellationToken) + { + return Client.StartSession(options, cancellationToken); + } + + Task IMongoClient.StartSessionAsync(ClientSessionOptions options, CancellationToken cancellationToken) + { + return Client.StartSessionAsync(options, cancellationToken); + } + + IChangeStreamCursor IMongoClient.Watch(PipelineDefinition, TResult> pipeline, ChangeStreamOptions options, CancellationToken cancellationToken) + { + return Client.Watch(pipeline, options, cancellationToken); + } + + IChangeStreamCursor IMongoClient.Watch(IClientSessionHandle session, PipelineDefinition, TResult> pipeline, ChangeStreamOptions options, CancellationToken cancellationToken) + { + return Client.Watch(session, pipeline, options, cancellationToken); + } + + Task> IMongoClient.WatchAsync(PipelineDefinition, TResult> pipeline, ChangeStreamOptions options, CancellationToken cancellationToken) + { + return Client.WatchAsync(pipeline, options, cancellationToken); + } + + Task> IMongoClient.WatchAsync(IClientSessionHandle session, PipelineDefinition, TResult> pipeline, ChangeStreamOptions options, CancellationToken cancellationToken) + { + return Client.WatchAsync(session, pipeline, options, cancellationToken); + } + + IMongoClient IMongoClient.WithReadConcern(ReadConcern readConcern) + { + return Client.WithReadConcern(readConcern); + } + + IMongoClient IMongoClient.WithReadPreference(ReadPreference readPreference) + { + return Client.WithReadPreference(readPreference); + } + + IMongoClient 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..71fb87390 --- /dev/null +++ b/MongoDB.Entities/MongoContext/MongoContext.cs @@ -0,0 +1,164 @@ +using MongoDB.Driver; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +#nullable enable +namespace MongoDB.Entities +{ + /// + /// MongoContext is a wrapper around an + /// + public partial class MongoServerContext : IMongoClient, IDisposable + { + /// + /// Creates a new server context + /// + /// The backing client, usually a + /// The options to configure the context + public MongoServerContext(IMongoClient client, MongoContextOptions? options = null) + { + Client = client; + Options = options ?? new(); + } + + /// + /// Copies a new MongoServerContext without the Session + /// + /// + public MongoServerContext(MongoServerContext other) + { + Client = other.Client; + Options = other.Options; + } + + + /// + /// The backing client + /// + public IMongoClient Client { get; } + + public MongoContextOptions Options { get; set; } + + /// + 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 + ((IMongoClient)this) + .ListDatabaseNamesAsync().ConfigureAwait(false)) + .ToListAsync().ConfigureAwait(false); + } + + private Type[]? _allEntitiyTypes; + public Type[] AllEntitiyTypes => _allEntitiyTypes ??= GetAllEntityTypes(); + 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(); + } + + //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; + + + /// + /// 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!"); + } + + /// + /// 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 + /// + /// 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); + } + + public void Dispose() + { + Session?.Dispose(); + Session = null; + } + } + +} diff --git a/MongoDB.Entities/MongoDB.Entities.csproj b/MongoDB.Entities/MongoDB.Entities.csproj index 64b4d2e3c..b741350c6 100644 --- a/MongoDB.Entities/MongoDB.Entities.csproj +++ b/MongoDB.Entities/MongoDB.Entities.csproj @@ -2,14 +2,14 @@ - 20.26.3 - + 20.26.1 + enable - hotfix for bulk updates not working with .ModifyWith(entity) - upgrade dependencies to latest - netstandard2.0 + netstandard2.1 MongoDB.Entities MongoDB.Entities Đĵ ΝιΓΞΗΛψΚ @@ -26,6 +26,7 @@ mongodb mongodb-orm mongodb-repo mongodb-repository entities nosql orm linq netcore repository aspnetcore netcore2 netcore3 dotnetstandard database persistance dal repo true 10.0 + enable diff --git a/MongoDB.Entities/Relationships/JoinRecord.cs b/MongoDB.Entities/Relationships/JoinRecord.cs index 6e92f4421..d11aa463d 100644 --- a/MongoDB.Entities/Relationships/JoinRecord.cs +++ b/MongoDB.Entities/Relationships/JoinRecord.cs @@ -1,21 +1,48 @@ -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 { /// - /// 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. + /// + 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. + /// + 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) { - /// - /// 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; } + 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. - /// - [AsObjectId] - public string ChildID { get; set; } + /// + /// 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 (TParentId ParentId, TChildId ChildId) GenerateNewID() + { + throw new NotImplementedException(); } } diff --git a/MongoDB.Entities/Relationships/Many.Queryable.cs b/MongoDB.Entities/Relationships/Many.Queryable.cs index c7c28fb7f..b1834440e 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.Context.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.Context.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.Context.Collection(null), + j => j.ParentID, + c => c.ID, + (_, c) => c); + } + else + { + return JoinQueryable(session, options) + .Where(j => j.ParentID == parent.ID) + .Join( + DB.Context.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.Remove.cs b/MongoDB.Entities/Relationships/Many.Remove.cs index 1647fd33b..21043ace3 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 diff --git a/MongoDB.Entities/Relationships/Many.cs b/MongoDB.Entities/Relationships/Many.cs index e3d5f9fa0..9d49933d1 100644 --- a/MongoDB.Entities/Relationships/Many.cs +++ b/MongoDB.Entities/Relationships/Many.cs @@ -6,141 +6,147 @@ 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. +/// 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; + 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; } - - /// - /// 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 @@ -155,10 +161,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..6292646d6 --- /dev/null +++ b/MongoDB.Entities/Relationships/NewMany.cs @@ -0,0 +1,232 @@ +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 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 IMany +{ + 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 +{ + TParent Parent { get; } + + FilterDefinition GetFilterForSingleDocument(); +} + +public interface IManyToMany : IMany +{ + bool IsParentOwner { get; } +} +public interface IManyToOne : IMany +{ +} + + +/// +/// Marker class +/// +/// +/// +public abstract class Many : IMany +{ + protected Many(PropertyInfo parentProperty, PropertyInfo childProperty) + { + ParentProperty = parentProperty; + ChildProperty = childProperty; + } + + internal PropertyInfo ParentProperty { get; } + 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 abstract class Many : Many, IMany +{ + 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, /*TODO: uncomment me Parent.ID*/ ""); +} + +public sealed class ManyToMany : Many, IManyToMany +{ + 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(); + } +} + +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) + { + 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()); + } +} + + +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 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); + 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, 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 + 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); + } +} \ No newline at end of file diff --git a/MongoDB.Entities/Relationships/One.cs b/MongoDB.Entities/Relationships/One.cs index aae1a4845..df2f4b3fb 100644 --- a/MongoDB.Entities/Relationships/One.cs +++ b/MongoDB.Entities/Relationships/One.cs @@ -1,99 +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 - public class One where T : IEntity + /// The actual entity this reference represents. + internal One(T entity) { - /// - /// The Id of the entity referenced by this instance. - /// - [AsObjectId] - public string ID { get; set; } - - public One() - { } - - /// - /// Initializes a reference to an entity in MongoDB. - /// - /// The actual entity this reference represents. - internal One(T entity) - { - entity.ThrowIfUnsaved(); - ID = entity.ID; - } + Cache = 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) - { - 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) - { - return new One(entity); - } + /// + /// 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); + } - /// - /// 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); - } + /// + /// 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); + } - /// - /// 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. + ///// + ///// + ///// 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 = context.Find(collectionName, collection).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) + //{ + // if (ID is null) + // { + // return default; + // } + + // return (await context.Find(collectionName, collection) + // .MatchID(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) + ///// + ///// 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(); + //} +} + +/// +/// 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 { - return (await new Find(session, null, tenantPrefix) - .Match(ID) - .Project(projection) - .ExecuteAsync(cancellation).ConfigureAwait(false)) - .SingleOrDefault(); + value?.ThrowIfUnsaved(); + _cache = value; + if (value is not null) + { + if (!EqualityComparer.Default.Equals(ID, value.ID)) + { + ID = value.ID!; + } + } + else + { + ID = default; + } } } -} + + 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]