Skip to content

Commit

Permalink
Readded support for ambient context-less scopes. Related to #532
Browse files Browse the repository at this point in the history
  • Loading branch information
dotnetjunkie committed Apr 14, 2018
1 parent 6ba6413 commit a83ae9e
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 5 deletions.
123 changes: 123 additions & 0 deletions src/SimpleInjector.Tests.Unit/ScopedLifestyleTests.cs
Expand Up @@ -969,6 +969,107 @@ public void GetInstance_RequestingScopeWithActiveLifetimeScopeAndDefaultScopedLi
Assert.AreSame(activeScope, resolvedScope);
}

[TestMethod]
public void GetInstance_ResolvingScopedDependencyDirectlyFromScope_ResolvesTheInstanceAsScoped()
{
// Arrange
var container = ContainerFactory.New();

// We need a 'dummy' scoped lifestyle to be able to use Lifestyle.Scoped
container.Options.DefaultScopedLifestyle = new AmbientlessScopedLifestyle();

container.Register<ILogger, NullLogger>(Lifestyle.Scoped);

var scope1 = new Scope(container);
var scope2 = new Scope(container);

// Act
var s1 = scope1.GetInstance<ServiceDependingOn<ILogger>>();
var s2 = scope1.GetInstance<ServiceDependingOn<ILogger>>();
var s3 = scope2.GetInstance<ServiceDependingOn<ILogger>>();

// Assert
Assert.AreSame(s1.Dependency, s2.Dependency, "Logger was expected to be scoped but was transient.");
Assert.AreNotSame(s3.Dependency, s2.Dependency, "Logger was expected to be scoped but was singleton.");
}

[TestMethod]
public void GetInstance_LambdaThatCallsBackIntoContainerExecutedFromScopeResolve_ResolvesTheInstanceAsScoped()
{
// Arrange
var container = ContainerFactory.New();

container.Options.DefaultScopedLifestyle = new AmbientlessScopedLifestyle();

// Calling back into the container to get a scoped instance, from within an instanceCreator lambda,
// should work, in case the the root object is resolved from a scope.
container.Register<ILogger>(() => container.GetInstance<NullLogger>());
container.Register<NullLogger>(Lifestyle.Scoped);

var scope = new Scope(container);

// Act
var s1 = scope.GetInstance<ServiceDependingOn<ILogger>>();
var s2 = scope.GetInstance<ServiceDependingOn<ILogger>>();

// Assert
Assert.AreSame(s1.Dependency, s2.Dependency, "Logger was expected to be scoped.");
}

[TestMethod]
public void GetInstance_ResolvingAnInstanceDependingOnScopeWithAnActiveLifetimeScopeButNoDefaultScopedLifestyleSet_Throws()
{
// Arrange
var container = ContainerFactory.New();

Scope activeScope = ThreadScopedLifestyle.BeginScope(container);

// Act
Action action = () => container.GetInstance<ServiceDependingOn<Scope>>();

// Assert
AssertThat.ThrowsWithExceptionMessageContains<ActivationException>(@"
you need to either resolve instances directly from the Scope using a Scope.GetInstance
overload, or you will have to set the Container.Options.DefaultScopedLifestyle property with
the required scoped lifestyle"
.TrimInside(),
action);
}

[TestMethod]
public void GetInstance_RequestingScopeWithActiveLifetimeScopeButNoDefaultScopedLifestyleSet_Throws()
{
// Arrange
var container = ContainerFactory.New();

Scope activeScope = ThreadScopedLifestyle.BeginScope(container);

// Act
Action action = () => container.GetInstance<Scope>();

// Assert
AssertThat.ThrowsWithExceptionMessageContains<ActivationException>(@"
resolve instances directly from the Scope using a Scope.GetInstance
overload, or you will have to set the Container.Options.DefaultScopedLifestyle property"
.TrimInside(),
action);
}

