Skip to content

Commit

Permalink
ScopedLifestyle.Flowing added to simplify #532.
Browse files Browse the repository at this point in the history
  • Loading branch information
dotnetjunkie committed Apr 15, 2018
1 parent cb40090 commit 7e31875
Show file tree
Hide file tree
Showing 8 changed files with 73 additions and 31 deletions.
8 changes: 1 addition & 7 deletions src/SimpleInjector.Tests.Unit/DecoratorTests.cs
Expand Up @@ -2478,7 +2478,7 @@ public void InjectedScopeDecorateeFactory_WhenSuppliedWithAScopeInstance_Creates
{
// Arrange
var container = ContainerFactory.New();
container.Options.DefaultScopedLifestyle = new AmbientlessScopedLifestyle();
container.Options.DefaultScopedLifestyle = ScopedLifestyle.Flowing;

container.Register<ICommandHandler<int>, NullCommandHandler<int>>(Lifestyle.Scoped);
container.RegisterDecorator(typeof(ICommandHandler<>), typeof(ScopedCommandHandlerProxy<>),
Expand Down Expand Up @@ -2524,12 +2524,6 @@ public ScopedCommandHandlerProxy(Func<Scope, ICommandHandler<T>> decorateeFactor
this.DecorateeFactory = decorateeFactory;
}
}

private sealed class AmbientlessScopedLifestyle : ScopedLifestyle
{
public AmbientlessScopedLifestyle() : base("Scoped") { }
protected internal override Func<Scope> CreateCurrentScopeProvider(Container c) => () => null;
}
}

public static class ContainerTestExtensions
Expand Down
13 changes: 4 additions & 9 deletions src/SimpleInjector.Tests.Unit/ScopedLifestyleTests.cs
Expand Up @@ -749,7 +749,7 @@ public void Verify_ExecutedWithinActiveScope_CreatesScopedInstancesInItsOwnScope
plugin = null;

// Act
// When calling verify, we expect DisposablePlugin to be created again.
// When calling verify, we expect DisposablePlugin to be created again, because verify gets its own scope
container.Verify();

// Assert
Expand Down Expand Up @@ -976,7 +976,7 @@ public void GetInstance_ResolvingScopedDependencyDirectlyFromScope_ResolvesTheIn
var container = ContainerFactory.New();

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

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

Expand All @@ -999,12 +999,13 @@ public void GetInstance_LambdaThatCallsBackIntoContainerExecutedFromScopeResolve
// Arrange
var container = ContainerFactory.New();

container.Options.DefaultScopedLifestyle = new AmbientlessScopedLifestyle();
container.Options.DefaultScopedLifestyle = ScopedLifestyle.Flowing;

// 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);
container.Register<ServiceDependingOn<ILogger>>();

var scope = new Scope(container);

Expand Down Expand Up @@ -1187,11 +1188,5 @@ 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;
}
}
}
8 changes: 6 additions & 2 deletions src/SimpleInjector/Container.Verification.cs
Expand Up @@ -50,12 +50,11 @@ public partial class Container
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;
return this.usingCurrentThreadResolveScope ? this.resolveScope.Value : null;
}

set
Expand Down Expand Up @@ -120,6 +119,11 @@ public void Verify(VerificationOption option)
? this.resolveScope.Value
: null;

internal void UseCurrentThreadResolveScope()
{
this.usingCurrentThreadResolveScope = true;
}

