Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions .openapi-generator/FILES
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ docs/RelationshipCondition.md
docs/SourceInfo.md
docs/Status.md
docs/Store.md
docs/StreamResultOfStreamedListObjectsResponse.md
docs/StreamedListObjectsResponse.md
docs/Tuple.md
docs/TupleChange.md
docs/TupleKey.md
Expand Down Expand Up @@ -157,6 +159,8 @@ src/OpenFga.Sdk/Model/RelationshipCondition.cs
src/OpenFga.Sdk/Model/SourceInfo.cs
src/OpenFga.Sdk/Model/Status.cs
src/OpenFga.Sdk/Model/Store.cs
src/OpenFga.Sdk/Model/StreamResultOfStreamedListObjectsResponse.cs
src/OpenFga.Sdk/Model/StreamedListObjectsResponse.cs
src/OpenFga.Sdk/Model/Tuple.cs
src/OpenFga.Sdk/Model/TupleChange.cs
src/OpenFga.Sdk/Model/TupleKey.cs
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

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

- feat: add support for [StreamedListObjects](https://openfga.dev/api/service#/Relationship%20Queries/StreamedListObjects). See [documentation](#streamed-list-objects)

## v0.9.0

### [0.9.0](https://github.com/openfga/dotnet-sdk/compare/v0.8.0...v0.9.0) (2025-12-01)
Expand Down
40 changes: 36 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ This is an autogenerated SDK for OpenFGA. It provides a wrapper around the [Open
- [Client Batch Check](#client-batch-check)
- [Expand](#expand)
- [List Objects](#list-objects)
- [Streamed List Objects](#streamed-list-objects)
- [List Relations](#list-relations)
- [List Users](#list-users)
- [Assertions](#assertions)
Expand Down Expand Up @@ -190,10 +191,6 @@ namespace Example {
Credentials = new Credentials() {
Method = CredentialsMethod.ClientCredentials,
Config = new CredentialsConfig() {
// API Token Issuer can contain:
// - a scheme, defaults to https
// - a path, defaults to /oauth/token
// - a port
ApiTokenIssuer = Environment.GetEnvironmentVariable("FGA_API_TOKEN_ISSUER"),
ApiAudience = Environment.GetEnvironmentVariable("FGA_API_AUDIENCE"),
ClientId = Environment.GetEnvironmentVariable("FGA_CLIENT_ID"),
Expand Down Expand Up @@ -844,6 +841,38 @@ var response = await fgaClient.ListObjects(body, options);
// response.Objects = ["document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a"]
```

##### Streamed List Objects

List objects of a particular type that the user has access to, using the streaming API.

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**: Returns all results without the 1000-object limit of the standard ListObjects API.

This is particularly useful when querying **computed relations** that may return large result sets.

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

```csharp
var options = new ClientListObjectsOptions {
AuthorizationModelId = "01GXSA8YR785C4FYS3C0RTG7B1",
Consistency = ConsistencyPreference.HIGHERCONSISTENCY
};

var objects = new List<string>();
await foreach (var response in fgaClient.StreamedListObjects(
new ClientListObjectsRequest {
User = "user:anne",
Relation = "can_read",
Type = "document"
},
options)) {
objects.Add(response.Object);
}

// objects = ["document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a"]
```

##### List Relations

List the relations a user has on an object.
Expand Down Expand Up @@ -1003,6 +1032,7 @@ namespace Example {
| [**ReadAuthorizationModel**](docs/OpenFgaApi.md#readauthorizationmodel) | **GET** /stores/{store_id}/authorization-models/{id} | Return a particular version of an authorization model |
| [**ReadAuthorizationModels**](docs/OpenFgaApi.md#readauthorizationmodels) | **GET** /stores/{store_id}/authorization-models | Return all the authorization models for a particular store |
| [**ReadChanges**](docs/OpenFgaApi.md#readchanges) | **GET** /stores/{store_id}/changes | Return a list of all the tuple changes |
| [**StreamedListObjects**](docs/OpenFgaApi.md#streamedlistobjects) | **POST** /stores/{store_id}/streamed-list-objects | Stream all objects of the given type that the user has a relation with |
| [**Write**](docs/OpenFgaApi.md#write) | **POST** /stores/{store_id}/write | Add or delete tuples from the store |
| [**WriteAssertions**](docs/OpenFgaApi.md#writeassertions) | **PUT** /stores/{store_id}/assertions/{authorization_model_id} | Upsert assertions for an authorization model ID |
| [**WriteAuthorizationModel**](docs/OpenFgaApi.md#writeauthorizationmodel) | **POST** /stores/{store_id}/authorization-models | Create a new authorization model |
Expand Down Expand Up @@ -1069,6 +1099,8 @@ namespace Example {
- [Model.SourceInfo](docs/SourceInfo.md)
- [Model.Status](docs/Status.md)
- [Model.Store](docs/Store.md)
- [Model.StreamResultOfStreamedListObjectsResponse](docs/StreamResultOfStreamedListObjectsResponse.md)
- [Model.StreamedListObjectsResponse](docs/StreamedListObjectsResponse.md)
- [Model.Tuple](docs/Tuple.md)
- [Model.TupleChange](docs/TupleChange.md)
- [Model.TupleKey](docs/TupleKey.md)
Expand Down
82 changes: 82 additions & 0 deletions docs/OpenFgaApi.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Method | HTTP request | Description
[**ReadAuthorizationModel**](OpenFgaApi.md#readauthorizationmodel) | **GET** /stores/{store_id}/authorization-models/{id} | Return a particular version of an authorization model
[**ReadAuthorizationModels**](OpenFgaApi.md#readauthorizationmodels) | **GET** /stores/{store_id}/authorization-models | Return all the authorization models for a particular store
[**ReadChanges**](OpenFgaApi.md#readchanges) | **GET** /stores/{store_id}/changes | Return a list of all the tuple changes
[**StreamedListObjects**](OpenFgaApi.md#streamedlistobjects) | **POST** /stores/{store_id}/streamed-list-objects | Stream all objects of the given type that the user has a relation with
[**Write**](OpenFgaApi.md#write) | **POST** /stores/{store_id}/write | Add or delete tuples from the store
[**WriteAssertions**](OpenFgaApi.md#writeassertions) | **PUT** /stores/{store_id}/assertions/{authorization_model_id} | Upsert assertions for an authorization model ID
[**WriteAuthorizationModel**](OpenFgaApi.md#writeauthorizationmodel) | **POST** /stores/{store_id}/authorization-models | Create a new authorization model
Expand Down Expand Up @@ -1160,6 +1161,87 @@ Name | Type | Description | Notes

[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md)

<a name="streamedlistobjects"></a>
# **StreamedListObjects**
> StreamResultOfStreamedListObjectsResponse StreamedListObjects (ListObjectsRequest body)

Stream all objects of the given type that the user has a relation with

The Streamed ListObjects API is very similar to the the ListObjects API, with two 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 specified in the flag OPENFGA_LIST_OBJECTS_DEADLINE.

### Example
```csharp
using System.Collections.Generic;
using System.Diagnostics;
using System.Net.Http;
using OpenFga.Sdk.Api;
using OpenFga.Sdk.Client;
using OpenFga.Sdk.Configuration;
using OpenFga.Sdk.Model;

namespace Example
{
public class StreamedListObjectsExample
{
public static void Main()
{
var configuration = new Configuration() {
ApiScheme = Environment.GetEnvironmentVariable("OPENFGA_API_SCHEME"), // optional, defaults to "https"
ApiHost = Environment.GetEnvironmentVariable("OPENFGA_API_HOST"), // required, define without the scheme (e.g. api.fga.example instead of https://api.fga.example)
StoreId = Environment.GetEnvironmentVariable("OPENFGA_STORE_ID"), // not needed when calling `CreateStore` or `ListStores`
};
HttpClient httpClient = new HttpClient();
var openFgaApi = new OpenFgaApi(config, httpClient);
var body = new ListObjectsRequest(); // ListObjectsRequest |

try
{
// Stream all objects of the given type that the user has a relation with
StreamResultOfStreamedListObjectsResponse response = await openFgaApi.StreamedListObjects(body);
Debug.WriteLine(response);
}
catch (ApiException e)
{
Debug.Print("Exception when calling OpenFgaApi.StreamedListObjects: " + e.Message );
Debug.Print("Status Code: "+ e.ErrorCode);
Debug.Print(e.StackTrace);
}
}
}
}
```

### Parameters

Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------

**body** | [**ListObjectsRequest**](ListObjectsRequest.md)| |

### Return type

[**StreamResultOfStreamedListObjectsResponse**](StreamResultOfStreamedListObjectsResponse.md)

### HTTP request headers

- **Content-Type**: application/json
- **Accept**: application/json


### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
| **200** | A successful response.(streaming responses) | - |
| **400** | Request failed due to invalid input. | - |
| **401** | Not authenticated. | - |
| **403** | Forbidden. | - |
| **404** | Request failed due to incorrect path. | - |
| **409** | Request was aborted due a transaction conflict. | - |
| **422** | Request timed out due to excessive request throttling. | - |
| **500** | Request failed due to internal server error. | - |

[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md)

<a name="write"></a>
# **Write**
> Object Write (WriteRequest body)
Expand Down
11 changes: 11 additions & 0 deletions docs/StreamResultOfStreamedListObjectsResponse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# OpenFga.Sdk.Model.StreamResultOfStreamedListObjectsResponse

## Properties

Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**Result** | [**StreamedListObjectsResponse**](StreamedListObjectsResponse.md) | | [optional]
**Error** | [**Status**](Status.md) | | [optional]

[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md)

11 changes: 11 additions & 0 deletions docs/StreamedListObjectsResponse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# OpenFga.Sdk.Model.StreamedListObjectsResponse
The response for a StreamedListObjects RPC.

## Properties

Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**Object** | **string** | |

[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md)

119 changes: 119 additions & 0 deletions example/StreamedListObjectsExample/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# 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**: Returns all results without the 1000-object limit of the standard ListObjects API.

This makes it ideal for scenarios where you need to retrieve large numbers of objects, especially when querying computed relations.

## 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

```bash
# From the SDK root directory, build the SDK first
dotnet build src/OpenFga.Sdk/OpenFga.Sdk.csproj

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

## What it does

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

## Authorization Model

The example demonstrates OpenFGA's **computed relations**:

```
type user

type document
relations
define owner: [user]
define viewer: [user]
define can_read: owner or viewer
```

**Why this matters:**
- We write tuples to `owner` and `viewer` (base permissions)
- We query `can_read` (computed from owner OR viewer)

**Example flow:**
1. Write: `user:anne owner document:1-1000`
2. Write: `user:anne viewer document:1001-2000`
3. Query: `StreamedListObjects(user:anne, relation:can_read, type:document)`
4. Result: All 2000 documents (because `can_read = owner OR viewer`)

## 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.)
Loading
Loading