[TestMethod]
public void GetInstance_ResolvingAnInstanceDependingOnScopeWithoutAnActiveScopeAndWithoutDefaultScopedLifestyleSet_Throws()
{
// Arrange
var container = ContainerFactory.New();

// Act
Action action = () => container.GetInstance<ServiceDependingOn<Scope>>();

// Act
AssertThat.ThrowsWithExceptionMessageContains<ActivationException>(
"you will have to set the Container.Options.DefaultScopedLifestyle property",
action);
}

[TestMethod]
public void GetInstance_ResolvingAnInstanceDependingOnScopeWithoutAnActiveScopeAndDefaultScopedLifestyleSet_Throws()
{
Expand Down Expand Up @@ -996,6 +1097,22 @@ public void MethodUnderTest_Scenario_Behavior()
container.Verify();
}

[TestMethod]
public void ScopeGetInstance_ResolvingAnInstanceDependingOnScope_InjectsThatActiveScope()
{
// Arrange
var container = ContainerFactory.New();

var activeScope = new Scope(container);

// Act
var service = activeScope.GetInstance<ServiceDependingOn<Scope>>();
Scope injectedScope = service.Dependency;

// Assert
Assert.AreSame(activeScope, injectedScope);
}

private class DisposablePlugin : IPlugin, IDisposable
{
private readonly Action<DisposablePlugin> disposing;
Expand Down Expand Up @@ -1070,5 +1187,11 @@ public FakeScopedLifestyle(Scope scope)

protected internal override Func<Scope> CreateCurrentScopeProvider(Container c) => () => this.scope;
}

private sealed class AmbientlessScopedLifestyle : ScopedLifestyle
{
public AmbientlessScopedLifestyle() : base("Scoped") { }
protected internal override Func<Scope> CreateCurrentScopeProvider(Container c) => () => null;
}
}
}
2 changes: 1 addition & 1 deletion src/SimpleInjector/Advanced/Internal/LazyScope.cs
Expand Up @@ -56,7 +56,7 @@ public Scope Value
{
if (this.scopeFactory != null)
{
this.value = this.container.GetVerificationScopeForCurrentThread() ?? this.scopeFactory.Invoke();
this.value = this.container.GetVerificationOrResolveScopeForCurrentThread() ?? this.scopeFactory.Invoke();
this.scopeFactory = null;
}

Expand Down
32 changes: 30 additions & 2 deletions src/SimpleInjector/Container.Verification.cs
Expand Up @@ -40,11 +40,37 @@ public partial class Container
// Flag to signal that the container's configuration is currently being verified.
private readonly ThreadLocal<bool> isVerifying = new ThreadLocal<bool>();

private readonly ThreadLocal<Scope> resolveScope = new ThreadLocal<Scope>();

private bool usingCurrentThreadResolveScope;

// Flag to signal that the container's configuration has been verified (at least once).
internal bool SuccesfullyVerified { get; private set; }

internal Scope VerificationScope { get; private set; }

// Allows to resolve directly from a scope instead of relying on an ambient context.
// TODO: Optimize performance for the common scenario where the resolveScope is never used.
internal Scope CurrentThreadResolveScope
{
get
{
return this.resolveScope.Value;
}

set
{
// PERF: We flag the use of the current-thread-resolve-scope to optimize getting the right
// scope. Most application's won't resolve directly from the scope, but from the container.
if (!this.usingCurrentThreadResolveScope)
{
this.usingCurrentThreadResolveScope = true;
}

this.resolveScope.Value = value;
}
}

/// <summary>
/// Verifies and diagnoses this <b>Container</b> instance. This method will call all registered
/// delegates, iterate registered collections and throws an exception if there was an error.
Expand Down Expand Up @@ -87,10 +113,12 @@ public void Verify(VerificationOption option)
// different thread.
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
#endif
internal Scope GetVerificationScopeForCurrentThread() =>
internal Scope GetVerificationOrResolveScopeForCurrentThread() =>
this.VerificationScope != null && this.IsVerifying
? this.VerificationScope
: null;
: this.usingCurrentThreadResolveScope
? this.resolveScope.Value
: null;

private void VerifyInternal(bool suppressLifestyleMismatchVerification)
{
Expand Down
18 changes: 17 additions & 1 deletion src/SimpleInjector/Lifestyles/ScopedScopeLifestyle.cs
Expand Up @@ -33,7 +33,7 @@ internal ScopedScopeLifestyle() : base("Scoped")
{
}

protected internal override Func<Scope> CreateCurrentScopeProvider(Container c) =>
protected internal override Func<Scope> CreateCurrentScopeProvider(Container c) =>
() => this.GetScopeFromDefaultScopedLifestyle(c);

protected override Scope GetCurrentScopeCore(Container c) => this.GetScopeFromDefaultScopedLifestyle(c);
Expand All @@ -47,9 +47,25 @@ private Scope GetScopeFromDefaultScopedLifestyle(Container container)
return lifestyle.GetCurrentScope(container) ?? ThrowThereIsNoActiveScopeException();
}

if (container.GetVerificationOrResolveScopeForCurrentThread() == null)
{
ThrowResolveFromScopeOrRegisterDefaultScopedLifestyleException();
}

// We return null instead of one of the scope returned from the previous method call,
// since the CreateCurrentScopeProvider contract states that we should return null.
return null;
}

