Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[OutputCaching] OutputCacheOptions.ApplicationServices is always null during initialization #55805

Open
1 task done
julealgon opened this issue May 20, 2024 · 0 comments · May be fixed by #55847
Open
1 task done

[OutputCaching] OutputCacheOptions.ApplicationServices is always null during initialization #55805

julealgon opened this issue May 20, 2024 · 0 comments · May be fixed by #55847
Labels
area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions feature-output-caching

Comments

@julealgon
Copy link

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

When configuring output caching using a configuration action, the ApplicationServices is not populated in time to be used by the action. This leads into an extremely unintuitive and noob-unfriendly API.

services.AddOutputCaching(o => 
{
    var myCachingOptions = o
        .ApplicationServices // <- this is `null` at this point
        .GetRequiredService<IOptions<MyCachingOptions>>()
        .Value;

   o.AddPolicy... // use my options here to build the policies
});

This happens because the ApplicationServices property is itself initialized using a custom IConfigureOptions implementation that is only added after the provided configuration delegate.

I think this design is not only unfriendly (as you now have a property that is only initialized in some circumstances) but hard to understand: why even add this property in the first place if whoever is configuring this option from the outside could just use another IConfigureOptions implementation themselves, or even call the various .AddOptions<T>.Configure<TDep...> overloads which are made for the purpose of using container dependencies to configure some given options?

Either the property should guarantee it's API (thus never be null), or it should be entirely removed and consumers should leverage existing mechanisms to perform configuration using dependencies.

The fact that this works makes little sense to me and should not be encouraged:

services
    .AddOutputCaching() // <- add blank call here which configures the `ApplicationServices` property 
    .AddOutputCaching(o => 
    {
        var myCachingOptions = o
            .ApplicationServices // <- this is now available due to order of inclusion of `IOptions` configurators
            .GetRequiredService<IOptions<MyCachingOptions>>()
            .Value;

       o.AddPolicy... // use my options here to build the policies
    });

At this point, I'd honestly rather do this which is much more idiomatic unless I'm missing something substantial:

services
    .AddOutputCaching()
    .AddOptions<OutputCacheOptions>().Configure<IOptions<MyCachingOptions>>((o, myCachingOptions) =>
    {
       o.AddPolicy... // use my options here to build the policies
    });

Which, again, completely defeats the purpose of this ApplicationServices property existing in the first place.

Expected Behavior

Many different behaviors could be possible here I think.

  1. Make sure ApplicationServices is never null. This would probably be only a matter of swapping the order of these 2 statements here:

    services.Configure(configureOptions);
    services.AddOutputCache();

  2. Remove ApplicationServices property altogether and have consumers rely on more standard strategies to leverage DI for configuration

While I think 1 is much easier, I would honestly suggest 2 as a better long-term solution that would lead people into the pit of success more often and result in more idiomatic configuration code.

Steps To Reproduce

  1. Call IServiceCollection.AddOutputCache(Action<OutputCacheOptions>) overload
  2. Attempt to use the ApplicationServices property on the passed-in options object
  3. Observe the null reference exception

Full repro project:

Exceptions (if any)

crit: Microsoft.AspNetCore.Hosting.Diagnostics[6]
      Application startup exception
      System.ArgumentNullException: Value cannot be null. (Parameter 'provider')
         at System.ThrowHelper.Throw(String paramName)
         at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
         at Program.<>c.<<Main>$>b__0_1(OutputCacheOptions o) in C:\git\julealgon\IssueRepros.OutputCacheNullApplicationServicesRepro\julealgon.IssueRepros.OutputCacheNullApplicationServicesRepro\Program.cs:line 11
         at Microsoft.Extensions.Options.OptionsFactory`1.Create(String name)
         at Microsoft.Extensions.Options.UnnamedOptionsManager`1.get_Value()
         at Microsoft.Extensions.DependencyInjection.OutputCacheServiceCollectionExtensions.<>c.<AddOutputCache>b__0_0(IServiceProvider sp)
         at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
         at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitRootCache(ServiceCallSite callSite, RuntimeResolverContext context)
         at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
         at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope)
         at Microsoft.Extensions.DependencyInjection.ServiceProvider.CreateServiceAccessor(ServiceIdentifier serviceIdentifier)
         at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
         at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope)
         at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType)
         at Microsoft.Extensions.Internal.ActivatorUtilities.ConstructorMatcher.CreateInstance(IServiceProvider provider)
         at Microsoft.Extensions.Internal.ActivatorUtilities.CreateInstance(IServiceProvider provider, Type instanceType, Object[] parameters)
         at Microsoft.AspNetCore.Builder.UseMiddlewareExtensions.ReflectionMiddlewareBinder.CreateMiddleware(RequestDelegate next)
         at Microsoft.AspNetCore.Builder.ApplicationBuilder.Build()
         at Microsoft.AspNetCore.Builder.ApplicationBuilder.Build()
         at Microsoft.AspNetCore.Hosting.GenericWebHostService.StartAsync(CancellationToken cancellationToken)
