diff --git a/.editorconfig b/.editorconfig index 67ae50291..eaf2b54d4 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,3 +10,4 @@ indent_size = 2 [*.cs] indent_style = tab indent_size = 4 +csharp_new_line_before_open_brace = all \ No newline at end of file diff --git a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs new file mode 100644 index 000000000..f8d2ce57a --- /dev/null +++ b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs @@ -0,0 +1,757 @@ +using Castle.MicroKernel.Lifestyle; +using Castle.MicroKernel.Registration; +using Castle.Windsor.Extensions.DependencyInjection.Extensions; +using Castle.Windsor.Extensions.DependencyInjection.Tests.Components; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Castle.Windsor.Extensions.DependencyInjection.Tests +{ + [CollectionDefinition(nameof(DoNotParallelize), DisableParallelization = true)] + public class DoNotParallelize { } + + /// + /// These is the original Castle Windsor Dependency Injection behavior. + /// + public class ResolveFromThreadpoolUnsafe_NetStatic : AbstractResolveFromThreadpoolUnsafe + { + public ResolveFromThreadpoolUnsafe_NetStatic() : base(false) + { + } + + #region "Singleton" + + /// + /// This test will Succeed is we use standard Castle Windsor Singleton lifestyle instead of the custom + /// NetStatic lifestyle. + /// + [Fact] + public async Task Cannot_Resolve_LifestyleNetStatic_From_WindsorContainer_NoRootScopeAvailable() + { + var serviceProvider = new ServiceCollection(); + var container = new WindsorContainer(); + var f = new WindsorServiceProviderFactory(container); + f.CreateBuilder(serviceProvider); + + container.Register( + Component.For().ImplementedBy().LifeStyle.NetStatic() + ); + + IServiceProvider sp = f.CreateServiceProvider(container); + + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + + TaskCompletionSource tcs = new TaskCompletionSource(); + + ThreadPool.UnsafeQueueUserWorkItem(state => + { + try + { + var actualUserService = container.Resolve(); + Assert.NotNull(actualUserService); + } + catch (Exception ex) + { + tcs.SetException(ex); + return; + } + tcs.SetResult(actualUserService); + }, null); + + // Wait for the work item to complete. + var ex = await Catches.ExceptionAsync(async () => + { + var task = tcs.Task; + IUserService result = await task; + // The test succeeds if we use standard Castle Windsor Singleton lifestyle instead of the custom NetStatic lifestyle. + Assert.NotNull(result); + }); + + // This test will fail if we use NetStatic lifestyle + Assert.NotNull(ex); + Assert.IsType(ex); + Assert.Equal("No root scope available.", ex.Message); + + (sp as IDisposable)?.Dispose(); + container.Dispose(); + } + + #endregion + } + + /// + /// Mapping NetStatic to usual Singleton lifestyle. + /// + public class ResolveFromThreadpoolUnsafe_Singleton : AbstractResolveFromThreadpoolUnsafe + { + public ResolveFromThreadpoolUnsafe_Singleton() : base(true) + { + } + + #region "Singleton" + + /// + /// This test will Succeed is we use standard Castle Windsor Singleton lifestyle instead of the custom + /// NetStatic lifestyle. + /// + [Fact] + public async Task Can_Resolve_LifestyleNetStatic_From_WindsorContainer() + { + var serviceProvider = new ServiceCollection(); + var container = new WindsorContainer(); + var f = new WindsorServiceProviderFactory(container); + f.CreateBuilder(serviceProvider); + + container.Register( + Component.For().ImplementedBy().LifeStyle.NetStatic() + ); + + IServiceProvider sp = f.CreateServiceProvider(container); + + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + + TaskCompletionSource tcs = new TaskCompletionSource(); + + ThreadPool.UnsafeQueueUserWorkItem(state => + { + try + { + var actualUserService = container.Resolve(); + Assert.NotNull(actualUserService); + } + catch (Exception ex) + { + tcs.SetException(ex); + return; + } + tcs.SetResult(actualUserService); + }, null); + + // Wait for the work item to complete. + var ex = await Catches.ExceptionAsync(async () => + { + var task = tcs.Task; + IUserService result = await task; + // The test succeeds if we use standard Castle Windsor Singleton lifestyle instead of the custom NetStatic lifestyle. + Assert.NotNull(result); + }); + + (sp as IDisposable)?.Dispose(); + container.Dispose(); + } + + #endregion + } + + /// + /// relying on static state (WindsorDependencyInjectionOptions) is not good for tests + /// that might run in parallel, can lead to false positives / negatives. + /// + [Collection(nameof(DoNotParallelize))] + public abstract class AbstractResolveFromThreadpoolUnsafe + { + protected AbstractResolveFromThreadpoolUnsafe(bool mapNetStaticToSingleton) + { + WindsorDependencyInjectionOptions.MapNetStaticToSingleton = mapNetStaticToSingleton; + } + + #region Singleton + + /* + * Singleton tests should never fail, given you have a container instance you should always + * be able to resolve a singleton from it. + */ + + [Fact] + public async Task Can_Resolve_LifestyleSingleton_From_ServiceProvider() + { + var serviceProvider = new ServiceCollection(); + var container = new WindsorContainer(); + var f = new WindsorServiceProviderFactory(container); + f.CreateBuilder(serviceProvider); + + container.Register( + Component.For().ImplementedBy() + ); + + IServiceProvider sp = f.CreateServiceProvider(container); + + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + + TaskCompletionSource tcs = new TaskCompletionSource(); + + ThreadPool.UnsafeQueueUserWorkItem(state => + { + try + { + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + } + catch (Exception ex) + { + tcs.SetException(ex); + return; + } + tcs.SetResult(actualUserService); + }, null); + + // Wait for the work item to complete. + var task = tcs.Task; + IUserService result = await task; + Assert.NotNull(result); + + (sp as IDisposable)?.Dispose(); + container.Dispose(); + } + + [Fact] + public async Task Can_Resolve_LifestyleSingleton_From_WindsorContainer() + { + var serviceProvider = new ServiceCollection(); + var container = new WindsorContainer(); + var f = new WindsorServiceProviderFactory(container); + f.CreateBuilder(serviceProvider); + + container.Register( + Component.For().ImplementedBy() + ); + + IServiceProvider sp = f.CreateServiceProvider(container); + + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + + TaskCompletionSource tcs = new TaskCompletionSource(); + + ThreadPool.UnsafeQueueUserWorkItem(state => + { + try + { + var actualUserService = container.Resolve(); + Assert.NotNull(actualUserService); + } + catch (Exception ex) + { + tcs.SetException(ex); + return; + } + tcs.SetResult(actualUserService); + }, null); + + // Wait for the work item to complete. + var task = tcs.Task; + IUserService result = await task; + Assert.NotNull(result); + + (sp as IDisposable)?.Dispose(); + container.Dispose(); + } + + [Fact] + public async Task Can_Resolve_LifestyleNetStatic_From_ServiceProvider() + { + var serviceProvider = new ServiceCollection(); + var container = new WindsorContainer(); + var f = new WindsorServiceProviderFactory(container); + f.CreateBuilder(serviceProvider); + + container.Register( + Component.For().ImplementedBy().LifeStyle.NetStatic() + ); + + IServiceProvider sp = f.CreateServiceProvider(container); + + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + + TaskCompletionSource tcs = new TaskCompletionSource(); + + ThreadPool.UnsafeQueueUserWorkItem(state => + { + try + { + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + } + catch (Exception ex) + { + tcs.SetException(ex); + return; + } + tcs.SetResult(actualUserService); + }, null); + + // Wait for the work item to complete. + var task = tcs.Task; + IUserService result = await task; + Assert.NotNull(result); + + (sp as IDisposable)?.Dispose(); + container.Dispose(); + } + + #endregion + + #region Scoped + + /* + * Scoped tests might fail if for whatever reason you do not have a current scope + * (like when you run from Threadpool.UnsafeQueueUserWorkItem). + */ + + /// + /// This test will fail because the service provider adapter + /// does not create a standard Castle Windsor scope + /// + [Fact] + public async Task Cannot_Resolve_LifestyleScoped_From_ServiceProvider() + { + var serviceProvider = new ServiceCollection(); + var container = new WindsorContainer(); + var f = new WindsorServiceProviderFactory(container); + f.CreateBuilder(serviceProvider); + + container.Register( + Component.For().ImplementedBy().LifestyleScoped() + ); + + IServiceProvider sp = f.CreateServiceProvider(container); + + // must create a standard Castle Windsor scope (not managed by the adapter) + using (var s = container.BeginScope()) + { + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + + TaskCompletionSource tcs = new TaskCompletionSource(); + + ThreadPool.UnsafeQueueUserWorkItem(state => + { + try + { + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + } + catch (Exception ex) + { + tcs.SetException(ex); + return; + } + tcs.SetResult(actualUserService); + }, null); + + // Wait for the work item to complete. + var ex = await Catches.ExceptionAsync(async () => + { + var task = tcs.Task; + IUserService result = await task; + Assert.NotNull(result); + }); + + Assert.NotNull(ex); + Assert.IsType(ex); + Assert.StartsWith("Scope was not available. Did you forget to call container.BeginScope()?", ex.Message); + } + + (sp as IDisposable)?.Dispose(); + container.Dispose(); + } + + /// + /// This test will fail because the service provider adapter + /// does not create a standard Castle Windsor scope + /// + [Fact] + public async Task Cannot_Resolve_LifestyleScoped_From_WindsorContainer() + { + var serviceProvider = new ServiceCollection(); + var container = new WindsorContainer(); + var f = new WindsorServiceProviderFactory(container); + f.CreateBuilder(serviceProvider); + + container.Register( + Component.For().ImplementedBy().LifestyleScoped() + ); + + IServiceProvider sp = f.CreateServiceProvider(container); + + // must create a standard Castle Windsor scope (not managed by the adapter) + using (var s = container.BeginScope()) + { + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + + TaskCompletionSource tcs = new TaskCompletionSource(); + + ThreadPool.UnsafeQueueUserWorkItem(state => + { + try + { + var actualUserService = container.Resolve(); + Assert.NotNull(actualUserService); + } + catch (Exception ex) + { + tcs.SetException(ex); + return; + } + tcs.SetResult(actualUserService); + }, null); + + // Wait for the work item to complete. + var ex = await Catches.ExceptionAsync(async () => + { + var task = tcs.Task; + IUserService result = await task; + Assert.NotNull(result); + }); + + Assert.NotNull(ex); + Assert.IsType(ex); + Assert.StartsWith("Scope was not available. Did you forget to call container.BeginScope()?", ex.Message); + } + + (sp as IDisposable)?.Dispose(); + container.Dispose(); + } + + /// + /// This test succeeds because WindsorScopedServiceProvider captured the root scope on creation + /// and forced it to be current before service resolution. + /// Scoped is tied to the rootscope = potential memory leak. + /// + [Fact] + public async Task Can_Resolve_LifestyleScopedToNetServiceScope_From_ServiceProvider_MemoryLeak() + { + var serviceProvider = new ServiceCollection(); + var container = new WindsorContainer(); + var f = new WindsorServiceProviderFactory(container); + f.CreateBuilder(serviceProvider); + + container.Register( + Component.For().ImplementedBy().LifeStyle.ScopedToNetServiceScope() + ); + + IServiceProvider sp = f.CreateServiceProvider(container); + + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + + TaskCompletionSource tcs = new TaskCompletionSource(); + + ThreadPool.UnsafeQueueUserWorkItem(state => + { + try + { + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + } + catch (Exception ex) + { + tcs.SetException(ex); + return; + } + tcs.SetResult(actualUserService); + }, null); + + // Wait for the work item to complete. + var task = tcs.Task; + IUserService result = await task; + Assert.NotNull(result); + + (sp as IDisposable)?.Dispose(); + container.Dispose(); + } + + [Fact] + public async Task Cannot_Resolve_LifestyleScopedToNetServiceScope_From_WindsorContainer() + { + var serviceProvider = new ServiceCollection(); + var container = new WindsorContainer(); + var f = new WindsorServiceProviderFactory(container); + f.CreateBuilder(serviceProvider); + + container.Register( + Component.For().ImplementedBy().LifeStyle.ScopedToNetServiceScope() + ); + + IServiceProvider sp = f.CreateServiceProvider(container); + + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + + TaskCompletionSource tcs = new TaskCompletionSource(); + + ThreadPool.UnsafeQueueUserWorkItem(state => + { + try + { + var actualUserService = container.Resolve(); + Assert.NotNull(actualUserService); + } + catch (Exception ex) + { + tcs.SetException(ex); + return; + } + tcs.SetResult(actualUserService); + }, null); + + // Wait for the work item to complete. + var ex = await Catches.ExceptionAsync(async () => + { + var task = tcs.Task; + IUserService result = await task; + Assert.NotNull(result); + }); + + Assert.NotNull(ex); + Assert.IsType(ex); + Assert.StartsWith("No scope available", ex.Message); + + (sp as IDisposable)?.Dispose(); + container.Dispose(); + } + + #endregion + + #region Transient + + /* + * Transient tests failure is questionable: + * - if you have a container you should be able to resolve transient without a scope, + * but they might be tracked by the container itself (or the IServiceProvider) + * - when windsor container is disposed all transient services are disposed as well + * - when a IServiceProvider is disposed all transient services (created by it) are disposed as well + * - problem is: we have una instance of a windsor container passed on to multiple instances of IServiceProvider + * one solution will be to tie the Transients to a scope, and the scope is tied to service provider + * when both of them are disposed, the transient services are disposed as well + */ + + [Fact] + public async Task Can_Resolve_LifestyleTransient_From_ServiceProvider() + { + var serviceProvider = new ServiceCollection(); + var container = new WindsorContainer(); + var f = new WindsorServiceProviderFactory(container); + f.CreateBuilder(serviceProvider); + + container.Register( + Component.For().ImplementedBy().LifestyleTransient() + ); + + IServiceProvider sp = f.CreateServiceProvider(container); + + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + + TaskCompletionSource tcs = new TaskCompletionSource(); + + ThreadPool.UnsafeQueueUserWorkItem(state => + { + try + { + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + } + catch (Exception ex) + { + tcs.SetException(ex); + return; + } + tcs.SetResult(actualUserService); + }, null); + + // Wait for the work item to complete. + var task = tcs.Task; + IUserService result = await task; + Assert.NotNull(result); + + (sp as IDisposable)?.Dispose(); + container.Dispose(); + } + + [Fact] + public async Task Can_Resolve_LifestyleTransient_From_WindsorContainer() + { + var serviceProvider = new ServiceCollection(); + var container = new WindsorContainer(); + var f = new WindsorServiceProviderFactory(container); + f.CreateBuilder(serviceProvider); + + container.Register( + Component.For().ImplementedBy().LifestyleTransient() + ); + + IServiceProvider sp = f.CreateServiceProvider(container); + + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + + TaskCompletionSource tcs = new TaskCompletionSource(); + + ThreadPool.UnsafeQueueUserWorkItem(state => + { + try + { + var actualUserService = container.Resolve(); + Assert.NotNull(actualUserService); + } + catch (Exception ex) + { + tcs.SetException(ex); + return; + } + tcs.SetResult(actualUserService); + }, null); + + // Wait for the work item to complete. + var task = tcs.Task; + IUserService result = await task; + Assert.NotNull(result); + + (sp as IDisposable)?.Dispose(); + container.Dispose(); + } + + /// + /// This test succeeds because WindsorScopedServiceProvider captured the root scope on creation + /// and forced it to be current before service resolution. + /// Transient is tied to the rootscope = potential memory leak. + /// + [Fact] + public async Task Can_Resolve_LifestyleNetTransient_From_ServiceProvider_MemoryLeak() + { + var serviceProvider = new ServiceCollection(); + var container = new WindsorContainer(); + var f = new WindsorServiceProviderFactory(container); + f.CreateBuilder(serviceProvider); + + container.Register( + Component.For().ImplementedBy().LifestyleNetTransient() + ); + + IServiceProvider sp = f.CreateServiceProvider(container); + + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + + TaskCompletionSource tcs = new TaskCompletionSource(); + + ThreadPool.UnsafeQueueUserWorkItem(state => + { + try + { + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + } + catch (Exception ex) + { + tcs.SetException(ex); + return; + } + tcs.SetResult(actualUserService); + }, null); + + // Wait for the work item to complete. + var task = tcs.Task; + IUserService result = await task; + Assert.NotNull(result); + + (sp as IDisposable)?.Dispose(); + container.Dispose(); + } + + [Fact] + public async Task Cannot_Resolve_LifestyleNetTransient_From_WindsorContainer_NoScopeAvailable() + { + var serviceProvider = new ServiceCollection(); + var container = new WindsorContainer(); + var f = new WindsorServiceProviderFactory(container); + f.CreateBuilder(serviceProvider); + + container.Register( + Component.For().ImplementedBy().LifestyleNetTransient() + ); + + IServiceProvider sp = f.CreateServiceProvider(container); + + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + + TaskCompletionSource tcs = new TaskCompletionSource(); + + ThreadPool.UnsafeQueueUserWorkItem(state => + { + try + { + var actualUserService = container.Resolve(); + Assert.NotNull(actualUserService); + } + catch (Exception ex) + { + tcs.SetException(ex); + return; + } + tcs.SetResult(actualUserService); + }, null); + + // Wait for the work item to complete. + var ex = await Catches.ExceptionAsync(async () => + { + var task = tcs.Task; + IUserService result = await task; + Assert.NotNull(result); + }); + + Assert.NotNull(ex); + Assert.IsType(ex); + Assert.StartsWith("No scope available", ex.Message); + + (sp as IDisposable)?.Dispose(); + container.Dispose(); + } + + #endregion + + /* + * Missing tests: we should also test what happens with injected IServiceProvider (what scope do they get?) + * Injected IServiceProvider might or might not have a scope (it depends on AsyncLocal value). + */ + } + + public static class Catches + { + public static Exception Exception(Action action) + { + try + { + action(); + } + catch (Exception e) + { + return e; + } + return null; + } + + public async static Task ExceptionAsync(Func func) + { + try + { + await func(); + } + catch (Exception e) + { + return e; + } + return null; + } + } +} diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Extensions/WindsorExtensions.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Extensions/WindsorExtensions.cs index 38d46d34d..5b7b95f9d 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Extensions/WindsorExtensions.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Extensions/WindsorExtensions.cs @@ -49,6 +49,13 @@ public static ComponentRegistration LifestyleNetTransient(th /// public static ComponentRegistration NetStatic(this LifestyleGroup lifestyle) where TService : class { + // I don't think we need this lifestyle at all, usual Singleton should be good enough; + // also we maybe don't need the whole rootscope thing. A normal scope set as current should be enough + // otherwise we should revert to static rootscope + if (WindsorDependencyInjectionOptions.MapNetStaticToSingleton) + { + return lifestyle.Singleton; + } return lifestyle .Scoped(); } diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs b/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs index 974c02ae8..ad27dce4e 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs @@ -18,17 +18,17 @@ namespace Castle.Windsor.Extensions.DependencyInjection using Castle.MicroKernel.Registration; using Castle.Windsor.Extensions.DependencyInjection.Extensions; - + using Microsoft.Extensions.DependencyInjection; - internal class RegistrationAdapter + internal static class RegistrationAdapter { public static IRegistration FromOpenGenericServiceDescriptor(Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) { ComponentRegistration registration = Component.For(service.ServiceType) .NamedAutomatically(UniqueComponentName(service)); - if(service.ImplementationType != null) + if (service.ImplementationType != null) { registration = UsingImplementation(registration, service); } @@ -65,11 +65,11 @@ public static IRegistration FromServiceDescriptor(Microsoft.Extensions.Dependenc public static string OriginalComponentName(string uniqueComponentName) { - if(uniqueComponentName == null) + if (uniqueComponentName == null) { return null; } - if(!uniqueComponentName.Contains("@")) + if (!uniqueComponentName.Contains("@")) { return uniqueComponentName; } @@ -79,11 +79,11 @@ public static string OriginalComponentName(string uniqueComponentName) internal static string UniqueComponentName(Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) { var result = ""; - if(service.ImplementationType != null) + if (service.ImplementationType != null) { result = service.ImplementationType.FullName; } - else if(service.ImplementationInstance != null) + else if (service.ImplementationInstance != null) { result = service.ImplementationInstance.GetType().FullName; } @@ -98,7 +98,8 @@ internal static string UniqueComponentName(Microsoft.Extensions.DependencyInject private static ComponentRegistration UsingFactoryMethod(ComponentRegistration registration, Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) where TService : class { - return registration.UsingFactoryMethod((kernel) => { + return registration.UsingFactoryMethod((kernel) => + { var serviceProvider = kernel.Resolve(); return service.ImplementationFactory(serviceProvider) as TService; }); @@ -116,7 +117,7 @@ private static ComponentRegistration UsingImplementation(Com private static ComponentRegistration ResolveLifestyle(ComponentRegistration registration, Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) where TService : class { - switch(service.Lifetime) + switch (service.Lifetime) { case ServiceLifetime.Singleton: return registration.LifeStyle.NetStatic(); @@ -124,7 +125,7 @@ private static ComponentRegistration ResolveLifestyle(Compon return registration.LifeStyle.ScopedToNetServiceScope(); case ServiceLifetime.Transient: return registration.LifestyleNetTransient(); - + default: throw new System.ArgumentException($"Invalid lifetime {service.Lifetime}"); } diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerRootScopeAccessor.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerRootScopeAccessor.cs index 25139efec..62dc0d763 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerRootScopeAccessor.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerRootScopeAccessor.cs @@ -12,22 +12,24 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Castle.Windsor.Extensions.DependencyInjection.Scope -{ +namespace Castle.Windsor.Extensions.DependencyInjection.Scope { using System; using Castle.MicroKernel.Context; using Castle.MicroKernel.Lifestyle.Scoped; - internal class ExtensionContainerRootScopeAccessor : IScopeAccessor - { - public ILifetimeScope GetScope(CreationContext context) - { - return ExtensionContainerScopeCache.Current.RootScope ?? throw new InvalidOperationException("No root scope available"); + internal class ExtensionContainerRootScopeAccessor : IScopeAccessor { + public ILifetimeScope GetScope(CreationContext context) { + /* + if (ExtensionContainerScopeCache.Current == null) { + // might be null in threads spawn from Threadpool.UnsafeQueueUserWorkItem + return null; + } + */ + return ExtensionContainerScopeCache.Current?.RootScope ?? throw new InvalidOperationException("No root scope available."); } - public void Dispose() - { + public void Dispose() { } } } diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScope.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScope.cs index 2ff5a5c49..9ad99907f 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScope.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScope.cs @@ -25,10 +25,9 @@ protected ExtensionContainerScope() internal override ExtensionContainerScopeBase RootScope { get; set; } - internal static ExtensionContainerScopeBase BeginScope() { - var scope = new ExtensionContainerScope { RootScope = ExtensionContainerScopeCache.Current.RootScope }; + var scope = new ExtensionContainerScope { RootScope = ExtensionContainerScopeCache.Current?.RootScope }; ExtensionContainerScopeCache.Current = scope; return scope; } diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeAccessor.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeAccessor.cs index 9042944d7..8eaaa9f1d 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeAccessor.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeAccessor.cs @@ -16,12 +16,13 @@ namespace Castle.Windsor.Extensions.DependencyInjection.Scope { using Castle.MicroKernel.Context; using Castle.MicroKernel.Lifestyle.Scoped; + using System; internal class ExtensionContainerScopeAccessor : IScopeAccessor { public ILifetimeScope GetScope(CreationContext context) { - return ExtensionContainerScopeCache.Current; + return ExtensionContainerScopeCache.Current ?? throw new InvalidOperationException("No scope available. Did you forget to call IServiceScopeFactory.CreateScope()?"); ; } public void Dispose() diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeCache.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeCache.cs index d89f9dd1f..cc48a8032 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeCache.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeCache.cs @@ -24,7 +24,8 @@ internal static class ExtensionContainerScopeCache /// Thrown when there is no scope available. internal static ExtensionContainerScopeBase Current { - get => current.Value ?? throw new InvalidOperationException("No scope available"); + // AysncLocal can be null in some cases (like Threadpool.UnsafeQueueUserWorkItem) + get => current.Value; // ?? throw new InvalidOperationException("No scope available. Did you forget to call IServiceScopeFactory.CreateScope()?"); set => current.Value = value; } } diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/WindsorDependencyInjectionOptions.cs b/src/Castle.Windsor.Extensions.DependencyInjection/WindsorDependencyInjectionOptions.cs new file mode 100644 index 000000000..ab1ec62ef --- /dev/null +++ b/src/Castle.Windsor.Extensions.DependencyInjection/WindsorDependencyInjectionOptions.cs @@ -0,0 +1,16 @@ +namespace Castle.Windsor.Extensions.DependencyInjection +{ + /// + /// Global settins to change the dependency injection behavior. + /// These settings should be set before the container is created. + /// + public static class WindsorDependencyInjectionOptions + { + /// + /// Map NetStatic lifestyle to Castle Windsor Singleton lifestyle. + /// The whole RootScope handling is disabled. + /// (defaut: false) + /// + public static bool MapNetStaticToSingleton { get; set; } + } +} diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/WindsorScopedServiceProvider.cs b/src/Castle.Windsor.Extensions.DependencyInjection/WindsorScopedServiceProvider.cs index 4e61f2f76..006c47532 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/WindsorScopedServiceProvider.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/WindsorScopedServiceProvider.cs @@ -18,7 +18,7 @@ namespace Castle.Windsor.Extensions.DependencyInjection using System; using System.Collections.Generic; using System.Reflection; - + using Castle.Windsor; using Castle.Windsor.Extensions.DependencyInjection.Scope; @@ -30,7 +30,7 @@ internal class WindsorScopedServiceProvider : IServiceProvider, ISupportRequired private bool disposing; private readonly IWindsorContainer container; - + public WindsorScopedServiceProvider(IWindsorContainer container) { this.container = container; @@ -39,27 +39,30 @@ public WindsorScopedServiceProvider(IWindsorContainer container) public object GetService(Type serviceType) { - using(_ = new ForcedScope(scope)) + using (_ = new ForcedScope(scope)) { - return ResolveInstanceOrNull(serviceType, true); + return ResolveInstanceOrNull(serviceType, true); } } public object GetRequiredService(Type serviceType) { - using(_ = new ForcedScope(scope)) + using (_ = new ForcedScope(scope)) { - return ResolveInstanceOrNull(serviceType, false); + return ResolveInstanceOrNull(serviceType, false); } } public void Dispose() { + // root scope should be tied to the root IserviceProvider, so + // it has to be disposed with the IserviceProvider to which is tied to if (!(scope is ExtensionContainerRootScope)) return; if (disposing) return; disposing = true; var disposableScope = scope as IDisposable; disposableScope?.Dispose(); + // disping the container here is questionable... what if I want to create another IServiceProvider form the factory? container.Dispose(); } private object ResolveInstanceOrNull(Type serviceType, bool isOptional)