Skip to content
Merged
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
11 changes: 10 additions & 1 deletion config/clients/dotnet/CHANGELOG.md.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,26 @@
- per-request headers support via `Headers` property on all client options classes
- `IRequestOptions` interface and `RequestOptions` class for API-level header support
- `IClientRequestOptions` interface and `ClientRequestOptions` class for client-level header support
- add header validation to prevent overiding of reserved headers
- add header validation to prevent overriding of reserved headers
- feat: add write conflict resolution options
- `ConflictOptions` to control behavior for duplicate writes and missing deletes
- `OnDuplicateWrites` option: `Error` (default) or `Ignore` for handling duplicate tuple writes
- `OnMissingDeletes` option: `Error` (default) or `Ignore` for handling missing tuple deletes
- Available in `ClientWriteOptions.Conflict` property

[!WARNING]
BREAKING CHANGES:

- **OpenFgaApi methods**: All API methods now accept an `IRequestOptions? options` parameter. If you are using the low-level `OpenFgaApi` directly, you may need to update your calls:

Before:

```csharp
await api.Check(storeId, body, cancellationToken);
```

After:

```csharp
var options = new RequestOptions {
Headers = new Dictionary<string, string> { { "X-Custom-Header", "value" } }
Expand All @@ -30,11 +37,13 @@ BREAKING CHANGES:
- **ClientRequestOptions renamed**: The base client request options interface has been renamed from `ClientRequestOptions` to `IClientRequestOptions` to better follow .NET naming conventions. A concrete `ClientRequestOptions` class is now also available. If you were casting to or implementing this interface, update your code:

Before:

```csharp
var options = obj as ClientRequestOptions;
```

After:

```csharp
var options = obj as IClientRequestOptions;
```
Expand Down
31 changes: 28 additions & 3 deletions config/clients/dotnet/template/Client/Client.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,20 @@ public class {{appShortName}}Client : IDisposable {
return authorizationModelId;
}

private static WriteRequestWrites.OnDuplicateEnum MapOnDuplicateWrites(OnDuplicateWrites? behavior) {
if (behavior == null) return WriteRequestWrites.OnDuplicateEnum.Error;
return behavior.Value == OnDuplicateWrites.Error
? WriteRequestWrites.OnDuplicateEnum.Error
: WriteRequestWrites.OnDuplicateEnum.Ignore;
}

private static WriteRequestDeletes.OnMissingEnum MapOnMissingDeletes(OnMissingDeletes? behavior) {
if (behavior == null) return WriteRequestDeletes.OnMissingEnum.Error;
return behavior.Value == OnMissingDeletes.Error
? WriteRequestDeletes.OnMissingEnum.Error
: WriteRequestDeletes.OnMissingEnum.Ignore;
}

/**********
* Stores *
**********/
Expand Down Expand Up @@ -341,10 +355,16 @@ public class {{appShortName}}Client : IDisposable {
AuthorizationModelId = authorizationModelId
};
if (body.Writes?.Count > 0) {
requestBody.Writes = new WriteRequestWrites(body.Writes.ConvertAll(key => key.ToTupleKey()));
requestBody.Writes = new WriteRequestWrites(
body.Writes.ConvertAll(key => key.ToTupleKey()),
MapOnDuplicateWrites(options?.Conflict?.OnDuplicateWrites)
);
}
if (body.Deletes?.Count > 0) {
requestBody.Deletes = new WriteRequestDeletes(body.Deletes.ConvertAll(key => key.ToTupleKeyWithoutCondition()));
requestBody.Deletes = new WriteRequestDeletes(
body.Deletes.ConvertAll(key => key.ToTupleKeyWithoutCondition()),
MapOnMissingDeletes(options?.Conflict?.OnMissingDeletes)
);
}

await api.Write(GetStoreId(options), requestBody, options, cancellationToken);
Expand All @@ -360,7 +380,12 @@ public class {{appShortName}}Client : IDisposable {
};
}

var clientWriteOpts = new ClientWriteOptions() { StoreId = StoreId, AuthorizationModelId = authorizationModelId, Headers = options?.Headers };
var clientWriteOpts = new ClientWriteOptions() {
StoreId = GetStoreId(options),
AuthorizationModelId = authorizationModelId,
Headers = options?.Headers,
Conflict = options?.Conflict
};

var writeChunks = body.Writes?.Chunk(maxPerChunk).ToList() ?? new List<ClientTupleKey[]>();
var writeResponses = new ConcurrentBag<ClientWriteSingleResponse>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,67 @@
{{>partial_header}}

using {{packageName}}.Model;
using System.Collections.Generic;

namespace {{packageName}}.Client.Model;

/// <summary>
/// Behavior for handling duplicate tuple writes
/// </summary>
public enum OnDuplicateWrites {
/// <summary>
/// Return an error when attempting to write a tuple that already exists (default)
/// </summary>
Error = 1,

/// <summary>
/// Silently ignore duplicate tuple writes (no-op)
/// </summary>
Ignore = 2
}

/// <summary>
/// Behavior for handling missing tuple deletes
/// </summary>
public enum OnMissingDeletes {
/// <summary>
/// Return an error when attempting to delete a tuple that doesn't exist (default)
/// </summary>
Error = 1,

/// <summary>
/// Silently ignore missing tuple deletes (no-op)
/// </summary>
Ignore = 2
}

/// <summary>
/// ConflictOptions - Controls behavior for duplicate writes and missing deletes
/// </summary>
public interface IConflictOptions {
/// <summary>
/// Controls behavior when writing a tuple that already exists
/// </summary>
OnDuplicateWrites? OnDuplicateWrites { get; set; }

/// <summary>
/// Controls behavior when deleting a tuple that doesn't exist
/// </summary>
OnMissingDeletes? OnMissingDeletes { get; set; }
}

public class ConflictOptions : IConflictOptions {
/// <summary>
/// Controls behavior when writing a tuple that already exists
/// </summary>
public OnDuplicateWrites? OnDuplicateWrites { get; set; }

/// <summary>
/// Controls behavior when deleting a tuple that doesn't exist
/// </summary>
public OnMissingDeletes? OnMissingDeletes { get; set; }
}

/// <summary>
/// TransactionOpts
/// </summary>
Expand Down Expand Up @@ -43,6 +101,7 @@ public class TransactionOptions : ITransactionOpts {

public interface IClientWriteOptions : IClientRequestOptionsWithAuthZModelId {
ITransactionOpts Transaction { get; set; }
IConflictOptions? Conflict { get; set; }
}

public class ClientWriteOptions : IClientWriteOptions {
Expand All @@ -57,6 +116,9 @@ public class ClientWriteOptions : IClientWriteOptions {
/// <inheritdoc />
public ITransactionOpts Transaction { get; set; }

/// <inheritdoc />
public IConflictOptions? Conflict { get; set; }

/// <inheritdoc />
public IDictionary<string, string>? Headers { get; set; }
}
Loading
Loading