private void VerifyInternal(bool suppressLifestyleMismatchVerification)
{
// Prevent multiple threads from starting verification at the same time. This could crash, because
Expand Down
Expand Up @@ -163,7 +163,6 @@ private Registration CreateRegistrationForUncontrolledCollection(Expression deco

private OverriddenParameter[] CreateOverriddenParameters(Expression decorateeExpression)
{
// TODO: Check this
ParameterInfo decorateeParameter =
GetDecorateeParameter(this.registeredServiceType, this.decoratorConstructor);

Expand Down
5 changes: 2 additions & 3 deletions src/SimpleInjector/Internals/CompilationHelpers.cs
Expand Up @@ -98,7 +98,6 @@ internal static Func<TResult> CompileExpression<TResult>(Container container, Ex
return compiledLambda ?? CompileLambda(resultType, expression);
}

// TODO: If we make this method a no-op, which unit tests do break? Does the new ambient-less scoping test break?
[MethodImpl(MethodImplOptions.NoInlining)]
internal static Expression OptimizeScopedRegistrationsInObjectGraph(Container container,
Expression expression)
Expand Down Expand Up @@ -142,8 +141,8 @@ internal static Delegate CompileLambda(Type resultType, Expression expression)
// Func<HomeController> factory = () =>
// {
// var scope1 = new LazyScope(scopeFactory1, container);
// var value1 = new LazyScopedRegistration<IRepository, RepositoryImpl>(reg1);
// var value2 = new LazyScopedRegistration<IService, ServiceImpl>(reg2);
// var value1 = new LazyScopedRegistration<RepositoryImpl>(reg1);
// var value2 = new LazyScopedRegistration<ServiceImpl>(reg2);
//
// return new HomeController(
// value1.GetInstance(scope1.Value), // Hits ThreadLocal, hits dictionary
Expand Down
49 changes: 49 additions & 0 deletions src/SimpleInjector/Lifestyles/FlowingScopedLifestyle.cs
@@ -0,0 +1,49 @@
#region Copyright Simple Injector Contributors
/* The Simple Injector is an easy-to-use Inversion of Control library for .NET
*
* Copyright (c) 2018 Simple Injector Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
* associated documentation files (the "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
* following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial
* portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
* LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
* EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
* USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#endregion

namespace SimpleInjector.Lifestyles
{
using System;

/// <summary>
/// This lifestyle can be used to implement ambient context-less scoping in Simple Injector. This lifestyle
/// can be set as DefaultScopedLifestyle and later used via Lifestyle.Scoped to register scoped instances,
/// while instances are resolved via Scope.GetInstance.
/// </summary>
internal sealed class FlowingScopedLifestyle : ScopedLifestyle
{
public FlowingScopedLifestyle() : base("Scoped")
{
}

protected internal override Func<Scope> CreateCurrentScopeProvider(Container container)
{
// Notify the container that we're using the thread-resolve scope.
container.UseCurrentThreadResolveScope();

return () => container.GetVerificationOrResolveScopeForCurrentThread();
}

protected override Scope GetCurrentScopeCore(Container container) =>
container.GetVerificationOrResolveScopeForCurrentThread();
}
}
12 changes: 3 additions & 9 deletions src/SimpleInjector/Lifestyles/ScopedScopeLifestyle.cs
Expand Up @@ -47,17 +47,11 @@ 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;
return container.GetVerificationOrResolveScopeForCurrentThread()
?? ThrowResolveFromScopeOrRegisterDefaultScopedLifestyleException();
}

private static void ThrowResolveFromScopeOrRegisterDefaultScopedLifestyleException()
private static Scope ThrowResolveFromScopeOrRegisterDefaultScopedLifestyleException()
{
throw new InvalidOperationException(
"To be able to resolve and inject Scope instances, you need to either resolve " +
Expand Down
8 changes: 8 additions & 0 deletions src/SimpleInjector/ScopedLifestyle.cs
Expand Up @@ -35,6 +35,14 @@ namespace SimpleInjector
/// </summary>
public abstract class ScopedLifestyle : Lifestyle
{
/// <summary>
/// Gets the scoped lifestyle that allows Scoped registrations to be resolved direclty from the
/// <see cref="Scope"/> by calling <see cref="Scope.GetInstance{TService}()"/>. This allows multiple
/// scopes to be active and overlap within the same logical context, such as a single thread, or an
/// asynchronous context.
/// </summary>
public static readonly ScopedLifestyle Flowing = new FlowingScopedLifestyle();

/// <summary>Initializes a new instance of the <see cref="ScopedLifestyle"/> class.</summary>
/// <param name="name">The user friendly name of this lifestyle.</param>
/// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is null (Nothing in VB)
Expand Down

0 comments on commit 7e31875

Please sign in to comment.