Skip to content

Commit 1830955

Browse files
committed
Fixed bug in missing root scope with orleans/kestrel.
The previous handling of root scope is wrong, the code set root scope in an AsyncLocal variable when WindsorServiceProviderFactoryBase was first creatd. The problem arise with kestrel or orleans in .NET 8, they use a Thread Pool that runs code outside AsyncLocal so it will break resolution. It was not possible to reproduce locally, but it was reproduced with production code. A repro for the bug still missing. Also we can support using a Global root scope only if we use only ONE CONTAINER in the .NET core DI, because basic structure does not allow to find the container that is resolving scoped component thus we cannot determin the right root context. Still work to do to support multiple container.
1 parent 3707b52 commit 1830955

File tree

8 files changed

+351
-37
lines changed

8 files changed

+351
-37
lines changed

src/Castle.Windsor.Extensions.DependencyInjection.Tests/CustomAssumptionTests.cs

Lines changed: 285 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
#if NET8_0_OR_GREATER
22
using Microsoft.Extensions.DependencyInjection;
3+
using Microsoft.Extensions.DependencyInjection.Specification.Fakes;
34
using System;
45
using System.Linq;
6+
using System.Threading;
7+
using System.Threading.Tasks;
58
using Xunit;
69

710
namespace Castle.Windsor.Extensions.DependencyInjection.Tests
@@ -36,28 +39,232 @@ public void Resolve_All()
3639
Assert.IsType<AnotherTestService>(keyedServices.Last());
3740
}
3841

