From 00d7fe9cb17c0ffb898c13c9fdf107add316d6f9 Mon Sep 17 00:00:00 2001 From: Brett Samblanet Date: Thu, 1 Oct 2020 14:07:45 -0700 Subject: [PATCH] improving DI scope behavior --- .../DependencyInjection/DryIoc/Container.cs | 12 ++- .../DependencyInjection/ScopedResolver.cs | 7 ++ src/WebJobs.Script/ScriptConstants.cs | 1 + .../JobHostServiceProviderTests.cs | 87 ++++++++++++++++++- 4 files changed, 105 insertions(+), 2 deletions(-) diff --git a/src/WebJobs.Script.WebHost/DependencyInjection/DryIoc/Container.cs b/src/WebJobs.Script.WebHost/DependencyInjection/DryIoc/Container.cs index d1bad000c0..5e000756f0 100644 --- a/src/WebJobs.Script.WebHost/DependencyInjection/DryIoc/Container.cs +++ b/src/WebJobs.Script.WebHost/DependencyInjection/DryIoc/Container.cs @@ -64,6 +64,8 @@ namespace DryIoc using MemberAssignmentExpr = System.Linq.Expressions.MemberAssignment; using FactoryDelegateExpr = System.Linq.Expressions.Expression; using global::Microsoft.Azure.WebJobs.Script.WebHost.DependencyInjection.DryIoc; + using global::Microsoft.Azure.WebJobs.Script; + using global::Microsoft.Azure.WebJobs.Script.Config; #endif /// IoC Container. Documentation is available at https://bitbucket.org/dadhi/dryioc. @@ -1941,7 +1943,15 @@ private Container(Rules rules, Ref registry, IScope singletonScope, _registry = registry; _singletonScope = singletonScope; - _scopeContext = scopeContext ?? new AsyncScopeContext(); + + _scopeContext = scopeContext; + + if (_scopeContext == null && !FeatureFlags.IsEnabled(ScriptConstants.FeatureFlagEnableEnhancedScopes)) + { + // Enhanced scopes do not need this context. + _scopeContext = new AsyncScopeContext(); + } + _ownCurrentScope = ownCurrentScope; _disposed = disposed; diff --git a/src/WebJobs.Script.WebHost/DependencyInjection/ScopedResolver.cs b/src/WebJobs.Script.WebHost/DependencyInjection/ScopedResolver.cs index 942c58a55f..4a6cf1da0f 100644 --- a/src/WebJobs.Script.WebHost/DependencyInjection/ScopedResolver.cs +++ b/src/WebJobs.Script.WebHost/DependencyInjection/ScopedResolver.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading.Tasks; using DryIoc; +using Microsoft.Azure.WebJobs.Script.Config; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.Azure.WebJobs.Script.WebHost.DependencyInjection @@ -55,6 +56,12 @@ internal ServiceScope CreateChildScope(IServiceScopeFactory rootScopeFactory) })); var scope = new ServiceScope(resolver, scopedRoot); + + if (FeatureFlags.IsEnabled(ScriptConstants.FeatureFlagEnableEnhancedScopes)) + { + scopedContext.UseInstance(scope.ServiceProvider); + } + ChildScopes.TryAdd(scope, null); scope.DisposalTask.ContinueWith(t => ChildScopes.TryRemove(scope, out object _)); diff --git a/src/WebJobs.Script/ScriptConstants.cs b/src/WebJobs.Script/ScriptConstants.cs index a78e693763..a5f370eefe 100644 --- a/src/WebJobs.Script/ScriptConstants.cs +++ b/src/WebJobs.Script/ScriptConstants.cs @@ -109,6 +109,7 @@ public static class ScriptConstants public const string FeatureFlagEnableActionResultHandling = "EnableActionResultHandling"; public const string FeatureFlagAllowSynchronousIO = "AllowSynchronousIO"; public const string FeatureFlagRelaxedAssemblyUnification = "RelaxedAssemblyUnification"; + public const string FeatureFlagEnableEnhancedScopes = "EnableEnhancedScopes"; public const string AdminJwtValidAudienceFormat = "https://{0}.azurewebsites.net/azurefunctions"; public const string AdminJwtValidIssuerFormat = "https://{0}.scm.azurewebsites.net"; diff --git a/test/WebJobs.Script.Tests/DependencyInjection/JobHostServiceProviderTests.cs b/test/WebJobs.Script.Tests/DependencyInjection/JobHostServiceProviderTests.cs index 3a4127d2f3..03f85bd18b 100644 --- a/test/WebJobs.Script.Tests/DependencyInjection/JobHostServiceProviderTests.cs +++ b/test/WebJobs.Script.Tests/DependencyInjection/JobHostServiceProviderTests.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Text; using Microsoft.Azure.WebJobs.Script.WebHost.DependencyInjection; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -12,6 +11,8 @@ namespace Microsoft.Azure.WebJobs.Script.Tests.DependencyInjection { public class JobHostServiceProviderTests { + private delegate object ServiceFactory(Type type); + [Fact] public void Dispose_OnJobHostScope_DoesNotDisposeRootSingletonService() { @@ -88,6 +89,90 @@ public void Dispose_OnJobHost_DoesNotDisposRootScopedService() Assert.False(rootService.Disposed); } + [Fact] + public void Scopes_ChildScopeIsIsolated() + { + using (new TestScopedEnvironmentVariable(EnvironmentSettingNames.AzureWebJobsFeatureFlags, ScriptConstants.FeatureFlagEnableEnhancedScopes)) + { + var services = new ServiceCollection(); + services.AddScoped(); + + var rootScopeFactory = new WebHostServiceProvider(new ServiceCollection()); + var jobHostProvider = new JobHostServiceProvider(services, rootScopeFactory, rootScopeFactory); + + var a1 = jobHostProvider.GetService(); + jobHostProvider.CreateScope(); + var a2 = jobHostProvider.GetService(); + Assert.NotNull(a1); + Assert.NotNull(a2); + Assert.Same(a1, a2); + } + } + + [Fact] + public void Scopes_Factories() + { + using (new TestScopedEnvironmentVariable(EnvironmentSettingNames.AzureWebJobsFeatureFlags, ScriptConstants.FeatureFlagEnableEnhancedScopes)) + { + IList serviceProviders = new List(); + + var services = new ServiceCollection(); + services.AddTransient(p => + { + serviceProviders.Add(p); + return new A(); + }); + + var rootScopeFactory = new WebHostServiceProvider(new ServiceCollection()); + var jobHostProvider = new JobHostServiceProvider(services, rootScopeFactory, rootScopeFactory); + + // Get this service twice. + // The IServiceProvider passed to the factory should be different because they are separate scopes. + var scope1 = jobHostProvider.CreateScope(); + scope1.ServiceProvider.GetService(); + + var scope2 = jobHostProvider.CreateScope(); + scope2.ServiceProvider.GetService(); + + Assert.Equal(2, serviceProviders.Count); + Assert.NotSame(serviceProviders[0], serviceProviders[1]); + } + } + + [Theory] + [InlineData("")] + [InlineData(ScriptConstants.FeatureFlagEnableEnhancedScopes)] + public void Scopes_DelegateFactory(string flag) + { + using (new TestScopedEnvironmentVariable(EnvironmentSettingNames.AzureWebJobsFeatureFlags, flag)) + { + var services = new ServiceCollection(); + + services.AddScoped(); + services.AddScoped(provider => (type) => provider.GetRequiredService(type)); + + var rootScopeFactory = new WebHostServiceProvider(new ServiceCollection()); + var jobHostProvider = new JobHostServiceProvider(services, rootScopeFactory, rootScopeFactory); + + var scope1 = jobHostProvider.CreateScope(); + var a1 = scope1.ServiceProvider.GetService()(typeof(A)); + + var scope2 = jobHostProvider.CreateScope(); + var a2 = scope2.ServiceProvider.GetService()(typeof(A)); + + Assert.NotNull(a1); + Assert.NotNull(a2); + Assert.NotSame(a1, a2); + } + } + + private class A + { + public A() + { + } + } + private class TestService : IService, IDisposable { public bool Disposed { get; private set; }