Skip to content

RegisterStep<T> with Action<LocalPluginContext> method group silently resolves to wrong overload, causing StackOverflow #6

@mkholt

Description

@mkholt

RegisterStep<T> with Action<LocalPluginContext> method group silently resolves to wrong overload, causing StackOverflow

Summary

When using RegisterStep<T>(EventOperation, ExecutionStage, Execute) where Execute is a method accepting LocalPluginContext, C# method group resolution silently picks the 2-type-parameter overload
RegisterStep<TEntity, TService> with TService = LocalPluginContext. This stores the action as sp => Execute(sp.GetRequiredService<LocalPluginContext>()), but LocalPluginContext is never registered in
the DI container built by BuildServiceProvider, resulting in a StackOverflowException at runtime.

Reproduction

public class MyPlugin : Plugin
{
    public MyPlugin()
    {
        // StackOverflow at runtime
        RegisterStep<Contact>(
            EventOperation.Update,
            ExecutionStage.PostOperation,
            Execute);
    }

    protected void Execute(LocalPluginContext localContext)
    {
        // Never reached
    }
}

Expected behavior

Either:

  • Compile error indicating the method group doesn't match any overload, or
  • Works the same as RegisterPluginStep, which correctly wraps the delegate

Actual behavior

Compiles without errors or warnings. At runtime, Plugin.Execute calls the stored action which does sp.GetRequiredService(). Since LocalPluginContext is not in the DI container, this throws, and the exception handling/tracing path overflows the stack.

Workarounds

Both of these work correctly:

  // Option 1: Use RegisterPluginStep (wraps correctly via Plugin.cs:173)
  RegisterPluginStep<Contact>(EventOperation.Update, ExecutionStage.PostOperation, Execute);

  // Option 2: Use explicit lambda instead of method group
  RegisterStep<Contact>(EventOperation.Update, ExecutionStage.PostOperation,
      sp => Execute(new LocalPluginContext(sp)));

Root cause

Plugin.cs has these overloads:

  // 1-type-param: Action<IExtendedServiceProvider>
  protected PluginStepConfigBuilder<T> RegisterStep<T>(
      EventOperation eventOperation, ExecutionStage executionStage, Action<IExtendedServiceProvider> action)

  // 2-type-param: Action<TService> — C# infers TService from method group
  protected PluginStepConfigBuilder<TEntity> RegisterStep<TEntity, TService>(
      EventOperation eventOperation, ExecutionStage executionStage, Action<TService> action)

Execute(LocalPluginContext) cannot convert to Action (overload 1), but C# can infer TService = LocalPluginContext for overload 2, so it silently picks that one.

RegisterPluginStep avoids this because it accepts Action directly and wraps it:

  protected PluginStepConfigBuilder<T> RegisterPluginStep<T>(
      EventOperation eventOperation, ExecutionStage executionStage, Action<LocalPluginContext> action)
  {
      return RegisterStep<T>(eventOperation.ToString(), executionStage, sp => action(new LocalPluginContext(sp)));
  }

Suggested fix

Add a RegisterStep overload that accepts Action directly, matching what RegisterPluginStep does but without the [Obsolete] attribute:

  protected PluginStepConfigBuilder<T> RegisterStep<T>(
      EventOperation eventOperation, ExecutionStage executionStage, Action<LocalPluginContext> action)
      where T : Entity, new()
  {
      return RegisterStep<T>(eventOperation.ToString(), executionStage, sp => action(new LocalPluginContext(sp)));
  }

This would take priority in method group resolution over the RegisterStep<TEntity, TService> overload since it's a better match (no type inference needed).

Alternatively, register LocalPluginContext in the DI container in ServiceProviderExtensions.BuildServiceProvider:

  services.AddScoped(sp => new LocalPluginContext(/* ... */));

Environment

  • Discovered via XrmMockup test suite, but the overload resolution issue is independent of XrmMockup

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions