Skip to content

IAsyncDisposable dependencies causes exception during teardown (NUnit) #913

@picolino

Description

@picolino

Reqnroll Version

Reqnroll.NUNit (2.3.0) + Reqnroll.Microsoft.Extensions.DependencyInjection (2.3.0)

Which test runner are you using?

NUnit

Test Runner Version Number

NUnit3TestAdapter 5.0.0

.NET Implementation

.NET 8.0

Test Execution Method

ReSharper Test Runner

Content of reqnroll.json configuration file

{
  "language": {
    "feature": "en-US"
  }
}

Issue Description

Problem

In my tests, I have DI setup:

services.AddScoped<IDbStorage, DbStorage>();

By default, the DI container disposes all classes when the scope is closed or the test suite ends (in the case of singleton registration).
My DbStorage class uses a third-party library that has an async disposal API only, DisposeAsync, implemented through the IAsyncDisposable interface. So, if I make my DbStorage implement IAsyncDisposable, I will get an exception during teardown:

TearDown : System.InvalidOperationException : 'DbStorage' type only implements IAsyncDisposable. Use DisposeAsync to dispose the container.
--TearDown
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.Dispose()
   at Reqnroll.Microsoft.Extensions.DependencyInjection.DependencyInjectionPlugin.AfterScenarioPluginLifecycleEventHandler(Object sender, RuntimePluginAfterScenarioEventArgs eventArgs)
   at Reqnroll.Plugins.RuntimePluginTestExecutionLifecycleEvents.RaiseAfterScenario(IObjectContainer objectContainer)
   at Reqnroll.Plugins.RuntimePluginTestExecutionLifecycleEventEmitter.RaiseExecutionLifecycleEvent(HookType hookType, IObjectContainer container)
   at Reqnroll.Infrastructure.TestExecutionEngine.FireRuntimePluginTestExecutionLifecycleEvents(HookType hookType)
   at Reqnroll.Infrastructure.TestExecutionEngine.FireEventsAsync(HookType hookType)
   at Reqnroll.Infrastructure.TestExecutionEngine.FireScenarioEventsAsync(HookType bindingEvent)
   at Reqnroll.Infrastructure.TestExecutionEngine.OnScenarioEndAsync()
   at Reqnroll.TestRunner.OnScenarioEndAsync()
   at Tests.Feature.TestTearDownAsync()
   at NUnit.Framework.Internal.TaskAwaitAdapter.GenericAdapter`1.GetResult()
   at NUnit.Framework.Internal.AsyncToSyncAdapter.Await[TResult](TestExecutionContext context, Func`1 invoke)
   at NUnit.Framework.Internal.AsyncToSyncAdapter.Await(TestExecutionContext context, Func`1 invoke)
   at NUnit.Framework.Internal.Commands.SetUpTearDownItem.RunSetUpOrTearDownMethod(TestExecutionContext context, IMethodInfo method)
   at NUnit.Framework.Internal.Commands.SetUpTearDownItem.RunTearDown(TestExecutionContext context)

As a workaround I need to implement IDisposable in DbStorage and call the async api from the synchronous Dispose method which is considered bad practice because it blocks the thread and consumes thread resources:

public class DbStorage : IDbStorage, IDisposable
{
   public void Dispose()
   {
       ThirdPartyLibraryDisposeAsync().GetAwaiter().GetResult();
   }
}

I want to make my DbStorage class only implement IAsyncDisposable to remove the sync -> async call and free up thread pool resources in CI runners.

Steps to Reproduce

  1. Register Scoped lifetime dependency that implements IAsyncDisposable interface using Reqnroll.Microsoft.Extensions.DependencyInjection plugin
  2. Launch test using this dependency
  3. Got System.InvalidOperationException error during teardown.

Link to Repro Project

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    duplicateThis issue or pull request already existsenhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions