Skip to content

InvokeActivity.Value is null due to property shadowing with new in derived classes #339

@corinagum

Description

@corinagum

Bug Description

When handling an adaptiveCard/action invoke, context.Activity.Value always returns null, even though the incoming JSON payload contains a valid value field and the debugger shows a non-null Value property of the derived type.

This affects any handler using the base InvokeActivity type:

[Invoke("adaptiveCard/action")]
public async Task<Response> OnInvoke(IContext<InvokeActivity> context)
{
    // This always evaluates to null
    if (context.Activity.Value != null)
    {
        // Never gets here
    }
}

Root Cause

The derived activity classes (e.g. AdaptiveCards.ActionActivity) use the new keyword to shadow the base Value property with a strongly-typed version:

  • Base (InvokeActivity): public object? Value { get; set; }
  • Derived (AdaptiveCards.ActionActivity): public new required Api.AdaptiveCards.InvokeValue Value { get; set; }

In C#, new creates a separate backing field — it does not override the base property. During JSON deserialization, the converter chain correctly creates the derived type (ActionActivity) and populates the derived Value property. However, the base InvokeActivity.Value is never set, so accessing Value through a base class reference returns null.

Deserialization flow

  1. InvokeActivity.JsonConverter.Read() sees "name": "adaptiveCard/" prefix → delegates to AdaptiveCardActivity
  2. AdaptiveCardActivity.JsonConverter.Read() sees "adaptiveCard/action" → deserializes as AdaptiveCards.ActionActivity
  3. ActionActivity.Value (derived, typed as InvokeValue) gets populated ✅
  4. InvokeActivity.Value (base, typed as object?) stays null

Debugger shows two Value properties

Property Type Value
Value (InvokeActivity) object? null
Value (ActionActivity) AdaptiveCards.InvokeValue has data ✅

Affected Classes

This same new shadowing pattern exists across all invoke subtypes:

  • AdaptiveCards.ActionActivitynew InvokeValue Value
  • Tasks.FetchActivity / Tasks.SubmitActivitynew TaskModules.Request Value
  • MessageExtensions.QueryActivitynew MessageExtensions.Query Value
  • SignIn.TokenExchangeActivitynew SignIn.ExchangeToken Value
  • HandoffActivitynew HandoffActivityValue Value
  • SearchActivitynew SearchValue Value

Any handler using a base type reference (InvokeActivity, AdaptiveCardActivity, etc.) will hit this same null issue.

Workaround

Cast to the derived type to access the value:

[Invoke("adaptiveCard/action")]
public async Task<Response> OnInvoke(IContext<InvokeActivity> context)
{
    if (context.Activity is AdaptiveCards.ActionActivity actionActivity)
    {
        var invokeValue = actionActivity.Value; // works
        var data = invokeValue.Action.Data;
    }
}

Microsoft.Teams.Api.Activities.Invokes.AdaptiveCards.ActionActivity

Possible Fixes

  • Option A: During deserialization, also populate the base InvokeActivity.Value property (sync both backing fields).
  • Option B: Have derived class setters sync the base property (e.g. when ActionActivity.Value is set, also set base.Value = value).
  • Option C: Redesign using generics (IInvokeActivity<TValue>) so the property is properly typed without shadowing.

2.1

public class InvokeActivity<TValue> : InvokeActivity
{
/// <summary>
/// Gets or sets the strongly-typed value associated with the invoke activity.
/// This property shadows the base class Value property but uses the same underlying storage,
/// ensuring no synchronization issues between typed and untyped access.
/// </summary>
public new TValue? Value
{
get => base.Value != null ? JsonSerializer.Deserialize<TValue>(base.Value.ToJsonString()) : default;
set => base.Value = value != null ? JsonSerializer.SerializeToNode(value) : null;
}

This uses a generic InvokeActivity<TValue> and extends InvokeActivity with synced base.Value.

Since we have the solution in 2.1, we can leave this bug open for the workaround to be available for customers. We can also discuss the possibility of getting the fix into pre-2.1 as well.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions