Skip to content

Commit 95199ec

Browse files
authored
feat(dotnet-sdk): add write conflict resolution options (#644)
2 parents c4dc587 + 200396c commit 95199ec

6 files changed

Lines changed: 649 additions & 10 deletions

File tree

config/clients/dotnet/CHANGELOG.md.mustache

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,26 @@
77
- per-request headers support via `Headers` property on all client options classes
88
- `IRequestOptions` interface and `RequestOptions` class for API-level header support
99
- `IClientRequestOptions` interface and `ClientRequestOptions` class for client-level header support
10-
- add header validation to prevent overiding of reserved headers
10+
- add header validation to prevent overriding of reserved headers
11+
- feat: add write conflict resolution options
12+
- `ConflictOptions` to control behavior for duplicate writes and missing deletes
13+
- `OnDuplicateWrites` option: `Error` (default) or `Ignore` for handling duplicate tuple writes
14+
- `OnMissingDeletes` option: `Error` (default) or `Ignore` for handling missing tuple deletes
15+
- Available in `ClientWriteOptions.Conflict` property
1116

1217
[!WARNING]
1318
BREAKING CHANGES:
1419

1520
- **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:
1621

1722
Before:
23+
1824
```csharp
1925
await api.Check(storeId, body, cancellationToken);
2026
```
2127

2228
After:
29+
2330
```csharp
2431
var options = new RequestOptions {
2532
Headers = new Dictionary<string, string> { { "X-Custom-Header", "value" } }
@@ -30,11 +37,13 @@ BREAKING CHANGES:
3037
- **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:
3138

3239
Before:
40+
3341
```csharp
3442
var options = obj as ClientRequestOptions;
3543
```
3644

3745
After:
46+
3847
```csharp
3948
var options = obj as IClientRequestOptions;
4049
```

config/clients/dotnet/template/Client/Client.mustache

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,20 @@ public class {{appShortName}}Client : IDisposable {
212212
return authorizationModelId;
213213
}
214214

215+
private static WriteRequestWrites.OnDuplicateEnum MapOnDuplicateWrites(OnDuplicateWrites? behavior) {
216+
if (behavior == null) return WriteRequestWrites.OnDuplicateEnum.Error;
217+
return behavior.Value == OnDuplicateWrites.Error
218+
? WriteRequestWrites.OnDuplicateEnum.Error
219+
: WriteRequestWrites.OnDuplicateEnum.Ignore;
220+
}
221+
222+
private static WriteRequestDeletes.OnMissingEnum MapOnMissingDeletes(OnMissingDeletes? behavior) {
223+
if (behavior == null) return WriteRequestDeletes.OnMissingEnum.Error;
224+
return behavior.Value == OnMissingDeletes.Error
225+
? WriteRequestDeletes.OnMissingEnum.Error
226+
: WriteRequestDeletes.OnMissingEnum.Ignore;
227+
}
228+
215229
/**********
216230
* Stores *
217231
**********/
@@ -341,10 +355,16 @@ public class {{appShortName}}Client : IDisposable {
341355
AuthorizationModelId = authorizationModelId
342356
};
343357
if (body.Writes?.Count > 0) {
344-
requestBody.Writes = new WriteRequestWrites(body.Writes.ConvertAll(key => key.ToTupleKey()));
358+
requestBody.Writes = new WriteRequestWrites(
359+
body.Writes.ConvertAll(key => key.ToTupleKey()),
360+
MapOnDuplicateWrites(options?.Conflict?.OnDuplicateWrites)
361+
);
345362
}
346363
if (body.Deletes?.Count > 0) {
347-
requestBody.Deletes = new WriteRequestDeletes(body.Deletes.ConvertAll(key => key.ToTupleKeyWithoutCondition()));
364+
requestBody.Deletes = new WriteRequestDeletes(
365+
body.Deletes.ConvertAll(key => key.ToTupleKeyWithoutCondition()),
366+
MapOnMissingDeletes(options?.Conflict?.OnMissingDeletes)
367+
);
348368
}
349369

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

363-
var clientWriteOpts = new ClientWriteOptions() { StoreId = StoreId, AuthorizationModelId = authorizationModelId, Headers = options?.Headers };
383+
var clientWriteOpts = new ClientWriteOptions() {
384+
StoreId = GetStoreId(options),
385+
AuthorizationModelId = authorizationModelId,
386+
Headers = options?.Headers,
387+
Conflict = options?.Conflict
388+
};
364389

365390
var writeChunks = body.Writes?.Chunk(maxPerChunk).ToList() ?? new List<ClientTupleKey[]>();
366391
var writeResponses = new ConcurrentBag<ClientWriteSingleResponse>();

config/clients/dotnet/template/Client/Model/ClientWriteOptions.mustache

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,67 @@
11
{{>partial_header}}
22

3+
using {{packageName}}.Model;
34
using System.Collections.Generic;
45

56
namespace {{packageName}}.Client.Model;
67

8+
/// <summary>
9+
/// Behavior for handling duplicate tuple writes
10+
/// </summary>
11+
public enum OnDuplicateWrites {
12+
/// <summary>
13+
/// Return an error when attempting to write a tuple that already exists (default)
14+
/// </summary>
15+
Error = 1,
16+
17+
/// <summary>
18+
/// Silently ignore duplicate tuple writes (no-op)
19+
/// </summary>
20+
Ignore = 2
21+
}
22+
23+
/// <summary>
24+
/// Behavior for handling missing tuple deletes
25+
/// </summary>
26+
public enum OnMissingDeletes {
27+
/// <summary>
28+
/// Return an error when attempting to delete a tuple that doesn't exist (default)
29+
/// </summary>
30+
Error = 1,
31+
32+
/// <summary>
33+
/// Silently ignore missing tuple deletes (no-op)
34+
/// </summary>
35+
Ignore = 2
36+
}
37+
38+
/// <summary>
39+
/// ConflictOptions - Controls behavior for duplicate writes and missing deletes
40+
/// </summary>
41+
public interface IConflictOptions {
42+
/// <summary>
43+
/// Controls behavior when writing a tuple that already exists
44+
/// </summary>
45+
OnDuplicateWrites? OnDuplicateWrites { get; set; }
46+
47+
/// <summary>
48+
/// Controls behavior when deleting a tuple that doesn't exist
49+
/// </summary>
50+
OnMissingDeletes? OnMissingDeletes { get; set; }
51+
}
52+
53+
public class ConflictOptions : IConflictOptions {
54+
/// <summary>
55+
/// Controls behavior when writing a tuple that already exists
56+
/// </summary>
57+
public OnDuplicateWrites? OnDuplicateWrites { get; set; }
58+
59+
/// <summary>
60+
/// Controls behavior when deleting a tuple that doesn't exist
61+
/// </summary>
62+
public OnMissingDeletes? OnMissingDeletes { get; set; }
63+
}
64+
765
/// <summary>
866
/// TransactionOpts
967
/// </summary>
@@ -43,6 +101,7 @@ public class TransactionOptions : ITransactionOpts {
43101

44102
public interface IClientWriteOptions : IClientRequestOptionsWithAuthZModelId {
45103
ITransactionOpts Transaction { get; set; }
104+
IConflictOptions? Conflict { get; set; }
46105
}
47106

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

119+
/// <inheritdoc />
120+
public IConflictOptions? Conflict { get; set; }
121+
60122
/// <inheritdoc />
61123
public IDictionary<string, string>? Headers { get; set; }
62124
}

0 commit comments

Comments
 (0)