Search code examples
c#botframework

Updating an activity in BotFramework after a timeout


I have a requirement that a Bot message posted to MS Teams should expire after a timeout period. To achieve this I am calling UpdateAsyncActivity() on a separate thread after a timeout, however this fails with a NullReferenceException:

System.NullReferenceException: Object reference not set to an instance of an object.
at Microsoft.Bot.Builder.BotFrameworkAdapter.UpdateActivityAsync(ITurnContext turnContext, Activity activity, CancellationToken cancellationToken)
at Microsoft.Bot.Builder.TurnContext.<>c__DisplayClass31_0.<<UpdateActivityAsync>g__ActuallyUpdateStuff|0>d.MoveNext()--- End of stack trace from previous location where exception was thrown ---
at Microsoft.Bot.Builder.TurnContext.UpdateActivityInternalAsync(Activity activity, IEnumerable`1 updateHandlers, Func`1 callAtBottom, CancellationToken cancellationToken)
at Microsoft.Bot.Builder.TurnContext.UpdateActivityAsync(IActivity activity, CancellationToken cancellationToken)
at NotifyController.ClearCard(ITurnContext turnContext, Activity timeoutActivity, Int32 timeoutInMinutes) in NotifyController.cs:line 48

The code looks something like this:

[Route("api/notify")]
[ApiController]
public class NotifyController : ControllerBase
{
    private IBotFrameworkHttpAdapter Adapter;
    private readonly string _appId;
    private readonly IConversationStorage _conversationStorage;

    public NotifyController(IBotFrameworkHttpAdapter adapter, IConfiguration configuration, IConversationStorage conversationStorage)
    {
        Adapter = adapter;
        _appId = configuration["MicrosoftAppId"] ?? string.Empty;
        _conversationStorage = conversationStorage;
    }

    [HttpPost]
    public async Task<IActionResult> PostForm([FromBody] RestData restData)
    {
        ConversationReference conversationReference =
            _conversationStorage.GetConversationFromStorage(restData.ConversationId);

        await ((BotAdapter)Adapter).ContinueConversationAsync(_appId, conversationReference, async (context, token) =>
            await BotCallback(restData.AdaptiveCard, context, token), default(CancellationToken));

        return new ContentResult();
    }

    private async Task BotCallback(string adaptiveCard, ITurnContext turnContext, CancellationToken cancellationToken)
    {
        var activity = MessageFactory.Attachment(adaptiveCard.ToAttachment());

        ResourceResponse response = await turnContext.SendActivityAsync(activity);

        var timeoutActivity = turnContext.Activity.CreateReply();
        timeoutActivity.Attachments.Add(AdaptiveCardExamples.TimeoutTryLater.ToAttachment());
        timeoutActivity.Id = response.Id;

        // SUCCESS
        //Thread.Sleep(10000);
        //await turnContext.UpdateActivityAsync(timeoutActivity);

        // FAIL
        Thread CardClearThread = new Thread(() => ClearCard(turnContext, timeoutActivity));
        CardClearThread.Start();
    }

    private async void ClearCard(ITurnContext turnContext, Activity timeoutActivity)
    {
        Thread.Sleep(10000);
        await turnContext.UpdateActivityAsync(timeoutActivity);
    }
}

I want the timeout to happen on a separate thread so that PostForm() returns a response as soon as the original message is sent.

I presume what is happening is that some aspect of turnContext is being disposed when the main thread returns, causing the sleeping thread to fail when it wakes up.

Is there a solution/alternative approach to this?


Solution

  • With in some mins , Bot has to response to the team otherwise this issue get popup best solution for this to implement proactive message concept

    https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-howto-proactive-message?view=azure-bot-service-4.0&tabs=csharp