From 057e684ec723e746a91614d0a133c400e8128d20 Mon Sep 17 00:00:00 2001 From: Darren Maddox Date: Wed, 20 Feb 2019 12:04:30 +0000 Subject: [PATCH 1/4] Added interceptors --- src/Cosmonaut/Cosmonaut.csproj | 5 + src/Cosmonaut/CosmosStore.cs | 54 +++-- src/Cosmonaut/CosmosStoreSettings.cs | 95 ++++++++- .../Extensions/ExpressionExtensions.cs | 1 + .../CosmosQueryInterceptionTests.cs | 187 ++++++++++++++++++ tests/Cosmonaut.System/CosmosStoreTests.cs | 55 +++--- tests/Cosmonaut.System/Models/Animal.cs | 6 +- 7 files changed, 349 insertions(+), 54 deletions(-) create mode 100644 tests/Cosmonaut.System/CosmosQueryInterceptionTests.cs diff --git a/src/Cosmonaut/Cosmonaut.csproj b/src/Cosmonaut/Cosmonaut.csproj index 883bdad..f1d4632 100644 --- a/src/Cosmonaut/Cosmonaut.csproj +++ b/src/Cosmonaut/Cosmonaut.csproj @@ -29,6 +29,11 @@ + + + + + diff --git a/src/Cosmonaut/CosmosStore.cs b/src/Cosmonaut/CosmosStore.cs index c2d19ec..caf0d55 100644 --- a/src/Cosmonaut/CosmosStore.cs +++ b/src/Cosmonaut/CosmosStore.cs @@ -1,14 +1,15 @@ -using System; +using Cosmonaut.Extensions; +using Cosmonaut.Response; +using Cosmonaut.Storage; +using Microsoft.Azure.Documents; +using Microsoft.Azure.Documents.Client; +using QueryInterceptor.Core; +using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; -using Cosmonaut.Extensions; -using Cosmonaut.Response; -using Cosmonaut.Storage; -using Microsoft.Azure.Documents; -using Microsoft.Azure.Documents.Client; namespace Cosmonaut { @@ -17,11 +18,11 @@ public sealed class CosmosStore : ICosmosStore where TEntity : public bool IsShared { get; internal set; } public string CollectionName { get; private set; } - + public string DatabaseName { get; } public CosmosStoreSettings Settings { get; } - + public ICosmonautClient CosmonautClient { get; } private readonly IDatabaseCreator _databaseCreator; @@ -82,6 +83,8 @@ public IQueryable Query(FeedOptions feedOptions = null) var queryable = CosmonautClient.Query(DatabaseName, CollectionName, GetFeedOptionsForQuery(feedOptions)); + queryable = ApplyInterception(queryable); + return IsShared ? queryable.Where(ExpressionExtensions.SharedCollectionExpression()) : queryable; } @@ -105,7 +108,7 @@ public async Task QuerySingleAsync(string sql, object parameters = null, F var queryable = CosmonautClient.Query(DatabaseName, CollectionName, collectionSharingFriendlySql, parameters, GetFeedOptionsForQuery(feedOptions)); return await queryable.SingleOrDefaultAsync(cancellationToken); } - + public async Task> QueryMultipleAsync(string sql, object parameters = null, FeedOptions feedOptions = null, CancellationToken cancellationToken = default) { var collectionSharingFriendlySql = sql.EnsureQueryIsCollectionSharingFriendly(); @@ -125,14 +128,14 @@ public async Task> AddAsync(TEntity entity, RequestOptio return await CosmonautClient.CreateDocumentAsync(DatabaseName, CollectionName, entity, GetRequestOptions(requestOptions, entity), cancellationToken); } - + public async Task> AddRangeAsync(IEnumerable entities, Func requestOptions = null, CancellationToken cancellationToken = default) { return await ExecuteMultiOperationAsync(entities, x => AddAsync(x, requestOptions?.Invoke(x), cancellationToken)); } - + public async Task> RemoveAsync( - Expression> predicate, + Expression> predicate, FeedOptions feedOptions = null, Func requestOptions = null, CancellationToken cancellationToken = default) @@ -148,7 +151,7 @@ public async Task> RemoveAsync(TEntity entity, RequestOp return await CosmonautClient.DeleteDocumentAsync(DatabaseName, CollectionName, documentId, GetRequestOptions(requestOptions, entity), cancellationToken).ExecuteCosmosCommand(entity); } - + public async Task> RemoveRangeAsync(IEnumerable entities, Func requestOptions = null, CancellationToken cancellationToken = default) { return await ExecuteMultiOperationAsync(entities, x => RemoveAsync(x, requestOptions?.Invoke(x), cancellationToken)); @@ -161,7 +164,7 @@ public async Task> UpdateAsync(TEntity entity, RequestOp return await CosmonautClient.UpdateDocumentAsync(DatabaseName, CollectionName, document, GetRequestOptions(requestOptions, entity), cancellationToken).ExecuteCosmosCommand(entity); } - + public async Task> UpdateRangeAsync(IEnumerable entities, Func requestOptions = null, CancellationToken cancellationToken = default) { return await ExecuteMultiOperationAsync(entities, x => UpdateAsync(x, requestOptions?.Invoke(x), cancellationToken)); @@ -178,7 +181,7 @@ public async Task> UpsertRangeAsync(IEnumerable< { return await ExecuteMultiOperationAsync(entities, x => UpsertAsync(x, requestOptions?.Invoke(x), cancellationToken)); } - + public async Task> RemoveByIdAsync(string id, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) { var response = await CosmonautClient.DeleteDocumentAsync(DatabaseName, CollectionName, id, @@ -208,12 +211,25 @@ public async Task FindAsync(string id, object partitionKeyValue, Cancel : null; return await FindAsync(id, requestOptions, cancellationToken); } - + + private IQueryable ApplyInterception(IQueryable queryable) + { + var interceptors = Settings.Interceptors?.Where(x => x.Type == typeof(TEntity)); + + if (!interceptors?.Any() ?? true) + { + return queryable; + } + + var visitors = Settings.Interceptors.Cast(); + return queryable.InterceptWith(visitors.ToArray()); + } + private void InitialiseCosmosStore(string overridenCollectionName) { IsShared = typeof(TEntity).UsesSharedCollection(); CollectionName = GetCosmosStoreCollectionName(overridenCollectionName); - + _databaseCreator.EnsureCreatedAsync(DatabaseName).ConfigureAwait(false).GetAwaiter().GetResult(); _collectionCreator.EnsureCreatedAsync(DatabaseName, CollectionName, Settings.DefaultCollectionThroughput, Settings.IndexingPolicy) .ConfigureAwait(false).GetAwaiter().GetResult(); @@ -235,7 +251,7 @@ private async Task> ExecuteMultiOperationAsync(I var entitiesList = entities.ToList(); if (!entitiesList.Any()) return multipleResponse; - + var results = (await entitiesList.Select(operationFunc).WhenAllTasksAsync()).ToList(); multipleResponse.SuccessfulEntities.AddRange(results.Where(x => x.IsSuccess)); multipleResponse.FailedEntities.AddRange(results.Where(x => !x.IsSuccess)); @@ -277,7 +293,7 @@ private RequestOptions GetRequestOptions(string id, RequestOptions requestOption private FeedOptions GetFeedOptionsForQuery(FeedOptions feedOptions) { - var shouldEnablePartitionQuery = (typeof(TEntity).HasPartitionKey() && feedOptions?.PartitionKey == null) + var shouldEnablePartitionQuery = (typeof(TEntity).HasPartitionKey() && feedOptions?.PartitionKey == null) || (feedOptions != null && feedOptions.EnableCrossPartitionQuery); if (feedOptions == null) diff --git a/src/Cosmonaut/CosmosStoreSettings.cs b/src/Cosmonaut/CosmosStoreSettings.cs index 5af202f..a49896d 100644 --- a/src/Cosmonaut/CosmosStoreSettings.cs +++ b/src/Cosmonaut/CosmosStoreSettings.cs @@ -1,7 +1,11 @@ -using System; -using Microsoft.Azure.Documents; +using Microsoft.Azure.Documents; using Microsoft.Azure.Documents.Client; using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; namespace Cosmonaut { @@ -19,7 +23,7 @@ public class CosmosStoreSettings public IndexingPolicy IndexingPolicy { get; set; } = CosmosConstants.DefaultIndexingPolicy; - public int DefaultCollectionThroughput { get; set; } = CosmosConstants.MinimumCosmosThroughput; + public int DefaultCollectionThroughput { get; set; } = CosmosConstants.MinimumCosmosThroughput; public JsonSerializerSettings JsonSerializerSettings { get; set; } @@ -27,6 +31,8 @@ public class CosmosStoreSettings public string CollectionPrefix { get; set; } = string.Empty; + public List Interceptors { get; } + public CosmosStoreSettings(string databaseName, string endpointUrl, string authKey, @@ -42,6 +48,7 @@ public CosmosStoreSettings(string databaseName, DatabaseName = databaseName ?? throw new ArgumentNullException(nameof(databaseName)); EndpointUrl = endpointUrl ?? throw new ArgumentNullException(nameof(endpointUrl)); AuthKey = authKey ?? throw new ArgumentNullException(nameof(authKey)); + Interceptors = new List(); settings?.Invoke(this); } @@ -52,18 +59,18 @@ public CosmosStoreSettings( ConnectionPolicy connectionPolicy = null, IndexingPolicy indexingPolicy = null, int defaultCollectionThroughput = CosmosConstants.MinimumCosmosThroughput) - : this(databaseName, - new Uri(endpointUrl), + : this(databaseName, + new Uri(endpointUrl), authKey, connectionPolicy, indexingPolicy, defaultCollectionThroughput) { } - + public CosmosStoreSettings( - string databaseName, - Uri endpointUrl, + string databaseName, + Uri endpointUrl, string authKey, ConnectionPolicy connectionPolicy = null, IndexingPolicy indexingPolicy = null, @@ -76,5 +83,77 @@ public CosmosStoreSettings( DefaultCollectionThroughput = defaultCollectionThroughput; IndexingPolicy = indexingPolicy ?? CosmosConstants.DefaultIndexingPolicy; } + + public void AddInterceptor(Expression> filter) where T : class + { + Interceptors.Add(new QueryInterceptor(filter)); + } + } + + public interface IQueryInterceptor + { + Type Type { get; } + } + + public class QueryInterceptor : ExpressionVisitor, IQueryInterceptor where TEntity : class + { + public Type Type { get; } + + public bool Applied { get; private set; } + + private readonly Expression> _predicate; + private ConstantExpression _constant; + + public QueryInterceptor(Expression> interceptor) + { + if (interceptor == null) + { + throw new ArgumentException(nameof(interceptor)); + } + + Type = typeof(TEntity); + _predicate = interceptor; + } + + public override Expression Visit(Expression node) + { + if (!(node is ConstantExpression constant)) + { + return base.Visit(node); + } + + if (!(constant.Value is IQueryable)) + { + return base.Visit(node); + } + + var method = GetLinqWhere(); + + Applied = true; + + return Expression.Call(method, constant, _predicate); + } + + private MethodInfo GetLinqWhere() + { + var method = typeof(Queryable).GetMethods() + .Where(x => x.Name == nameof(Queryable.Where)) + .Select(x => x.MakeGenericMethod(new[] { typeof(TEntity) })) + .Single(methodInfo => + { + var parameters = methodInfo.GetParameters(); + + if (parameters.Count() == 2 + && parameters[0].ParameterType == typeof(IQueryable) + && parameters[1].ParameterType == typeof(Expression>)) + { + return true; + } + + return false; + }); + + return method; + } } } \ No newline at end of file diff --git a/src/Cosmonaut/Extensions/ExpressionExtensions.cs b/src/Cosmonaut/Extensions/ExpressionExtensions.cs index 85253ff..8724f29 100644 --- a/src/Cosmonaut/Extensions/ExpressionExtensions.cs +++ b/src/Cosmonaut/Extensions/ExpressionExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq.Expressions; namespace Cosmonaut.Extensions diff --git a/tests/Cosmonaut.System/CosmosQueryInterceptionTests.cs b/tests/Cosmonaut.System/CosmosQueryInterceptionTests.cs new file mode 100644 index 0000000..a16eb93 --- /dev/null +++ b/tests/Cosmonaut.System/CosmosQueryInterceptionTests.cs @@ -0,0 +1,187 @@ +using Cosmonaut.System.Models; +using FluentAssertions; +using Microsoft.Azure.Documents.Client; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace Cosmonaut.System +{ + public class CosmosQueryInterceptionTests : IDisposable + { + private readonly ICosmonautClient _cosmonautClient; + private readonly Uri _emulatorUri = new Uri("https://localhost:8081"); + private readonly string _databaseId = $"DB{nameof(CosmosStoreTests)}"; + private readonly string _collectionName = $"COL{nameof(CosmosStoreTests)}"; + private readonly string _emulatorKey = + "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; + + private readonly ConnectionPolicy _connectionPolicy = new ConnectionPolicy + { + ConnectionProtocol = Protocol.Tcp, + ConnectionMode = ConnectionMode.Direct + }; + + public CosmosQueryInterceptionTests() + { + _cosmonautClient = new CosmonautClient(_emulatorUri, _emulatorKey, _connectionPolicy); + Seed().GetAwaiter().GetResult(); + } + + [Fact] + public void WhenInteceptorIsAppliedToQuery_ThenResultsShouldHaveOneItem() + { + var cosmosStoreSettings = new CosmosStoreSettings(_databaseId, _emulatorUri, _emulatorKey, + settings => + { + settings.AddInterceptor(x => x.Name == "Darren"); + }); + + var cosmosStore = new CosmosStore(cosmosStoreSettings, _collectionName); + + cosmosStore.Query().ToList().Should().HaveCount(1); + } + + [Fact] + public void WhenInteceptorIsAppliedToAnotherType_ThenResultsShouldBeUnaffected() + { + var cosmosStoreSettings = new CosmosStoreSettings(_databaseId, _emulatorUri, _emulatorKey, + settings => + { + settings.AddInterceptor(x => x.Name == "Darren"); + }); + + var cosmosStore = new CosmosStore(cosmosStoreSettings, _collectionName); + + cosmosStore.Query().ToList().Should().HaveCount(_cats.Count); + } + + [Fact] + public void WhenInteceptorIsAppliedAndQueryHaveFilterLogic_ThenThereShouldBeOneResult() + { + var cosmosStoreSettings = new CosmosStoreSettings(_databaseId, _emulatorUri, _emulatorKey, + settings => + { + settings.AddInterceptor(x => x.Name == "Nick"); + }); + + var cosmosStore = new CosmosStore(cosmosStoreSettings, _collectionName); + + var results = cosmosStore.Query().Where(x => x.Name == "Nick").ToList(); + results.Should().HaveCount(1); + } + + [Fact] + public void WhenInteceptorIsAppliedAndHasMultipleQueryHaveFilterLogic_ThenThereShouldBeOneResult() + { + var cosmosStoreSettings = new CosmosStoreSettings(_databaseId, _emulatorUri, _emulatorKey, + settings => + { + settings.AddInterceptor(x => x.Name == "Nick"); + }); + + var cosmosStore = new CosmosStore(cosmosStoreSettings, _collectionName); + + var results = cosmosStore.Query().Where(x => x.Name == "Nick").Where(x => x.Name == "Nick").ToList(); + results.Should().HaveCount(1); + results.Single().Name.Should().BeEquivalentTo("Nick"); + } + + [Fact] + public void WhenInteceptorIsAppliedAndHasSelect_ThenThereShouldBeOneResults() + { + var cosmosStoreSettings = new CosmosStoreSettings(_databaseId, _emulatorUri, _emulatorKey, + settings => + { + settings.AddInterceptor(x => x.Name == "Nick"); + }); + + var cosmosStore = new CosmosStore(cosmosStoreSettings, _collectionName); + + var results = cosmosStore.Query().Select(x => x.Name).ToList(); + results.Should().HaveCount(1); + results.Single().Equals("Nick"); + } + + [Fact] + public void WhenInteceptorIsAppliedAndHasWhereAndSelect_ThenThereShouldBeOneResults() + { + var cosmosStoreSettings = new CosmosStoreSettings(_databaseId, _emulatorUri, _emulatorKey, + settings => + { + settings.AddInterceptor(x => x.Name == "Nick"); + }); + + var cosmosStore = new CosmosStore(cosmosStoreSettings, _collectionName); + + var results = cosmosStore.Query().Where(x => x.Name == "Nick").Select(x => x.Name).ToList(); + results.Should().HaveCount(1); + results.Single().Equals("Nick"); + } + + [Fact] + public void WhenQueryInterceptorIsAppliedAndHasOrderBy_EnsureOneResult() + { + var cosmosStoreSettings = new CosmosStoreSettings(_databaseId, _emulatorUri, _emulatorKey, + settings => + { + settings.AddInterceptor(x => x.Name == "Nick"); + }); + + var cosmosStore = new CosmosStore(cosmosStoreSettings, _collectionName); + + var results = cosmosStore.Query().OrderBy(x => x.Name).ToList(); + results.Should().HaveCount(1); + results.Single().Name.Should().BeEquivalentTo("Nick"); + } + + [Fact] + public void WhenInceptorUsesDateArithmetic_EnsureResultsAreFiltered() + { + var cosmosStoreSettings = new CosmosStoreSettings(_databaseId, _emulatorUri, _emulatorKey, + settings => + { + settings.AddInterceptor(x => x.DateOfBirth > new DateTime(1994, 1, 1)); + }); + + var cosmosStore = new CosmosStore(cosmosStoreSettings, _collectionName); + var results = cosmosStore.Query().ToList(); + results.Should().HaveCount(2); + results.Should().Contain(x => x.Name == "Darren"); + results.Should().Contain(x => x.Name == "Nick"); + } + + private async Task Seed() + { + var settings = new CosmosStoreSettings(_databaseId, _emulatorUri, _emulatorKey); + var store = new CosmosStore(settings, _collectionName); + await store.AddRangeAsync(_cats); + } + + public void Dispose() + { + _cosmonautClient.DeleteDatabaseAsync(_databaseId).GetAwaiter().GetResult(); + } + + private readonly List _cats = new List + { + new Cat + { + Name = "Darren", + DateOfBirth = new DateTime(2000, 1, 1) + }, + new Cat + { + Name = "Nick", + DateOfBirth = new DateTime(1995, 1, 1) + }, + new Cat + { + Name = "Tom", + DateOfBirth = new DateTime(1990, 1, 1) + } + }; + } +} diff --git a/tests/Cosmonaut.System/CosmosStoreTests.cs b/tests/Cosmonaut.System/CosmosStoreTests.cs index 47c3b50..ce36489 100644 --- a/tests/Cosmonaut.System/CosmosStoreTests.cs +++ b/tests/Cosmonaut.System/CosmosStoreTests.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using Cosmonaut.Extensions; +using Cosmonaut.Extensions; using Cosmonaut.Extensions.Microsoft.DependencyInjection; using Cosmonaut.Response; using Cosmonaut.System.Models; @@ -13,6 +8,11 @@ using Microsoft.Azure.Documents.Client; using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; using Xunit; namespace Cosmonaut.System @@ -163,7 +163,7 @@ public async Task WhenEntitiesAreAddedAndIdExists_ThenTheyFail() addedResults.SuccessfulEntities.Count.Should().Be(0); addedResults.FailedEntities.Select(x => x.Exception).Should().AllBeAssignableTo(); addedResults.FailedEntities.Select(x => x.Exception).Cast().Select(x => x.StatusCode).Should().AllBeEquivalentTo(HttpStatusCode.Conflict); - addedResults.FailedEntities.Select(x=>x.CosmosOperationStatus).Should().AllBeEquivalentTo(CosmosOperationStatus.Conflict); + addedResults.FailedEntities.Select(x => x.CosmosOperationStatus).Should().AllBeEquivalentTo(CosmosOperationStatus.Conflict); } [Fact] @@ -171,17 +171,20 @@ public async Task WhenEntitiesAreAddedAndTheyChangedWithAccessCondition_ThenThey { var cosmosStore = _serviceProvider.GetService>(); var response = await ExecuteMultipleAddOperationsForType(list => cosmosStore.AddRangeAsync(list), 10); - + var addedCats = response.SuccessfulEntities .Select(x => JsonConvert.DeserializeObject(x.ResourceResponse.Resource.ToString())).ToList(); - addedCats.ForEach(x => x.Name = "different Name"); + addedCats.ForEach(x => x.Name = "different Name"); await cosmosStore.UpdateRangeAsync(addedCats); - var updatedResults = await cosmosStore.UpdateRangeAsync(addedCats, cat => new RequestOptions{AccessCondition = new AccessCondition + var updatedResults = await cosmosStore.UpdateRangeAsync(addedCats, cat => new RequestOptions { - Type = AccessConditionType.IfMatch, - Condition = cat.Etag - }}); + AccessCondition = new AccessCondition + { + Type = AccessConditionType.IfMatch, + Condition = cat.Etag + } + }); response.IsSuccess.Should().BeTrue(); response.FailedEntities.Count.Should().Be(0); @@ -190,7 +193,7 @@ public async Task WhenEntitiesAreAddedAndTheyChangedWithAccessCondition_ThenThey updatedResults.FailedEntities.Count.Should().Be(10); updatedResults.SuccessfulEntities.Count.Should().Be(0); updatedResults.FailedEntities.Select(x => x.Exception).Should().AllBeAssignableTo(); - updatedResults.FailedEntities.Select(x => x.Exception).Cast().Select(x=>x.StatusCode).Should().AllBeEquivalentTo(HttpStatusCode.PreconditionFailed); + updatedResults.FailedEntities.Select(x => x.Exception).Cast().Select(x => x.StatusCode).Should().AllBeEquivalentTo(HttpStatusCode.PreconditionFailed); updatedResults.FailedEntities.Select(x => x.CosmosOperationStatus).Should().AllBeEquivalentTo(CosmosOperationStatus.PreconditionFailed); } @@ -206,7 +209,7 @@ public async Task WhenValidEntitiesAreRemoved_ThenRemovedResultsAreSuccessful() var addedLions = await ExecuteMultipleAddOperationsForType(list => lionStore.AddRangeAsync(list)); var addedBirds = await ExecuteMultipleAddOperationsForType(list => birdStore.AddRangeAsync(list)); - await ExecuteMultipleAddOperationsForType(() => catStore.RemoveRangeAsync(addedCats.SuccessfulEntities.Select(x=>x.Entity)), HttpStatusCode.NoContent, addedCats.SuccessfulEntities.Select(x => x.Entity).ToList()); + await ExecuteMultipleAddOperationsForType(() => catStore.RemoveRangeAsync(addedCats.SuccessfulEntities.Select(x => x.Entity)), HttpStatusCode.NoContent, addedCats.SuccessfulEntities.Select(x => x.Entity).ToList()); await ExecuteMultipleAddOperationsForType(() => dogStore.RemoveRangeAsync(addedDogs.SuccessfulEntities.Select(x => x.Entity)), HttpStatusCode.NoContent, addedDogs.SuccessfulEntities.Select(x => x.Entity).ToList()); await ExecuteMultipleAddOperationsForType(() => lionStore.RemoveRangeAsync(addedLions.SuccessfulEntities.Select(x => x.Entity)), HttpStatusCode.NoContent, addedLions.SuccessfulEntities.Select(x => x.Entity).ToList()); await ExecuteMultipleAddOperationsForType(() => birdStore.RemoveRangeAsync(addedBirds.SuccessfulEntities.Select(x => x.Entity)), HttpStatusCode.NoContent, addedBirds.SuccessfulEntities.Select(x => x.Entity).ToList()); @@ -266,7 +269,7 @@ void ValidateBadUpdateResponse(CosmosMultipleResponse cosmosMultipleRespon var dogStore = _serviceProvider.GetService>(); var lionStore = _serviceProvider.GetService>(); var birdStore = _serviceProvider.GetService>(); - var addedCats = new List {new Cat {CatId = Guid.NewGuid().ToString()}}; + var addedCats = new List { new Cat { CatId = Guid.NewGuid().ToString() } }; var addedDogs = new List { new Dog { Id = Guid.NewGuid().ToString() } }; var addedLions = new List { new Lion { Id = Guid.NewGuid().ToString() } }; var addedBirds = new List { new Bird { Id = Guid.NewGuid().ToString() } }; @@ -281,7 +284,7 @@ void ValidateBadUpdateResponse(CosmosMultipleResponse cosmosMultipleRespon ValidateBadUpdateResponse(lionResults); ValidateBadUpdateResponse(birdResults); } - + [Fact] public async Task WhenValidEntitiesAreUpserted_ThenUpsertedResultsAreSuccessful() { @@ -317,7 +320,7 @@ public async Task WhenValidEntitiesAreAdded_ThenTheyCanBeQueriedFor() var lions = await lionStore.QueryMultipleAsync("select * from c"); var birds = await birdStore.Query().ToListAsync(); - cats.Should().BeEquivalentTo(addedCats.SuccessfulEntities.Select(x=>x.Entity), ExcludeEtagCheck()); + cats.Should().BeEquivalentTo(addedCats.SuccessfulEntities.Select(x => x.Entity), ExcludeEtagCheck()); dogs.Should().BeEquivalentTo(addedDogs.SuccessfulEntities.Select(x => x.Entity)); lions.Should().BeEquivalentTo(addedLions.SuccessfulEntities.Select(x => x.Entity), config => { @@ -373,7 +376,7 @@ public async Task WhenValidSingleEntitiesAreAdded_ThenTheyCanBeFoundAsSinglesAsy var catFound = await catStore.Query().FirstAsync(); var dogFound = await dogStore.Query().FirstOrDefaultAsync(); var lionFound = await lionStore.Query().SingleAsync(); - var birdFound = await birdStore.Query(new FeedOptions{MaxItemCount = 100}).SingleOrDefaultAsync(); + var birdFound = await birdStore.Query(new FeedOptions { MaxItemCount = 100 }).SingleOrDefaultAsync(); catFound.Should().BeEquivalentTo(JsonConvert.DeserializeObject(addedCat.ResourceResponse.Resource.ToString())); dogFound.Should().BeEquivalentTo(JsonConvert.DeserializeObject(addedDog.ResourceResponse.Resource.ToString())); @@ -474,9 +477,9 @@ public async Task WhenPaginatedQueryExecutesWithSkipTake_ThenPaginatedResultsAre { var catStore = _serviceProvider.GetService>(); var addedCats = (await ExecuteMultipleAddOperationsForType(list => catStore.AddRangeAsync(list), 15)) - .SuccessfulEntities.Select(x=>x.Entity).OrderBy(x=>x.Name).ToList(); + .SuccessfulEntities.Select(x => x.Entity).OrderBy(x => x.Name).ToList(); - var firstPage = await catStore.Query().WithPagination(1, 5).OrderBy(x=>x.Name).ToListAsync(); + var firstPage = await catStore.Query().WithPagination(1, 5).OrderBy(x => x.Name).ToListAsync(); var secondPage = await catStore.Query().WithPagination(2, 5).OrderBy(x => x.Name).ToListAsync(); var thirdPage = await catStore.Query().WithPagination(3, 5).OrderBy(x => x.Name).ToListAsync(); var fourthPage = await catStore.Query().WithPagination(4, 5).OrderBy(x => x.Name).ToListAsync(); @@ -512,7 +515,7 @@ public async Task WhenPaginatedQueryAndFeedOptionsExecutesWithNextPageAsync_Then var addedCats = (await ExecuteMultipleAddOperationsForType(list => catStore.AddRangeAsync(list), 15)) .SuccessfulEntities.Select(x => x.Entity).OrderBy(x => x.Name).ToList(); - var firstPage = await catStore.Query(new FeedOptions{ RequestContinuation = "SomethingBad", MaxItemCount = 666}).WithPagination(1, 5).OrderBy(x => x.Name).ToPagedListAsync(); + var firstPage = await catStore.Query(new FeedOptions { RequestContinuation = "SomethingBad", MaxItemCount = 666 }).WithPagination(1, 5).OrderBy(x => x.Name).ToPagedListAsync(); var secondPage = await firstPage.GetNextPageAsync(); var thirdPage = await secondPage.GetNextPageAsync(); var fourthPage = await thirdPage.GetNextPageAsync(); @@ -554,12 +557,12 @@ public async Task WhenPaginatedQueryExecutesWithContinuationToken_ThenPaginatedR } private async Task> ExecuteMultipleAddOperationsForType( - Func, Task>> operationFunc, int itemCount = 50) + Func, Task>> operationFunc, int itemCount = 50) where T : Animal, new() { var items = new List(); - - for (var i = 0; i < itemCount; i++){items.Add(new T { Name = Guid.NewGuid().ToString() });} + + for (var i = 0; i < itemCount; i++) { items.Add(new T { Name = Guid.NewGuid().ToString() }); } var addedCats = await operationFunc(items); @@ -598,7 +601,7 @@ public void Dispose() { _cosmonautClient.DeleteDatabaseAsync(_databaseId).GetAwaiter().GetResult(); } - + private void AddCosmosStores(ServiceCollection serviceCollection) { serviceCollection diff --git a/tests/Cosmonaut.System/Models/Animal.cs b/tests/Cosmonaut.System/Models/Animal.cs index b0c6f70..9592d6a 100644 --- a/tests/Cosmonaut.System/Models/Animal.cs +++ b/tests/Cosmonaut.System/Models/Animal.cs @@ -1,7 +1,11 @@ -namespace Cosmonaut.System.Models +using System; + +namespace Cosmonaut.System.Models { public class Animal { public string Name { get; set; } + + public DateTime? DateOfBirth { get; set; } } } \ No newline at end of file From 6958970002281420b01c08b114f5be683cd659df Mon Sep 17 00:00:00 2001 From: Darren Maddox Date: Wed, 20 Feb 2019 12:09:48 +0000 Subject: [PATCH 2/4] Fixed some of the feedback --- src/Cosmonaut/Cosmonaut.csproj | 4 -- src/Cosmonaut/CosmosStoreSettings.cs | 70 ++----------------- .../Interception/QueryInterceptor.cs | 69 ++++++++++++++++++ 3 files changed, 73 insertions(+), 70 deletions(-) create mode 100644 src/Cosmonaut/Interception/QueryInterceptor.cs diff --git a/src/Cosmonaut/Cosmonaut.csproj b/src/Cosmonaut/Cosmonaut.csproj index f1d4632..2efe20a 100644 --- a/src/Cosmonaut/Cosmonaut.csproj +++ b/src/Cosmonaut/Cosmonaut.csproj @@ -32,8 +32,4 @@ - - - - diff --git a/src/Cosmonaut/CosmosStoreSettings.cs b/src/Cosmonaut/CosmosStoreSettings.cs index a49896d..ba783ee 100644 --- a/src/Cosmonaut/CosmosStoreSettings.cs +++ b/src/Cosmonaut/CosmosStoreSettings.cs @@ -1,11 +1,10 @@ -using Microsoft.Azure.Documents; +using Cosmonaut.Interception; +using Microsoft.Azure.Documents; using Microsoft.Azure.Documents.Client; using Newtonsoft.Json; using System; using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; -using System.Reflection; namespace Cosmonaut { @@ -31,7 +30,7 @@ public class CosmosStoreSettings public string CollectionPrefix { get; set; } = string.Empty; - public List Interceptors { get; } + public IReadOnlyList Interceptors { get; } public CosmosStoreSettings(string databaseName, string endpointUrl, @@ -82,6 +81,7 @@ public CosmosStoreSettings( ConnectionPolicy = connectionPolicy; DefaultCollectionThroughput = defaultCollectionThroughput; IndexingPolicy = indexingPolicy ?? CosmosConstants.DefaultIndexingPolicy; + Interceptors = new List(); } public void AddInterceptor(Expression> filter) where T : class @@ -94,66 +94,4 @@ public interface IQueryInterceptor { Type Type { get; } } - - public class QueryInterceptor : ExpressionVisitor, IQueryInterceptor where TEntity : class - { - public Type Type { get; } - - public bool Applied { get; private set; } - - private readonly Expression> _predicate; - private ConstantExpression _constant; - - public QueryInterceptor(Expression> interceptor) - { - if (interceptor == null) - { - throw new ArgumentException(nameof(interceptor)); - } - - Type = typeof(TEntity); - _predicate = interceptor; - } - - public override Expression Visit(Expression node) - { - if (!(node is ConstantExpression constant)) - { - return base.Visit(node); - } - - if (!(constant.Value is IQueryable)) - { - return base.Visit(node); - } - - var method = GetLinqWhere(); - - Applied = true; - - return Expression.Call(method, constant, _predicate); - } - - private MethodInfo GetLinqWhere() - { - var method = typeof(Queryable).GetMethods() - .Where(x => x.Name == nameof(Queryable.Where)) - .Select(x => x.MakeGenericMethod(new[] { typeof(TEntity) })) - .Single(methodInfo => - { - var parameters = methodInfo.GetParameters(); - - if (parameters.Count() == 2 - && parameters[0].ParameterType == typeof(IQueryable) - && parameters[1].ParameterType == typeof(Expression>)) - { - return true; - } - - return false; - }); - - return method; - } - } } \ No newline at end of file diff --git a/src/Cosmonaut/Interception/QueryInterceptor.cs b/src/Cosmonaut/Interception/QueryInterceptor.cs new file mode 100644 index 0000000..0f3d816 --- /dev/null +++ b/src/Cosmonaut/Interception/QueryInterceptor.cs @@ -0,0 +1,69 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace Cosmonaut.Interception +{ + public class QueryInterceptor : ExpressionVisitor, IQueryInterceptor where TEntity : class + { + public Type Type { get; } + + public bool Applied { get; private set; } + + private readonly Expression> _predicate; + private ConstantExpression _constant; + + public QueryInterceptor(Expression> interceptor) + { + if (interceptor == null) + { + throw new ArgumentException(nameof(interceptor)); + } + + Type = typeof(TEntity); + _predicate = interceptor; + } + + public override Expression Visit(Expression node) + { + if (!(node is ConstantExpression constant)) + { + return base.Visit(node); + } + + if (!(constant.Value is IQueryable)) + { + return base.Visit(node); + } + + var method = GetLinqWhere(); + + Applied = true; + + return Expression.Call(method, constant, _predicate); + } + + private MethodInfo GetLinqWhere() + { + var method = typeof(Queryable).GetMethods() + .Where(x => x.Name == nameof(Queryable.Where)) + .Select(x => x.MakeGenericMethod(new[] { typeof(TEntity) })) + .Single(methodInfo => + { + var parameters = methodInfo.GetParameters(); + + if (parameters.Count() == 2 + && parameters[0].ParameterType == typeof(IQueryable) + && parameters[1].ParameterType == typeof(Expression>)) + { + return true; + } + + return false; + }); + + return method; + } + } +} \ No newline at end of file From 2cd9fdc163048f6b519f39581036ab9b895e7aaa Mon Sep 17 00:00:00 2001 From: Darren Maddox Date: Wed, 20 Feb 2019 12:57:07 +0000 Subject: [PATCH 3/4] * ApplyInterception because an extension method * Removed QueryInterceptor.Core --- src/Cosmonaut/Cosmonaut.csproj | 1 - src/Cosmonaut/CosmosStore.cs | 19 +-- src/Cosmonaut/CosmosStoreSettings.cs | 2 +- .../Extensions/IQueryableExtensions.cs | 30 +++++ .../Interception/QueryInterceptor.cs | 4 +- .../QueryTranslation/QueryTranslator.cs | 77 +++++++++++ .../QueryTranslatorProviderAsync.cs | 122 ++++++++++++++++++ 7 files changed, 234 insertions(+), 21 deletions(-) create mode 100644 src/Cosmonaut/Extensions/IQueryableExtensions.cs create mode 100644 src/Cosmonaut/Interception/QueryTranslation/QueryTranslator.cs create mode 100644 src/Cosmonaut/Interception/QueryTranslation/QueryTranslatorProviderAsync.cs diff --git a/src/Cosmonaut/Cosmonaut.csproj b/src/Cosmonaut/Cosmonaut.csproj index 2efe20a..883bdad 100644 --- a/src/Cosmonaut/Cosmonaut.csproj +++ b/src/Cosmonaut/Cosmonaut.csproj @@ -29,7 +29,6 @@ - diff --git a/src/Cosmonaut/CosmosStore.cs b/src/Cosmonaut/CosmosStore.cs index caf0d55..46b448f 100644 --- a/src/Cosmonaut/CosmosStore.cs +++ b/src/Cosmonaut/CosmosStore.cs @@ -3,7 +3,6 @@ using Cosmonaut.Storage; using Microsoft.Azure.Documents; using Microsoft.Azure.Documents.Client; -using QueryInterceptor.Core; using System; using System.Collections.Generic; using System.Linq; @@ -81,9 +80,8 @@ internal CosmosStore(ICosmonautClient cosmonautClient, public IQueryable Query(FeedOptions feedOptions = null) { var queryable = - CosmonautClient.Query(DatabaseName, CollectionName, GetFeedOptionsForQuery(feedOptions)); - - queryable = ApplyInterception(queryable); + CosmonautClient.Query(DatabaseName, CollectionName, GetFeedOptionsForQuery(feedOptions)) + .ApplyInterception(Settings.Interceptors); return IsShared ? queryable.Where(ExpressionExtensions.SharedCollectionExpression()) : queryable; } @@ -212,19 +210,6 @@ public async Task FindAsync(string id, object partitionKeyValue, Cancel return await FindAsync(id, requestOptions, cancellationToken); } - private IQueryable ApplyInterception(IQueryable queryable) - { - var interceptors = Settings.Interceptors?.Where(x => x.Type == typeof(TEntity)); - - if (!interceptors?.Any() ?? true) - { - return queryable; - } - - var visitors = Settings.Interceptors.Cast(); - return queryable.InterceptWith(visitors.ToArray()); - } - private void InitialiseCosmosStore(string overridenCollectionName) { IsShared = typeof(TEntity).UsesSharedCollection(); diff --git a/src/Cosmonaut/CosmosStoreSettings.cs b/src/Cosmonaut/CosmosStoreSettings.cs index ba783ee..50e2ca9 100644 --- a/src/Cosmonaut/CosmosStoreSettings.cs +++ b/src/Cosmonaut/CosmosStoreSettings.cs @@ -30,7 +30,7 @@ public class CosmosStoreSettings public string CollectionPrefix { get; set; } = string.Empty; - public IReadOnlyList Interceptors { get; } + public List Interceptors { get; } public CosmosStoreSettings(string databaseName, string endpointUrl, diff --git a/src/Cosmonaut/Extensions/IQueryableExtensions.cs b/src/Cosmonaut/Extensions/IQueryableExtensions.cs new file mode 100644 index 0000000..77b4fe5 --- /dev/null +++ b/src/Cosmonaut/Extensions/IQueryableExtensions.cs @@ -0,0 +1,30 @@ +using Cosmonaut.Interception.QueryTranslation; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; + +namespace Cosmonaut.Extensions +{ + public static class IQueryableExtensions + { + public static IQueryable ApplyInterception(this IQueryable queryable, IEnumerable interceptors) + { + interceptors = interceptors?.Where(x => x.Type == typeof(TEntity)); + + if (!interceptors?.Any() ?? true) + { + return queryable; + } + + var visitors = interceptors.Cast(); + return queryable.InterceptWith(visitors.ToArray()); + } + + public static IQueryable InterceptWith(this IQueryable source, params ExpressionVisitor[] visitors) + { + return new QueryTranslator(source, visitors); + } + } +} + diff --git a/src/Cosmonaut/Interception/QueryInterceptor.cs b/src/Cosmonaut/Interception/QueryInterceptor.cs index 0f3d816..21330bf 100644 --- a/src/Cosmonaut/Interception/QueryInterceptor.cs +++ b/src/Cosmonaut/Interception/QueryInterceptor.cs @@ -12,7 +12,7 @@ public class QueryInterceptor : ExpressionVisitor, IQueryInterceptor wh public bool Applied { get; private set; } private readonly Expression> _predicate; - private ConstantExpression _constant; + private readonly ConstantExpression _constant; public QueryInterceptor(Expression> interceptor) { @@ -46,7 +46,7 @@ public override Expression Visit(Expression node) private MethodInfo GetLinqWhere() { - var method = typeof(Queryable).GetMethods() + var method = typeof(Queryable).GetRuntimeMethods() .Where(x => x.Name == nameof(Queryable.Where)) .Select(x => x.MakeGenericMethod(new[] { typeof(TEntity) })) .Single(methodInfo => diff --git a/src/Cosmonaut/Interception/QueryTranslation/QueryTranslator.cs b/src/Cosmonaut/Interception/QueryTranslation/QueryTranslator.cs new file mode 100644 index 0000000..180197a --- /dev/null +++ b/src/Cosmonaut/Interception/QueryTranslation/QueryTranslator.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; + +namespace Cosmonaut.Interception.QueryTranslation +{ + internal class QueryTranslator : IOrderedQueryable + { + private readonly Expression _expression; + private readonly QueryTranslatorProviderAsync _provider; + + /// + /// Initializes a new instance of the class. + /// + /// The source. + /// The visitors. + public QueryTranslator(IQueryable source, IEnumerable visitors) + { + _expression = Expression.Constant(this); + _provider = new QueryTranslatorProviderAsync(source, visitors); + } + + /// + /// Initializes a new instance of the class. + /// + /// The source. + /// The expression. + /// The visitors. + public QueryTranslator(IQueryable source, Expression expression, IEnumerable visitors) + { + _expression = expression; + _provider = new QueryTranslatorProviderAsync(source, visitors); + } + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// + /// A that can be used to iterate through the collection. + /// + public IEnumerator GetEnumerator() + { + return ((IEnumerable)_provider.ExecuteEnumerable(_expression)).GetEnumerator(); + } + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// + /// An object that can be used to iterate through the collection. + /// + IEnumerator IEnumerable.GetEnumerator() + { + return _provider.ExecuteEnumerable(_expression).GetEnumerator(); + } + + /// + /// Gets the type of the element(s) that are returned when the expression tree associated with this instance of is executed. + /// + /// A that represents the type of the element(s) that are returned when the expression tree associated with this object is executed. + public Type ElementType => typeof(T); + + /// + /// Gets the expression tree that is associated with the instance of . + /// + /// The that is associated with this instance of . + public Expression Expression => _expression; + + /// + /// Gets the query provider that is associated with this data source. + /// + /// The that is associated with this data source. + public IQueryProvider Provider => _provider; + } +} diff --git a/src/Cosmonaut/Interception/QueryTranslation/QueryTranslatorProviderAsync.cs b/src/Cosmonaut/Interception/QueryTranslation/QueryTranslatorProviderAsync.cs new file mode 100644 index 0000000..f8256f4 --- /dev/null +++ b/src/Cosmonaut/Interception/QueryTranslation/QueryTranslatorProviderAsync.cs @@ -0,0 +1,122 @@ +using Cosmonaut.Extensions; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +namespace Cosmonaut.Interception.QueryTranslation +{ + + internal class QueryTranslatorProviderAsync : ExpressionVisitor, IQueryProvider + { + private static readonly TraceSource _traceSource = new TraceSource(typeof(QueryTranslatorProviderAsync).Name); + private readonly IEnumerable _visitors; + + internal IQueryable Source { get; } + + public IQueryProvider OriginalProvider { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The source. + /// The visitors. + public QueryTranslatorProviderAsync(IQueryable source, IEnumerable visitors) + { + Source = source; + OriginalProvider = source.Provider; + _visitors = visitors; + } + + public IQueryable CreateQuery(Expression expression) + { + return new QueryTranslator(Source, expression, _visitors); + } + + public IQueryable CreateQuery(Expression expression) + { + Type elementType = expression.Type.GenericTypeArguments.First(); + return (IQueryable)Activator.CreateInstance(typeof(QueryTranslator<>).MakeGenericType(elementType), Source, expression, _visitors); + } + + public TResult Execute(Expression expression) + { + var translated = VisitAllAndOptimize(expression); + + return Source.Provider.Execute(translated); + } + + public object Execute(Expression expression) + { + return Execute(expression); + } + + public Task ExecuteAsync(Expression expression) + { + return ExecuteAsync(expression, CancellationToken.None); + } + + public Task ExecuteAsync(Expression expression, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + // In case Source.Provider is not a IDbAsyncQueryProvider or EntityQueryProvider, just start a new Task + return Task.Factory.StartNew(() => Execute(expression), cancellationToken); + } + + public Task ExecuteAsync(Expression expression) + { + return ExecuteAsync(expression, CancellationToken.None); + } + + public Task ExecuteAsync(Expression expression, CancellationToken cancellationToken) + { + return ExecuteAsync(expression, cancellationToken); + } + + internal IEnumerable ExecuteEnumerable(Expression expression) + { + var translated = VisitAllAndOptimize(expression); + + return Source.Provider.CreateQuery(translated); + } + + private Expression VisitAllAndOptimize(Expression expression) + { + // Run all visitors in order + var visitors = new ExpressionVisitor[] { this }.Concat(_visitors); + + var translated = visitors.Aggregate(expression, (expr, visitor) => visitor.Visit(expr)); + + return translated; + } + + /// + /// Visits the . + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression. + protected override Expression VisitConstant(ConstantExpression node) + { + // Fix up the Expression tree to work with the underlying LINQ provider + if (node.Type.GetTypeInfo().IsGenericType && node.Type.GetGenericTypeDefinition() == typeof(QueryTranslator<>)) + { + + if (((IQueryable)node.Value).Provider is QueryTranslatorProviderAsync provider) + { + return provider.Source.Expression; + } + + return Source.Expression; + } + + return base.VisitConstant(node); + } + } +} + From 5d98081fb83687c6befacc2acf981f6a7337bf7b Mon Sep 17 00:00:00 2001 From: Darren Maddox Date: Wed, 20 Feb 2019 13:07:02 +0000 Subject: [PATCH 4/4] Updated tests to use async method --- .../CosmosQueryInterceptionTests.cs | 50 ++++++++++++------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/tests/Cosmonaut.System/CosmosQueryInterceptionTests.cs b/tests/Cosmonaut.System/CosmosQueryInterceptionTests.cs index a16eb93..1f4cf92 100644 --- a/tests/Cosmonaut.System/CosmosQueryInterceptionTests.cs +++ b/tests/Cosmonaut.System/CosmosQueryInterceptionTests.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading.Tasks; using Xunit; +using Cosmonaut.Extensions; namespace Cosmonaut.System { @@ -31,7 +32,7 @@ public CosmosQueryInterceptionTests() } [Fact] - public void WhenInteceptorIsAppliedToQuery_ThenResultsShouldHaveOneItem() + public async Task WhenInteceptorIsAppliedToQuery_ThenResultsShouldHaveOneItem() { var cosmosStoreSettings = new CosmosStoreSettings(_databaseId, _emulatorUri, _emulatorKey, settings => @@ -40,12 +41,12 @@ public void WhenInteceptorIsAppliedToQuery_ThenResultsShouldHaveOneItem() }); var cosmosStore = new CosmosStore(cosmosStoreSettings, _collectionName); - - cosmosStore.Query().ToList().Should().HaveCount(1); + var results = await cosmosStore.Query().ToListAsync(); + results.ToList().Should().HaveCount(1); } [Fact] - public void WhenInteceptorIsAppliedToAnotherType_ThenResultsShouldBeUnaffected() + public async Task WhenInteceptorIsAppliedToAnotherType_ThenResultsShouldBeUnaffected() { var cosmosStoreSettings = new CosmosStoreSettings(_databaseId, _emulatorUri, _emulatorKey, settings => @@ -55,11 +56,12 @@ public void WhenInteceptorIsAppliedToAnotherType_ThenResultsShouldBeUnaffected() var cosmosStore = new CosmosStore(cosmosStoreSettings, _collectionName); - cosmosStore.Query().ToList().Should().HaveCount(_cats.Count); + var results = await cosmosStore.Query().ToListAsync(); + results.Should().HaveCount(_cats.Count); } [Fact] - public void WhenInteceptorIsAppliedAndQueryHaveFilterLogic_ThenThereShouldBeOneResult() + public async Task WhenInteceptorIsAppliedAndQueryHaveFilterLogic_ThenThereShouldBeOneResult() { var cosmosStoreSettings = new CosmosStoreSettings(_databaseId, _emulatorUri, _emulatorKey, settings => @@ -69,12 +71,15 @@ public void WhenInteceptorIsAppliedAndQueryHaveFilterLogic_ThenThereShouldBeOneR var cosmosStore = new CosmosStore(cosmosStoreSettings, _collectionName); - var results = cosmosStore.Query().Where(x => x.Name == "Nick").ToList(); + var results = await cosmosStore.Query() + .Where(x => x.Name == "Nick") + .ToListAsync(); + results.Should().HaveCount(1); } [Fact] - public void WhenInteceptorIsAppliedAndHasMultipleQueryHaveFilterLogic_ThenThereShouldBeOneResult() + public async Task WhenInteceptorIsAppliedAndHasMultipleQueryHaveFilterLogic_ThenThereShouldBeOneResult() { var cosmosStoreSettings = new CosmosStoreSettings(_databaseId, _emulatorUri, _emulatorKey, settings => @@ -84,13 +89,17 @@ public void WhenInteceptorIsAppliedAndHasMultipleQueryHaveFilterLogic_ThenThereS var cosmosStore = new CosmosStore(cosmosStoreSettings, _collectionName); - var results = cosmosStore.Query().Where(x => x.Name == "Nick").Where(x => x.Name == "Nick").ToList(); + var results = await cosmosStore.Query() + .Where(x => x.Name == "Nick") + .Where(x => x.Name == "Nick") + .ToListAsync(); + results.Should().HaveCount(1); results.Single().Name.Should().BeEquivalentTo("Nick"); } [Fact] - public void WhenInteceptorIsAppliedAndHasSelect_ThenThereShouldBeOneResults() + public async Task WhenInteceptorIsAppliedAndHasSelect_ThenThereShouldBeOneResults() { var cosmosStoreSettings = new CosmosStoreSettings(_databaseId, _emulatorUri, _emulatorKey, settings => @@ -100,13 +109,13 @@ public void WhenInteceptorIsAppliedAndHasSelect_ThenThereShouldBeOneResults() var cosmosStore = new CosmosStore(cosmosStoreSettings, _collectionName); - var results = cosmosStore.Query().Select(x => x.Name).ToList(); + var results = await cosmosStore.Query().Select(x => x.Name).ToListAsync(); results.Should().HaveCount(1); results.Single().Equals("Nick"); } [Fact] - public void WhenInteceptorIsAppliedAndHasWhereAndSelect_ThenThereShouldBeOneResults() + public async Task WhenInteceptorIsAppliedAndHasWhereAndSelect_ThenThereShouldBeOneResults() { var cosmosStoreSettings = new CosmosStoreSettings(_databaseId, _emulatorUri, _emulatorKey, settings => @@ -116,13 +125,17 @@ public void WhenInteceptorIsAppliedAndHasWhereAndSelect_ThenThereShouldBeOneResu var cosmosStore = new CosmosStore(cosmosStoreSettings, _collectionName); - var results = cosmosStore.Query().Where(x => x.Name == "Nick").Select(x => x.Name).ToList(); + var results = await cosmosStore.Query() + .Where(x => x.Name == "Nick") + .Select(x => x.Name) + .ToListAsync(); + results.Should().HaveCount(1); results.Single().Equals("Nick"); } [Fact] - public void WhenQueryInterceptorIsAppliedAndHasOrderBy_EnsureOneResult() + public async Task WhenQueryInterceptorIsAppliedAndHasOrderBy_EnsureOneResult() { var cosmosStoreSettings = new CosmosStoreSettings(_databaseId, _emulatorUri, _emulatorKey, settings => @@ -132,13 +145,16 @@ public void WhenQueryInterceptorIsAppliedAndHasOrderBy_EnsureOneResult() var cosmosStore = new CosmosStore(cosmosStoreSettings, _collectionName); - var results = cosmosStore.Query().OrderBy(x => x.Name).ToList(); + var results = await cosmosStore.Query() + .OrderBy(x => x.Name) + .ToListAsync(); + results.Should().HaveCount(1); results.Single().Name.Should().BeEquivalentTo("Nick"); } [Fact] - public void WhenInceptorUsesDateArithmetic_EnsureResultsAreFiltered() + public async Task WhenInceptorUsesDateArithmetic_EnsureResultsAreFiltered() { var cosmosStoreSettings = new CosmosStoreSettings(_databaseId, _emulatorUri, _emulatorKey, settings => @@ -147,7 +163,7 @@ public void WhenInceptorUsesDateArithmetic_EnsureResultsAreFiltered() }); var cosmosStore = new CosmosStore(cosmosStoreSettings, _collectionName); - var results = cosmosStore.Query().ToList(); + var results = await cosmosStore.Query().ToListAsync(); results.Should().HaveCount(2); results.Should().Contain(x => x.Name == "Darren"); results.Should().Contain(x => x.Name == "Nick");