Coroutines and Flow
Kotlin coroutines map onto .NET's own async model. suspend fun becomes async/Task<T>, cancellation maps to CancellationToken, and disposing the owning object cancels any coroutine it started. Flow<T> becomes IAsyncEnumerable<T>, and StateFlow<T> adds a synchronously readable .Value.
Kotlin | C# |
|---|---|
|
|
|
|
|
|
|
|
|
|
coroutine cancellation |
|
structured concurrency |
|
suspend fun
From AsyncCatService.kt:
Every suspend fun becomes an async Task<T> method suffixed Async. Overloads on the same class resolve as an ordinary C# overload set. A top-level suspend fun (not a class method) cannot be overloaded: a second overload with the same name collides on one native symbol. Move it onto a class or object, or give it a distinct name.
suspend fun returning a nullable type
A nullable primitive, String?, or object return carries its ? all the way through:
null stays distinct from 0 or an empty string, the same as a nullable synchronous return.
suspend fun returning a collection
List<T>, Set<T>, and Map<K, V> returns are spelled the same way a property of that type is. Any other generic return (Pair<A, B>, Result<T>, Flow<T>, a nullable collection List<T>?) has no C# binding and is skipped with a diagnostic naming the member; expose the values through separate suspend functions instead.
suspend fun returning an interface
The result is typed with the interface itself, the same as a synchronous interface return, and resolves back to the caller's own instance if a C# type implemented that interface. See Interfaces, abstract classes and sealed classes.
suspend fun returning StateFlow<T>
A suspend fun can suspend before handing back a StateFlow<T>, for example to build it lazily. The outer suspend stays a Task; once awaited, .Value and await foreach behave exactly like an ordinary StateFlow<T> (below).
Only a class method is supported; a top-level function returning StateFlow<T> has no binding.
StateFlow<T> element type is an interface
The element can itself be an interface. It is spelled and read through the interface, the same as any other interface element:
A C#-implemented keeper booked earlier and read back through .Value is the same instance the caller passed in.
suspend () -> R lambdas
InvokeAsync optionally accepts a CancellationToken; cancelling it throws TaskCanceledException from the awaited call.
Cancellation and disposal
Disposing the owning object cancels any coroutine it started, including children launched with coroutineScope { launch { ... } }:
Every generated async method also accepts an explicit CancellationToken, independent of Dispose(); cancelling it fails only that call, not sibling calls on the same object:
DisposeAsync() drains instead of cancelling: it waits for in-flight coroutines to finish naturally before releasing the handle.
A class that both implements an exported interface and has suspend/Flow members still gets Dispose()/DisposeAsync() on its own base list, alongside the interface:
Hold it as IAsyncDisposable through a field, a cast, or await using, and it resolves correctly.
Flow<T>
Flow<T> becomes KotlinFlow<T> : IAsyncEnumerable<T>. It is cold: each await foreach re-runs the Kotlin flow from the start. WithCancellation stops the enumeration early.
An element type that is an interface, or a List<T>/Set<T>/Map<K, V>, is spelled and read exactly like the same type at a property or suspend return, described above.
StateFlow<T>
StateFlow<T> becomes KotlinStateFlow<T> : KotlinFlow<T>: hot, always has a current .Value, and replays that value as the first element of any new await foreach. It never completes on its own, so bound the enumeration with a CancellationToken or a break:
KotlinStateFlow<T> upcasts to KotlinFlow<T> and IAsyncEnumerable<T>, mirroring Kotlin's own StateFlow : Flow. T can be a sealed base class; .Value and await foreach both materialize the correct generated subclass, see Interfaces, abstract classes and sealed classes.
Settable .Value on MutableStateFlow<T>
A member whose declared type is MutableStateFlow<T>, not narrowed to StateFlow<T> (the common private val _x = MutableStateFlow(...)/val x: StateFlow<T> = _x.asStateFlow() idiom stays get-only), surfaces as KotlinMutableStateFlow<T> : KotlinStateFlow<T> with a settable .Value:
The write lands in Kotlin synchronously and is visible to any live collector as its next (conflated) emission. Writing is safe from any thread. Kotlin's MutableStateFlow.value setter calls equals on the previous value to decide whether to conflate; if that equals throws, the throw propagates out of the C# write as KotlinInvalidOperationException, unlike the get-only .Value read, which never throws.
Reassigning the whole MutableStateFlow<T> member itself (a var holding a different flow instance) is not supported; only writes through .Value are.
Nullable StateFlow<T?> and StateFlow<T>?
A StateFlow can be nullable in the element (StateFlow<T?>) or the member itself (StateFlow<T>?), independently, and the two compose:
A null element crossing await foreach is a genuine emission, not the end of the stream. Writing a nullable element or a nullable member, and a suspend fun returning StateFlow<T?> or StateFlow<T>?, are not supported.
Parameters on Flow, StateFlow, and suspend members
A List/Set/Map, primitive, String, or class/object/sealed-type parameter on a Flow-, StateFlow-, or suspend-returning member crosses the same way it does anywhere else, spelled exactly as the same member's return position spells it:
Any other generic parameter (Pair<A, B>, Array<T>, a lambda), an enum, Instant/Duration/Uuid, a value class, an interface, or a nullable object parameter is not supported at these positions and is skipped with a diagnostic naming the member. Pass a class/object/sealed handle, a List/Set/Map, or a primitive/String instead, or split the parameter across separate members.
Limitations
SharedFlow<T>(hot, multi-subscriber) is not supported.StateFlow<SomeEnum>/MutableStateFlow<SomeEnum>:.Valuehas no enum reader.CompareAndSet,Update,Emit,TryEmit,ReplayCache, andSubscriptionCountonMutableStateFlow<T>are not exposed.A nullable-element or nullable-member
MutableStateFlowwrite, and asuspend funreturningMutableStateFlow<T>, are not supported.A top-level
suspend funreturningStateFlow<T>(class methods only) orFlow<T>(no binding at all) is not supported.StateFlow<T>orFlow<T>as a function parameter, or as a generic type argument, is not supported.A nullable
Flow<T>?, and aPair, a nullable collection (List<T>?), or a collection of a sealed base as aFlow/StateFlowelement, are not supported.Boolean?/Char?value elements on a nullableStateFloware not supported.A
suspend inline fun <reified T> Receiver.f(...): Result<T>extension has no bridge at all:inlineplusreifiederase at the native boundary, andsuspendneeds a concrete continuation type. It is skipped with a diagnostic naming the extension.
Flow, StateFlow, and suspend dispatch are AOT- and trim-safe; see Publishing Kotlin to C#: AOT and trimming.