dotnet/systemweb-adapters

HttpRuntime.Cache throws "System.InvalidOperationException: No runtime is currently available" in unit tests

afshinm opened this issue · 14 comments

Describe the bug

Any call to HttpRuntime.Cache in class libraries fail with the System.InvalidOperationException: No runtime is currently available exception:

Test method Portal.Tests.Data.ForumTest.DataSelector_HappyPath threw exception:
System.TypeInitializationException: The type initializer for 'Portal.Data.UserAgentMatch' threw an exception. --->
    System.InvalidOperationException: **No runtime is currently available**
    at System.Web.HttpRuntime.get_Current()
    at System.Web.HttpRuntime.get_Cache()

This probably because .Cache tries to access the HostingEnvironmentAccessor here https://github.com/dotnet/systemweb-adapters/blob/main/src/Microsoft.AspNetCore.SystemWebAdapters/HttpRuntime.cs#L20 which is not available in a unit test environment. In System.Web, the cache object gets created at runtime if it's not available which explains why this is not happening in the .NET Framework tests.

To Reproduce

Create a class library that calls HttpRutime.Cache[key] and test this method in a unit tests.

Exceptions (if any)

Test method Portal.Tests.Data.ForumTest.DataSelector_HappyPath threw exception:
System.TypeInitializationException: The type initializer for 'Portal.Data.UserAgentMatch' threw an exception. --->
    System.InvalidOperationException: **No runtime is currently available**
    at System.Web.HttpRuntime.get_Current()
    at System.Web.HttpRuntime.get_Cache()

Further technical details

Please include the following if applicable:

ASP.NET Framework Application:

  • Technologies and versions used (i.e. MVC/WebForms/etc): MVC
  • .NET Framework Version: 4.7.2
  • IIS Version: 10
  • Windows Version: 11

ASP.NET Core Application:

  • Targeted .NET version: 6
  • .NET SDK version: 6

I just opened a PR to show how this can be reproduced and discuss a potential fix #402

Not sure if we need to make changes to the code, but rather add documentation. Using xunit test fixtures (which is what we do internally slightly differently), the following allows you to test:

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.SystemWebAdapters;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Web;
using System.Web.Hosting;

namespace TestProject1
{
    /// <summary>
    /// This is an xUnit concept that allows us to ensure all tests in classes marked with this collection are run sequentially
    /// </summary>
    [CollectionDefinition(nameof(SystemWebAdatpersHostedTests), DisableParallelization = true)]
    public class SystemWebAdatpersHostedTests
    {
    }

    [Collection(nameof(SystemWebAdatpersHostedTests))]
    public class RuntimeTests
    {
        /// <summary>
        /// This starts up a host in the background that allows us to initialize <see cref="HttpRuntime"/> and <see cref="HostingEnvironment"/>
        /// with values we want for testing with the <paramref name="configure"/> option.
        /// </summary>
        /// <param name="configure">Configuration for the hosting and runtime options.</param>
        public static async Task<IDisposable> EnableRuntimeAsync(Action<SystemWebAdaptersOptions>? configure = null, CancellationToken token = default)
            => await new HostBuilder()
               .ConfigureWebHost(webBuilder =>
               {
                   webBuilder
                       .UseTestServer()
                       .ConfigureServices(services =>
                       {
                           services.AddSystemWebAdapters();

                           if (configure is not null)
                           {
                               services.AddOptions<SystemWebAdaptersOptions>()
                                .Configure(configure);
                           }
                       })
                       .Configure(app =>
                       {
                           // No need to configure pipeline for tests
                       });
               })
               .StartAsync(token);

        [Fact]
        public async Task RuntimeEnabled()
        {
            using (await EnableRuntimeAsync(options => options.AppDomainAppPath = "path"))
            {
                Assert.True(HostingEnvironment.IsHosted);
                Assert.Equal("path", HttpRuntime.AppDomainAppPath);
            }

            Assert.False(HostingEnvironment.IsHosted);
        }
    }
}

@twsouthwick That seems like a good idea, too, but it probably requires a lot of changes to existing test classes. Let me try that out today and give you feedback.

@twsouthwick Seems like the SystemWebAdaptersOptions is excluded here https://github.com/dotnet/systemweb-adapters/pull/389/files#diff-bc1e96f031aab9e01db74de5ca6c6cfde0ed05fd8eebaa064deae8c0ada12a62R14 -- I'm not able to import it, or maybe this was introduced recently? I'm using v1.2.0.

Yeah it's new - you'll need to use a ci build of 1.3 preview

You should be able to do something similar with 1.2 without configuring things

@twsouthwick Thanks for confirming. I actually tried it without the SystemWebAdaptersOptions bit but I still got the No runtime is currently available exception after trying to reference HttpRuntime.Cache. I'm going to debug this a bit more.

@twsouthwick I just updated our package version to 1.3.0-ci build and I can confirm that the EnableRuntimeAsync works as expected in 1.3.0-ci, but with 1.2.0 (latest version) the No runtime is currently available exception occurs.

I'm happy to either update the docs or add a test helper which would configure the HostBuilder.

If I remember correctly, before 1.3 it was tied to HttpContext.Current, and part of the change for 1.3 was to enable it outside of a request.

@twsouthwick Ah, that actually makes sense then.

I'm happy to either update the docs or add a test helper which would configure the HostBuilder, which one do you think makes more sense?

I think docs would be a good choice for now. We don't have a test-specific package, and unless there's something specific we need, docs should suffice

@twsouthwick Just opened a PR to update the README #406. I'm not sure if README is the best place to add this, I was thinking maybe this guide could also be updated https://learn.microsoft.com/en-us/aspnet/core/migration/inc/overview?view=aspnetcore-7.0.

I think I'd prefer a the docs if you can open a PR there. It probably makes sense to create a new page "Unit Testing" or something similar

@twsouthwick can we close this issue now that dotnet/AspNetCore.Docs#30537 is merged?