Files
SpacetimeDB/sdks/csharp/src/ProcedureCallbacks.cs
Jason Larabie 78c28d4936 Add C# client SDK procedures (#3666)
# Description of Changes

Closes: #3533 
Updated the C# SDK to handle procedures and procedure callbacks in a
similar fashion to the Rust client as well as added the codegen to
support it.

# API and ABI breaking changes

N/A

# Expected complexity level and risk

2 - This adds a new testing frame that should be removed once procedures
are handled with C# module bindings

# Testing

Added /sdks/csharp/examples~/regression-tests/procedure-client to match
modules/sdk-test-procedure which we can roll into the standard
regression-tests once C# supports the procedure attribute.

- [x] Add C# client test of sdk-test-procedure

---------

Signed-off-by: Jason Larabie <jason@clockworklabs.io>
2025-11-21 19:39:21 +00:00

79 lines
2.7 KiB
C#

using System;
using System.Collections.Generic;
using SpacetimeDB.BSATN;
using SpacetimeDB.ClientApi;
namespace SpacetimeDB
{
public delegate void ProcedureCallback<T>(IProcedureEventContext ctx, ProcedureCallbackResult<T> result);
public readonly struct ProcedureCallbackResult<T>
{
public readonly bool IsSuccess;
public readonly T? Value;
public readonly Exception? Error;
public static ProcedureCallbackResult<T> Success(T value) => new(true, value, null);
public static ProcedureCallbackResult<T> Failure(Exception error) => new(false, default, error);
private ProcedureCallbackResult(bool isSuccess, T? value, Exception? error)
{
IsSuccess = isSuccess;
Value = value;
Error = error;
}
}
internal sealed class ProcedureCallbacks
{
private readonly Dictionary<uint, IProcedureCallbackWrapper> callbacks = new();
public void RegisterCallback<T>(uint requestId, ProcedureCallback<T> callback)
where T : IStructuralReadWrite, new()
{
callbacks[requestId] = new ProcedureCallbackWrapper<T>(callback);
}
public bool TryResolveCallback(IProcedureEventContext ctx, uint requestId, ProcedureResult result)
{
if (callbacks.Remove(requestId, out var wrapper))
{
wrapper.Invoke(ctx, result);
return true;
}
return false;
}
}
internal interface IProcedureCallbackWrapper
{
void Invoke(IProcedureEventContext ctx, ProcedureResult result);
}
internal sealed class ProcedureCallbackWrapper<T> : IProcedureCallbackWrapper
where T : IStructuralReadWrite, new()
{
private readonly ProcedureCallback<T> callback;
public ProcedureCallbackWrapper(ProcedureCallback<T> callback)
{
this.callback = callback;
}
public void Invoke(IProcedureEventContext ctx, ProcedureResult result)
{
var callbackResult = result.Status switch
{
ProcedureStatus.Returned(var bytes) =>
ProcedureCallbackResult<T>.Success(BSATNHelpers.Decode<T>(bytes.ToArray())),
ProcedureStatus.InternalError(var error) =>
ProcedureCallbackResult<T>.Failure(new Exception($"Procedure failed: {error}")),
ProcedureStatus.OutOfEnergy =>
ProcedureCallbackResult<T>.Failure(new Exception("Procedure execution aborted due to insufficient energy")),
_ => ProcedureCallbackResult<T>.Failure(new Exception("Unknown procedure status"))
};
callback(ctx, callbackResult);
}
}
}