Skip to content

Commit aa1f374

Browse files
committed
Add documentation for using Cypher parameters and enhance integration tests for nested objects and arrays
1 parent 08cdde7 commit aa1f374

File tree

2 files changed

+270
-0
lines changed

2 files changed

+270
-0
lines changed

README.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,99 @@ await using (var reader = await cmd.ExecuteReaderAsync())
4848
}
4949
```
5050

51+
## Using Cypher Parameters
52+
53+
You can pass parameters to your Cypher queries to avoid SQL injection and improve query reusability. Parameters are referenced in Cypher queries using the `$` prefix (e.g., `$name`, `$age`).
54+
55+
### Using a Dictionary
56+
57+
```csharp
58+
using Npgsql;
59+
using Npgsql.Age;
60+
using Npgsql.Age.Types;
61+
using System.Collections.Generic;
62+
63+
var parameters = new Dictionary<string, object?>
64+
{
65+
["name"] = "Alice",
66+
["age"] = 30
67+
};
68+
69+
await using (var cmd = dataSource.CreateCypherCommand(
70+
"graph1",
71+
"CREATE (p:Person {name: $name, age: $age}) RETURN p",
72+
parameters))
73+
await using (var reader = await cmd.ExecuteReaderAsync())
74+
{
75+
while (await reader.ReadAsync())
76+
{
77+
var agtypeResult = reader.GetValue<Agtype>(0);
78+
Vertex person = agtypeResult.GetVertex();
79+
Console.WriteLine($"Created: {person}");
80+
}
81+
}
82+
```
83+
84+
### Using a JSON String
85+
86+
You can also pass parameters as a JSON string:
87+
88+
```csharp
89+
string parametersJson = """{"name": "Bob", "age": 25}""";
90+
91+
await using (var cmd = dataSource.CreateCypherCommand(
92+
"graph1",
93+
"CREATE (p:Person {name: $name, age: $age}) RETURN p",
94+
parametersJson))
95+
{
96+
await cmd.ExecuteNonQueryAsync();
97+
}
98+
```
99+
100+
### Complex Parameters
101+
102+
Parameters can include nested objects and arrays:
103+
104+
```csharp
105+
var parameters = new Dictionary<string, object?>
106+
{
107+
["person"] = new Dictionary<string, object>
108+
{
109+
["name"] = "Charlie",
110+
["age"] = 35,
111+
["hobbies"] = new[] { "reading", "cycling" }
112+
}
113+
};
114+
115+
await using (var cmd = dataSource.CreateCypherCommand(
116+
"graph1",
117+
"CREATE (p:Person {name: $person.name, age: $person.age}) RETURN p",
118+
parameters))
119+
{
120+
await cmd.ExecuteNonQueryAsync();
121+
}
122+
```
123+
124+
### Null Values
125+
126+
Null parameter values are supported:
127+
128+
```csharp
129+
var parameters = new Dictionary<string, object?>
130+
{
131+
["name"] = "David",
132+
["email"] = null // Optional property
133+
};
134+
135+
await using (var cmd = dataSource.CreateCypherCommand(
136+
"graph1",
137+
"CREATE (p:Person {name: $name}) RETURN p",
138+
parameters))
139+
{
140+
await cmd.ExecuteNonQueryAsync();
141+
}
142+
```
143+
51144
## Acknowledgements
52145

53146
* This project is a fork of [Apache AGE](https://github.com/Allison-E/pg-age).

test/Npgsql.AgeTests/AgeIntegrationTests.cs

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,4 +414,181 @@ public async Task ExecuteCypherQueryAsync_WithEmptyParameters_Should_Work()
414414

415415
await DropTempGraphAsync(graphName);
416416
}
417+
418+
[Fact]
419+
public async Task ExecuteCypherQueryAsync_WithNestedObject_Should_Work()
420+
{
421+
var graphName = await CreateTempGraphAsync();
422+
await using var connection = await DataSource.OpenConnectionAsync();
423+
424+
var parameters = new Dictionary<string, object?>
425+
{
426+
["person"] = new Dictionary<string, object>
427+
{
428+
["name"] = "Alice",
429+
["age"] = 30,
430+
["address"] = new Dictionary<string, object>
431+
{
432+
["city"] = "Seattle",
433+
["zipCode"] = "98101",
434+
},
435+
},
436+
};
437+
438+
await using var command = connection.CreateCypherCommand(
439+
graphName,
440+
"CREATE (p:Person {name: $person.name, age: $person.age, city: $person.address.city, zipCode: $person.address.zipCode}) RETURN p",
441+
parameters
442+
);
443+
await using var dataReader = await command.ExecuteReaderAsync();
444+
445+
Assert.NotNull(dataReader);
446+
Assert.True(dataReader.HasRows);
447+
Assert.True(await dataReader.ReadAsync());
448+
449+
var result = await dataReader.GetFieldValueAsync<Agtype?>(0);
450+
var vertex = result?.GetVertex();
451+
452+
Assert.NotNull(vertex);
453+
Assert.Equal("Alice", vertex?.Properties["name"]);
454+
Assert.Equal(30, vertex?.Properties["age"]);
455+
Assert.Equal("Seattle", vertex?.Properties["city"]);
456+
Assert.Equal("98101", vertex?.Properties["zipCode"]);
457+
458+
await DropTempGraphAsync(graphName);
459+
}
460+
461+
[Fact]
462+
public async Task ExecuteCypherQueryAsync_WithArrayParameter_Should_Work()
463+
{
464+
var graphName = await CreateTempGraphAsync();
465+
await using var connection = await DataSource.OpenConnectionAsync();
466+
467+
var parameters = new Dictionary<string, object?>
468+
{
469+
["hobbies"] = new[] { "reading", "cycling", "photography" },
470+
["scores"] = new[] { 85, 92, 78, 95 },
471+
};
472+
473+
await using var command = connection.CreateCypherCommand(
474+
graphName,
475+
"RETURN $hobbies, $scores",
476+
parameters
477+
);
478+
await using var dataReader = await command.ExecuteReaderAsync();
479+
480+
Assert.NotNull(dataReader);
481+
Assert.True(dataReader.HasRows);
482+
Assert.True(await dataReader.ReadAsync());
483+
484+
var hobbiesResult = await dataReader.GetFieldValueAsync<Agtype?>(0);
485+
var scoresResult = await dataReader.GetFieldValueAsync<Agtype?>(1);
486+
487+
Assert.Equal(
488+
new object[] { "reading", "cycling", "photography" },
489+
hobbiesResult?.GetList()
490+
);
491+
Assert.Equal(new object[] { 85, 92, 78, 95 }, scoresResult?.GetList());
492+
493+
await DropTempGraphAsync(graphName);
494+
}
495+
496+
[Fact]
497+
public async Task ExecuteCypherQueryAsync_WithNestedArraysAndObjects_Should_Work()
498+
{
499+
var graphName = await CreateTempGraphAsync();
500+
await using var connection = await DataSource.OpenConnectionAsync();
501+
502+
var parameters = new Dictionary<string, object?>
503+
{
504+
["data"] = new Dictionary<string, object>
505+
{
506+
["tags"] = new[] { "developer", "architect" },
507+
["metadata"] = new Dictionary<string, object>
508+
{
509+
["level"] = "senior",
510+
["years"] = 10,
511+
},
512+
["projects"] = new[]
513+
{
514+
new Dictionary<string, object>
515+
{
516+
["name"] = "Project A",
517+
["status"] = "completed",
518+
},
519+
new Dictionary<string, object>
520+
{
521+
["name"] = "Project B",
522+
["status"] = "active",
523+
},
524+
},
525+
},
526+
};
527+
528+
await using var command = connection.CreateCypherCommand(
529+
graphName,
530+
"RETURN $data.tags, $data.metadata.level, $data.metadata.years, $data.projects",
531+
parameters
532+
);
533+
await using var dataReader = await command.ExecuteReaderAsync();
534+
535+
Assert.NotNull(dataReader);
536+
Assert.True(dataReader.HasRows);
537+
Assert.True(await dataReader.ReadAsync());
538+
539+
var tagsResult = await dataReader.GetFieldValueAsync<Agtype?>(0);
540+
var levelResult = await dataReader.GetFieldValueAsync<Agtype?>(1);
541+
var yearsResult = await dataReader.GetFieldValueAsync<Agtype?>(2);
542+
var projectsResult = await dataReader.GetFieldValueAsync<Agtype?>(3);
543+
544+
Assert.Equal(new object[] { "developer", "architect" }, tagsResult?.GetList());
545+
Assert.Equal("senior", levelResult?.GetString());
546+
Assert.Equal(10, yearsResult?.GetInt32());
547+
548+
var projects = projectsResult?.GetList();
549+
Assert.NotNull(projects);
550+
Assert.Equal(2, projects.Count);
551+
552+
await DropTempGraphAsync(graphName);
553+
}
554+
555+
[Fact]
556+
public async Task ExecuteCypherQueryAsync_WithArrayInVertexCreation_Should_Work()
557+
{
558+
var graphName = await CreateTempGraphAsync();
559+
await using var connection = await DataSource.OpenConnectionAsync();
560+
561+
var parameters = new Dictionary<string, object?>
562+
{
563+
["name"] = "Bob",
564+
["skills"] = new[] { "C#", "Python", "SQL" },
565+
["certifications"] = new[] { "Azure", "AWS" },
566+
};
567+
568+
await using var command = connection.CreateCypherCommand(
569+
graphName,
570+
"CREATE (p:Developer {name: $name, skills: $skills, certifications: $certifications}) RETURN p",
571+
parameters
572+
);
573+
await using var dataReader = await command.ExecuteReaderAsync();
574+
575+
Assert.NotNull(dataReader);
576+
Assert.True(dataReader.HasRows);
577+
Assert.True(await dataReader.ReadAsync());
578+
579+
var result = await dataReader.GetFieldValueAsync<Agtype?>(0);
580+
var vertex = result?.GetVertex();
581+
582+
Assert.NotNull(vertex);
583+
Assert.Equal("Bob", vertex?.Properties["name"]);
584+
585+
var skills = vertex?.Properties["skills"] as System.Collections.IList;
586+
Assert.NotNull(skills);
587+
Assert.Equal(3, skills.Count);
588+
Assert.Contains("C#", skills.Cast<object>());
589+
Assert.Contains("Python", skills.Cast<object>());
590+
Assert.Contains("SQL", skills.Cast<object>());
591+
592+
await DropTempGraphAsync(graphName);
593+
}
417594
}

0 commit comments

Comments
 (0)