fail: Microsoft.Extensions.Hosting.Internal.Host[11]
      Hosting failed to start
      System.ArgumentNullException: Value cannot be null. (Parameter 'provider')
         at System.ThrowHelper.Throw(String paramName)
         at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
         at Program.<>c.<<Main>$>b__0_1(OutputCacheOptions o) in C:\git\julealgon\IssueRepros.OutputCacheNullApplicationServicesRepro\julealgon.IssueRepros.OutputCacheNullApplicationServicesRepro\Program.cs:line 11
         at Microsoft.Extensions.Options.OptionsFactory`1.Create(String name)
         at Microsoft.Extensions.Options.UnnamedOptionsManager`1.get_Value()
         at Microsoft.Extensions.DependencyInjection.OutputCacheServiceCollectionExtensions.<>c.<AddOutputCache>b__0_0(IServiceProvider sp)
         at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
         at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitRootCache(ServiceCallSite callSite, RuntimeResolverContext context)
         at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
         at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope)
         at Microsoft.Extensions.DependencyInjection.ServiceProvider.CreateServiceAccessor(ServiceIdentifier serviceIdentifier)
         at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
         at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope)
         at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType)
         at Microsoft.Extensions.Internal.ActivatorUtilities.ConstructorMatcher.CreateInstance(IServiceProvider provider)
         at Microsoft.Extensions.Internal.ActivatorUtilities.CreateInstance(IServiceProvider provider, Type instanceType, Object[] parameters)
         at Microsoft.AspNetCore.Builder.UseMiddlewareExtensions.ReflectionMiddlewareBinder.CreateMiddleware(RequestDelegate next)
         at Microsoft.AspNetCore.Builder.ApplicationBuilder.Build()
         at Microsoft.AspNetCore.Builder.ApplicationBuilder.Build()
         at Microsoft.AspNetCore.Hosting.GenericWebHostService.StartAsync(CancellationToken cancellationToken)
         at Microsoft.Extensions.Hosting.Internal.Host.<StartAsync>b__15_1(IHostedService service, CancellationToken token)
         at Microsoft.Extensions.Hosting.Internal.Host.ForeachService[T](IEnumerable`1 services, CancellationToken token, Boolean concurrent, Boolean abortOnFirstException, List`1 exceptions, Func`3 operation)
Unhandled exception. System.ArgumentNullException: Value cannot be null. (Parameter 'provider')
   at System.ThrowHelper.Throw(String paramName)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
   at Program.<>c.<<Main>$>b__0_1(OutputCacheOptions o) in C:\git\julealgon\IssueRepros.OutputCacheNullApplicationServicesRepro\julealgon.IssueRepros.OutputCacheNullApplicationServicesRepro\Program.cs:line 11
   at Microsoft.Extensions.Options.OptionsFactory`1.Create(String name)
   at Microsoft.Extensions.Options.UnnamedOptionsManager`1.get_Value()
   at Microsoft.Extensions.DependencyInjection.OutputCacheServiceCollectionExtensions.<>c.<AddOutputCache>b__0_0(IServiceProvider sp)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitRootCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.CreateServiceAccessor(ServiceIdentifier serviceIdentifier)
   at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType)
   at Microsoft.Extensions.Internal.ActivatorUtilities.ConstructorMatcher.CreateInstance(IServiceProvider provider)
   at Microsoft.Extensions.Internal.ActivatorUtilities.CreateInstance(IServiceProvider provider, Type instanceType, Object[] parameters)
   at Microsoft.AspNetCore.Builder.UseMiddlewareExtensions.ReflectionMiddlewareBinder.CreateMiddleware(RequestDelegate next)
   at Microsoft.AspNetCore.Builder.ApplicationBuilder.Build()
   at Microsoft.AspNetCore.Builder.ApplicationBuilder.Build()
   at Microsoft.AspNetCore.Hosting.GenericWebHostService.StartAsync(CancellationToken cancellationToken)
   at Microsoft.Extensions.Hosting.Internal.Host.<StartAsync>b__15_1(IHostedService service, CancellationToken token)
   at Microsoft.Extensions.Hosting.Internal.Host.ForeachService[T](IEnumerable`1 services, CancellationToken token, Boolean concurrent, Boolean abortOnFirstException, List`1 exceptions, Func`3 operation)
   at Microsoft.Extensions.Hosting.Internal.Host.StartAsync(CancellationToken cancellationToken)
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.Run(IHost host)
   at Program.<Main>$(String[] args) in C:\git\julealgon\IssueRepros.OutputCacheNullApplicationServicesRepro\julealgon.IssueRepros.OutputCacheNullApplicationServicesRepro\Program.cs:line 24

.NET Version

net8.0

Anything else?

No response

@dotnet-issue-labeler dotnet-issue-labeler bot added the needs-area-label Used by the dotnet-issue-labeler to label those issues which couldn't be triaged automatically label May 20, 2024
@mkArtakMSFT mkArtakMSFT added area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions and removed needs-area-label Used by the dotnet-issue-labeler to label those issues which couldn't be triaged automatically labels May 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions feature-output-caching
Projects
None yet
3 participants