42+
[Fact]
43+
public void Scoped_keyed_service_resolved_by_thread_outside_scope()
44+
{
45+
Boolean stop = false;
46+
Boolean shouldResolve = false;
47+
ITestService resolvedInThread = null;
48+
var thread = new Thread(_ =>
49+
{
50+
while (!stop)
51+
{
52+
Thread.Sleep(100);
53+
if (shouldResolve)
54+
{
55+
stop = true;
56+
resolvedInThread = _serviceProvider.GetRequiredKeyedService<ITestService>("porcodio");
57+
}
58+
}
59+
});
60+
thread.Start();
61+
62+
var serviceCollection = GetServiceCollection();
63+
serviceCollection.AddKeyedScoped<ITestService, TestService>("porcodio");
64+
_serviceProvider = BuildServiceProvider(serviceCollection);
65+
66+
//resolved outside scope
67+
ITestService resolvedOutsideScope = _serviceProvider.GetRequiredKeyedService<ITestService>("porcodio");
68+
69+
// resolve in scope
70+
ITestService resolvedInScope;
71+
using (var scope = _serviceProvider.CreateScope())
72+
{
73+
resolvedInScope = scope.ServiceProvider.GetRequiredKeyedService<ITestService>("porcodio");
74+
}
75+
76+
shouldResolve = true;
77+
//now wait for the original thread to finish
78+
thread.Join(1000 * 10);
79+
Assert.NotNull(resolvedInThread);
80+
Assert.NotNull(resolvedOutsideScope);
81+
Assert.NotNull(resolvedInScope);
82+
83+
Assert.NotEqual(resolvedInScope, resolvedOutsideScope);
84+
Assert.NotEqual(resolvedInScope, resolvedInThread);
85+
Assert.Equal(resolvedOutsideScope, resolvedInThread);
86+
}
87+
88+
[Fact]
89+
public void Scoped_service_resolved_outside_scope()
90+
{
91+
var serviceCollection = GetServiceCollection();
92+
serviceCollection.AddScoped<ITestService, TestService>();
93+
_serviceProvider = BuildServiceProvider(serviceCollection);
94+
95+
//resolved outside scope
96+
ITestService resolvedOutsideScope = _serviceProvider.GetRequiredService<ITestService>();
97+
Assert.NotNull(resolvedOutsideScope);
98+
99+
// resolve in scope
100+
ITestService resolvedInScope;
101+
using (var scope = _serviceProvider.CreateScope())
102+
{
103+
resolvedInScope = scope.ServiceProvider.GetRequiredService<ITestService>();
104+
}
105+
Assert.NotNull(resolvedInScope);
106+
Assert.NotEqual(resolvedInScope, resolvedOutsideScope);
107+
108+
ITestService resolvedAgainOutsideScope = _serviceProvider.GetRequiredService<ITestService>();
109+
Assert.NotNull(resolvedAgainOutsideScope);
110+
Assert.Equal(resolvedOutsideScope, resolvedAgainOutsideScope);
111+
}
112+
113+
[Fact]
114+
public void Scoped_service_resolved_outside_scope_in_another_thread()
115+
{
116+
var serviceCollection = GetServiceCollection();
117+
serviceCollection.AddScoped<ITestService, TestService>();
118+
_serviceProvider = BuildServiceProvider(serviceCollection);
119+
120+
var task = Task.Run(() =>
121+
{
122+
//resolved outside scope
123+
ITestService resolvedOutsideScope = _serviceProvider.GetRequiredService<ITestService>();
124+
Assert.NotNull(resolvedOutsideScope);
125+
126+
// resolve in scope
127+
ITestService resolvedInScope;
128+
using (var scope = _serviceProvider.CreateScope())
129+
{
130+
resolvedInScope = scope.ServiceProvider.GetRequiredService<ITestService>();
131+
}
132+
Assert.NotNull(resolvedInScope);
133+
Assert.NotEqual(resolvedInScope, resolvedOutsideScope);
134+
135+
ITestService resolvedAgainOutsideScope = _serviceProvider.GetRequiredService<ITestService>();
136+
Assert.NotNull(resolvedAgainOutsideScope);
137+
Assert.Equal(resolvedOutsideScope, resolvedAgainOutsideScope);
138+
return true;
139+
});
140+
141+
Assert.True(task.Result);
142+
}
143+
144+
[Fact]
145+
public async void Scoped_service_resolved_outside_scope_in_another_unsafe_thread()
146+
{
147+
var serviceCollection = GetServiceCollection();
148+
serviceCollection.AddScoped<ITestService, TestService>();
149+
_serviceProvider = BuildServiceProvider(serviceCollection);
150+
151+
var tsc = new TaskCompletionSource();
152+
var worker = new QueueUserWorkItemWorker(_serviceProvider, tsc);
153+
ThreadPool.UnsafeQueueUserWorkItem(worker, false);
154+
await tsc.Task;
155+
156+
Assert.Null(worker.ExecuteException);
157+
Assert.NotNull(worker.ResolvedOutsideScope);
158+
Assert.NotNull(worker.ResolvedInScope);
159+
Assert.NotEqual(worker.ResolvedInScope, worker.ResolvedOutsideScope);
160+
}
161+
162+
[Fact]
163+
public async void Simulate_async_timer_without_wait()
164+
{
165+
Boolean stop = false;
166+
Boolean shouldResolve = false;
167+
ITestService resolvedInThread = null;
168+
async Task ExecuteAsync()
169+
{
170+
while (!stop)
171+
{
172+
await Task.Delay(100);
173+
if (shouldResolve)
174+
{
175+
stop = true;
176+
resolvedInThread = _serviceProvider.GetService<ITestService>();
177+
}
178+
}
179+
}
180+
//fire and forget
181+
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
182+
var task = ExecuteAsync();
183+
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
184+
185+
await Task.Delay(500);
186+
187+
var serviceCollection = GetServiceCollection();
188+
serviceCollection.AddScoped<ITestService, TestService>();
189+
_serviceProvider = BuildServiceProvider(serviceCollection);
190+
191+
//resolved outside scope
192+
ITestService resolvedOutsideScope = _serviceProvider.GetRequiredService<ITestService>();
193+
194+
// resolve in scope
195+
ITestService resolvedInScope;
196+
using (var scope = _serviceProvider.CreateScope())
197+
{
198+
resolvedInScope = scope.ServiceProvider.GetRequiredService<ITestService>();
199+
}
200+
201+
shouldResolve = true;
202+
await task;
203+
Assert.NotNull(resolvedInThread);
204+
Assert.NotNull(resolvedOutsideScope);
205+
Assert.NotNull(resolvedInScope);
206+
207+
Assert.NotEqual(resolvedInScope, resolvedOutsideScope);
208+
Assert.NotEqual(resolvedInScope, resolvedInThread);
209+
Assert.Equal(resolvedOutsideScope, resolvedInThread);
210+
}
211+
212+
private class QueueUserWorkItemWorker : IThreadPoolWorkItem
213+
{
214+
private readonly IServiceProvider _provider;
215+
private readonly TaskCompletionSource _taskCompletionSource;
216+
217+
public QueueUserWorkItemWorker(IServiceProvider provider, TaskCompletionSource taskCompletionSource)
218+
{
219+
_provider = provider;
220+
_taskCompletionSource = taskCompletionSource;
221+
}
222+
223+
public ITestService ResolvedOutsideScope { get; private set; }
224+
public ITestService ResolvedInScope { get; private set; }
225+
public Exception ExecuteException { get; private set; }
226+
227+
public void Execute()
228+
{
229+
try
230+
{
231+
ResolvedOutsideScope = _provider.GetService<ITestService>();
232+
using (var scope = _provider.CreateScope())
233+
{
234+
ResolvedInScope = scope.ServiceProvider.GetRequiredService<ITestService>();
235+
}
236+
}
237+
catch (Exception ex)
238+
{
239+
ExecuteException = ex;
240+
}
241+
242+
_taskCompletionSource.SetResult();
243+
}
244+
}
245+
39246
protected abstract IServiceCollection GetServiceCollection();
40247

