Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New .NET driver manual #450

Draft
wants to merge 27 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions dotnet-manual/antora.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ nav:
- modules/ROOT/content-nav.adoc
asciidoc:
attributes:
dotnet-driver-version: '5.27' # the version of the api docs published at https://neo4j.com/docs/api/dotnet-driver/
dotnet-driver-apidoc-release: '5.0' # the github branch or release that contains examples included in docs
dotnet-examples: https://raw.githubusercontent.com/neo4j/neo4j-dotnet-driver/{dotnet-driver-apidoc-release}/Neo4j.Driver/Neo4j.Driver.Tests.Integration
dotned-driver-version: '5.27.0' # the version of the package published
common-partial: 5@common-content:ROOT:partial$
common-image: 5@common-content:ROOT:image$
36 changes: 30 additions & 6 deletions dotnet-manual/modules/ROOT/content-nav.adoc
Original file line number Diff line number Diff line change
@@ -1,7 +1,31 @@
* xref:index.adoc[]
* xref:get-started.adoc[]
* xref:client-applications.adoc[]
* xref:cypher-workflow.adoc[]
* xref:session-api.adoc[]
* xref:index.adoc[Quickstart]

* *Regular workflow*

* xref:install.adoc[Installation]
* xref:connect.adoc[Connect to the database]
* xref:query-simple.adoc[Query the database]
* xref:object-graph-mapping.adoc[]

* *Advanced usage*

* xref:transactions.adoc[Run your own transactions]
* xref:result-summary.adoc[Explore the query execution summary]
* xref:async.adoc[Run asynchronous queries]
* xref:bookmarks.adoc[Coordinate parallel transactions]
* xref:query-advanced.adoc[Further query mechanisms]
* xref:reactive.adoc[Control results flow with reactive streams]
* xref:performance.adoc[Performance recommendations]

* *Reference*

* xref:connect-advanced.adoc[Advanced connection information]
* xref:data-types.adoc[Data types and mapping to Cypher types]
* xref:upgrade.adoc[]
* xref:terminology.adoc[]
* link:https://neo4j.com/docs/api/dotnet-driver/current/[API documentation, window=_blank]

* *GraphAcademy courses*

* link:https://graphacademy.neo4j.com/courses/modeling-fundamentals/?ref=docs-dotnet[Graph Data Modeling Fundamentals]
* link:https://graphacademy.neo4j.com/courses/cypher-intermediate-queries/?ref=docs-dotnet[Intermediate Cypher Queries]
* link:https://graphacademy.neo4j.com/courses/app-dotnet/?ref=docs-dotnet[Building Neo4j Applications with .NET]
92 changes: 92 additions & 0 deletions dotnet-manual/modules/ROOT/pages/async.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
= Run non-blocking asynchronous queries
:page-toclevels: 0

The examples in xref:query-simple.adoc[Query the database] and xref:transactions.adoc[Run your own transactions] use the driver in its synchronous form.
This means that, when running a query against the database, your application waits for the server to retrieve all the results and transmit them to the driver.
This is not a problem for most use cases, but for queries that have a long processing time or a large result set, asynchronous handling may speed up your application.

== Asynchronous managed transactions

You run an asynchronous transaction through an link:https://neo4j.com/docs/api/java-driver/current/org.neo4j.driver/org/neo4j/driver/async/AsyncSession.html[`AsyncSession`].
The flow is similar to that of xref:transactions.adoc[regular transactions], except that async transaction functions return a link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/CompletionStage.html[`CompletionStage`] object (which may be further converted into link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/CompletableFuture.html[`CompletableFuture`]).


[source, java]
----
package demo;

import java.util.Map;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

import org.neo4j.driver.async.AsyncSession;
import org.neo4j.driver.AuthTokens;
import org.neo4j.driver.Driver;
import org.neo4j.driver.GraphDatabase;
import org.neo4j.driver.summary.ResultSummary;
import org.neo4j.driver.SessionConfig;

public class App {

public static void main(String... args) throws ExecutionException, InterruptedException {
final String dbUri = "<URI for Neo4j database>";
final String dbUser = "<Username>";
final String dbPassword = "<Password>";

try (var driver = GraphDatabase.driver(dbUri, AuthTokens.basic(dbUser, dbPassword))) { // <1>
driver.verifyConnectivity();

var summary = printAllPeople(driver);
// Block as long as necessary (for demonstration purposes)
System.out.println(summary.get()); // <8>
}
}

public static CompletableFuture<ResultSummary> printAllPeople(Driver driver) {
var session = driver.session(AsyncSession.class, SessionConfig.builder().withDatabase("neo4j").build()); // <2>

var query = """
UNWIND ['Alice', 'Bob', 'Sofia', 'Charles'] AS name
MERGE (p:Person {name: name}) RETURN p.name
""";
var summary = session.executeWriteAsync(tx -> tx.runAsync(query) // <3>
.thenCompose(resCursor -> resCursor.forEachAsync(record -> { // <4>
System.out.println(record.get(0).asString());
})))
.whenComplete((result, error) -> { // <5>
session.closeAsync();
})
.toCompletableFuture(); // <6>

return summary; // <7>
}
}
----

