From 684e980633de66c31603d6999acf14e02edb672c Mon Sep 17 00:00:00 2001 From: Steve Lorello <42971704+slorello89@users.noreply.github.com> Date: Mon, 12 Sep 2022 03:56:18 -0400 Subject: [PATCH] allows users to not save enumerated data in the StateManager (#194) --- src/Redis.OM/RedisConnectionProvider.cs | 10 +++++ src/Redis.OM/SearchExtensions.cs | 14 +++---- src/Redis.OM/Searching/IRedisCollection.cs | 5 +++ src/Redis.OM/Searching/RedisCollection.cs | 40 +++++++++++++++++-- .../Searching/RedisCollectionEnumerator.cs | 10 ++++- src/Redis.OM/Searching/RedisQueryProvider.cs | 2 +- .../RediSearchTests/SearchFunctionalTests.cs | 23 +++++++++++ 7 files changed, 92 insertions(+), 12 deletions(-) diff --git a/src/Redis.OM/RedisConnectionProvider.cs b/src/Redis.OM/RedisConnectionProvider.cs index 8c531148..8a361442 100644 --- a/src/Redis.OM/RedisConnectionProvider.cs +++ b/src/Redis.OM/RedisConnectionProvider.cs @@ -70,5 +70,15 @@ public RedisConnectionProvider(IConnectionMultiplexer connectionMultiplexer) /// A RedisCollection. public IRedisCollection RedisCollection(int chunkSize = 100) where T : notnull => new RedisCollection(Connection, chunkSize); + + /// + /// Gets a redis collection. + /// + /// The type the collection will be retrieving. + /// Whether or not the RedisCollection should maintain the state of documents it enumerates. + /// Size of chunks to use during pagination, larger chunks = larger payloads returned but fewer round trips. + /// A RedisCollection. + public IRedisCollection RedisCollection(bool saveState, int chunkSize = 100) + where T : notnull => new RedisCollection(Connection, saveState, chunkSize); } } diff --git a/src/Redis.OM/SearchExtensions.cs b/src/Redis.OM/SearchExtensions.cs index 77b11de7..e42a6ca4 100644 --- a/src/Redis.OM/SearchExtensions.cs +++ b/src/Redis.OM/SearchExtensions.cs @@ -97,7 +97,7 @@ public static IRedisCollection Where(this IRedisCollection source, Expr null, GetMethodInfo(Where, source, expression), new[] { source.Expression, Expression.Quote(expression) }); - return new RedisCollection((RedisQueryProvider)source.Provider, exp, source.StateManager, combined, source.ChunkSize); + return new RedisCollection((RedisQueryProvider)source.Provider, exp, source.StateManager, combined, source.SaveState, source.ChunkSize); } /// @@ -116,7 +116,7 @@ public static IRedisCollection Select(this IRedisCollection source null, GetMethodInfo(Select, source, expression), new[] { source.Expression, Expression.Quote(expression) }); - return new RedisCollection((RedisQueryProvider)source.Provider, exp, source.StateManager, null, source.ChunkSize); + return new RedisCollection((RedisQueryProvider)source.Provider, exp, source.StateManager, null, source.SaveState, source.ChunkSize); } /// @@ -134,7 +134,7 @@ public static IRedisCollection Skip(this IRedisCollection source, int c null, GetMethodInfo(Skip, source, count), new[] { source.Expression, Expression.Constant(count) }); - return new RedisCollection((RedisQueryProvider)source.Provider, exp, source.StateManager, collection.BooleanExpression, source.ChunkSize); + return new RedisCollection((RedisQueryProvider)source.Provider, exp, source.StateManager, collection.BooleanExpression, source.SaveState, source.ChunkSize); } /// @@ -152,7 +152,7 @@ public static IRedisCollection Take(this IRedisCollection source, int c null, GetMethodInfo(Take, source, count), new[] { source.Expression, Expression.Constant(count) }); - return new RedisCollection((RedisQueryProvider)source.Provider, exp, source.StateManager, collection.BooleanExpression, source.ChunkSize); + return new RedisCollection((RedisQueryProvider)source.Provider, exp, source.StateManager, collection.BooleanExpression, source.SaveState, source.ChunkSize); } /// @@ -480,7 +480,7 @@ public static IRedisCollection GeoFilter(this IRedisCollection source, Expression.Constant(lat), Expression.Constant(radius), Expression.Constant(unit)); - return new RedisCollection((RedisQueryProvider)source.Provider, exp, source.StateManager, collection.BooleanExpression, source.ChunkSize); + return new RedisCollection((RedisQueryProvider)source.Provider, exp, source.StateManager, collection.BooleanExpression, source.SaveState, source.ChunkSize); } /// @@ -499,7 +499,7 @@ public static IRedisCollection OrderBy(this IRedisCollection so null, GetMethodInfo(OrderBy, source, expression), new[] { source.Expression, Expression.Quote(expression) }); - return new RedisCollection((RedisQueryProvider)source.Provider, exp, source.StateManager, collection.BooleanExpression, source.ChunkSize); + return new RedisCollection((RedisQueryProvider)source.Provider, exp, source.StateManager, collection.BooleanExpression, source.SaveState, source.ChunkSize); } /// @@ -518,7 +518,7 @@ public static IRedisCollection OrderByDescending(this IRedisCollec null, GetMethodInfo(OrderByDescending, source, expression), new[] { source.Expression, Expression.Quote(expression) }); - return new RedisCollection((RedisQueryProvider)source.Provider, exp, source.StateManager, collection.BooleanExpression, source.ChunkSize); + return new RedisCollection((RedisQueryProvider)source.Provider, exp, source.StateManager, collection.BooleanExpression, source.SaveState, source.ChunkSize); } /// diff --git a/src/Redis.OM/Searching/IRedisCollection.cs b/src/Redis.OM/Searching/IRedisCollection.cs index 203bf9b6..3b2c38d3 100644 --- a/src/Redis.OM/Searching/IRedisCollection.cs +++ b/src/Redis.OM/Searching/IRedisCollection.cs @@ -13,6 +13,11 @@ namespace Redis.OM.Searching /// The type. public interface IRedisCollection : IOrderedQueryable, IAsyncEnumerable { + /// + /// Gets a value indicating whether gets whether the collection is meant to save the state of the records enumerated into it. + /// + bool SaveState { get; } + /// /// Gets the collection state manager. /// diff --git a/src/Redis.OM/Searching/RedisCollection.cs b/src/Redis.OM/Searching/RedisCollection.cs index 54826d26..c4a76b7e 100644 --- a/src/Redis.OM/Searching/RedisCollection.cs +++ b/src/Redis.OM/Searching/RedisCollection.cs @@ -31,6 +31,18 @@ public class RedisCollection : IRedisCollection /// Connection to Redis. /// Size of chunks to pull back during pagination, defaults to 100. public RedisCollection(IRedisConnection connection, int chunkSize = 100) + : this(connection, true, chunkSize) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Determines whether or not the Redis Colleciton will maintain it's state internally. + /// Connection to Redis. + /// Size of chunks to pull back during pagination, defaults to 100. + /// Thrown if the root attribute of the Redis Colleciton is not decorated with a DocumentAttribute. + public RedisCollection(IRedisConnection connection, bool saveState, int chunkSize) { var t = typeof(T); DocumentAttribute rootAttribute = t.GetCustomAttribute(); @@ -41,6 +53,7 @@ public RedisCollection(IRedisConnection connection, int chunkSize = 100) ChunkSize = chunkSize; _connection = connection; + SaveState = saveState; StateManager = new RedisCollectionStateManager(rootAttribute); Initialize(new RedisQueryProvider(connection, StateManager, rootAttribute, ChunkSize), null, null); } @@ -51,13 +64,15 @@ public RedisCollection(IRedisConnection connection, int chunkSize = 100) /// Query Provider. /// Expression to be parsed for the query. /// Manager of the internal state of the collection. + /// Whether or not the StateManager will maintain the state. /// Size of chunks to pull back during pagination, defaults to 100. /// The expression to build the filter from. - internal RedisCollection(RedisQueryProvider provider, Expression expression, RedisCollectionStateManager stateManager, Expression>? booleanExpression, int chunkSize = 100) + internal RedisCollection(RedisQueryProvider provider, Expression expression, RedisCollectionStateManager stateManager, Expression>? booleanExpression, bool saveState, int chunkSize = 100) { StateManager = stateManager; _connection = provider.Connection; ChunkSize = chunkSize; + SaveState = saveState; Initialize(provider, expression, booleanExpression); } @@ -70,6 +85,9 @@ internal RedisCollection(RedisQueryProvider provider, Expression expression, Red /// public IQueryProvider Provider { get; private set; } = default!; + /// + public bool SaveState { get; } + /// /// Gets manages the state of the items queried from Redis. /// @@ -484,7 +502,7 @@ public T Single(Expression> expression) public IEnumerator GetEnumerator() { StateManager.Clear(); - return new RedisCollectionEnumerator(Expression, _connection, ChunkSize, StateManager, BooleanExpression); + return new RedisCollectionEnumerator(Expression, _connection, ChunkSize, StateManager, BooleanExpression, SaveState); } /// @@ -496,6 +514,14 @@ IEnumerator IEnumerable.GetEnumerator() /// public void Save() { + if (!SaveState) + { + throw new InvalidOperationException( + "The RedisCollection has been instructed to not maintain the state of records enumerated by " + + "Redis making the attempt to Save Invalid. Please initialize the RedisCollection with saveState " + + "set to true to Save documents in the RedisCollection"); + } + var diff = StateManager.DetectDifferences(); foreach (var item in diff) { @@ -516,6 +542,14 @@ public void Save() /// public async ValueTask SaveAsync() { + if (!SaveState) + { + throw new InvalidOperationException( + "The RedisCollection has been instructed to not maintain the state of records enumerated by " + + "Redis making the attempt to Save Invalid. Please initialize the RedisCollection with saveState " + + "set to true to Save documents in the RedisCollection"); + } + var diff = StateManager.DetectDifferences(); var tasks = new List>(); foreach (var item in diff) @@ -600,7 +634,7 @@ public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToke { var provider = (RedisQueryProvider)Provider; StateManager.Clear(); - return new RedisCollectionEnumerator(Expression, provider.Connection, ChunkSize, StateManager, BooleanExpression); + return new RedisCollectionEnumerator(Expression, provider.Connection, ChunkSize, StateManager, BooleanExpression, SaveState); } private static MethodInfo GetMethodInfo(Func f, T1 unused) diff --git a/src/Redis.OM/Searching/RedisCollectionEnumerator.cs b/src/Redis.OM/Searching/RedisCollectionEnumerator.cs index 14e628dd..089e8999 100644 --- a/src/Redis.OM/Searching/RedisCollectionEnumerator.cs +++ b/src/Redis.OM/Searching/RedisCollectionEnumerator.cs @@ -22,6 +22,7 @@ internal class RedisCollectionEnumerator : IEnumerator, IAsyncEnumerator _records = new (new RedisReply(new RedisReply[] { 0 })); @@ -36,7 +37,8 @@ internal class RedisCollectionEnumerator : IEnumerator, IAsyncEnumeratorthe size of a chunk to pull back. /// the state manager. /// The main boolean expression to use to build the filter. - public RedisCollectionEnumerator(Expression exp, IRedisConnection connection, int chunkSize, RedisCollectionStateManager stateManager, Expression>? booleanExpression) + /// Determins whether the records from the RedisCollection are stored in the StateManager. + public RedisCollectionEnumerator(Expression exp, IRedisConnection connection, int chunkSize, RedisCollectionStateManager stateManager, Expression>? booleanExpression, bool saveState) { Type rootType; var t = typeof(T); @@ -63,6 +65,7 @@ public RedisCollectionEnumerator(Expression exp, IRedisConnection connection, in _connection = connection; _stateManager = stateManager; + _saveState = saveState; } /// @@ -176,6 +179,11 @@ private async ValueTask GetNextChunkAsync() private void ConcatenateRecords() { + if (!_saveState) + { + return; + } + foreach (var record in _records.Documents) { if (_stateManager.Data.ContainsKey(record.Key) || _primitiveType != null) diff --git a/src/Redis.OM/Searching/RedisQueryProvider.cs b/src/Redis.OM/Searching/RedisQueryProvider.cs index 1942d069..06b39e5e 100644 --- a/src/Redis.OM/Searching/RedisQueryProvider.cs +++ b/src/Redis.OM/Searching/RedisQueryProvider.cs @@ -96,7 +96,7 @@ public IQueryable CreateQuery(Expression expression) where TElement : notnull { var booleanExpression = expression as Expression>; - return new RedisCollection(this, expression, StateManager, booleanExpression); + return new RedisCollection(this, expression, StateManager, booleanExpression, true); } /// diff --git a/test/Redis.OM.Unit.Tests/RediSearchTests/SearchFunctionalTests.cs b/test/Redis.OM.Unit.Tests/RediSearchTests/SearchFunctionalTests.cs index 35dbedb4..0c019350 100644 --- a/test/Redis.OM.Unit.Tests/RediSearchTests/SearchFunctionalTests.cs +++ b/test/Redis.OM.Unit.Tests/RediSearchTests/SearchFunctionalTests.cs @@ -810,5 +810,28 @@ public async Task TestMultipleContains() Assert.Contains(people, x =>x.Id == person1.Id); Assert.Contains(people, x =>x.Id == person2.Id); } + + [Fact] + public async Task TestShouldFailForSave() + { + var expectedText = "The RedisCollection has been instructed to not maintain the state of records enumerated by " + + "Redis making the attempt to Save Invalid. Please initialize the RedisCollection with saveState " + + "set to true to Save documents in the RedisCollection"; + var collection = new RedisCollection(_connection, false, 100); + var ex = await Assert.ThrowsAsync(collection.SaveAsync().AsTask); + Assert.Equal(expectedText, ex.Message); + ex = Assert.Throws(() =>collection.Save()); + Assert.Equal(expectedText, ex.Message); + } + + [Fact] + public async Task TestStatelessCollection() + { + var collection = new RedisCollection(_connection, false, 10000); + var res = await collection.ToListAsync(); + Assert.True(res.Count >= 1); + Assert.Equal(0,collection.StateManager.Data.Count); + Assert.Equal(0,collection.StateManager.Snapshot.Count); + } } }