From 815281a678af2834759665efd2adf106d3d6d39a Mon Sep 17 00:00:00 2001 From: Richard Irons <115992270+RichardIrons-neo4j@users.noreply.github.com> Date: Fri, 28 Apr 2023 11:16:51 +0100 Subject: [PATCH] ExecuteQuery API ready for release (#700) * Transformation fluent working * tidying up; xml docs * remove wrong file * Licensing text * Fix build problem * Transformation fluent working * tidying up; xml docs * remove wrong file * Licensing text * Fix build problem * fixups * Map/Filter/Reduce working (no xml docs) * Tests for stream processor * XML docs * Review notes part 1 * Review notes part 2 * CofigureAwaits * readme * Docs fixes * Spelling * ConfigureAwait --- .../Direct/ResultIT.cs | 7 +- .../Internals/Cluster/ExistingCluster.cs | 6 +- .../Neo4j.Driver.Tests.Integration.csproj | 4 +- .../BookmarkManager/NewBookmarkManager.cs | 3 +- .../Protocol/Driver/DriverClose.cs | 2 +- .../Protocol/DriverQuery/ExecuteQuery.cs | 4 +- .../Types/CypherToNative.cs | 2 +- .../Neo4j.Driver.Tests/AsyncSessionTests.cs | 1 - .../ExecutableQuery/ExecutableQueryTests.cs | 396 ++++++++++++++++++ .../BookmarkManagerFactoryTests.cs | 1 - .../DefaultBookmarkManagerTests.cs | 1 - .../Neo4j.Driver.Tests.csproj | 3 +- .../{Preview => }/BookmarkManagerConfig.cs | 2 +- .../{Preview => ExecuteQuery}/EagerResult.cs | 11 +- .../ExecuteQuery/IExecutableQuery.cs | 146 +++++++ .../{Preview => ExecuteQuery}/QueryConfig.cs | 2 +- Neo4j.Driver/Neo4j.Driver/GraphDatabase.cs | 8 + .../{Preview => }/IBookmarkManager.cs | 9 +- .../{Preview => }/IBookmarkManagerFactory.cs | 4 +- Neo4j.Driver/Neo4j.Driver/IDriver.cs | 12 + .../Neo4j.Driver/Internal/AsyncSession.cs | 1 - .../Internal/BookmarkManagerFactory.cs | 3 - .../Internal/DefaultBookmarkManager.cs | 1 - Neo4j.Driver/Neo4j.Driver/Internal/Driver.cs | 65 ++- .../Internal/ExecuteQuery/DriverRowSource.cs | 76 ++++ .../Internal/ExecuteQuery/ExecutableQuery.cs | 133 ++++++ .../Internal/ExecuteQuery/IQueryRowSource.cs | 29 ++ .../Internal/ExecuteQuery/ReduceToList.cs | 36 ++ .../ExecuteQuery/ReducedExecutableQuery.cs | 54 +++ .../StreamProcessorExecutableQuery.cs | 42 ++ .../Neo4j.Driver/Internal/IInternalDriver.cs | 10 +- Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj | 3 + .../Neo4j.Driver.csproj.DotSettings | 27 +- .../Preview/FluentQueries/ExecutableQuery.cs | 100 ----- .../Preview/FluentQueries/IExecutableQuery.cs | 70 ---- .../Neo4j.Driver/Preview/GraphDatabase.cs | 36 -- .../Neo4j.Driver/Preview/PreviewExtensions.cs | 98 ----- Neo4j.Driver/Neo4j.Driver/SessionConfig.cs | 9 +- README.md | 81 ++-- 39 files changed, 1086 insertions(+), 412 deletions(-) create mode 100644 Neo4j.Driver/Neo4j.Driver.Tests/ExecutableQuery/ExecutableQueryTests.cs rename Neo4j.Driver/Neo4j.Driver/{Preview => }/BookmarkManagerConfig.cs (99%) rename Neo4j.Driver/Neo4j.Driver/{Preview => ExecuteQuery}/EagerResult.cs (78%) create mode 100644 Neo4j.Driver/Neo4j.Driver/ExecuteQuery/IExecutableQuery.cs rename Neo4j.Driver/Neo4j.Driver/{Preview => ExecuteQuery}/QueryConfig.cs (99%) rename Neo4j.Driver/Neo4j.Driver/{Preview => }/IBookmarkManager.cs (87%) rename Neo4j.Driver/Neo4j.Driver/{Preview => }/IBookmarkManagerFactory.cs (90%) create mode 100644 Neo4j.Driver/Neo4j.Driver/Internal/ExecuteQuery/DriverRowSource.cs create mode 100644 Neo4j.Driver/Neo4j.Driver/Internal/ExecuteQuery/ExecutableQuery.cs create mode 100644 Neo4j.Driver/Neo4j.Driver/Internal/ExecuteQuery/IQueryRowSource.cs create mode 100644 Neo4j.Driver/Neo4j.Driver/Internal/ExecuteQuery/ReduceToList.cs create mode 100644 Neo4j.Driver/Neo4j.Driver/Internal/ExecuteQuery/ReducedExecutableQuery.cs create mode 100644 Neo4j.Driver/Neo4j.Driver/Internal/ExecuteQuery/StreamProcessorExecutableQuery.cs delete mode 100644 Neo4j.Driver/Neo4j.Driver/Preview/FluentQueries/ExecutableQuery.cs delete mode 100644 Neo4j.Driver/Neo4j.Driver/Preview/FluentQueries/IExecutableQuery.cs delete mode 100644 Neo4j.Driver/Neo4j.Driver/Preview/GraphDatabase.cs delete mode 100644 Neo4j.Driver/Neo4j.Driver/Preview/PreviewExtensions.cs diff --git a/Neo4j.Driver/Neo4j.Driver.Tests.Integration/Direct/ResultIT.cs b/Neo4j.Driver/Neo4j.Driver.Tests.Integration/Direct/ResultIT.cs index a3a5aeacc..18015181d 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests.Integration/Direct/ResultIT.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests.Integration/Direct/ResultIT.cs @@ -177,7 +177,12 @@ public async Task GetNotification() notification.Code.Should().NotBeNullOrEmpty(); notification.Description.Should().NotBeNullOrEmpty(); notification.Title.Should().NotBeNullOrEmpty(); - notification.Severity.Should().NotBeNullOrEmpty(); + notification.RawSeverityLevel.Should().NotBeNullOrEmpty(); + +#pragma warning disable CS0618 // deprecated method + notification.Severity.Should().Be(notification.RawSeverityLevel); +#pragma warning restore CS0618 + notification.Position.Should().NotBeNull(); } finally diff --git a/Neo4j.Driver/Neo4j.Driver.Tests.Integration/Internals/Cluster/ExistingCluster.cs b/Neo4j.Driver/Neo4j.Driver.Tests.Integration/Internals/Cluster/ExistingCluster.cs index dc172d4e0..90f8681dd 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests.Integration/Internals/Cluster/ExistingCluster.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests.Integration/Internals/Cluster/ExistingCluster.cs @@ -45,13 +45,13 @@ public static bool IsClusterProvided() var uri = Environment.GetEnvironmentVariable(ClusterUri); var password = Environment.GetEnvironmentVariable(ClusterPassword); // both of the two above env var should be provided. - return !uri.IsNullOrEmpty() && !password.IsNullOrEmpty(); + return !string.IsNullOrEmpty(uri) && !string.IsNullOrEmpty(password); } private static string GetEnvOrThrow(string env) { var value = Environment.GetEnvironmentVariable(env); - if (value.IsNullOrEmpty()) + if (string.IsNullOrEmpty(value)) { throw new ArgumentException($"Missing env variable {env}"); } @@ -62,6 +62,6 @@ private static string GetEnvOrThrow(string env) private static string GetEnvOrDefault(string env, string defaultValue) { var value = Environment.GetEnvironmentVariable(env); - return value.IsNullOrEmpty() ? defaultValue : value; + return string.IsNullOrEmpty(value) ? defaultValue : value; } } diff --git a/Neo4j.Driver/Neo4j.Driver.Tests.Integration/Neo4j.Driver.Tests.Integration.csproj b/Neo4j.Driver/Neo4j.Driver.Tests.Integration/Neo4j.Driver.Tests.Integration.csproj index ed0b8d8cb..11fb4ea15 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests.Integration/Neo4j.Driver.Tests.Integration.csproj +++ b/Neo4j.Driver/Neo4j.Driver.Tests.Integration/Neo4j.Driver.Tests.Integration.csproj @@ -19,11 +19,11 @@ - + - + diff --git a/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Protocol/BookmarkManager/NewBookmarkManager.cs b/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Protocol/BookmarkManager/NewBookmarkManager.cs index 52b5d498d..559b61b3f 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Protocol/BookmarkManager/NewBookmarkManager.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Protocol/BookmarkManager/NewBookmarkManager.cs @@ -18,7 +18,6 @@ using System; using System.Threading; using System.Threading.Tasks; -using Neo4j.Driver.Preview; using Newtonsoft.Json; namespace Neo4j.Driver.Tests.TestBackend; @@ -62,7 +61,7 @@ async Task NotifyBookmarks(string[] bookmarks, CancellationToken _) } BookmarkManager = - Preview.GraphDatabase.BookmarkManagerFactory.NewBookmarkManager( + GraphDatabase.BookmarkManagerFactory.NewBookmarkManager( new BookmarkManagerConfig(initialBookmarks, BookmarkSupplier, NotifyBookmarks)); return Task.CompletedTask; diff --git a/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Protocol/Driver/DriverClose.cs b/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Protocol/Driver/DriverClose.cs index ec1816eb3..d35de0eae 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Protocol/Driver/DriverClose.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Protocol/Driver/DriverClose.cs @@ -26,7 +26,7 @@ internal class DriverClose : IProtocolObject public override async Task Process() { var driver = ((NewDriver)ObjManager.GetObject(data.driverId)).Driver; - await driver.CloseAsync(); + await driver.DisposeAsync(); } public override string Respond() diff --git a/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Protocol/DriverQuery/ExecuteQuery.cs b/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Protocol/DriverQuery/ExecuteQuery.cs index 6947e6734..52a3526cd 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Protocol/DriverQuery/ExecuteQuery.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Protocol/DriverQuery/ExecuteQuery.cs @@ -19,7 +19,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Neo4j.Driver.Preview; using Newtonsoft.Json; namespace Neo4j.Driver.Tests.TestBackend; @@ -28,7 +27,8 @@ internal class ExecuteQuery : IProtocolObject { public ExecuteQueryDto data { get; set; } - [JsonIgnore] public EagerResult> Result { get; set; } + [JsonIgnore] + public EagerResult> Result { get; set; } public override async Task Process() { diff --git a/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Types/CypherToNative.cs b/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Types/CypherToNative.cs index 2270fda73..e882fb6ed 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Types/CypherToNative.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Types/CypherToNative.cs @@ -49,7 +49,7 @@ public static Dictionary ConvertDictionaryToNative( internal class SimpleValue { - public object? value { get; set; } + public object value { get; set; } } public class DateTimeParameterValue diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/AsyncSessionTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/AsyncSessionTests.cs index 7d84c9e8f..8bf9902e5 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/AsyncSessionTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/AsyncSessionTests.cs @@ -28,7 +28,6 @@ using Neo4j.Driver.Internal.MessageHandling; using Neo4j.Driver.Internal.Messaging; using Neo4j.Driver.Internal.Routing; -using Neo4j.Driver.Preview; using Xunit; namespace Neo4j.Driver.Tests diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/ExecutableQuery/ExecutableQueryTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/ExecutableQuery/ExecutableQueryTests.cs new file mode 100644 index 000000000..2628f0eb1 --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver.Tests/ExecutableQuery/ExecutableQueryTests.cs @@ -0,0 +1,396 @@ +// Copyright (c) "Neo4j" +// Neo4j Sweden AB [http://neo4j.com] +// +// This file is part of Neo4j. +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Moq; +using Moq.AutoMock; +using Neo4j.Driver.Internal.Result; +using Xunit; + +namespace Neo4j.Driver.Tests.ExecutableQuery +{ + public class ExecutableQueryTests + { + [Fact] + public async Task ShouldReturnSimpleList() + { + var autoMock = new AutoMocker(MockBehavior.Loose); + + autoMock.GetMock>() + .Setup(x => x.GetRowsAsync(It.IsAny>(), It.IsAny())) + .Callback( + (Action p, CancellationToken _) => + { + for (var i = 0; i < 10; i++) + { + p(i); + } + }); + + var subject = new ExecutableQuery(autoMock.GetMock>().Object, i => i); + + var results = new List(); + await subject.GetRowsAsync(i => results.Add(i), CancellationToken.None); + + results.Should().BeEquivalentTo(Enumerable.Range(0, 10)); + } + + [Fact] + public async Task ShouldReturnFilteredList() + { + var autoMock = new AutoMocker(MockBehavior.Loose); + + autoMock.GetMock>() + .Setup(x => x.GetRowsAsync(It.IsAny>(), It.IsAny())) + .Callback( + (Action p, CancellationToken _) => + { + for (var i = 0; i < 10; i++) + { + p(i); + } + }) + .ReturnsAsync(new ExecutionSummary(null, null)); + + var subject = new ExecutableQuery(autoMock.GetMock>().Object, i => i); + + var result = await subject + .WithFilter(i => i < 5) + .ExecuteAsync(); + + result.Result.Should().BeEquivalentTo(Enumerable.Range(0, 5)); + } + + [Fact] + public async Task ShouldReturnMappedList() + { + var autoMock = new AutoMocker(MockBehavior.Loose); + + autoMock.GetMock>() + .Setup(x => x.GetRowsAsync(It.IsAny>(), It.IsAny())) + .Callback( + (Action p, CancellationToken _) => + { + for (var i = 0; i < 10; i++) + { + p(i); + } + }) + .ReturnsAsync(new ExecutionSummary(null, null)); + + var subject = new ExecutableQuery(autoMock.GetMock>().Object, i => i); + + var result = await subject + .WithMap(i => i + 100) + .ExecuteAsync(); + + result.Result.Should().BeEquivalentTo(Enumerable.Range(100, 10)); + } + + [Fact] + public async Task ShouldReturnCorrectResultSummary() + { + var autoMock = new AutoMocker(MockBehavior.Loose); + + var query = new Query("fake cypher"); + var summary = new SummaryBuilder( + query, + autoMock.GetMock().Object).Build(); + + var keys = new[] { "alpha", "bravo", "charlie" }; + + autoMock.GetMock>() + .Setup(x => x.GetRowsAsync(It.IsAny>(), It.IsAny())) + .Callback( + (Action p, CancellationToken _) => + { + for (var i = 0; i < 10; i++) + { + p(i); + } + }) + .ReturnsAsync(new ExecutionSummary(summary, keys)); + + var subject = new ExecutableQuery(autoMock.GetMock>().Object, i => i); + + var result = await subject + .WithMap(i => i + 100) + .WithMap(i => i + 100) + .WithFilter(i => i < 5) + .ExecuteAsync(); + + result.Keys.Should().BeSameAs(keys); + result.Summary.Should().BeSameAs(summary); + } + + [Fact] + public async Task ShouldReturnMappedAndFilteredList() + { + var autoMock = new AutoMocker(MockBehavior.Loose); + + autoMock.GetMock>() + .Setup(x => x.GetRowsAsync(It.IsAny>(), It.IsAny())) + .Callback( + (Action p, CancellationToken _) => + { + for (var i = 0; i < 10; i++) + { + p(i); + } + }) + .ReturnsAsync(new ExecutionSummary(null, null)); + + var subject = new ExecutableQuery(autoMock.GetMock>().Object, i => i); + + var result = await subject + .WithMap(i => i + 100) + .WithFilter(i => i < 105) + .ExecuteAsync(); + + result.Result.Should().BeEquivalentTo(Enumerable.Range(100, 5)); + } + + [Fact] + public async Task ShouldReturnMultiMappedAndFilteredList() + { + var autoMock = new AutoMocker(MockBehavior.Loose); + + autoMock.GetMock>() + .Setup(x => x.GetRowsAsync(It.IsAny>(), It.IsAny())) + .Callback( + (Action p, CancellationToken _) => + { + for (var i = 0; i < 100; i++) + { + p(i); + } + }) + .ReturnsAsync(new ExecutionSummary(null, null)); + + var subject = new ExecutableQuery(autoMock.GetMock>().Object, i => i); + + var result = await subject + .WithMap(i => i * 2) // 0, 2, 4.. 198 + .WithFilter(i => i < 100) // 0, 2, 4.. 98 + .WithMap(i => i / 2) // 0, 1, 2.. 49 + .WithFilter(i => i < 20) // 0, 1, 2.. 19 + .WithMap(i => i * 3) // 0, 3, 6.. 57 + .WithFilter(i => i < 10) // 0, 3, 6, 9 + .ExecuteAsync(); + + result.Result.Should().BeEquivalentTo(new[] { 0, 3, 6, 9 }); + } + + [Fact] + public async Task ShouldReturnReducedValue() + { + var autoMock = new AutoMocker(MockBehavior.Loose); + + autoMock.GetMock>() + .Setup(x => x.GetRowsAsync(It.IsAny>(), It.IsAny())) + .Callback( + (Action p, CancellationToken _) => + { + for (var i = 0; i < 10; i++) + { + p(i); + } + }) + .ReturnsAsync(new ExecutionSummary(null, null)); + + var subject = new ExecutableQuery(autoMock.GetMock>().Object, i => i); + + var result = await subject + .WithReduce(() => 0, (x, y) => x + y) + .ExecuteAsync(); + + result.Result.Should().Be(45); + } + + [Fact] + public async Task ShouldReturnMappedReducedValue() + { + var autoMock = new AutoMocker(MockBehavior.Loose); + + autoMock.GetMock>() + .Setup(x => x.GetRowsAsync(It.IsAny>(), It.IsAny())) + .Callback( + (Action p, CancellationToken _) => + { + for (var i = 0; i < 10; i++) + { + p(i); + } + }) + .ReturnsAsync(new ExecutionSummary(null, null)); + + var subject = new ExecutableQuery(autoMock.GetMock>().Object, i => i); + + var result = await subject + .WithMap(i => i * 10) + .WithReduce(() => 0, (x, y) => x + y) + .ExecuteAsync(); + + result.Result.Should().Be(450); + } + + [Fact] + public async Task ShouldReturnMappedTransformedReducedValue() + { + var autoMock = new AutoMocker(MockBehavior.Loose); + + autoMock.GetMock>() + .Setup(x => x.GetRowsAsync(It.IsAny>(), It.IsAny())) + .Callback( + (Action p, CancellationToken _) => + { + for (var i = 0; i < 10; i++) + { + p(i); + } + }) + .ReturnsAsync(new ExecutionSummary(null, null)); + + var subject = new ExecutableQuery(autoMock.GetMock>().Object, i => i); + + var result = await subject + .WithMap(i => i * 10) + .WithReduce(() => 0, (x, y) => x + y, i => $"<{(i * 2)}>") + .ExecuteAsync(); + + result.Result.Should().Be("<900>"); + } + + [Fact] + public async Task ShouldSetConfig() + { + var autoMock = new AutoMocker(MockBehavior.Loose); + var config = new QueryConfig(); + + var driver = autoMock.GetMock>(); + driver + .Setup(x => x.GetRowsAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync(new ExecutionSummary(null, null)); + + var subject = new ExecutableQuery(driver.Object, i => i); + + await subject + .WithConfig(config) + .ExecuteAsync(); + + driver.Verify(x => x.SetConfig(config)); + } + + [Fact] + public async Task ShouldSetParametersWithObject() + { + var autoMock = new AutoMocker(MockBehavior.Loose); + var parameters = new { Shaken = true, Stirred = false }; + + var driver = autoMock.GetMock>(); + driver + .Setup(x => x.GetRowsAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync(new ExecutionSummary(null, null)); + + var subject = new ExecutableQuery(driver.Object, i => i); + + await subject + .WithParameters(parameters) + .ExecuteAsync(); + + driver.Verify(x => x.SetParameters(parameters)); + } + + [Fact] + public async Task ShouldSetParametersWithDictionary() + { + var autoMock = new AutoMocker(MockBehavior.Loose); + var parameters = new Dictionary + { + ["shaken"] = true, + ["stirred"] = false + }; + + var driver = autoMock.GetMock>(); + driver + .Setup(x => x.GetRowsAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync(new ExecutionSummary(null, null)); + + var subject = new ExecutableQuery(driver.Object, i => i); + + await subject + .WithParameters(parameters) + .ExecuteAsync(); + + driver.Verify(x => x.SetParameters(parameters)); + } + + [Fact] + public async Task ShouldUseStreamProcessor() + { + var autoMock = new AutoMocker(MockBehavior.Loose); + + async IAsyncEnumerable GetInts(int start, int count) + { + foreach(var i in Enumerable.Range(start, count)) + { + await Task.Yield(); + yield return i; + } + } + + var driverMock = autoMock.GetMock>(); + driverMock + .Setup( + x => x.ProcessStreamAsync( + It.IsAny, Task>>(), + It.IsAny())) + .Returns, Task>, CancellationToken>( + (p, _) => + Task.FromResult( + new EagerResult( + p(GetInts(0, 10)).GetAwaiter().GetResult(), + null, + null))); + + var subject = new ExecutableQuery(driverMock.Object, i => i); + + var rnd = Random.Shared.Next(); + + var queryExecution = await subject.WithStreamProcessor( + async stream => + { + var result = rnd; + + await foreach (var i in stream) + { + result += i; + } + + return result; + }) + .ExecuteAsync(); + + queryExecution.Result.Should().Be(45 + rnd); + } + } +} diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Internal/BookmarkManager/BookmarkManagerFactoryTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/BookmarkManager/BookmarkManagerFactoryTests.cs index 1dd94485c..64b374d3e 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Internal/BookmarkManager/BookmarkManagerFactoryTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/BookmarkManager/BookmarkManagerFactoryTests.cs @@ -18,7 +18,6 @@ using System; using System.Threading.Tasks; using FluentAssertions; -using Neo4j.Driver.Preview; using Xunit; namespace Neo4j.Driver.Internal.BookmarkManager diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Internal/BookmarkManager/DefaultBookmarkManagerTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/BookmarkManager/DefaultBookmarkManagerTests.cs index c31989b3f..e69a2af5b 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Internal/BookmarkManager/DefaultBookmarkManagerTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/BookmarkManager/DefaultBookmarkManagerTests.cs @@ -20,7 +20,6 @@ using System.Threading.Tasks; using FluentAssertions; using Moq; -using Neo4j.Driver.Preview; using Xunit; namespace Neo4j.Driver.Internal.BookmarkManager diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Neo4j.Driver.Tests.csproj b/Neo4j.Driver/Neo4j.Driver.Tests/Neo4j.Driver.Tests.csproj index 66915c7ea..e1bc43962 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Neo4j.Driver.Tests.csproj +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Neo4j.Driver.Tests.csproj @@ -19,7 +19,8 @@ - + + diff --git a/Neo4j.Driver/Neo4j.Driver/Preview/BookmarkManagerConfig.cs b/Neo4j.Driver/Neo4j.Driver/BookmarkManagerConfig.cs similarity index 99% rename from Neo4j.Driver/Neo4j.Driver/Preview/BookmarkManagerConfig.cs rename to Neo4j.Driver/Neo4j.Driver/BookmarkManagerConfig.cs index 148c1b69f..5d819b5b3 100644 --- a/Neo4j.Driver/Neo4j.Driver/Preview/BookmarkManagerConfig.cs +++ b/Neo4j.Driver/Neo4j.Driver/BookmarkManagerConfig.cs @@ -21,7 +21,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Neo4j.Driver.Preview; +namespace Neo4j.Driver; /// /// The record encapsulates configuration values for initializing a new diff --git a/Neo4j.Driver/Neo4j.Driver/Preview/EagerResult.cs b/Neo4j.Driver/Neo4j.Driver/ExecuteQuery/EagerResult.cs similarity index 78% rename from Neo4j.Driver/Neo4j.Driver/Preview/EagerResult.cs rename to Neo4j.Driver/Neo4j.Driver/ExecuteQuery/EagerResult.cs index 2ff3f8d30..cc2cb39e2 100644 --- a/Neo4j.Driver/Neo4j.Driver/Preview/EagerResult.cs +++ b/Neo4j.Driver/Neo4j.Driver/ExecuteQuery/EagerResult.cs @@ -15,12 +15,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Neo4j.Driver.Preview; +namespace Neo4j.Driver; /// -/// There is no guarantee that anything in Neo4j.Driver.Preview namespace will be in a next minor version. -/// Complete result from a cypher query. +/// Complete, materialized result from a cypher query. /// +/// The type of the value that will be in the property. public sealed class EagerResult { internal EagerResult(T result, IResultSummary summary, string[] keys) @@ -31,19 +31,16 @@ internal EagerResult(T result, IResultSummary summary, string[] keys) } /// - /// There is no guarantee that anything in Neo4j.Driver.Preview namespace will be in a next minor version. /// Least common set of fields in . /// public string[] Keys { get; init; } /// - /// There is no guarantee that anything in Neo4j.Driver.Preview namespace will be in a next minor version. - /// All Records from query. + /// The materialized result of the query. /// public T Result { get; init; } /// - /// There is no guarantee that anything in Neo4j.Driver.Preview namespace will be in a next minor version. /// Query summary. /// public IResultSummary Summary { get; init; } diff --git a/Neo4j.Driver/Neo4j.Driver/ExecuteQuery/IExecutableQuery.cs b/Neo4j.Driver/Neo4j.Driver/ExecuteQuery/IExecutableQuery.cs new file mode 100644 index 000000000..de7f32ce2 --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver/ExecuteQuery/IExecutableQuery.cs @@ -0,0 +1,146 @@ +// Copyright (c) "Neo4j" +// Neo4j Sweden AB [http://neo4j.com] +// +// This file is part of Neo4j. +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Neo4j.Driver; + +/// +/// Exposes methods for configuring and executing a driver-level query. +/// +/// The type of items the query will receive. +/// The type of items the query will output. +public interface IExecutableQuery : IConfiguredQuery +{ + /// + /// Sets the query config on the query to be executed. + /// + /// The config to set. + /// The same instance to allow method chaining. + IExecutableQuery WithConfig(QueryConfig config); + + /// + /// Sets the parameters on the query to be executed. + /// + /// A dictionary of parameter values, keyed by their names. + /// The same instance to allow method chaining. + IExecutableQuery WithParameters(Dictionary parameters); + + /// + /// Sets the parameters on the query to be executed. + /// + /// An object whose properties have names matching the names of the query's parameters + /// and values that should be used as those parameters' values. + /// The same instance to allow method chaining. + IExecutableQuery WithParameters(object parameters); + + /// + /// Specifies a stream processor that will process the that results + /// from a query, and return the value that will be given as the result of the query. + /// + /// An asynchronous method that will process the supplied asynchronous + /// stream of records. + /// The type of the return value from . + /// The same instance which can only be used to execute the query. + IReducedExecutableQuery WithStreamProcessor( + Func, Task> streamProcessor); +} + +/// +/// A query that can no longer be configured. +/// +/// The type of items that will be input into this instance. +/// The type of items that will be output from this instance. +public interface IConfiguredQuery +{ + /// + /// Specifies a filter that will be applied to all results in the query. If the specified delegate returns + /// true, the row will be included in the results. If it returns false, it will be excluded. + /// + /// The predicate that will decide whether an item is present in the results. + /// The same instance for method chaining. + IConfiguredQuery WithFilter(Func filter); + + /// + /// Specifies a mapping that will be used to turn items of type into items of type + /// . + /// + /// The mapping function. + /// The output type of the mapping function. + /// A new instance whose input type is and whose input type is + /// . + /// + IConfiguredQuery WithMap(Func map); + + /// + /// Specifies a method of reducing many items of type into one instance of type + /// . + /// + /// The initial value of the resulting value. + /// A method that will accumulate each value of type into + /// the result value. + /// The type of the reduced result. + /// The same instance which can only be used to execute the query. + IReducedExecutableQuery WithReduce( + Func seed, + Func accumulate); + + /// + /// Specifies a method of reducing many items of type into one instance of type + /// , using one method to accumulate the values into a value of type + /// and another to turn the accumulated value into a result. + /// + /// The initial value of the accumulating value. + /// A method that will accumulate each value of type into + /// the accumulated value. + /// A method that will turn the accumulated value into a value of type + /// . + /// + /// The type of the reduced result. + /// The same instance which can only be used to execute the query. + IReducedExecutableQuery WithReduce( + Func seed, + Func accumulate, + Func selectResult); + + /// + /// Executes the query. + /// + /// A cancellation token that can be used to cancel the operation. + /// An That contains the result of the query and + /// information about the execution. + Task>> ExecuteAsync(CancellationToken token = default); +} + +/// +/// A query that has been configured fully and now can only be executed. +/// +/// The type of result that the returned +/// from the will contain. +public interface IReducedExecutableQuery +{ + /// + /// Executes the query. + /// + /// A cancellation token that can be used to cancel the operation. + /// An That contains the result of the query and + /// information about the execution. + Task> ExecuteAsync(CancellationToken token = default); +} diff --git a/Neo4j.Driver/Neo4j.Driver/Preview/QueryConfig.cs b/Neo4j.Driver/Neo4j.Driver/ExecuteQuery/QueryConfig.cs similarity index 99% rename from Neo4j.Driver/Neo4j.Driver/Preview/QueryConfig.cs rename to Neo4j.Driver/Neo4j.Driver/ExecuteQuery/QueryConfig.cs index ed0d0908f..2faf5c952 100644 --- a/Neo4j.Driver/Neo4j.Driver/Preview/QueryConfig.cs +++ b/Neo4j.Driver/Neo4j.Driver/ExecuteQuery/QueryConfig.cs @@ -19,7 +19,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Neo4j.Driver.Preview; +namespace Neo4j.Driver; /// Configuration for running queries using the simplified api. public class QueryConfig diff --git a/Neo4j.Driver/Neo4j.Driver/GraphDatabase.cs b/Neo4j.Driver/Neo4j.Driver/GraphDatabase.cs index 8e3096401..8e522a5b6 100644 --- a/Neo4j.Driver/Neo4j.Driver/GraphDatabase.cs +++ b/Neo4j.Driver/Neo4j.Driver/GraphDatabase.cs @@ -185,6 +185,14 @@ public static IDriver Driver(Uri uri, IAuthToken authToken, Action + /// Gets a new , which can construct a new default + /// instance.
+ /// The instance should be passed to + /// when opening a new session with . + ///
+ public static IBookmarkManagerFactory BookmarkManagerFactory => new BookmarkManagerFactory(); + internal static IDriver CreateDriver( Uri uri, Config config, diff --git a/Neo4j.Driver/Neo4j.Driver/Preview/IBookmarkManager.cs b/Neo4j.Driver/Neo4j.Driver/IBookmarkManager.cs similarity index 87% rename from Neo4j.Driver/Neo4j.Driver/Preview/IBookmarkManager.cs rename to Neo4j.Driver/Neo4j.Driver/IBookmarkManager.cs index f9982f1c3..57f43503e 100644 --- a/Neo4j.Driver/Neo4j.Driver/Preview/IBookmarkManager.cs +++ b/Neo4j.Driver/Neo4j.Driver/IBookmarkManager.cs @@ -18,14 +18,13 @@ using System.Threading; using System.Threading.Tasks; -namespace Neo4j.Driver.Preview; +namespace Neo4j.Driver; /// -/// Preview: Subject to change.
The interface is intended for -/// implementation by classes that provide convenient interfacing with in both the driver and user -/// code. +/// The interface is intended for implementation by classes that provide convenient +/// interfacing with in both the driver and user code. ///
-/// +/// public interface IBookmarkManager { /// diff --git a/Neo4j.Driver/Neo4j.Driver/Preview/IBookmarkManagerFactory.cs b/Neo4j.Driver/Neo4j.Driver/IBookmarkManagerFactory.cs similarity index 90% rename from Neo4j.Driver/Neo4j.Driver/Preview/IBookmarkManagerFactory.cs rename to Neo4j.Driver/Neo4j.Driver/IBookmarkManagerFactory.cs index d93fee1e8..ffaab3106 100644 --- a/Neo4j.Driver/Neo4j.Driver/Preview/IBookmarkManagerFactory.cs +++ b/Neo4j.Driver/Neo4j.Driver/IBookmarkManagerFactory.cs @@ -15,10 +15,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Neo4j.Driver.Preview; +namespace Neo4j.Driver; /// -/// Preview: Subject to change.
The interface is intended for +/// The interface is intended for /// classes that construct instances of an implementation. ///
public interface IBookmarkManagerFactory diff --git a/Neo4j.Driver/Neo4j.Driver/IDriver.cs b/Neo4j.Driver/Neo4j.Driver/IDriver.cs index 9c4ad1c60..06e711c4b 100644 --- a/Neo4j.Driver/Neo4j.Driver/IDriver.cs +++ b/Neo4j.Driver/Neo4j.Driver/IDriver.cs @@ -16,6 +16,7 @@ // limitations under the License. using System; +using System.Collections.Generic; using System.Threading.Tasks; namespace Neo4j.Driver; @@ -93,4 +94,15 @@ public interface IDriver : IDisposable, IAsyncDisposable /// cluster support multi-databases, otherwise false. /// Task SupportsMultiDbAsync(); + + /// + /// Gets an that can be used to configure and execute a query + /// using fluent method chaining. + /// + /// The cypher of the query. + /// + /// An that can be used to configure and execute a query using + /// fluent method chaining. + /// + IExecutableQuery ExecutableQuery(string cypher); } diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/AsyncSession.cs b/Neo4j.Driver/Neo4j.Driver/Internal/AsyncSession.cs index a679dccd3..1eaf200a0 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/AsyncSession.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/AsyncSession.cs @@ -20,7 +20,6 @@ using System.Linq; using System.Threading.Tasks; using Neo4j.Driver.Internal.Connector; -using Neo4j.Driver.Preview; using static Neo4j.Driver.Internal.Logging.DriverLoggerUtil; using static Neo4j.Driver.Internal.Util.ConfigBuilders; diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/BookmarkManagerFactory.cs b/Neo4j.Driver/Neo4j.Driver/Internal/BookmarkManagerFactory.cs index 64aaeded1..19da92e19 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/BookmarkManagerFactory.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/BookmarkManagerFactory.cs @@ -14,9 +14,6 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. - -using Neo4j.Driver.Preview; - namespace Neo4j.Driver.Internal; internal class BookmarkManagerFactory : IBookmarkManagerFactory diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/DefaultBookmarkManager.cs b/Neo4j.Driver/Neo4j.Driver/Internal/DefaultBookmarkManager.cs index 8089d82bb..d8eb1ff6f 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/DefaultBookmarkManager.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/DefaultBookmarkManager.cs @@ -20,7 +20,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using Neo4j.Driver.Preview; namespace Neo4j.Driver.Internal; diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Driver.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Driver.cs index 06fe81cdd..972486863 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/Driver.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Driver.cs @@ -22,7 +22,6 @@ using Neo4j.Driver.Internal.Metrics; using Neo4j.Driver.Internal.Routing; using Neo4j.Driver.Internal.Util; -using Neo4j.Driver.Preview; namespace Neo4j.Driver.Internal; @@ -97,6 +96,15 @@ public IInternalAsyncSession Session(Action action, bool r return session; } + public Task> ExecuteQueryAsync( + Query query, + Func, Task> streamProcessor, + QueryConfig config = null, + CancellationToken cancellationToken = default) + { + return ExecuteQueryAsyncInternal(query, config, cancellationToken, TransformCursor(streamProcessor)); + } + public Task CloseAsync() { return Interlocked.CompareExchange(ref _closedMarker, 1, 0) == 0 @@ -133,6 +141,12 @@ public Task SupportsMultiDbAsync() return _connectionProvider.SupportsMultiDbAsync(); } + //Non public facing api. Used for testing with testkit only + public IRoutingTable GetRoutingTable(string database) + { + return _connectionProvider.GetRoutingTable(database); + } + public void Dispose() { Dispose(true); @@ -145,17 +159,31 @@ public ValueTask DisposeAsync() : new ValueTask(Task.CompletedTask); } - public Task> ExecuteQueryAsync( + public async Task GetRowsAsync( Query query, - Func, ValueTask> streamProcessor, - QueryConfig config = null, - CancellationToken cancellationToken = default) + QueryConfig config, + Action streamProcessor, + CancellationToken cancellationToken + ) { - return ExecuteQueryAsyncInternal( - query, - config, - cancellationToken, - TransformCursor(streamProcessor)); + async Task Process(IAsyncEnumerable records) + { + await foreach (var record in records.ConfigureAwait(false)) + { + streamProcessor(record); + } + + return 0; + } + + var eagerResult = await ExecuteQueryAsyncInternal( + query, + config, + cancellationToken, + TransformCursor(Process)) + .ConfigureAwait(false); + + return new ExecutionSummary(eagerResult.Summary, eagerResult.Keys); } private void Close() @@ -163,12 +191,6 @@ private void Close() CloseAsync().GetAwaiter().GetResult(); } - //Non public facing api. Used for testing with testkit only - public IRoutingTable GetRoutingTable(string database) - { - return _connectionProvider.GetRoutingTable(database); - } - private void Dispose(bool disposing) { if (IsClosed) @@ -223,16 +245,21 @@ private async Task> ExecuteQueryAsyncInternal( } } + public IExecutableQuery ExecutableQuery(string cypher) + { + return new ExecutableQuery(new DriverRowSource(this, cypher), x => x); + } + private static Func>> TransformCursor( - Func, ValueTask> streamProcessor) + Func, Task> streamProcessor) { async Task> TransformCursorImpl( IResultCursor cursor, CancellationToken cancellationToken) { - var processedStream = await streamProcessor(cursor); + var processedStream = await streamProcessor(cursor).ConfigureAwait(false); var summary = await cursor.ConsumeAsync().ConfigureAwait(false); - var keys = await cursor.KeysAsync(); + var keys = await cursor.KeysAsync().ConfigureAwait(false); return new EagerResult(processedStream, summary, keys); } diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/ExecuteQuery/DriverRowSource.cs b/Neo4j.Driver/Neo4j.Driver/Internal/ExecuteQuery/DriverRowSource.cs new file mode 100644 index 000000000..d639ba27a --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver/Internal/ExecuteQuery/DriverRowSource.cs @@ -0,0 +1,76 @@ +// Copyright (c) "Neo4j" +// Neo4j Sweden AB [http://neo4j.com] +// +// This file is part of Neo4j. +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Neo4j.Driver.Internal; + +namespace Neo4j.Driver; + +internal interface IDriverRowSource : IQueryRowSource +{ + void SetConfig(QueryConfig config); + void SetParameters(Dictionary parameters); + void SetParameters(object parameters); + Task> ProcessStreamAsync( + Func, Task> streamProcessor, + CancellationToken cancellationToken = default); +} + +internal class DriverRowSource : IDriverRowSource +{ + private readonly IInternalDriver _driver; + private Query _query; + private QueryConfig _queryConfig; + + internal DriverRowSource(IInternalDriver driver, string cypher) + { + _driver = driver; + _query = new Query(cypher); + } + + public Task GetRowsAsync( + Action rowProcessor, + CancellationToken cancellationToken = default) + { + return _driver.GetRowsAsync(_query, _queryConfig, rowProcessor, cancellationToken); + } + + public Task> ProcessStreamAsync( + Func, Task> streamProcessor, + CancellationToken cancellationToken = default) + { + return _driver.ExecuteQueryAsync(_query, streamProcessor, _queryConfig, cancellationToken); + } + + public void SetConfig(QueryConfig config) + { + _queryConfig = config; + } + + public void SetParameters(Dictionary parameters) + { + _query = new Query(_query.Text, parameters); + } + + public void SetParameters(object parameters) + { + _query = new Query(_query.Text, parameters); + } +} diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/ExecuteQuery/ExecutableQuery.cs b/Neo4j.Driver/Neo4j.Driver/Internal/ExecuteQuery/ExecutableQuery.cs new file mode 100644 index 000000000..ca2e549f3 --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver/Internal/ExecuteQuery/ExecutableQuery.cs @@ -0,0 +1,133 @@ +// Copyright (c) "Neo4j" +// Neo4j Sweden AB [http://neo4j.com] +// +// This file is part of Neo4j. +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Neo4j.Driver; + +internal class ExecutableQuery : IExecutableQuery, IQueryRowSource +{ + private readonly IQueryRowSource _rowSource; + private readonly Func _mapper; + private readonly List> _filters = new(); + private Func _reduceSeed; + private Action _accumulateValue; + + internal ExecutableQuery( + IQueryRowSource rowSource, + Func mapper) + { + _rowSource = rowSource; + _mapper = mapper; + } + + public IExecutableQuery WithConfig(QueryConfig config) + { + if (_rowSource is IDriverRowSource driverRowSource) + { + driverRowSource.SetConfig(config); + } + + return this; + } + + public IExecutableQuery WithParameters(object parameters) + { + if (_rowSource is IDriverRowSource driverRowSource) + { + driverRowSource.SetParameters(parameters); + } + + return this; + } + + public IExecutableQuery WithParameters(Dictionary parameters) + { + if (_rowSource is IDriverRowSource driverRowSource) + { + driverRowSource.SetParameters(parameters); + } + + return this; + } + + /// + public IReducedExecutableQuery WithStreamProcessor( + Func, Task> streamProcessor) + { + if (_rowSource is IDriverRowSource driverRowSource) + { + return new StreamProcessorExecutableQuery(driverRowSource, streamProcessor); + } + + // this can't actually happen, throwing to satisfy the compiler and for safety + throw new InvalidOperationException("WithStreamProcessor cannot be called on nested queries"); + } + + public IConfiguredQuery WithFilter(Func filter) + { + _filters.Add(filter); + return this; + } + + public IConfiguredQuery WithMap( + Func map) + { + return new ExecutableQuery(this, map); + } + + public IReducedExecutableQuery WithReduce( + Func seed, + Func accumulate, + Func selectResult) + { + return new ReducedExecutableQuery(this, seed, accumulate, selectResult); + } + + public IReducedExecutableQuery WithReduce( + Func seed, + Func accumulate) + { + return new ReducedExecutableQuery(this, seed, accumulate, x => x); + } + + public Task>> ExecuteAsync(CancellationToken token = default) + { + return WithReduce(ReduceToList.Seed, ReduceToList.Accumulate, ReduceToList.SelectResult) + .ExecuteAsync(token); + } + + public Task GetRowsAsync( + Action rowProcessor, + CancellationToken cancellationToken = default) + { + void ProcessRow(TIn rowItem) + { + var mapped = _mapper(rowItem); + if (_filters.All(f => f(mapped))) + { + rowProcessor(mapped); + } + } + + return _rowSource.GetRowsAsync(ProcessRow, cancellationToken); + } +} diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/ExecuteQuery/IQueryRowSource.cs b/Neo4j.Driver/Neo4j.Driver/Internal/ExecuteQuery/IQueryRowSource.cs new file mode 100644 index 000000000..65a7befc1 --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver/Internal/ExecuteQuery/IQueryRowSource.cs @@ -0,0 +1,29 @@ +// Copyright (c) "Neo4j" +// Neo4j Sweden AB [http://neo4j.com] +// +// This file is part of Neo4j. +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Neo4j.Driver; + +internal record ExecutionSummary(IResultSummary Summary, string[] Keys); + +internal interface IQueryRowSource +{ + Task GetRowsAsync(Action rowProcessor, CancellationToken cancellationToken = default); +} diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/ExecuteQuery/ReduceToList.cs b/Neo4j.Driver/Neo4j.Driver/Internal/ExecuteQuery/ReduceToList.cs new file mode 100644 index 000000000..bf2a38c1a --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver/Internal/ExecuteQuery/ReduceToList.cs @@ -0,0 +1,36 @@ +// Copyright (c) "Neo4j" +// Neo4j Sweden AB [http://neo4j.com] +// +// This file is part of Neo4j. +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Generic; + +namespace Neo4j.Driver; + +internal static class ReduceToList +{ + public static List Seed() => new(); + + public static List Accumulate(List list, T item) + { + list.Add(item); + return list; + } + + public static IReadOnlyList SelectResult(List accumulation) + { + return accumulation; + } +} diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/ExecuteQuery/ReducedExecutableQuery.cs b/Neo4j.Driver/Neo4j.Driver/Internal/ExecuteQuery/ReducedExecutableQuery.cs new file mode 100644 index 000000000..581dae5e9 --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver/Internal/ExecuteQuery/ReducedExecutableQuery.cs @@ -0,0 +1,54 @@ +// Copyright (c) "Neo4j" +// Neo4j Sweden AB [http://neo4j.com] +// +// This file is part of Neo4j. +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Neo4j.Driver; + +internal class ReducedExecutableQuery : IReducedExecutableQuery +{ + private readonly Query _query; + private readonly IQueryRowSource _rowSource; + private readonly QueryConfig _queryConfig; + private readonly Func _seed; + private readonly Func _accumulate; + private readonly Func _selectResult; + + internal ReducedExecutableQuery( + IQueryRowSource rowSource, + Func seed, + Func accumulate, + Func selectResult) + { + _rowSource = rowSource; + _seed = seed; + _accumulate = accumulate; + _selectResult = selectResult; + } + + public async Task> ExecuteAsync(CancellationToken token = default) + { + var accumulator = _seed(); + var executionSummary = await _rowSource.GetRowsAsync( + item => accumulator = _accumulate(accumulator, item), + token); + + return new EagerResult(_selectResult(accumulator), executionSummary.Summary, executionSummary.Keys); + } +} diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/ExecuteQuery/StreamProcessorExecutableQuery.cs b/Neo4j.Driver/Neo4j.Driver/Internal/ExecuteQuery/StreamProcessorExecutableQuery.cs new file mode 100644 index 000000000..42ee6b736 --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver/Internal/ExecuteQuery/StreamProcessorExecutableQuery.cs @@ -0,0 +1,42 @@ +// Copyright (c) "Neo4j" +// Neo4j Sweden AB [http://neo4j.com] +// +// This file is part of Neo4j. +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Neo4j.Driver; + +internal class StreamProcessorExecutableQuery : IReducedExecutableQuery +{ + private readonly IDriverRowSource _driverRowSource; + private readonly Func, Task> _streamProcessor; + + public StreamProcessorExecutableQuery( + IDriverRowSource driverRowSource, + Func,Task> streamProcessor) + { + _driverRowSource = driverRowSource; + _streamProcessor = streamProcessor; + } + + public Task> ExecuteAsync(CancellationToken token = default) + { + return _driverRowSource.ProcessStreamAsync(_streamProcessor, token); + } +} diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/IInternalDriver.cs b/Neo4j.Driver/Neo4j.Driver/Internal/IInternalDriver.cs index 42a45e1d7..fd354e858 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/IInternalDriver.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/IInternalDriver.cs @@ -19,7 +19,6 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Neo4j.Driver.Preview; namespace Neo4j.Driver.Internal; @@ -29,7 +28,14 @@ internal interface IInternalDriver : IDriver Task> ExecuteQueryAsync( Query query, - Func, ValueTask> streamProcessor, + Func, Task> streamProcessor, QueryConfig config = null, CancellationToken cancellationToken = default); + + Task GetRowsAsync( + Query query, + QueryConfig config, + Action streamProcessor, + CancellationToken cancellationToken + ); } diff --git a/Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj b/Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj index 6838d26a3..cfab22afd 100644 --- a/Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj +++ b/Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj @@ -56,4 +56,7 @@ + + + diff --git a/Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj.DotSettings b/Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj.DotSettings index af250cf20..4d1da9fe5 100644 --- a/Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj.DotSettings +++ b/Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj.DotSettings @@ -1,19 +1,14 @@ - + Library - True + True + True True + True + True + True - True - True - True - True - True + True + True + True + True + True diff --git a/Neo4j.Driver/Neo4j.Driver/Preview/FluentQueries/ExecutableQuery.cs b/Neo4j.Driver/Neo4j.Driver/Preview/FluentQueries/ExecutableQuery.cs deleted file mode 100644 index afa030a3d..000000000 --- a/Neo4j.Driver/Neo4j.Driver/Preview/FluentQueries/ExecutableQuery.cs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) "Neo4j" -// Neo4j Sweden AB [http://neo4j.com] -// -// This file is part of Neo4j. -// -// Licensed under the Apache License, Version 2.0 (the "License"). -// You may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Neo4j.Driver.Internal; - -namespace Neo4j.Driver.Preview.FluentQueries; - -internal class ExecutableQuery : IExecutableQuery -{ - private readonly IInternalDriver _driver; - private Query _query; - private QueryConfig _queryConfig; - private readonly Func, ValueTask> _streamProcessor; - - private ExecutableQuery( - Query query, - IInternalDriver driver, - QueryConfig queryConfig, - Func, ValueTask> streamProcessor) - { - _query = query; - _driver = driver; - _queryConfig = queryConfig; - _streamProcessor = streamProcessor; - } - - public IExecutableQuery WithConfig(QueryConfig config) - { - _queryConfig = config; - return this; - } - - public IExecutableQuery WithParameters(object parameters) - { - _query = new Query(_query.Text, parameters); - return this; - } - - public IExecutableQuery WithParameters(Dictionary parameters) - { - _query = new Query(_query.Text, parameters); - return this; - } - - public IExecutableQuery WithStreamProcessor( - Func, ValueTask> streamProcessor) - { - return new ExecutableQuery(_query, _driver, _queryConfig, streamProcessor); - } - - // removing since behaviour is different to WithParameters, pending discussion - // public IExecutableQuery WithParameter(string name, object value) - // { - // _query.Parameters[name] = value; - // return this; - // } - - public Task> ExecuteAsync(CancellationToken cancellationToken = default) - { - return _driver.ExecuteQueryAsync( - _query, - _streamProcessor, - _queryConfig, - cancellationToken); - } - - public static ExecutableQuery> GetDefault(IInternalDriver driver, string cypher) - { - return new ExecutableQuery>(new Query(cypher), driver, null, ToListAsync); - } - - private static async ValueTask> ToListAsync(IAsyncEnumerable enumerable) - { - var result = new List(); - await foreach (var item in enumerable) - { - result.Add(item); - } - - return result; - } -} diff --git a/Neo4j.Driver/Neo4j.Driver/Preview/FluentQueries/IExecutableQuery.cs b/Neo4j.Driver/Neo4j.Driver/Preview/FluentQueries/IExecutableQuery.cs deleted file mode 100644 index 84e516505..000000000 --- a/Neo4j.Driver/Neo4j.Driver/Preview/FluentQueries/IExecutableQuery.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) "Neo4j" -// Neo4j Sweden AB [http://neo4j.com] -// -// This file is part of Neo4j. -// -// Licensed under the Apache License, Version 2.0 (the "License"). -// You may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace Neo4j.Driver.Preview.FluentQueries; - -/// There is no guarantee that anything in Neo4j.Driver.Preview namespace will be in a next minor version. -public interface IExecutableQuery -{ - /// - /// There is no guarantee that anything in Neo4j.Driver.Preview namespace will be in a next minor version. - /// Adds the specified config to the executable query. - /// - /// The query config to use. - /// The executable query object allowing method chaining. - IExecutableQuery WithConfig(QueryConfig config); - - /// - /// There is no guarantee that anything in Neo4j.Driver.Preview namespace will be in a next minor version. - /// Sets the named parameters on the query. - /// - /// The query parameters, specified as an object which is then converted into key-value pairs. - /// The executable query object allowing method chaining. - IExecutableQuery WithParameters(object parameters); - - /// - /// There is no guarantee that anything in Neo4j.Driver.Preview namespace will be in a next minor version. - /// Sets the named parameters on the query. - /// - /// - /// The query's parameters, whose values should not be changed while the query is used in a - /// session/transaction. - /// - /// The executable query object allowing method chaining. - IExecutableQuery WithParameters(Dictionary parameters); - - /// - /// There is no guarantee that anything in Neo4j.Driver.Preview namespace will be in a next minor version. - /// Adds a stream processor function that will be called, passing the of records - /// returned from the query. The value returned from this callback property will be present in the - /// property of the result returned from executing the query. - /// - /// The stream processor function. - /// The executable query object allowing method chaining. - IExecutableQuery WithStreamProcessor( - Func, ValueTask> streamProcessor); - - /// Executes the query as configured and returns the results, fully materialised. - /// A cancellation token that can be used to cancel the asynchronous operation. - /// An containing the results of the query. - Task> ExecuteAsync(CancellationToken cancellationToken = default); -} diff --git a/Neo4j.Driver/Neo4j.Driver/Preview/GraphDatabase.cs b/Neo4j.Driver/Neo4j.Driver/Preview/GraphDatabase.cs deleted file mode 100644 index 324514742..000000000 --- a/Neo4j.Driver/Neo4j.Driver/Preview/GraphDatabase.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) "Neo4j" -// Neo4j Sweden AB [http://neo4j.com] -// -// This file is part of Neo4j. -// -// Licensed under the Apache License, Version 2.0 (the "License"). -// You may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -using Neo4j.Driver.Internal; - -namespace Neo4j.Driver.Preview; - -/// -/// Methods being considered for moving to the Neo4j.Driver. There is no guarantee that anything in -/// Neo4j.Driver.Preview namespace will be in a next minor version. -/// -public static class GraphDatabase -{ - /// - /// Preview: Bookmark Manager API is still under consideration.
There is no guarantee that anything in - /// Neo4j.Driver.Preview namespace will be in a next minor version.
Gets a new - /// , which can construct a new default instance.
- /// The instance should be passed to when opening a new - /// session with . - ///
- public static IBookmarkManagerFactory BookmarkManagerFactory => new BookmarkManagerFactory(); -} diff --git a/Neo4j.Driver/Neo4j.Driver/Preview/PreviewExtensions.cs b/Neo4j.Driver/Neo4j.Driver/Preview/PreviewExtensions.cs deleted file mode 100644 index 5253cc501..000000000 --- a/Neo4j.Driver/Neo4j.Driver/Preview/PreviewExtensions.cs +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright (c) "Neo4j" -// Neo4j Sweden AB [http://neo4j.com] -// -// This file is part of Neo4j. -// -// Licensed under the Apache License, Version 2.0 (the "License"). -// You may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -using System.Collections.Generic; -using Neo4j.Driver.Internal; -using Neo4j.Driver.Preview.FluentQueries; - -namespace Neo4j.Driver.Preview; - -/// -/// There is no guarantee that anything in Neo4j.Driver.Preview namespace will be in a next minor version. -///
This class provides access to preview APIs on existing non-static classes. -///
-public static class PreviewExtensions -{ - /// - /// There is no guarantee that anything in Neo4j.Driver.Preview namespace will be in a next minor version. - ///
Sets the for maintaining bookmarks for the lifetime of the session. - ///
- /// This instance. - /// An instance of to use in the session. - /// this instance. - public static SessionConfigBuilder WithBookmarkManager( - this SessionConfigBuilder builder, - IBookmarkManager bookmarkManager) - { - return builder.WithBookmarkManager(bookmarkManager); - } - - /// - /// There is no guarantee that anything in Neo4j.Driver.Preview namespace will be in a next minor version. - /// Gets an that can be used to configure and execute a query using fluent - /// method chaining. - /// - /// - /// The following example configures and executes a simple query, then iterates over the results. - /// - /// var eagerResult = await driver - /// .ExecutableQuery("MATCH (m:Movie) WHERE m.released > $releaseYear RETURN m.title AS title") - /// .WithParameters(new { releaseYear = 2005 }) - /// .ExecuteAsync(); - /// - /// foreach(var record in eagerResult.Result) - /// { - /// Console.WriteLine(record["title"].As<string>()); - /// } - /// - /// - /// The following example gets a single scalar value from a query. - /// - /// var born = await driver - /// .ExecutableQuery("MATCH (p:Person WHERE p.name = $name) RETURN p.born AS born") - /// .WithStreamProcessor(async stream => (await stream.Where(_ => true).FirstAsync())["born"].As<int>()) - /// .WithParameters(new Dictionary<string, object> { ["name"] = "Tom Hanks" }) - /// .ExecuteAsync(); - /// - /// Console.WriteLine($"Tom Hanks born {born.Result}"); - /// - /// - /// The driver. - /// The cypher of the query. - /// - /// An that can be used to configure and execute a query using - /// fluent method chaining. - /// - public static IExecutableQuery> ExecutableQuery(this IDriver driver, string cypher) - { - return ExecutableQuery>.GetDefault((IInternalDriver)driver, cypher); - } - - /// - /// There is no guarantee that anything in Neo4j.Driver.Preview namespace will be in a next minor version. - ///
Preview: This method will be removed and replaced with a readonly property "BookmarkManager" on the - /// class.
Gets the configured preview bookmark manager from this - /// instance. - ///
- /// - /// This instance. - /// This 's configured instance. - public static IBookmarkManager GetBookmarkManager(this SessionConfig config) - { - return config.BookmarkManager; - } -} diff --git a/Neo4j.Driver/Neo4j.Driver/SessionConfig.cs b/Neo4j.Driver/Neo4j.Driver/SessionConfig.cs index 48753b8b3..171c74a96 100644 --- a/Neo4j.Driver/Neo4j.Driver/SessionConfig.cs +++ b/Neo4j.Driver/Neo4j.Driver/SessionConfig.cs @@ -20,7 +20,6 @@ using System.Linq; using Neo4j.Driver.Internal; using Neo4j.Driver.Internal.Types; -using Neo4j.Driver.Preview; namespace Neo4j.Driver; @@ -281,8 +280,12 @@ internal SessionConfig Build() return _config; } - /// marked as internal until API is solidified. - internal SessionConfigBuilder WithBookmarkManager(IBookmarkManager bookmarkManager) + /// + /// Sets the for maintaining bookmarks for the lifetime of the session. + /// + /// An instance of to use in the session. + /// this instance. + public SessionConfigBuilder WithBookmarkManager(IBookmarkManager bookmarkManager) { _config.BookmarkManager = bookmarkManager; return this; diff --git a/README.md b/README.md index dfd1b3e70..f77a18796 100644 --- a/README.md +++ b/README.md @@ -61,53 +61,72 @@ PM> Install-Package Neo4j.Driver.Signed Connect to a Neo4j database ```csharp -IDriver driver = GraphDatabase.Driver("neo4j://localhost:7687", AuthTokens.Basic("username", "pasSW0rd")); -IAsyncSession session = driver.AsyncSession(o => o.WithDatabase("neo4j")); -try -{ - IResultCursor cursor = await session.RunAsync("CREATE (n) RETURN n"); - await cursor.ConsumeAsync(); -} -finally -{ - await session.CloseAsync(); -} - -... -await driver.CloseAsync(); +using var driver = GraphDatabase.Driver("bolt://localhost:7687", AuthTokens.Basic("neo4j", "password")); +var queryOperation = await driver.ExecutableQuery("CREATE (n) RETURN n").ExecuteAsync(); ``` There are a few points that need to be highlighted when adding this driver into your project: -* Each `IDriver` instance maintains a pool of connections inside, as a result, it is recommended to only use **one - driver per application**. -* It is considerably cheap to create new sessions and transactions, as sessions and transactions do not create new - connections as long as there are free connections available in the connection pool. +* Each `IDriver` instance maintains a pool of connections inside; as a result, it is recommended that you use **only one driver per application**. +* It is considerably cheap to create new sessions and transactions, as sessions and transactions do not create new connections as long as there are free connections available in the connection pool. * The driver is thread-safe, while the session or the transaction is not thread-safe. ### Parsing Result Values -#### Record Stream +#### Query Execution Result -A cypher execution result is comprised of a stream records followed by a result summary. -The records inside the result are accessible via `FetchAsync` and `Current` methods on `IResultCursor`. -Our recommended way to access these result records is to make use of methods provided by `ResultCursorExtensions` such -as `SingleAsync`, `ToListAsync`, and `ForEachAsync`. +The result of executing a query in the way shown above is an `EagerResult`, where `T` is the type of data returned from the query. In the simplest case, this will be an `IReadOnlyList`, which is a fully materialized list of the records returned by the query. Other types of results can be used by using the fluent API exposed by the `IExecutableQuery` type. -Process result records using `ResultCursorExtensions`: +##### Examples ```csharp -IResultCursor cursor = await session.RunAsync("MATCH (a:Person) RETURN a.name as name"); -List people = await cursor.ToListAsync(record => record["name"].As()); +var queryOp = await driver + .ExecutableQuery("MATCH (person:Person) RETURN person") + .WithMap(r => r["person"].As()["name"].As()) + .ExecuteAsync(); +// queryOp.Result is IReadOnlyList + +var queryOp = await driver + .ExecutableQuery("MATCH (person:Person) RETURN person") + .WithMap(r => r["person"].As()["name"].As()) + .WithFilter(s => s.StartsWith("A")) + .ExecuteAsync(); +// queryOp.Result is IReadOnlyList - note that this type of filtering +// is done on the client and is not a substitute for filtering in the Cypher + +var queryOp = await driver + .ExecutableQuery("MATCH (person:Person) RETURN person") + .WithMap(r => r["person"].As()["age"].As()) + .WithReduce(() => 0, (r, n) => r + n) + .ExecuteAsync(); +// queryOp.Result is `int`, the sum of all the ages + +var queryOp = await driver + .ExecutableQuery("MATCH (person:Person) RETURN person") + .WithStreamProcessor( + async stream => + { + double total = 0; + int count = 0; + + await foreach(var record in stream) + { + var ages = record["person"].As()["age"].As(); + total += age; + count++; + } + + return total / count; + }) + .ExecuteAsync(); +// queryOp.Result is `double`, the average of all the ages ``` -The records are exposed as a record stream in the sense that: +#### Record Stream -* A record is accessible once it is received by the client. It is not needed for the whole result set to be received - before it can be visited. -* Each record can only be visited (a.k.a. consumed) once. +A cypher execution result is comprised of a stream records followed by a result summary. The stream can be accessed directly by using the `WithStreamProcessor` method as shown above. The stream implements `IAsyncEnumerable` and as such can be accessed with normal Linq methods by adding the `System.Linq.Async` package to your project. The `ExecuteAsync` method will return an `EagerResult`, where `T` is the type of value returned from the method passed to `WithStreamProcessor`. This value will be stored in the `Result` property of the `EagerResult`, with the `Summary` and `Keys` property containing further information about the execution of the query. -Records on a result cannot be accessed if the session or transaction where the result is created has been closed. +Process result records using `ResultCursorExtensions`: #### Value Types