<1> Driver creation is the same in the synchronous and asynchronous versions.
<2> An asynchronous session is created by providing `AsyncSession.class` as first parameter to link:https://neo4j.com/docs/api/java-driver/current/org.neo4j.driver/org/neo4j/driver/Driver.html#session(java.lang.Class,org.neo4j.driver.SessionConfig)[`Driver.session()`], which returns an link:https://neo4j.com/docs/api/java-driver/current/org.neo4j.driver/org/neo4j/driver/async/AsyncSession.html[`AsyncSession`] object.
Note that async sessions may not be opened as resources with `try` statements, as the driver can't know when it is safe to close them.
<3> As for regular transactions, `.executeWriteAsync()` (and `executeReadAsync()`) take a transaction function callback.
Inside the transaction function, run queries with link:https://neo4j.com/docs/api/java-driver/current/org.neo4j.driver/org/neo4j/driver/async/AsyncQueryRunner.html#runAsync(org.neo4j.driver.Query)[`.runAsync()`].
Each query run returns a link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/CompletionStage.html[`CompletionStage`].
<4> Optionally use methods on `CompletionStage` to process the result in the asynchronous runner.
The query's result set is available as an link:https://neo4j.com/docs/api/java-driver/current/org.neo4j.driver/org/neo4j/driver/internal/cursor/AsyncResultCursor.html[`AsyncResultCursor`], which implements a similar set of methods for processing the result to those of synchronous transactions (see xref:transactions.adoc#process-result[Transactions -> Process query results]). +
Inner object types are the same as the synchronous case (i.e. `Record`, `ResultSummary`).
<5> Optionally run operations once the query has completed, such as closing the driver session.
<6> link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/CompletableFuture.html[`CompletableFuture`] is a convenient type to return back to the caller.
<7> Contrary to synchronous transactions, `.executeWriteAsync()` and `executeReadAsync()` return the xref:result-summary.adoc[result summary] only.
**Result processing and handling must be done inside the asynchronous runner.**
<8> link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/CompletableFuture.html#get()[`.get()`] waits as long as necessary for the future to complete, and then returns its result.

[CAUTION]
The methods `.executeReadAsync()` and `.executeWriteAsync()` have replaced `.readTransactionAsync()` and `.writeTransactionAsync()`, which are deprecated in version 5.x and will be removed in version 6.0.


ifndef::backend-pdf[]
[discrete.glossary]
== Glossary

include::{common-partial}/glossary.adoc[]
include::../partials/glossary.adoc[]
endif::[]
174 changes: 174 additions & 0 deletions dotnet-manual/modules/ROOT/pages/bookmarks.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
= Coordinate parallel transactions

When working with a Neo4j cluster, <<causal_consistency>> is enforced by default in most cases, which guarantees that a query is able to read changes made by previous queries.
The same does not happen by default for multiple xref:transactions.adoc[transactions] running in parallel though.
In that case, you can use _bookmarks_ to have one transaction wait for the result of another to be propagated across the cluster before running its own work.
This is not a requirement, and *you should only use bookmarks if you _need_ casual consistency across different transactions*, as waiting for bookmarks can have a negative performance impact.

A _bookmark_ is a token that represents some state of the database.
By passing one or multiple bookmarks along with a query, the server will make sure that the query does not get executed before the represented state(s) have been established.


== Bookmarks with `.ExecutableQuery()`

When xref:query-simple.adoc[querying the database with `.ExecutableQuery()`], the driver manages bookmarks for you.
In this case, you have the guarantee that subsequent queries can read previous changes with no further action.

[source, csharp]
----
await driver.ExecutableQuery("<QUERY 1>").AsyncExecute();

// subsequent .ExecutableQuery() calls will be causally chained

await driver.ExecutableQueryAsync("<QUERY 2>").ExecuteAsync(); // can read result of <QUERY 1>
await driver.ExecutableQueryAsync("<QUERY 3>").ExecuteAsync(); // can read result of <QUERY 2>
----

To disable bookmark management and causal consistency, use `enableBookmarkManager: false` in the query configuration.

[source, csharp]
----
await driver.executableQuery("<QUERY>")
.WithConfig(new QueryConfig(enableBookmarkManager: false))
.ExecuteAsync();
----


== Bookmarks within a single session

Bookmark management happens automatically for queries run within a single session, so you can trust that queries inside one session are causally chained.

[source, csharp]
----
using var session = driver.AsyncSession(conf => conf.WithDatabase("neo4j"));
await session.ExecuteWriteAsync(async tx => await tx.RunAsync("<QUERY 1>"));
await session.ExecuteWriteAsync(async tx => await tx.RunAsync("<QUERY 2>")); // can read QUERY 1
await session.ExecuteWriteAsync(async tx => await tx.RunAsync("<QUERY 3>")); // can read QUERY 1,2
----


== Bookmarks across multiple sessions

If your application uses multiple sessions, you may need to ensure that one session has completed all its transactions before another session is allowed to run its queries.