41248
protected abstract IServiceProvider BuildServiceProvider(IServiceCollection serviceCollection);
42249

43-
public void Dispose()
44-
{
45-
Dispose(true);
46-
GC.SuppressFinalize(this);
47-
}
48-
49-
protected virtual void Dispose(bool disposing)
50-
{
51-
if (disposing)
52-
{
53-
// Dispose managed resources
54-
if (_serviceProvider is IDisposable disposable)
55-
{
56-
disposable.Dispose();
57-
}
58-
}
59-
// Dispose unmanaged resources
60-
}
250+
public void Dispose()
251+
{
252+
Dispose(true);
253+
GC.SuppressFinalize(this);
254+
}
255+
256+
protected virtual void Dispose(bool disposing)
257+
{
258+
if (disposing)
259+
{
260+
// Dispose managed resources
261+
if (_serviceProvider is IDisposable disposable)
262+
{
263+
disposable.Dispose();
264+
}
265+
}
266+
// Dispose unmanaged resources
267+
}
61268
}
62269

63270
public class RealCustomAssumptionTests : CustomAssumptionTests
@@ -75,6 +282,8 @@ protected override IServiceProvider BuildServiceProvider(IServiceCollection serv
75282

76283
public class CastleWindsorCustomAssumptionTests : CustomAssumptionTests
77284
{
285+
private IWindsorContainer _container;
286+
78287
protected override IServiceCollection GetServiceCollection()
79288
{
80289
return new TestServiceCollection();
@@ -83,8 +292,64 @@ protected override IServiceCollection GetServiceCollection()
83292
protected override IServiceProvider BuildServiceProvider(IServiceCollection serviceCollection)
84293
{
85294
var factory = new WindsorServiceProviderFactory();
86-
var container = factory.CreateBuilder(serviceCollection);
87-
return factory.CreateServiceProvider(container);
295+
_container = factory.CreateBuilder(serviceCollection);
296+
return factory.CreateServiceProvider(_container);
297+
}
298+
299+
[Fact]
300+
public void Try_to_resolve_scoped_directly_with_castle_windsor_container()
301+
{
302+
var serviceCollection = GetServiceCollection();
303+
serviceCollection.AddScoped<ITestService, TestService>();
304+
var provider = BuildServiceProvider(serviceCollection);
305+
306+
//resolved outside scope
307+
ITestService resolvedOutsideScope = _container.Resolve<ITestService>();
308+
Assert.NotNull(resolvedOutsideScope);
309+
310+
// resolve in scope
311+
ITestService resolvedInScope;
312+
using (var scope = provider.CreateScope())
313+
{
314+
resolvedInScope = _container.Resolve<ITestService>();
315+
}
316+
Assert.NotNull(resolvedInScope);
317+
Assert.NotEqual(resolvedInScope, resolvedOutsideScope);
318+
319+
ITestService resolvedAgainOutsideScope = _container.Resolve<ITestService>();
320+
Assert.NotNull(resolvedAgainOutsideScope);
321+
Assert.Equal(resolvedOutsideScope, resolvedAgainOutsideScope);
322+
}
323+
324+
[Fact]
325+
public void TryToResolveScopedInOtherThread()
326+
{
327+
var serviceCollection = GetServiceCollection();
328+
serviceCollection.AddScoped<ITestService, TestService>();
329+
var provider = BuildServiceProvider(serviceCollection);
330+
331+
var task = Task.Run(() =>
332+
{
333+
//resolved outside scope
334+
ITestService resolvedOutsideScope = _container.Resolve<ITestService>();
335+
Assert.NotNull(resolvedOutsideScope);
336+
337+
// resolve in scope
338+
ITestService resolvedInScope;
339+
using (var scope = provider.CreateScope())
340+
{
341+
resolvedInScope = _container.Resolve<ITestService>();
342+
}
343+
Assert.NotNull(resolvedInScope);
344+
Assert.NotEqual(resolvedInScope, resolvedOutsideScope);
345+
346+
ITestService resolvedAgainOutsideScope = _container.Resolve<ITestService>();
347+
Assert.NotNull(resolvedAgainOutsideScope);
348+
Assert.Equal(resolvedOutsideScope, resolvedAgainOutsideScope);
349+
return true;
350+
});
351+
352+
Assert.True(task.Result);
88353
}
89354
}
90355

src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using Castle.MicroKernel.Lifestyle;
1+
using Castle.MicroKernel;
2+
using Castle.MicroKernel.Lifestyle;
23
using Castle.MicroKernel.Registration;
34
using Castle.Windsor.Extensions.DependencyInjection.Extensions;
45
using Castle.Windsor.Extensions.DependencyInjection.Tests.Components;
@@ -512,8 +513,8 @@ public async Task Cannot_Resolve_LifestyleScopedToNetServiceScope_From_WindsorCo
512513
});
513514

