Skip to content

Commit 2f4d426

Browse files
refactor: improve example to demonstrate computed relations
- Update authorization model to show owner/viewer/can_read pattern - Write tuples to base relations (owner, viewer) - Query computed relation (can_read = owner OR viewer) - Demonstrates OpenFGA's value: derived permissions from base relations - Update CHANGELOG to be brief with link to README (per @rhamzeh feedback) - Remove OPENFGA_LIST_OBJECTS_DEADLINE from manually-written docs - Clarify no pagination limit vs server timeout in all documentation - Add detailed explanation of computed relations in example README Addresses feedback from @aaguiarz and @SoulPancake: - openfga/js-sdk#280 (comment) - Shows why StreamedListObjects is valuable for computed relations All tests passing. Example builds successfully.
1 parent 520e78e commit 2f4d426

File tree

5 files changed

+100
-25
lines changed

5 files changed

+100
-25
lines changed

CHANGELOG.md

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,7 @@
22

33
## [Unreleased](https://github.com/openfga/dotnet-sdk/compare/v0.8.0...HEAD)
44

5-
- feat: add support for [StreamedListObjects](https://openfga.dev/api/service#/Relationship%20Queries/StreamedListObjects)
6-
- New `StreamedListObjects` method that returns `IAsyncEnumerable<StreamedListObjectsResponse>`
7-
- Streams objects as they are received instead of waiting for complete response
8-
- No pagination limits - only constrained by server timeout (OPENFGA_LIST_OBJECTS_DEADLINE)
9-
- Supports all ListObjects parameters: authorization model ID, consistency, contextual tuples, context
10-
- Proper resource cleanup on early termination and cancellation
11-
- See [example](example/StreamedListObjectsExample) for usage
5+
- feat: add support for [StreamedListObjects](https://openfga.dev/api/service#/Relationship%20Queries/StreamedListObjects). See [documentation](#streamed-list-objects)
126
- fix: ApiToken credentials no longer cause reserved header exception (#146)
137

148
## v0.8.0

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -786,10 +786,13 @@ var response = await fgaClient.ListObjects(body, options);
786786

787787
##### Streamed List Objects
788788

789-
The Streamed ListObjects API is very similar to the ListObjects API, with two differences:
789+
List objects of a particular type that the user has access to, using the streaming API.
790790

791-
1. Instead of collecting all objects before returning a response, it streams them to the client as they are collected.
792-
2. The number of results returned is only limited by the execution timeout specified in the flag `OPENFGA_LIST_OBJECTS_DEADLINE`.
791+
The Streamed ListObjects API is very similar to the ListObjects API, with two key differences:
792+
1. **Streaming Results**: Instead of collecting all objects before returning a response, it streams them to the client as they are collected.
793+
2. **No Pagination Limit**: Returns all results without the 1000-object limit of the standard ListObjects API.
794+
795+
This is particularly useful when querying **computed relations** that may return large result sets.
793796

794797
[API Documentation](https://openfga.dev/api/service#/Relationship%20Queries/StreamedListObjects)
795798

example/StreamedListObjectsExample/README.md

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ Demonstrates using `StreamedListObjects` to retrieve objects via the streaming A
77
The Streamed ListObjects API is very similar to the ListObjects API, with two key differences:
88

99
1. **Streaming Results**: Instead of collecting all objects before returning a response, it streams them to the client as they are collected.
10-
2. **No Pagination Limit**: The number of results returned is only limited by the execution timeout specified in the flag `OPENFGA_LIST_OBJECTS_DEADLINE`.
10+
2. **No Pagination Limit**: Returns all results without the 1000-object limit of the standard ListObjects API.
1111

12-
This makes it ideal for scenarios where you need to retrieve large numbers of objects without being constrained by pagination limits.
12+
This makes it ideal for scenarios where you need to retrieve large numbers of objects, especially when querying computed relations.
1313

1414
## Prerequisites
1515

@@ -30,12 +30,41 @@ dotnet run
3030
## What it does
3131

3232
- Creates a temporary store
33-
- Writes a simple authorization model
34-
- Adds 2000 tuples
35-
- Streams results via `StreamedListObjects`
33+
- Writes an authorization model with **computed relations**
34+
- Adds 2000 tuples (1000 owners + 1000 viewers)
35+
- Queries the **computed `can_read` relation** via `StreamedListObjects`
36+
- Shows all 2000 results (demonstrating computed relations)
3637
- Shows progress (first 3 objects and every 500th)
3738
- Cleans up the store
3839

40+
## Authorization Model
41+
42+
The example demonstrates OpenFGA's **computed relations**:
43+
44+
```
45+
type user
46+
47+
type document
48+
relations
49+
define owner: [user]
50+
define viewer: [user]
51+
define can_read: owner or viewer ← COMPUTED RELATION
52+
```
53+
54+
**Why this matters:**
55+
- We write tuples to `owner` and `viewer` (base permissions)
56+
- We query `can_read` (computed from owner OR viewer)
57+
- This shows OpenFGA's power: derived permissions from base relations
58+
- Without OpenFGA, you'd need to duplicate data or run multiple queries
59+
60+
**Example flow:**
61+
1. Write: `user:anne owner document:1-1000`
62+
2. Write: `user:anne viewer document:1001-2000`
63+
3. Query: `StreamedListObjects(user:anne, relation:can_read, type:document)`
64+
4. Result: All 2000 documents (because `can_read = owner OR viewer`)
65+
66+
This demonstrates why `StreamedListObjects` is valuable - computed relations can return large result sets!
67+
3968
## Key Features Demonstrated
4069

4170
### IAsyncEnumerable Pattern

example/StreamedListObjectsExample/StreamedListObjectsExample.cs

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,54 @@ public static async Task Main() {
3232
Type = "document",
3333
Relations = new Dictionary<string, Userset> {
3434
{
35-
"can_read", new Userset {
35+
"owner", new Userset {
36+
This = new object()
37+
}
38+
},
39+
{
40+
"viewer", new Userset {
3641
This = new object()
3742
}
43+
},
44+
{
45+
"can_read", new Userset {
46+
Union = new Usersets {
47+
Child = new List<Userset> {
48+
new() {
49+
ComputedUserset = new ObjectRelation {
50+
Relation = "owner"
51+
}
52+
},
53+
new() {
54+
ComputedUserset = new ObjectRelation {
55+
Relation = "viewer"
56+
}
57+
}
58+
}
59+
}
60+
}
3861
}
3962
},
4063
Metadata = new Metadata {
4164
Relations = new Dictionary<string, RelationMetadata> {
4265
{
43-
"can_read", new RelationMetadata {
66+
"owner", new RelationMetadata {
4467
DirectlyRelatedUserTypes = new List<RelationReference> {
4568
new() { Type = "user" }
4669
}
4770
}
71+
},
72+
{
73+
"viewer", new RelationMetadata {
74+
DirectlyRelatedUserTypes = new List<RelationReference> {
75+
new() { Type = "user" }
76+
}
77+
}
78+
},
79+
{
80+
"can_read", new RelationMetadata {
81+
DirectlyRelatedUserTypes = new List<RelationReference>()
82+
}
4883
}
4984
}
5085
}
@@ -58,24 +93,36 @@ public static async Task Main() {
5893
AuthorizationModelId = authModel.AuthorizationModelId
5994
});
6095

61-
Console.WriteLine("Writing tuples");
96+
Console.WriteLine("Writing tuples (1000 as owner, 1000 as viewer)");
6297
var tuples = new List<ClientTupleKey>();
63-
for (int i = 1; i <= 2000; i++) {
98+
99+
// Write 1000 documents where anne is the owner
100+
for (int i = 1; i <= 1000; i++) {
101+
tuples.Add(new ClientTupleKey {
102+
User = "user:anne",
103+
Relation = "owner",
104+
Object = $"document:{i}"
105+
});
106+
}
107+
108+
// Write 1000 documents where anne is a viewer
109+
for (int i = 1001; i <= 2000; i++) {
64110
tuples.Add(new ClientTupleKey {
65111
User = "user:anne",
66-
Relation = "can_read",
112+
Relation = "viewer",
67113
Object = $"document:{i}"
68114
});
69115
}
116+
70117
await fga.WriteTuples(tuples);
71118
Console.WriteLine($"Wrote {tuples.Count} tuples");
72119

73-
Console.WriteLine("Streaming objects...");
120+
Console.WriteLine("Streaming objects via computed 'can_read' relation...");
74121
var count = 0;
75122
await foreach (var response in fga.StreamedListObjects(
76123
new ClientListObjectsRequest {
77124
User = "user:anne",
78-
Relation = "can_read",
125+
Relation = "can_read", // Computed: owner OR viewer
79126
Type = "document"
80127
},
81128
new ClientListObjectsOptions {

src/OpenFga.Sdk/Client/Client.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -504,9 +504,11 @@ public async Task<ListObjectsResponse> ListObjects(IClientListObjectsRequest bod
504504
/**
505505
* StreamedListObjects - Stream all objects of a particular type that the user has a certain relation to (evaluates)
506506
*
507-
* The Streamed ListObjects API is very similar to the ListObjects API, with two differences:
508-
* 1. Instead of collecting all objects before returning a response, it streams them to the client as they are collected.
509-
* 2. The number of results returned is only limited by the execution timeout specified in the flag OPENFGA_LIST_OBJECTS_DEADLINE.
507+
* The Streamed ListObjects API is very similar to the ListObjects API, with two key differences:
508+
* 1. Streaming Results: Instead of collecting all objects before returning a response, it streams them to the client as they are collected.
509+
* 2. No Pagination Limit: Returns all results without the 1000-object limit of the standard ListObjects API.
510+
*
511+
* This is particularly useful when querying computed relations that may return large result sets.
510512
*
511513
* Returns an async enumerable that yields StreamedListObjectsResponse objects as they are received from the server.
512514
*/

0 commit comments

Comments
 (0)