In the example below, `sessionA` and `sessionB` are allowed to run concurrently, while `sessionC` waits until their results have been propagated.
This guarantees the `Person` nodes `sessionC` wants to act on actually exist.

.Coordinate multiple sessions using bookmarks
[source, csharp]
----
using Neo4j.Driver;

const string dbUri = "<URI for Neo4j database>";
const string dbUser = "<Username>";
const string dbPassword = "<Password>";

await using var driver = GraphDatabase.Driver(dbUri, AuthTokens.Basic(dbUser, dbPassword));
await driver.VerifyConnectivityAsync();

await createSomeFriends(driver);

async Task createSomeFriends(IDriver driver) {
Bookmarks savedBookmarks = Bookmarks.From(new List<string>()); // to collect the sessions' bookmarks

// Create the first person and employment relationship
using var sessionA = driver.AsyncSession(conf => conf.WithDatabase("neo4j"));
await sessionA.ExecuteWriteAsync(tx => createPerson(tx, "Alice"));
await sessionA.ExecuteWriteAsync(tx => employ(tx, "Alice", "Wayne Enterprises"));
savedBookmarks += sessionA.LastBookmarks; // <1>

// Create the second person and employment relationship
using var sessionB = driver.AsyncSession(conf => conf.WithDatabase("neo4j"));
await sessionB.ExecuteWriteAsync(tx => createPerson(tx, "Bob"));
await sessionB.ExecuteWriteAsync(tx => employ(tx, "Bob", "LexCorp"));
savedBookmarks += sessionB.LastBookmarks; // <1>

// Create a friendship between the two people created above
using var sessionC = driver.AsyncSession(conf => conf
.WithDatabase("neo4j")
.WithBookmarks(savedBookmarks) // <2>
);
await sessionC.ExecuteWriteAsync(tx => createFriendship(tx, "Alice", "Bob"));
await sessionC.ExecuteWriteAsync(tx => printFriendships(tx));
}

// Create a person node
async Task createPerson(IAsyncQueryRunner tx, string name) {
await tx.RunAsync("MERGE (:Person {name: $name})", new { name = name });
}

// Create an employment relationship to a pre-existing company node
// This relies on the person first having been created.
async Task employ(IAsyncQueryRunner tx, string personName, string companyName) {
await tx.RunAsync(@"
MATCH (person:Person {name: $personName})
MATCH (company:Company {name: $companyName})
CREATE (person)-[:WORKS_FOR]->(company)
", new { personName = personName, companyName = companyName }
);
}

// Create a friendship between two people
async Task createFriendship(IAsyncQueryRunner tx, string nameA, string nameB) {
await tx.RunAsync(@"
MATCH (a:Person {name: $nameA})
MATCH (b:Person {name: $nameB})
MERGE (a)-[:KNOWS]->(b)
", new { nameA = nameA, nameB = nameB }
);
}

// Retrieve and display all friendships
async Task printFriendships(IAsyncQueryRunner tx) {
var result = await tx.RunAsync("MATCH (a)-[:KNOWS]->(b) RETURN a.name, b.name");
while (await result.FetchAsync()) {
var record = result.Current;
Console.WriteLine(record.Get<string>("a.name") + " knows " + record.Get<string>("b.name"));
}
}
----

<1> Collect and combine bookmarks from different sessions using `AsyncSession.LastBookmarks`, storing them in a link:https://neo4j.com/docs/api/dotnet-driver/current/api/Neo4j.Driver.Bookmarks.html[`Bookmarks`] object.
<2> Use them to initialize another session with the `.WithBookmarks()` config method.

image:{common-image}/driver-passing-bookmarks.svg[]

[TIP]
The use of bookmarks can negatively impact performance, since all queries are forced to wait for the latest changes to be propagated across the cluster.
For simple use-cases, try to group queries within a single transaction, or within a single session.


== Mix `.ExecutableQuery()` and sessions

To ensure causal consistency among transactions executed partly with `.ExecutableQuery()` and partly with sessions, you can retrieve the default bookmark manager for `ExecutableQuery` instances through `driver.executableQueryBookmarkManager()` and pass it to new sessions through the `.WithBookmarkManager()` config method.
This will ensure that all work is executed under the same bookmark manager and thus causally consistent.

[source, java]
----
// import org.neo4j.driver.Driver;
// import org.neo4j.driver.SessionConfig;

driver.executableQuery("<QUERY 1>").execute();

try (var session = driver.session(SessionConfig.builder()
.withBookmarkManager(driver.executableQueryBookmarkManager())
.build())) {

// every query inside this session will be causally chained
// (i.e., can read what was written by <QUERY 1>)
session.executeWriteWithoutResult(tx -> tx.run("<QUERY 2>"));
}

// subsequent executableQuery calls will also be causally chained
// (i.e., can read what was written by <QUERY 2>)
driver.executableQuery("<QUERY 3>").execute();
----


ifndef::backend-pdf[]
[discrete.glossary]
== Glossary

include::{common-partial}/glossary.adoc[]
include::../partials/glossary.adoc[]
endif::[]
Loading
Loading