514515
Assert.NotNull(ex);
515-
Assert.IsType<InvalidOperationException>(ex);
516-
Assert.StartsWith("No scope available", ex.Message);
516+
Assert.IsType<ComponentResolutionException>(ex);
517+
Assert.StartsWith("Could not obtain scope for component", ex.Message);
517518

518519
(sp as IDisposable)?.Dispose();
519520
container.Dispose();
@@ -711,8 +712,8 @@ public async Task Cannot_Resolve_LifestyleNetTransient_From_WindsorContainer_NoS
711712
});
712713

713714
Assert.NotNull(ex);
714-
Assert.IsType<InvalidOperationException>(ex);
715-
Assert.StartsWith("No scope available", ex.Message);
715+
Assert.IsType<ComponentResolutionException>(ex);
716+
Assert.StartsWith("Could not obtain scope for component", ex.Message);
716717

717718
(sp as IDisposable)?.Dispose();
718719
container.Dispose();

src/Castle.Windsor.Extensions.DependencyInjection/Resolvers/LoggerDependencyResolver.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ namespace Castle.Windsor.Extensions.DependencyInjection.Resolvers
1818
using Castle.Core;
1919
using Castle.MicroKernel;
2020
using Castle.MicroKernel.Context;
21-
21+
2222
using Microsoft.Extensions.Logging;
2323

2424
public class LoggerDependencyResolver : ISubDependencyResolver

0 commit comments

Comments
 (0)