private static void ThrowResolveFromScopeOrRegisterDefaultScopedLifestyleException()
{
throw new InvalidOperationException(
"To be able to resolve and inject Scope instances, you need to either resolve " +
"instances directly from the Scope using a Scope.GetInstance overload, or you will " +
"have to set the Container.Options.DefaultScopedLifestyle property with the required " +
"scoped lifestyle for your type of application.");
}

private static Scope ThrowThereIsNoActiveScopeException()
{
throw new InvalidOperationException("There is no active scope.");
Expand Down
35 changes: 35 additions & 0 deletions src/SimpleInjector/Scope.cs
Expand Up @@ -84,6 +84,41 @@ private enum DisposeState

internal Scope ParentScope { get; }

/// <summary>Gets an instance of the given <typeparamref name="TService"/> for the current scope.</summary>
/// <typeparam name="TService">The type of the service to resolve.</typeparam>
/// <returns>An instance of the given service type.</returns>
public TService GetInstance<TService>() where TService : class
{
return (TService)this.GetInstance(typeof(TService));
}

/// <summary>Gets an instance of the given <paramref name="serviceType" /> for the current scope.</summary>
/// <param name="serviceType">The type of the service to resolve.</param>
/// <returns>An instance of the given service type.</returns>
public object GetInstance(Type serviceType)
{
Requires.IsNotNull(serviceType, nameof(serviceType));

if (this.Container == null)
{
throw new InvalidOperationException(
"This method can only be called on Scope instances that are related to a Container. " +
"Please use the overloaded constructor of Scope create an instance with a Container.");
}

Scope originalScope = this.Container.CurrentThreadResolveScope;

try
{
this.Container.CurrentThreadResolveScope = this;
return this.Container.GetInstance(serviceType);
}
finally
{
this.Container.CurrentThreadResolveScope = originalScope;
}
}

/// <summary>
/// Allows registering an <paramref name="action"/> delegate that will be called when the scope ends,
/// but before the scope disposes any instances.
Expand Down
2 changes: 1 addition & 1 deletion src/SimpleInjector/ScopedLifestyle.cs
Expand Up @@ -204,7 +204,7 @@ private Scope GetCurrentScopeInternal(Container container)
{
// If we are running verification in the current thread, we prefer returning a verification scope
// over a real active scope (issue #95).
return container.GetVerificationScopeForCurrentThread()
return container.GetVerificationOrResolveScopeForCurrentThread()
?? this.GetCurrentScopeCore(container);
}

Expand Down

0 comments on commit a83ae9e

Please sign in to comment.