Files
rekhoff 39f01289e5 C# implementation of Transactions for Procedures (#3809)
# Description of Changes

Implements the C# equivalent of #3638

This implement uses inheritance, where abstract base classes (like
`ProcedureContextBase` in `ProcedureContext.cs`) store the core of the
implementation, and then generated wrappers (like `ProcedureContext` in
the generated FFI.cs file) inherit from them.

For error handling, we work like Rust's implementation of `Result<T,E>`
but we require `where E : Exception` because of how exceptions work in
C#. Transaction-level failures come back as a `TxOutcome` and user
errors should follow the `Result<T,E>` pattern. In this implementation,
we have `UnwrapOrThrow()` throws exceptions directly because of C#'s
error handling pattern.

Unlike the Rust implementation's direct `Result` propagation, we are
using an `AbortGuard` pattern (in `ProcedureContext.cs`) for exception
handling, which uses `IDisposable` for automatic cleanup.

Most changes should have fairly similar Rust-equivalents beyond that.
For module authors, the changes here allow for the transation logic to
work like:
```csharp
ctx.TryWithTx<ResultType, Exception>(tx => {
    // transaction logic
    return Result<ResultType, Exception>.Ok(result);
});
```
This change includes a number of tests added to the
`sdks/csharp/examples~/regression-tests/`'s `server` and `client` to
validate the behavior of the changes. `server` changes provide further
usage examples for module authors.

# API and ABI breaking changes

Should not be a breaking change

# Expected complexity level and risk

2

# Testing

- [x] Created Regression Tests that show transitions in procedures
working in various ways, all of which pass.
2025-12-18 18:41:47 +00:00

78 lines
2.0 KiB
C#

namespace SpacetimeDB.Internal;
using System;
using System.IO;
using System.Text;
using SpacetimeDB.BSATN;
/// <summary>
/// Represents a procedure that can be registered and invoked by the module runtime.
/// </summary>
public interface IProcedure
{
/// <summary>
/// Creates a procedure definition for registration with the module system.
/// </summary>
RawProcedureDefV9 MakeProcedureDef(ITypeRegistrar registrar);
/// <summary>
/// Invokes the procedure with the given arguments and context.
/// </summary>
byte[] Invoke(BinaryReader reader, IProcedureContext ctx);
}
/// <summary>
/// Represents the context for a procedure call.
/// </summary>
public interface IProcedureContext
{
/// <summary>
/// Gets the identity of the current procedure caller.
/// </summary>
/// <returns>The identity of the caller.</returns>
public static Identity GetIdentity()
{
FFI.identity(out var identity);
return identity;
}
}
/// <summary>
/// Internal interface for procedure context with additional functionality.
/// </summary>
public interface IInternalProcedureContext : IProcedureContext
{
TxContext EnterTxContext(long timestampMicros);
void ExitTxContext();
}
/// <summary>
/// Provides utility methods for procedure-related functionality.
/// </summary>
public static class ProcedureExtensions
{
/// <summary>
/// Schedules an immediate volatile, non-atomic procedure call.
/// </summary>
public static void VolatileNonatomicScheduleImmediate(string name, MemoryStream args)
{
var name_bytes = Encoding.UTF8.GetBytes(name);
var args_bytes = args.ToArray();
try
{
FFI.volatile_nonatomic_schedule_immediate(
name_bytes,
(uint)name_bytes.Length,
args_bytes,
(uint)args_bytes.Length
);
}
catch (Exception ex)
{
Log.Error($"Failed to schedule procedure {name}: {ex}");
throw;
}
}
}