Bug Description
Two bugs in the C# streaming path. Both are the parity of microsoft/teams.ts#549, fixed in microsoft/teams.ts#553, but the C# version of Bug 1 manifests via a different mechanism (??=) than the TS version.
Bug 1: AddStreamFinal() cannot override an existing ChannelData.StreamType
Location: Libraries/Microsoft.Teams.Api/Activities/Message/MessageActivity.cs:208-221
public MessageActivity AddStreamFinal()
{
ChannelData ??= new();
ChannelData.StreamId ??= Id;
ChannelData.StreamType ??= StreamType.Final; // ← only sets if null
AddEntity(new StreamInfoEntity()
{
StreamId = Id,
StreamType = StreamType.Final
});
return this;
}
The method's contract — judged by name — is to mark this activity as a final stream message. But ??= makes it impossible to override an existing StreamType. In the streaming path:
Stream.Update("Thinking...") (Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/AspNetCorePlugin.Stream.cs:75) emits a typing activity with ChannelData.StreamType = Informative.
Flush() accumulates that into _channelData via _channelData.Merge(...) (line 152).
Close() builds the final activity with WithData(_channelData) (line 104), populating activity.ChannelData.StreamType = Informative.
AddStreamFinal() (line 106) runs ??= against StreamType.Informative (non-null) — no-op. The final wire activity has channelData.streamType = "informative" even though the streaminfo entity correctly has streamType = "final".
This is the parity of TS Bug 1 — Teams receives an inconsistent final stream state.
Fix
Change ??= to direct assignment in AddStreamFinal():
ChannelData.StreamId = Id;
ChannelData.StreamType = StreamType.Final;
This is a minor public-API behavior change (callers who pre-set StreamType would now have it overridden by AddStreamFinal), but it aligns the method with its name and contract.
Bug 2: Race condition in Stream.Close()
Location: Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/AspNetCorePlugin.Stream.cs:90-93
while (_id is null || _queue.Count > 0)
{
await Task.Delay(50, cancellationToken);
}
This wait checks _id and _queue but not whether Flush() is currently holding _lock (a SemaphoreSlim). Flush() acquires the lock at line 127 and drains the queue before await SendActivity(...) (lines 173, 181). During those awaits:
_queue.Count == 0 (drained)
_id is set (assigned on line 202 after first send)
_lock is still held
Close() exits the wait early and sends the final activity, racing the in-flight chunk.
Fix
Add _lock.CurrentCount == 0 to the loop condition:
while (_id is null || _queue.Count > 0 || _lock.CurrentCount == 0)
The _lock.CurrentCount primitive is already used in this file (line 88, the early-return check).
I have fixes + unit tests for both bugs ready to submit as a PR.
Bug Description
Two bugs in the C# streaming path. Both are the parity of microsoft/teams.ts#549, fixed in microsoft/teams.ts#553, but the C# version of Bug 1 manifests via a different mechanism (
??=) than the TS version.Bug 1:
AddStreamFinal()cannot override an existingChannelData.StreamTypeLocation:
Libraries/Microsoft.Teams.Api/Activities/Message/MessageActivity.cs:208-221The method's contract — judged by name — is to mark this activity as a final stream message. But
??=makes it impossible to override an existingStreamType. In the streaming path:Stream.Update("Thinking...")(Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/AspNetCorePlugin.Stream.cs:75) emits a typing activity withChannelData.StreamType = Informative.Flush()accumulates that into_channelDatavia_channelData.Merge(...)(line 152).Close()builds the final activity withWithData(_channelData)(line 104), populatingactivity.ChannelData.StreamType = Informative.AddStreamFinal()(line 106) runs??=againstStreamType.Informative(non-null) — no-op. The final wire activity haschannelData.streamType = "informative"even though the streaminfo entity correctly hasstreamType = "final".This is the parity of TS Bug 1 — Teams receives an inconsistent final stream state.
Fix
Change
??=to direct assignment inAddStreamFinal():This is a minor public-API behavior change (callers who pre-set StreamType would now have it overridden by
AddStreamFinal), but it aligns the method with its name and contract.Bug 2: Race condition in
Stream.Close()Location:
Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/AspNetCorePlugin.Stream.cs:90-93This wait checks
_idand_queuebut not whetherFlush()is currently holding_lock(aSemaphoreSlim).Flush()acquires the lock at line 127 and drains the queue beforeawait SendActivity(...)(lines 173, 181). During those awaits:_queue.Count == 0(drained)_idis set (assigned on line 202 after first send)_lockis still heldClose()exits the wait early and sends the final activity, racing the in-flight chunk.Fix
Add
_lock.CurrentCount == 0to the loop condition:The
_lock.CurrentCountprimitive is already used in this file (line 88, the early-return check).I have fixes + unit tests for both bugs ready to submit as a PR.