-
Notifications
You must be signed in to change notification settings - Fork 0
RegisterStep<T> with Action<LocalPluginContext> method group silently resolves to wrong overload, causing StackOverflow #6
Description
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