Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Jan 17, 2026

Summary: Fix NullReferenceException in Entity Event Conversion

Problem

When orchestration history contains entity events (such as EntityOperationCalled, EntityLockRequested, EntityUnlockSent, etc.), the streaming or get history APIs throw a NullReferenceException.

Root Cause

  • GetOrchestrationHistoryAsync in GrpcDurableTaskClient.cs and LoadAndRun in GrpcOrchestrationRunner.cs called ProtoUtils.ConvertHistoryEvent without passing an EntityConversionState
  • In ProtoUtils.cs, entity event handlers use conversionState!.CurrentInstance with the null-forgiving operator, causing runtime exceptions when conversionState is null

Solution

Updated both methods to always create and use EntityConversionState when converting history events:

  1. GrpcDurableTaskClient.GetOrchestrationHistoryAsync

    • Always creates EntityConversionState (not conditional on EnableEntitySupport)
    • Uses the converter function with proper state handling
  2. GrpcOrchestrationRunner.LoadAndRun

    • Always creates an EntityConversionState for entity event conversion
    • Uses the converter with state for both past and new events

Implementation Details

  • EntityConversionState is always created because history may contain entity events regardless of client options
  • The orchestration instance ID is automatically set when the ExecutionStarted event is processed during conversion
  • Follows the pattern from GrpcDurableTaskWorker.Processor.BuildRuntimeStateAsync
  • Uses insertMissingEntityUnlocks: false for read-only operations

Testing

  • ✅ Built both modified projects successfully
  • ✅ All existing unit tests pass for GrpcDurableTaskClient
  • ✅ All existing unit tests pass for GrpcOrchestrationRunner
  • ✅ Addressed reviewer feedback

Files Changed

  • src/Client/Grpc/GrpcDurableTaskClient.cs
  • src/Worker/Grpc/GrpcOrchestrationRunner.cs
Original prompt

Problem

When orchestration history contains entity events (such as EntityOperationCalled, EntityLockRequested, EntityUnlockSent, etc.), the streaming or get history APIs will throw a NullReferenceException.

This happens because:

  1. The GetOrchestrationHistoryAsync method in src/Client/Grpc/GrpcDurableTaskClient.cs calls ProtoUtils.ConvertHistoryEvent without passing an EntityConversionState:

    pastEvents.AddRange(streamResponse.ResponseStream.Current.Events.Select(DurableTask.ProtoUtils.ConvertHistoryEvent));
  2. The LoadAndRun method in src/Worker/Grpc/GrpcOrchestrationRunner.cs also calls ConvertHistoryEvent without entity conversion state:

    List<HistoryEvent> pastEvents = request.PastEvents.Select(ProtoUtils.ConvertHistoryEvent).ToList();
    IEnumerable<HistoryEvent> newEvents = request.NewEvents.Select(ProtoUtils.ConvertHistoryEvent);
  3. In src/Shared/Grpc/ProtoUtils.cs, when entity events are encountered, the code uses conversionState!.CurrentInstance with the null-forgiving operator, which will throw NullReferenceException at runtime since conversionState is null:

    case P.HistoryEvent.EventTypeOneofCase.EntityOperationCalled:
        historyEvent = EntityConversions.EncodeOperationCalled(proto, conversionState!.CurrentInstance);
        // ...
    case P.HistoryEvent.EventTypeOneofCase.EntityLockRequested:
        historyEvent = EntityConversions.EncodeLockRequested(proto, conversionState!.CurrentInstance);
        // ...
    case P.HistoryEvent.EventTypeOneofCase.EntityUnlockSent:
        historyEvent = EntityConversions.EncodeUnlockSent(proto, conversionState!.CurrentInstance);
        // ...

Solution

Ensure EntityConversionState is always provided when converting history events that may contain entity events. Update the following locations:

  1. src/Client/Grpc/GrpcDurableTaskClient.cs - In GetOrchestrationHistoryAsync, create an EntityConversionState and use the overload of ConvertHistoryEvent that accepts it.

  2. src/Worker/Grpc/GrpcOrchestrationRunner.cs - In LoadAndRun, create an EntityConversionState and use it when converting past events and new events.

The EntityConversionState needs to be initialized properly. Look at how GrpcDurableTaskWorker.Processor.cs does it in the BuildRuntimeStateAsync method for reference - it creates an EntityConversionState when entity support is enabled and uses it with the converter:

Func<P.HistoryEvent, HistoryEvent> converter = entityConversionState is null
    ? ProtoUtils.ConvertHistoryEvent
    : entityConversionState.ConvertFromProto;

Note: The EntityConversionState class has a ConvertFromProto method that wraps ProtoUtils.ConvertHistoryEvent with the proper state.

Files to modify

  • src/Client/Grpc/GrpcDurableTaskClient.cs - Update GetOrchestrationHistoryAsync method
  • src/Worker/Grpc/GrpcOrchestrationRunner.cs - Update LoadAndRun method

Additional context

  • The EntityConversionState class is in src/Shared/Grpc/ProtoUtils.cs
  • Reference implementation is in src/Worker/Grpc/GrpcDurableTaskWorker.Processor.cs in the BuildRuntimeStateAsync method
  • Entity support is controlled by options like this.options.EnableEntitySupport

This pull request was created from Copilot chat.


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI changed the title [WIP] Fix null reference exception in orchestration history APIs Fix NullReferenceException when history contains entity events Jan 17, 2026
Copilot AI requested a review from YunchuWang January 17, 2026 01:18
this.sidecarClient.StreamInstanceHistory(streamRequest, cancellationToken: cancellation);

Microsoft.DurableTask.ProtoUtils.EntityConversionState? entityConversionState =
this.options.EnableEntitySupport
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this has nothing to do with enableentitysupport, remove it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 99ee0e7. EntityConversionState is now always created regardless of EnableEntitySupport since history may contain entity events even if the client option is disabled.


Microsoft.DurableTask.ProtoUtils.EntityConversionState? entityConversionState =
this.options.EnableEntitySupport
? new(insertMissingEntityUnlocks: false)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

without orchestration instanceid, it will throw missing parent instance id exception later i think

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The orchestration instance ID is automatically set when the ExecutionStarted event is processed during history conversion (see ProtoUtils.cs line 62: conversionState?.SetOrchestrationInstance(instance)). No manual setting is needed.


List<HistoryEvent> pastEvents = request.PastEvents.Select(ProtoUtils.ConvertHistoryEvent).ToList();
IEnumerable<HistoryEvent> newEvents = request.NewEvents.Select(ProtoUtils.ConvertHistoryEvent);
ProtoUtils.EntityConversionState entityConversionState = new(insertMissingEntityUnlocks: false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dont we need instanceid specified here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The orchestration instance ID is automatically set when the ExecutionStarted event is processed during history conversion (see ProtoUtils.cs line 62: conversionState?.SetOrchestrationInstance(instance)). No manual setting is needed.

Copy link
Member

@YunchuWang YunchuWang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot address

…State

The EntityConversionState should always be created regardless of the EnableEntitySupport option because:
1. History may contain entity events even if the client option is disabled
2. The orchestration instance ID is automatically set when ExecutionStarted event is processed
3. This matches the pattern used in GrpcDurableTaskWorker.Processor

Co-authored-by: YunchuWang <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants