Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
84992d8
feat: add StreamedListObjects support
daniel-jonathan Nov 5, 2025
bead1e7
feat: add StreamedListObjects support
daniel-jonathan Nov 5, 2025
5ce96b4
refactor: use using statement for cancellation registration
daniel-jonathan Nov 5, 2025
35fb401
fix: improve streaming cancellation and test reliability
daniel-jonathan Nov 5, 2025
71df2e8
Merge branch 'main' into feat/streamed_list_objects
daniel-jonathan Nov 10, 2025
eb03120
test: use FgaConstants for test values instead of hardcoded strings
daniel-jonathan Nov 10, 2025
1064700
docs: simplify StreamedListObjects example and enhance README
daniel-jonathan Nov 10, 2025
de2ffb9
docs: add StreamedListObjects section to main README
daniel-jonathan Nov 10, 2025
a66c34b
test: add custom headers and rate limit tests for StreamedListObjects
daniel-jonathan Nov 10, 2025
4a2ec7a
refactor: use using statement for CancellationTokenSource
daniel-jonathan Nov 10, 2025
0b61657
fix: address CodeQL and resource disposal issues
daniel-jonathan Nov 12, 2025
d5aa28b
fix: address CodeQL and resource ContainsKey issue Addresses CodeQL, …
daniel-jonathan Nov 12, 2025
b28428a
refactor: add using statements for all IDisposable resources in tests
daniel-jonathan Nov 12, 2025
2e4e35e
refactor: remove useless assignments in tests
daniel-jonathan Nov 12, 2025
bd0aa89
refactor: Updated commenting and Assertion messaging based on CodeRab…
daniel-jonathan Nov 12, 2025
520e78e
refactor: Fixed dispose issue based on CodeQL feedback
daniel-jonathan Nov 12, 2025
2f4d426
refactor: improve example to demonstrate computed relations
daniel-jonathan Nov 13, 2025
1becd72
fix: batch tuple writes to respect 100-tuple limit
daniel-jonathan Nov 13, 2025
aa40ebd
fix: prevent logging sensitive data in error handler
daniel-jonathan Nov 13, 2025
826d8e5
refactor: Updated the example README based on review feedback
daniel-jonathan Nov 13, 2025
db885db
test: add comprehensive tests for partial NDJSON streaming handling
daniel-jonathan Nov 15, 2025
bb2bea8
Merge branch 'main' into feat/streamed_list_objects
daniel-jonathan Nov 15, 2025
99cca29
feat: add StreamedListObjects API support
daniel-jonathan Nov 19, 2025
9ca91cd
perf: optimize dictionary lookups in model Equals methods
daniel-jonathan Nov 19, 2025
4c98c13
perf: optimize model Equals methods for CodeQL compliance
daniel-jonathan Nov 19, 2025
67d019e
Merge branch 'main' into feat/streamed_list_objects
SoulPancake Dec 1, 2025
1c8b560
Merge branch 'main' into feat/streamed_list_objects
SoulPancake Dec 1, 2025
a50dce3
Merge branch 'main' into feat/streamed_list_objects
SoulPancake Dec 8, 2025
c01c35f
Merge branch 'main' into feat/streamed_list_objects
SoulPancake Jan 9, 2026
677c4d5
fix: remove dupl package reference
SoulPancake Jan 12, 2026
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

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

