diff --git a/Src/Couchbase.Linq.IntegrationTests/QueryTests.cs b/Src/Couchbase.Linq.IntegrationTests/QueryTests.cs index ce04150..4778ebc 100644 --- a/Src/Couchbase.Linq.IntegrationTests/QueryTests.cs +++ b/Src/Couchbase.Linq.IntegrationTests/QueryTests.cs @@ -174,7 +174,7 @@ public void Map2PocoTests_Simple_Projections_StartsWith() var context = new BucketContext(TestSetup.Bucket); var beers = from b in context.Query() - where b.Type == "beer" && b.Name.StartsWith("563") + where b.Type == "beer" && b.Name.StartsWith("Amendment") select new { name = b.Name, abv = b.Abv }; var results = beers.Take(1).ToList(); @@ -1000,7 +1000,7 @@ public void SubqueryTests_ArraySubqueryContains() var context = new BucketContext(TestSetup.Bucket); var breweries = from brewery in context.Query() - where brewery.Type == "brewery" && brewery.Address.Contains("563 Second Street") + where brewery.Type == "brewery" && brewery.Address.Contains("210 Aberdeen Dr.") orderby brewery.Name select new {name = brewery.Name, addresses = brewery.Address}; diff --git a/Src/Couchbase.Linq.IntegrationTests/SingleQueryTests.cs b/Src/Couchbase.Linq.IntegrationTests/SingleQueryTests.cs index 7a7f357..91aafee 100644 --- a/Src/Couchbase.Linq.IntegrationTests/SingleQueryTests.cs +++ b/Src/Couchbase.Linq.IntegrationTests/SingleQueryTests.cs @@ -46,7 +46,7 @@ public void Single_HasResult() var context = new BucketContext(TestSetup.Bucket); var beers = from beer in context.Query() - where beer.Name == "21A IPA" + where beer.Name == "Amendment Pale Ale" select new {beer.Name}; Console.WriteLine(beers.Single().Name); @@ -58,8 +58,8 @@ public async Task SingleAsync_HasResult() var context = new BucketContext(TestSetup.Bucket); var beers = from beer in context.Query() - where beer.Name == "21A IPA" - select new {beer.Name}; + where beer.Name == "Amendment Pale Ale" + select new { beer.Name }; Console.WriteLine((await beers.SingleAsync()).Name); } @@ -72,7 +72,7 @@ public async Task SingleAsync_WithPredicate_HasResult() var beers = from beer in context.Query() select new {beer.Name}; - var result = await beers.SingleAsync(p => p.Name == "21A IPA"); + var result = await beers.SingleAsync(p => p.Name == "Amendment Pale Ale"); Console.WriteLine(result.Name); } @@ -137,7 +137,7 @@ public void SingleOrDefault_HasResult() var context = new BucketContext(TestSetup.Bucket); var beers = from beer in context.Query() - where beer.Name == "21A IPA" + where beer.Name == "Amendment Pale Ale" select new {beer.Name}; var aBeer = beers.SingleOrDefault(); @@ -151,7 +151,7 @@ public async Task SingleOrDefaultAsync_HasResult() var context = new BucketContext(TestSetup.Bucket); var beers = from beer in context.Query() - where beer.Name == "21A IPA" + where beer.Name == "Amendment Pale Ale" select new {beer.Name}; var aBeer = await beers.SingleOrDefaultAsync(); @@ -167,7 +167,7 @@ public async Task SingleOrDefaultAsync_WithPredicate_HasResult() var beers = from beer in context.Query() select new {beer.Name}; - var aBeer = await beers.SingleOrDefaultAsync(p => p.Name == "21A IPA"); + var aBeer = await beers.SingleOrDefaultAsync(p => p.Name == "Amendment Pale Ale"); Assert.IsNotNull(aBeer); Console.WriteLine(aBeer.Name); } diff --git a/Src/Couchbase.Linq.UnitTests/Metadata/ContextMetadataTests.cs b/Src/Couchbase.Linq.UnitTests/Metadata/ContextMetadataTests.cs index 33768b4..bd6ee00 100644 --- a/Src/Couchbase.Linq.UnitTests/Metadata/ContextMetadataTests.cs +++ b/Src/Couchbase.Linq.UnitTests/Metadata/ContextMetadataTests.cs @@ -1,7 +1,5 @@ -using System; -using Couchbase.Linq.Metadata; +using Couchbase.Linq.Metadata; using Couchbase.Linq.UnitTests.Documents; -using Moq; using NUnit.Framework; namespace Couchbase.Linq.UnitTests.Metadata @@ -47,6 +45,10 @@ public void ctor_ValidInitializer() private class TestContext : BucketContext { + public TestContext() : base(QueryFactory.CreateMockBucket("default")) + { + } + public IDocumentSet Beers { get; set; } public IDocumentSet Routes { get; set; } diff --git a/Src/Couchbase.Linq.UnitTests/N1QLTestBase.cs b/Src/Couchbase.Linq.UnitTests/N1QLTestBase.cs index caf6af8..2759501 100644 --- a/Src/Couchbase.Linq.UnitTests/N1QLTestBase.cs +++ b/Src/Couchbase.Linq.UnitTests/N1QLTestBase.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Linq.Expressions; +using System.Security.Cryptography.X509Certificates; using Couchbase.Core.IO.Serializers; using Couchbase.Core.Version; using Couchbase.KeyValue; @@ -13,6 +14,7 @@ using Microsoft.Extensions.Logging; using Moq; using Newtonsoft.Json.Serialization; +using Remotion.Linq; namespace Couchbase.Linq.UnitTests { @@ -109,30 +111,8 @@ internal string CreateN1QlQuery(IBucket bucket, Expression expression, ClusterVe return visitor.GetQuery(); } - protected virtual IQueryable CreateQueryable(string bucketName) - { - return CreateQueryable(bucketName, QueryExecutor); - } - - internal virtual IQueryable CreateQueryable(string bucketName, IAsyncQueryExecutor queryExecutor) - { - var mockCluster = new Mock(); - mockCluster - .Setup(p => p.ClusterServices) - .Returns(ServiceProvider); - - var mockBucket = new Mock(); - mockBucket.SetupGet(e => e.Name).Returns(bucketName); - mockBucket.SetupGet(e => e.Cluster).Returns(mockCluster.Object); - - var mockCollection = new Mock(); - mockCollection - .SetupGet(p => p.Scope.Bucket) - .Returns(mockBucket.Object); - - return new CollectionQueryable(mockCollection.Object, - QueryParserHelper.CreateQueryParser(mockCluster.Object), queryExecutor); - } + protected virtual IQueryable CreateQueryable(string bucketName) => + QueryFactory.Queryable(bucketName, N1QlHelpers.DefaultScopeName, N1QlHelpers.DefaultCollectionName, QueryExecutor); protected void SetContractResolver(IContractResolver contractResolver) { diff --git a/Src/Couchbase.Linq.UnitTests/QueryFactory.cs b/Src/Couchbase.Linq.UnitTests/QueryFactory.cs index 6e963a9..467db5d 100644 --- a/Src/Couchbase.Linq.UnitTests/QueryFactory.cs +++ b/Src/Couchbase.Linq.UnitTests/QueryFactory.cs @@ -2,8 +2,13 @@ using Couchbase.Core.IO.Serializers; using Couchbase.Core.Version; using Couchbase.KeyValue; +using Couchbase.Linq.Execution; +using Couchbase.Linq.Filters; +using Couchbase.Linq.QueryGeneration; using Couchbase.Linq.Serialization; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Moq; namespace Couchbase.Linq.UnitTests @@ -11,22 +16,39 @@ namespace Couchbase.Linq.UnitTests internal class QueryFactory { public static IQueryable Queryable(IBucket bucket) => - Queryable(bucket.Name, "_default", "_default"); + Queryable(bucket.Name, N1QlHelpers.DefaultScopeName, N1QlHelpers.DefaultCollectionName); public static IQueryable Queryable(IBucket bucket, string scopeName, string collectionName) => Queryable(bucket.Name, scopeName, collectionName); public static IQueryable Queryable(string bucketName) => - Queryable(bucketName, "_default", "_default"); + Queryable(bucketName, N1QlHelpers.DefaultScopeName, N1QlHelpers.DefaultCollectionName); - public static IQueryable Queryable(string bucketName, string scopeName, string collectionName) + public static IQueryable Queryable(string bucketName, string scopeName, string collectionName) => + Queryable(bucketName, scopeName, collectionName, Mock.Of()); + + public static IQueryable Queryable(string bucketName, string scopeName, string collectionName, IAsyncQueryExecutor queryExecutor) + { + var mockCollection = CreateMockCollection(bucketName, scopeName, collectionName); + + return new CollectionQueryable(mockCollection, + new ClusterQueryProvider( + QueryParserHelper.CreateQueryParser(mockCollection.Scope.Bucket.Cluster), + queryExecutor)); + } + + public static ICouchbaseCollection CreateMockCollection(string bucketName, string scopeName, string collectionName) => + CreateMockBucket(bucketName).Scope(scopeName).Collection(collectionName); + + public static IBucket CreateMockBucket(string bucketName) { var serializer = new DefaultSerializer(); - var services = new ServiceCollection(); + IServiceCollection services = new ServiceCollection(); services.AddSingleton(serializer); - services.AddLogging(); + services.AddSingleton(new DocumentFilterManager()); + services.Add(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(NullLogger<>))); services.AddSingleton(Mock.Of()); services.AddSingleton( new DefaultSerializationConverterProvider(serializer, @@ -38,21 +60,42 @@ public static IQueryable Queryable(string bucketName, string scopeName, st .Returns(services.BuildServiceProvider()); var mockBucket = new Mock(); - mockBucket.SetupGet(e => e.Name).Returns(bucketName); - mockBucket.SetupGet(e => e.Cluster).Returns(mockCluster.Object); - - var mockCollection = new Mock(); - mockCollection - .SetupGet(p => p.Scope.Bucket) - .Returns(mockBucket.Object); - mockCollection - .SetupGet(p => p.Scope.Name) - .Returns(scopeName); - mockCollection - .SetupGet(p => p.Name) - .Returns(collectionName); - - return new CollectionQueryable(mockCollection.Object, default); + mockBucket + .SetupGet(e => e.Name) + .Returns(bucketName); + mockBucket + .SetupGet(e => e.Cluster) + .Returns(mockCluster.Object); + mockBucket + .Setup(e => e.Scope(It.IsAny())) + .Returns((string scopeName) => + { + var mockScope = new Mock(); + mockScope + .SetupGet(p => p.Name) + .Returns(scopeName); + mockScope + .SetupGet(p => p.Bucket) + .Returns(mockBucket.Object); + mockScope + .Setup(e => e.Collection(It.IsAny())) + .Returns((string collectionName) => + { + var mockCollection = new Mock(); + mockCollection + .SetupGet(p => p.Name) + .Returns(collectionName); + mockCollection + .SetupGet(p => p.Scope) + .Returns(mockScope.Object); + + return mockCollection.Object; + }); + + return mockScope.Object; + }); + + return mockBucket.Object; } } } \ No newline at end of file diff --git a/Src/Couchbase.Linq/BucketContext.cs b/Src/Couchbase.Linq/BucketContext.cs index 47bc6bb..6b62294 100644 --- a/Src/Couchbase.Linq/BucketContext.cs +++ b/Src/Couchbase.Linq/BucketContext.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using Couchbase.Linq.Execution; using Couchbase.Linq.Filters; using Couchbase.Linq.Metadata; using Couchbase.Linq.Utils; @@ -13,15 +14,7 @@ namespace Couchbase.Linq public class BucketContext : IBucketContext { private readonly DocumentFilterManager _documentFilterManager; - - /// - /// Unit testing seam only, do not use! - /// -#pragma warning disable 8618 - internal BucketContext() -#pragma warning restore 8618 - { - } + internal IAsyncQueryProvider QueryProvider { get; } /// /// Creates a new BucketContext for a given Couchbase bucket. @@ -29,7 +22,9 @@ internal BucketContext() /// Bucket referenced by the new BucketContext. public BucketContext(IBucket bucket) { - Bucket = bucket ?? throw new ArgumentNullException(nameof(bucket)); + ThrowHelpers.ThrowIfNull(bucket); + + Bucket = bucket; try { @@ -42,6 +37,16 @@ public BucketContext(IBucket bucket) $"{nameof(DocumentFilterManager)} has not been registered with the Couchbase Cluster. Be sure {nameof(LinqClusterOptionsExtensions.AddLinq)} is called on ${nameof(ClusterOptions)} during bootstrap."); } + var cluster = bucket.Cluster; + var innerQueryProvider = new ClusterQueryProvider( + QueryParserHelper.CreateQueryParser(cluster), + new ClusterQueryExecutor(cluster) + { + QueryTimeoutProvider = () => QueryTimeout + }); + + QueryProvider = new DelayedFilterQueryProvider(innerQueryProvider, _documentFilterManager); + var myType = GetType(); if (myType != typeof(BucketContext)) { @@ -70,9 +75,9 @@ public IQueryable Query(BucketQueryOptions options) internal IQueryable Query(string scope, string collection, BucketQueryOptions options = BucketQueryOptions.None) { - IQueryable query = new CollectionQueryable(Bucket.Scope(scope).Collection(collection), QueryTimeout); + IQueryable query = new CollectionQueryable(Bucket.Scope(scope).Collection(collection), QueryProvider); - if ((options & BucketQueryOptions.SuppressFilters) == BucketQueryOptions.None) + if (!options.HasFlag(BucketQueryOptions.SuppressFilters)) { query = _documentFilterManager.ApplyFilters(query); } diff --git a/Src/Couchbase.Linq/CollectionQueryable.cs b/Src/Couchbase.Linq/CollectionQueryable.cs index 8063a54..da8bda2 100644 --- a/Src/Couchbase.Linq/CollectionQueryable.cs +++ b/Src/Couchbase.Linq/CollectionQueryable.cs @@ -1,75 +1,38 @@ -using System; -using System.Collections.Generic; -using System.Linq.Expressions; -using System.Threading; -using Couchbase.KeyValue; +using Couchbase.KeyValue; using Couchbase.Linq.Execution; -using Couchbase.Linq.QueryGeneration; -using Remotion.Linq; -using Remotion.Linq.Parsing.Structure; +using Couchbase.Linq.Utils; namespace Couchbase.Linq { /// /// The main entry point and executor of the query. /// - /// - internal class CollectionQueryable : QueryableBase, ICollectionQueryable + /// Document type to query. + internal sealed class CollectionQueryable : CouchbaseQueryable, ICollectionQueryable { - private readonly ICouchbaseCollection? _collection; - /// - public string CollectionName => _collection?.Name ?? N1QlHelpers.DefaultCollectionName; - + public string CollectionName { get; } /// - public string ScopeName => _collection?.Scope.Name ?? N1QlHelpers.DefaultScopeName; - + public string ScopeName { get; } /// - public string BucketName => _collection?.Scope.Bucket.Name ?? ""; + public string BucketName { get; } /// /// Initializes a new instance of the class. /// /// The collection. - /// The query parser. - /// The executor. - /// is . - public CollectionQueryable(ICouchbaseCollection collection, IQueryParser queryParser, IAsyncQueryExecutor executor) - : base(new ClusterQueryProvider(queryParser, executor)) + /// The query provider to execute the query. + public CollectionQueryable(ICouchbaseCollection collection, IAsyncQueryProvider provider) : base(provider) { - _collection = collection ?? throw new ArgumentNullException(nameof(collection)); - } + ThrowHelpers.ThrowIfNull(collection); - /// - /// Initializes a new instance of the class. - /// - /// Used to build new expressions as more methods are applied to the query. - /// The provider. - /// The expression. - public CollectionQueryable(IAsyncQueryProvider provider, Expression expression) - : base(provider, expression) - { - } + CollectionName = collection.Name; - /// - /// Initializes a new instance of the class. - /// - /// The collection. - /// Query timeout, if null uses cluster default. - public CollectionQueryable(ICouchbaseCollection collection, TimeSpan? queryTimeout) - : this(collection, - QueryParserHelper.CreateQueryParser(collection.Scope.Bucket.Cluster), - new ClusterQueryExecutor(collection.Scope.Bucket.Cluster) - { - QueryTimeout = queryTimeout - }) - { + var scope = collection.Scope; + ScopeName = scope.Name; + BucketName = scope.Bucket.Name; } - - public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) => - ((IAsyncQueryProvider) Provider).ExecuteAsync>(Expression) - .GetAsyncEnumerator(cancellationToken); } } \ No newline at end of file diff --git a/Src/Couchbase.Linq/CouchbaseQueryable.cs b/Src/Couchbase.Linq/CouchbaseQueryable.cs new file mode 100644 index 0000000..1f9a4ec --- /dev/null +++ b/Src/Couchbase.Linq/CouchbaseQueryable.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Threading; +using Couchbase.Linq.Execution; +using Remotion.Linq; + +namespace Couchbase.Linq +{ + /// + /// The executor of the query. + /// + /// + internal class CouchbaseQueryable : QueryableBase, IAsyncEnumerable + { + /// + /// Initializes a new instance of the class. + /// + /// Used to build new expressions as more methods are applied to the query. + /// The provider. + /// The expression. + public CouchbaseQueryable(IAsyncQueryProvider provider, Expression expression) + : base(provider, expression) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Used by subclasses to create a root queryable. + /// The provider. + protected CouchbaseQueryable(IAsyncQueryProvider provider) + : base(provider) + { + } + + public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) => + ((IAsyncQueryProvider) Provider).ExecuteAsync>(Expression) + .GetAsyncEnumerator(cancellationToken); + } +} \ No newline at end of file diff --git a/Src/Couchbase.Linq/DocumentSet`1.cs b/Src/Couchbase.Linq/DocumentSet`1.cs index 92fcd01..323684a 100644 --- a/Src/Couchbase.Linq/DocumentSet`1.cs +++ b/Src/Couchbase.Linq/DocumentSet`1.cs @@ -5,7 +5,7 @@ using System.Linq.Expressions; using System.Threading; using Couchbase.KeyValue; -using Couchbase.Linq.Extensions; +using Couchbase.Linq.Execution; using Couchbase.Linq.Utils; namespace Couchbase.Linq @@ -16,10 +16,10 @@ namespace Couchbase.Linq /// Type of the document. internal class DocumentSet : IDocumentSet, ICollectionQueryable { - private readonly BucketContext _bucketContext; + private readonly IAsyncQueryProvider _queryProvider; /// - public string BucketName => _bucketContext.Bucket.Name; + public string BucketName { get; } /// public string ScopeName { get; } @@ -28,26 +28,18 @@ internal class DocumentSet : IDocumentSet, ICollectionQueryable public string CollectionName { get; } /// - public ICouchbaseCollection Collection => _bucketContext.Bucket.Scope(ScopeName).Collection(CollectionName); + public ICouchbaseCollection Collection { get; } public DocumentSet(BucketContext bucketContext, string scopeName, string collectionName) { - // ReSharper disable ConditionIsAlwaysTrueOrFalse - if (bucketContext == null) - { - ThrowHelpers.ThrowArgumentNullException(nameof(bucketContext)); - } - if (scopeName == null) - { - ThrowHelpers.ThrowArgumentNullException(nameof(scopeName)); - } - if (collectionName == null) - { - ThrowHelpers.ThrowArgumentNullException(nameof(collectionName)); - } - // ReSharper restore ConditionIsAlwaysTrueOrFalse - - _bucketContext = bucketContext; + ThrowHelpers.ThrowIfNull(bucketContext); + ThrowHelpers.ThrowIfNull(scopeName); + ThrowHelpers.ThrowIfNull(collectionName); + + _queryProvider = bucketContext.QueryProvider; + Collection = bucketContext.Bucket.Scope(scopeName).Collection(collectionName); + + BucketName = bucketContext.Bucket.Name; ScopeName = scopeName; CollectionName = collectionName; @@ -55,28 +47,23 @@ public DocumentSet(BucketContext bucketContext, string scopeName, string collect Expression = Expression.Constant(this); } - /// - /// Makes a new queryable for each query. This way the latest settings, such as timeout, are - /// collected. - /// - private IQueryable MakeQueryable() => - _bucketContext.Query(ScopeName, CollectionName); - #region IQueryable + private CouchbaseQueryable MakeQueryable() => new(_queryProvider, Expression); + public IEnumerator GetEnumerator() => MakeQueryable().GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public Type ElementType => typeof(T); + public Type ElementType { get; } = typeof(T); public Expression Expression { get; } - public IQueryProvider Provider => MakeQueryable().Provider; + public IQueryProvider Provider => _queryProvider; #endregion #region IAsyncEnumerable public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) => - MakeQueryable().AsAsyncEnumerable().GetAsyncEnumerator(cancellationToken); + MakeQueryable().GetAsyncEnumerator(cancellationToken); #endregion } diff --git a/Src/Couchbase.Linq/Execution/ClusterQueryExecutor.cs b/Src/Couchbase.Linq/Execution/ClusterQueryExecutor.cs index 0473a3a..165e176 100644 --- a/Src/Couchbase.Linq/Execution/ClusterQueryExecutor.cs +++ b/Src/Couchbase.Linq/Execution/ClusterQueryExecutor.cs @@ -7,7 +7,6 @@ using Couchbase.Core.IO.Serializers; using Couchbase.Core.Version; using Couchbase.Linq.Clauses; -using Couchbase.Linq.Operators; using Couchbase.Linq.QueryGeneration; using Couchbase.Linq.QueryGeneration.MemberNameResolvers; using Couchbase.Linq.Utils; @@ -31,9 +30,9 @@ internal class ClusterQueryExecutor : IAsyncQueryExecutor _serializer ??= _cluster.ClusterServices.GetRequiredService(); /// - /// Query timeout, if null uses cluster default. + /// Query timeout callback, if null uses the cluster default. /// - public TimeSpan? QueryTimeout { get; set; } + public Func? QueryTimeoutProvider { get; set; } /// /// Creates a new BucketQueryExecutor. @@ -82,9 +81,10 @@ private LinqQueryOptions GetQueryOptions(QueryModel queryModel, ScalarResultBeha queryOptions.ConsistentWith(combinedMutationState); } - if (QueryTimeout != null) + var queryTimeout = QueryTimeoutProvider?.Invoke(); + if (queryTimeout is not null) { - queryOptions.Timeout(QueryTimeout.Value); + queryOptions.Timeout(queryTimeout.GetValueOrDefault()); } return queryOptions; @@ -151,9 +151,9 @@ public async IAsyncEnumerable ExecuteCollectionAsync(string statement, Lin } public T ExecuteScalar(QueryModel queryModel)=> - ExecuteSingle(queryModel, false); + ExecuteSingle(queryModel, false)!; - public T ExecuteSingle(QueryModel queryModel, bool returnDefaultWhenEmpty) => + public T? ExecuteSingle(QueryModel queryModel, bool returnDefaultWhenEmpty) => returnDefaultWhenEmpty ? ExecuteCollection(queryModel).SingleOrDefault() : ExecuteCollection(queryModel).Single(); @@ -180,21 +180,24 @@ public string GenerateQuery(QueryModel queryModel, out ScalarResultBehavior scal var serializer = Serializer as IExtendedTypeSerializer; -#pragma warning disable CS0618 // Type or member is obsolete var memberNameResolver = serializer != null ? (IMemberNameResolver)new ExtendedTypeSerializerMemberNameResolver(serializer) : - (IMemberNameResolver)new JsonNetMemberNameResolver(JsonConvert.DefaultSettings().ContractResolver); -#pragma warning restore CS0618 // Type or member is obsolete + (IMemberNameResolver)new JsonNetMemberNameResolver(JsonConvert.DefaultSettings!().ContractResolver!); var methodCallTranslatorProvider = new DefaultMethodCallTranslatorProvider(); + var clusterVersionTask = _clusterVersionProvider.GetVersionAsync(); + var clusterVersion = clusterVersionTask.IsCompleted + ? clusterVersionTask.Result + // TODO: Don't use .Result to block + : clusterVersionTask.AsTask().Result; // Must convert ValueTask to Task to safely await the result if it is not completed + var queryGenerationContext = new N1QlQueryGenerationContext { MemberNameResolver = memberNameResolver, MethodCallTranslatorProvider = methodCallTranslatorProvider, Serializer = serializer, - // TODO: Don't use .Result - ClusterVersion = _clusterVersionProvider.GetVersionAsync().Result ?? FeatureVersions.DefaultVersion, + ClusterVersion = clusterVersion ?? FeatureVersions.DefaultVersion, LoggerFactory = _cluster.ClusterServices.GetRequiredService() }; @@ -202,7 +205,7 @@ public string GenerateQuery(QueryModel queryModel, out ScalarResultBehavior scal visitor.VisitQueryModel(queryModel); var query = visitor.GetQuery(); - _logger.LogDebug("Generated query: {0}", query); + _logger.LogDebug("Generated query: {query}", query); scalarResultBehavior = visitor.ScalarResultBehavior; return query; diff --git a/Src/Couchbase.Linq/Execution/ClusterQueryProvider.cs b/Src/Couchbase.Linq/Execution/ClusterQueryProvider.cs index 02c93ad..e4846b8 100644 --- a/Src/Couchbase.Linq/Execution/ClusterQueryProvider.cs +++ b/Src/Couchbase.Linq/Execution/ClusterQueryProvider.cs @@ -22,12 +22,8 @@ public ClusterQueryProvider(IQueryParser queryParser, IAsyncQueryExecutor execut { } - public override IQueryable CreateQuery(Expression expression) - { - return (IQueryable) Activator.CreateInstance( - typeof(CollectionQueryable<>).MakeGenericType(typeof(T)), - this, expression); - } + public override IQueryable CreateQuery(Expression expression) => + new CouchbaseQueryable(this, expression); public T ExecuteAsync(Expression expression, CancellationToken cancellationToken = default) { @@ -39,7 +35,7 @@ public T ExecuteAsync(Expression expression, CancellationToken cancellationTo { var executeAsyncMethod = ExecuteAsyncMethod.MakeGenericMethod(sequence.ResultItemType); - return (T) executeAsyncMethod.Invoke(Executor, new object[] {queryModel, cancellationToken}); + return (T) executeAsyncMethod.Invoke(Executor, new object[] {queryModel, cancellationToken})!; } else if (streamedDataInfo is AsyncStreamedValueInfo streamedValue) { diff --git a/Src/Couchbase.Linq/Execution/DelayedFilterQueryProvider.cs b/Src/Couchbase.Linq/Execution/DelayedFilterQueryProvider.cs new file mode 100644 index 0000000..ef7200b --- /dev/null +++ b/Src/Couchbase.Linq/Execution/DelayedFilterQueryProvider.cs @@ -0,0 +1,128 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading; +using Couchbase.Linq.Filters; +using Couchbase.Linq.Utils; +using Remotion.Linq.Utilities; + +namespace Couchbase.Linq.Execution +{ + /// + /// Implementation of which applies filters to instances + /// as each query is executed. This allows a long-lived which still functions correctly + /// if document filters are changed during its lifetime. + /// + internal sealed class DelayedFilterQueryProvider : IAsyncQueryProvider + { + private static readonly MethodInfo s_genericCreateQueryMethod = + ((Func>)CreateQuery).Method.GetGenericMethodDefinition(); + + private readonly IAsyncQueryProvider _innerQueryProvider; + private readonly ApplyFiltersExpressionVisitor _applyFiltersExpressionVisitor; + + public DelayedFilterQueryProvider(IAsyncQueryProvider innerQueryProvider, DocumentFilterManager filterManager) + { + ThrowHelpers.ThrowIfNull(innerQueryProvider); + ThrowHelpers.ThrowIfNull(filterManager); + + _innerQueryProvider = innerQueryProvider; + _applyFiltersExpressionVisitor = new ApplyFiltersExpressionVisitor(filterManager); + } + + public IQueryable CreateQuery(Expression expression) + { + ThrowHelpers.ThrowIfNull(expression); + + if (!ItemTypeReflectionUtility.TryGetItemTypeOfClosedGenericIEnumerable(expression.Type, out var itemType)) + { + throw new ArgumentException($"Expected a closed generic type implementing IEnumerable, but found '{expression.Type}'.", nameof(expression)); + } + + return (IQueryable)s_genericCreateQueryMethod.MakeGenericMethod(itemType).Invoke(null, new object[] { this, expression })!; + } + + public IQueryable CreateQuery(Expression expression) + { + ThrowHelpers.ThrowIfNull(expression); + return CreateQuery(this, expression); + } + + private static IQueryable CreateQuery(DelayedFilterQueryProvider provider, Expression expression) => + new CouchbaseQueryable(provider, expression); + + public object? Execute(Expression expression) + { + ThrowHelpers.ThrowIfNull(expression); + + return _innerQueryProvider.Execute(ApplyFilters(expression)); + } + + public TResult Execute(Expression expression) + { + ThrowHelpers.ThrowIfNull(expression); + + return _innerQueryProvider.Execute(ApplyFilters(expression)); + } + + public TResult ExecuteAsync(Expression expression, CancellationToken cancellationToken = default) + { + ThrowHelpers.ThrowIfNull(expression); + + return _innerQueryProvider.ExecuteAsync(ApplyFilters(expression), cancellationToken); + } + + // Apply filters to IDocumentSet constants on-demand for each query in case they are modified between queries + private Expression ApplyFilters(Expression expression) => _applyFiltersExpressionVisitor.Visit(expression); + + /// + /// Visits the expression tree finding the innermost constants + /// and replaces them with new Queryable instances that apply the filters. Because a query may + /// include multiple extents of multiple types, this visitor must be able to handle multiple different + /// types for T. + /// + private sealed class ApplyFiltersExpressionVisitor : ExpressionVisitor + { + private static readonly MethodInfo s_genericApplyFiltersMethod = + ((Func, IQueryable>)ApplyFilters).Method.GetGenericMethodDefinition(); + + private readonly DocumentFilterManager _filterManager; + + public ApplyFiltersExpressionVisitor(DocumentFilterManager filterManager) + { + _filterManager = filterManager; + } + + protected override Expression VisitConstant(ConstantExpression node) + { + if (node.Value is IDocumentSet documentSet) + { + var filterMethod = s_genericApplyFiltersMethod.MakeGenericMethod(documentSet.ElementType); + var filteredQueryable = filterMethod.Invoke(null, new object[] { _filterManager, documentSet })!; + + if (!ReferenceEquals(documentSet, filteredQueryable)) + { + // Replace the constant with a new queryable that applies the filters + // if a different queryable is returned. + + return ((IQueryable) filteredQueryable).Expression; + } + } + + return node; + } + + private static IQueryable ApplyFilters(DocumentFilterManager filterManager, IQueryable source) + { + var filters = filterManager.GetFilterSet(); + if (filters is null) + { + return source; + } + + return filters.ApplyFilters(source); + } + } + } +} diff --git a/Src/Couchbase.Linq/Filters/DocumentFilterSet.cs b/Src/Couchbase.Linq/Filters/DocumentFilterSet.cs index 1666b70..997a627 100644 --- a/Src/Couchbase.Linq/Filters/DocumentFilterSet.cs +++ b/Src/Couchbase.Linq/Filters/DocumentFilterSet.cs @@ -1,7 +1,7 @@ -using System; -using System.Collections; +using System.Collections; using System.Collections.Generic; using System.Linq; +using Couchbase.Linq.Utils; namespace Couchbase.Linq.Filters { @@ -13,23 +13,16 @@ namespace Couchbase.Linq.Filters /// public class DocumentFilterSet : IEnumerable> { - private readonly SortedSet> _sortedSet = - new SortedSet>(new PriorityComparer()); + private readonly SortedSet> _sortedSet; /// /// Create an DocumentFilterSet, filled with a set of filters. /// public DocumentFilterSet(IEnumerable> filters) { - if (filters == null) - { - throw new ArgumentNullException(nameof(filters)); - } + ThrowHelpers.ThrowIfNull(filters); - foreach (var filter in filters) - { - _sortedSet.Add(filter); - } + _sortedSet = new SortedSet>(filters, new PriorityComparer()); } /// @@ -45,10 +38,7 @@ public DocumentFilterSet(params IDocumentFilter[] filters) /// public IQueryable ApplyFilters(IQueryable source) { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } + ThrowHelpers.ThrowIfNull(source); foreach (var filter in this) { @@ -63,10 +53,19 @@ public IQueryable ApplyFilters(IQueryable source) IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - private class PriorityComparer : IComparer> + private sealed class PriorityComparer : IComparer> { - public int Compare(IDocumentFilter x, IDocumentFilter y) + public int Compare(IDocumentFilter? x, IDocumentFilter? y) { + if (x is null) + { + return y is null ? 0 : -1; + } + else if (y is null) + { + return 1; + } + return x.Priority.CompareTo(y.Priority); } } diff --git a/Src/Couchbase.Linq/IDocumentSet.cs b/Src/Couchbase.Linq/IDocumentSet.cs new file mode 100644 index 0000000..5637abe --- /dev/null +++ b/Src/Couchbase.Linq/IDocumentSet.cs @@ -0,0 +1,11 @@ +using System.Linq; + +namespace Couchbase.Linq +{ + /// + /// A set of documents in a Couchbase collection. + /// + public interface IDocumentSet : IQueryable + { + } +} diff --git a/Src/Couchbase.Linq/IDocumentSet`1.cs b/Src/Couchbase.Linq/IDocumentSet`1.cs index 4ac78a9..bc4d264 100644 --- a/Src/Couchbase.Linq/IDocumentSet`1.cs +++ b/Src/Couchbase.Linq/IDocumentSet`1.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Linq; using Couchbase.KeyValue; namespace Couchbase.Linq @@ -9,7 +8,7 @@ namespace Couchbase.Linq /// /// Type of the document. // ReSharper disable once TypeParameterCanBeVariant - public interface IDocumentSet : IQueryable + public interface IDocumentSet : IDocumentSet, IQueryable { /// /// The couchbase collection for these documents.