- feat: add support for [StreamedListObjects](https://openfga.dev/api/service#/Relationship%20Queries/StreamedListObjects)
Comment thread
daniel-jonathan marked this conversation as resolved.
Outdated
- New `StreamedListObjects` method that returns `IAsyncEnumerable<StreamedListObjectsResponse>`
- Streams objects as they are received instead of waiting for complete response
- No pagination limits - only constrained by server timeout (OPENFGA_LIST_OBJECTS_DEADLINE)
- Supports all ListObjects parameters: authorization model ID, consistency, contextual tuples, context
- Proper resource cleanup on early termination and cancellation
- See [example](example/StreamedListObjectsExample) for usage
- fix: ApiToken credentials no longer cause reserved header exception (#146)

## v0.8.0
Expand Down
114 changes: 114 additions & 0 deletions example/StreamedListObjectsExample/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Streamed List Objects Example

Demonstrates using `StreamedListObjects` to retrieve objects via the streaming API in the .NET SDK.

## What is StreamedListObjects?

The Streamed ListObjects API is very similar to the ListObjects API, with two key differences:

1. **Streaming Results**: Instead of collecting all objects before returning a response, it streams them to the client as they are collected.
2. **No Pagination Limit**: The number of results returned is only limited by the execution timeout specified in the flag `OPENFGA_LIST_OBJECTS_DEADLINE`.

This makes it ideal for scenarios where you need to retrieve large numbers of objects without being constrained by pagination limits.

## Prerequisites

- .NET 6.0 or higher
Comment thread
daniel-jonathan marked this conversation as resolved.
- OpenFGA server running on `http://localhost:8080` (or set `FGA_API_URL`)
Comment thread
daniel-jonathan marked this conversation as resolved.

## Running

### Using the local build

```bash
# From the SDK root directory, build the SDK first
cd /Users/danieljonathan/Workspaces/openfga/dotnet-sdk
Comment thread
daniel-jonathan marked this conversation as resolved.
Outdated
dotnet build src/OpenFga.Sdk/OpenFga.Sdk.csproj

# Then run the example
cd example/StreamedListObjectsExample
dotnet run
```

### Using environment variables

You can configure the example using environment variables:

```bash
export FGA_API_URL="http://localhost:8080"
# Optional OAuth credentials
export FGA_CLIENT_ID="your-client-id"
export FGA_CLIENT_SECRET="your-client-secret"
export FGA_API_AUDIENCE="your-api-audience"
export FGA_API_TOKEN_ISSUER="your-token-issuer"

dotnet run
```

## What it does

1. Creates a temporary store
2. Writes a simple authorization model with `user` and `document` types
3. Writes 10 sample tuples (user:anne can_read document:1-10)
4. Demonstrates the difference between `ListObjects` and `StreamedListObjects`
5. Shows how to handle early cancellation and cleanup
6. Demonstrates using `CancellationToken` for timeout control
7. Cleans up by deleting the temporary store

## Key Features Demonstrated

### IAsyncEnumerable Pattern

The `StreamedListObjects` method returns `IAsyncEnumerable<StreamedListObjectsResponse>`, which is the idiomatic .NET way to handle streaming data:

```csharp
await foreach (var response in fgaClient.StreamedListObjects(request)) {
Console.WriteLine($"Received: {response.Object}");
}
```

### Early Break and Cleanup

The streaming implementation properly handles early termination:

```csharp
await foreach (var response in fgaClient.StreamedListObjects(request)) {
Console.WriteLine(response.Object);
if (someCondition) {
break; // Stream is automatically cleaned up
}
}
```

### Cancellation Support

Full support for `CancellationToken`:

```csharp
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(5));

try {
await foreach (var response in fgaClient.StreamedListObjects(request, options, cts.Token)) {
Console.WriteLine(response.Object);
}
}
catch (OperationCanceledException) {
Console.WriteLine("Operation cancelled");
}
```

## Benefits Over ListObjects

- **No Pagination**: Retrieve all objects in a single streaming request
- **Lower Memory**: Objects are processed as they arrive, not held in memory
- **Early Termination**: Can stop streaming at any point without wasting resources
- **Better for Large Results**: Ideal when expecting hundreds or thousands of objects

## Performance Considerations

- Streaming starts immediately - no need to wait for all results
- HTTP connection remains open during streaming
- Properly handles cleanup if consumer stops early
- Supports all the same options as `ListObjects` (consistency, contextual tuples, etc.)

198 changes: 198 additions & 0 deletions example/StreamedListObjectsExample/StreamedListObjectsExample.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
using OpenFga.Sdk.Client;
Comment thread
daniel-jonathan marked this conversation as resolved.
using OpenFga.Sdk.Client.Model;
using OpenFga.Sdk.Configuration;
using OpenFga.Sdk.Model;

namespace StreamedListObjectsExample;

/// <summary>
/// Example demonstrating the StreamedListObjects API.
///
/// The Streamed ListObjects API is very similar to the ListObjects API, with two key differences:
/// 1. Instead of collecting all objects before returning a response, it streams them to the client as they are collected.
/// 2. The number of results returned is only limited by the execution timeout (OPENFGA_LIST_OBJECTS_DEADLINE).
///
/// This makes it ideal for scenarios where you need to retrieve large numbers of objects without pagination limits.
/// </summary>
public class StreamedListObjectsExample {
public static async Task Main() {
try {
// Configure credentials (if needed)
var credentials = new Credentials();
if (Environment.GetEnvironmentVariable("FGA_CLIENT_ID") != null) {
credentials.Method = CredentialsMethod.ClientCredentials;
credentials.Config = new CredentialsConfig {
ApiAudience = Environment.GetEnvironmentVariable("FGA_API_AUDIENCE"),
ApiTokenIssuer = Environment.GetEnvironmentVariable("FGA_API_TOKEN_ISSUER"),
ClientId = Environment.GetEnvironmentVariable("FGA_CLIENT_ID"),
ClientSecret = Environment.GetEnvironmentVariable("FGA_CLIENT_SECRET")
};
}

// Initialize the OpenFGA client
var configuration = new ClientConfiguration {
ApiUrl = Environment.GetEnvironmentVariable("FGA_API_URL") ?? "http://localhost:8080",
Credentials = credentials
};
var fgaClient = new OpenFgaClient(configuration);

// Create a temporary store
Console.WriteLine("Creating temporary store...");
var store = await fgaClient.CreateStore(new ClientCreateStoreRequest {
Name = "Streamed List Objects Demo"
});
fgaClient.StoreId = store.Id;
Console.WriteLine($"Created store with ID: {store.Id}");

// Write a simple authorization model
Console.WriteLine("\nWriting authorization model...");
var authModel = await fgaClient.WriteAuthorizationModel(new ClientWriteAuthorizationModelRequest {
SchemaVersion = "1.1",
TypeDefinitions = new List<TypeDefinition> {
new() {
Type = "user",
Relations = new Dictionary<string, Userset>()
},
new() {
Type = "document",
Relations = new Dictionary<string, Userset> {
{
"can_read", new Userset {
This = new object()
}
}
},
Metadata = new Metadata {
Relations = new Dictionary<string, RelationMetadata> {
{
"can_read", new RelationMetadata {
DirectlyRelatedUserTypes = new List<RelationReference> {
new() {
Type = "user"
}
}
}
}
}
}
}
}
});
fgaClient.AuthorizationModelId = authModel.AuthorizationModelId;
Console.WriteLine($"Created authorization model with ID: {authModel.AuthorizationModelId}");

// Write sample tuples
Console.WriteLine("\nWriting sample tuples...");
var tuples = new List<ClientTupleKey>();
for (int i = 1; i <= 10; i++) {
tuples.Add(new ClientTupleKey {
User = "user:anne",
Relation = "can_read",
Object = $"document:{i}"
Comment thread
daniel-jonathan marked this conversation as resolved.
Outdated
});
}

await fgaClient.WriteTuples(tuples);
Console.WriteLine($"Wrote {tuples.Count} tuples to the store");

// Compare StreamedListObjects vs ListObjects
Console.WriteLine("\n--- Comparing StreamedListObjects vs ListObjects ---\n");

// Example 1: Using ListObjects (standard paginated API)
Console.WriteLine("Using ListObjects (standard API):");
var standardResponse = await fgaClient.ListObjects(new ClientListObjectsRequest {
User = "user:anne",
Relation = "can_read",
Type = "document"
}, new ClientListObjectsOptions {
Consistency = ConsistencyPreference.HIGHERCONSISTENCY
});

Console.WriteLine($"ListObjects returned {standardResponse.Objects.Count} objects:");
foreach (var obj in standardResponse.Objects) {
Console.WriteLine($" - {obj}");
}

// Example 2: Using StreamedListObjects (streaming API)
Console.WriteLine("\nUsing StreamedListObjects (streaming API):");
var streamedCount = 0;
var streamedObjects = new List<string>();

await foreach (var response in fgaClient.StreamedListObjects(
new ClientListObjectsRequest {
User = "user:anne",
Relation = "can_read",
Type = "document"
},
new ClientListObjectsOptions {
Consistency = ConsistencyPreference.HIGHERCONSISTENCY
})) {

Console.WriteLine($" - {response.Object}");
streamedObjects.Add(response.Object);
streamedCount++;
}

Console.WriteLine($"StreamedListObjects streamed {streamedCount} objects");

// Example 3: Demonstrating early cancellation
Console.WriteLine("\n--- Demonstrating Early Cancellation ---\n");
Console.WriteLine("Streaming with early break (stopping after 5 objects):");

var cancelCount = 0;
await foreach (var response in fgaClient.StreamedListObjects(
new ClientListObjectsRequest {
User = "user:anne",
Relation = "can_read",
Type = "document"
})) {

Console.WriteLine($" - {response.Object}");
cancelCount++;

if (cancelCount >= 5) {
Console.WriteLine("Breaking early - stream automatically cleaned up");
break; // Stream is automatically disposed
}
}

// Example 4: Using CancellationToken
Console.WriteLine("\n--- Demonstrating CancellationToken ---\n");
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(5)); // Cancel after 5 seconds

Console.WriteLine("Streaming with 5-second timeout:");
try {
var tokenCount = 0;
await foreach (var response in fgaClient.StreamedListObjects(
new ClientListObjectsRequest {
User = "user:anne",
Relation = "can_read",
Type = "document"
},
new ClientListObjectsOptions(),
cts.Token)) {

Console.WriteLine($" - {response.Object}");
tokenCount++;
}
Console.WriteLine($"Completed streaming {tokenCount} objects");
}
catch (OperationCanceledException) {
Console.WriteLine("Streaming cancelled via CancellationToken");
}

// Clean up
Console.WriteLine("\nCleaning up...");
await fgaClient.DeleteStore();
Console.WriteLine("Deleted temporary store");
Console.WriteLine("\n✓ Example completed successfully!");
}
catch (Exception ex) {
Console.Error.WriteLine($"Error: {ex.Message}");
Console.Error.WriteLine(ex.StackTrace);
Environment.Exit(1);
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>

<!-- To target the released version, uncomment this section -->
<!-- <ItemGroup>-->
<!-- <PackageReference Include="OpenFga.Sdk" Version="0.5.1"><PrivateAssets>all</PrivateAssets></PackageReference>-->
<!-- </ItemGroup>-->

<!-- To target the local build, use project reference -->
<ItemGroup>
<ProjectReference Include="..\..\src\OpenFga.Sdk\OpenFga.Sdk.csproj" />
</ItemGroup>

</Project>

Loading
Loading