Skip to content

Commit

Permalink
Add UseFriendlyObjectDisposedException option (#87)
Browse files Browse the repository at this point in the history
  • Loading branch information
sungam3r authored Apr 15, 2022
1 parent 0e3fc51 commit a8f4222
Show file tree
Hide file tree
Showing 10 changed files with 148 additions and 22 deletions.
3 changes: 3 additions & 0 deletions .github/codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# https://docs.codecov.com/docs/codecov-yaml
comment:
behavior: new
5 changes: 4 additions & 1 deletion .github/workflows/label.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
# https://github.com/actions/labeler/blob/master/README.md

name: Labeler
on: [pull_request]
on:
pull_request_target:
types:
- opened # when PR is opened

jobs:
label:
Expand Down
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>

<PropertyGroup>
<VersionPrefix>1.0.2-preview</VersionPrefix>
<VersionPrefix>1.0.3-preview</VersionPrefix>
<LangVersion>latest</LangVersion>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ namespace SteroidsDI
{
public ServiceProviderAdvancedOptions() { }
public bool AllowRootProviderResolve { get; set; }
public bool UseFriendlyObjectDisposedException { get; set; }
public bool ValidateParallelScopes { get; set; }
}
}
12 changes: 6 additions & 6 deletions src/SteroidsDI.Tests/Cases/AllowRootProviderResolveTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ public void Should_Work_When_No_Scopes_And_AllowRootProviderResolve_Enabled()
.AddSingleton<ScopedAsSingleton>()
.AddSingleton<Service>();

using (var provider = services.BuildServiceProvider())
using (var rootProvider = services.BuildServiceProvider())
{
using (var scope = provider.CreateScope())
using (var scope = rootProvider.CreateScope())
{
var service = scope.ServiceProvider.GetService<Service>()!;
service.Scoped.Value.ShouldNotBeNull();
Expand All @@ -48,9 +48,9 @@ public void Should_Throw_When_No_Scopes_And_AllowRootProviderResolve_Disabled()
.AddSingleton<ScopedAsSingleton>()
.AddSingleton<Service>();

using (var provider = services.BuildServiceProvider())
using (var rootProvider = services.BuildServiceProvider())
{
using (var scope = provider.CreateScope())
using (var scope = rootProvider.CreateScope())
{
var service = scope.ServiceProvider.GetService<Service>()!;
Should.Throw<InvalidOperationException>(() => service.Scoped.Value).Message.ShouldBe(@"The current scope is missing. Unable to resolve service 'ScopedAsSingleton' from the root service provider.
Expand All @@ -69,9 +69,9 @@ public void Should_Throw_When_No_Scopes_And_No_Service_Registered_And_AllowRootP
.AddDefer()
.AddSingleton<Service>();

using (var provider = services.BuildServiceProvider())
using (var rootProvider = services.BuildServiceProvider())
{
using (var scope = provider.CreateScope())
using (var scope = rootProvider.CreateScope())
{
var service = scope.ServiceProvider.GetService<Service>()!;
Should.Throw<InvalidOperationException>(() => service.Scoped.Value).Message.ShouldBe("No service for type 'SteroidsDI.Tests.Cases.AllowRootProviderResolveTests+ScopedAsSingleton' has been registered.");
Expand Down
12 changes: 6 additions & 6 deletions src/SteroidsDI.Tests/Cases/AspNetCoreHttpScopeProviderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,26 @@ public void Should_Throw_If_Called_With_Null_Provider()
[Test]
public void Should_Return_Null_If_Called_Out_Of_HttpContext()
{
var provider = new ServiceCollection().AddHttpScope().BuildServiceProvider();
new AspNetCoreHttpScopeProvider().GetScopedServiceProvider(provider).ShouldBe(null);
var rootProvider = new ServiceCollection().AddHttpScope().BuildServiceProvider();
new AspNetCoreHttpScopeProvider().GetScopedServiceProvider(rootProvider).ShouldBe(null);
}

[Test]
public void Should_Return_Null_If_Called_With_HttpContext_Without_Provider()
{
var provider = new ServiceCollection().AddHttpScope().BuildServiceProvider();
var rootProvider = new ServiceCollection().AddHttpScope().BuildServiceProvider();
new HttpContextAccessor().HttpContext = new DefaultHttpContext();
new AspNetCoreHttpScopeProvider().GetScopedServiceProvider(provider).ShouldBeNull();
new AspNetCoreHttpScopeProvider().GetScopedServiceProvider(rootProvider).ShouldBeNull();
}

[Test]
public void Should_Return_Provider_If_Called_With_HttpContext_With_Provider()
{
var provider = new ServiceCollection().AddHttpScope().BuildServiceProvider();
var rootProvider = new ServiceCollection().AddHttpScope().BuildServiceProvider();
new HttpContextAccessor().HttpContext = new DefaultHttpContext()
{
RequestServices = new ServiceCollection().BuildServiceProvider()
};
new AspNetCoreHttpScopeProvider().GetScopedServiceProvider(provider).ShouldNotBeNull();
new AspNetCoreHttpScopeProvider().GetScopedServiceProvider(rootProvider).ShouldNotBeNull();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using Shouldly;
using SteroidsDI.Core;

namespace SteroidsDI.Tests.Cases;

internal class UseFriendlyObjectDisposedExceptionTests
{
[Test]
public void ObjectDisposedException_UseFriendlyObjectDisposedException_Enabled()
{
var services = new ServiceCollection()
.AddDefer()
.AddScoped<ScopedService>()
.AddSingleton<SingletonService>()
.AddGenericScope<UseFriendlyObjectDisposedExceptionTests>();

using (var rootProvider = services.BuildServiceProvider())
{
var service = rootProvider.GetRequiredService<SingletonService>();
var outerScope = new Scoped<UseFriendlyObjectDisposedExceptionTests>(rootProvider.GetRequiredService<IScopeFactory>());

var cts1 = new CancellationTokenSource();
var cts2 = new CancellationTokenSource();

var t = Task.Run(() =>
{
cts1.Cancel();
cts2.Token.WaitHandle.WaitOne();
var ex = Should.Throw<ObjectDisposedException>(() => service.Scoped.Value);
ex.Message.ShouldBe(@"ObjectDisposedException occurred while resolving service 'ScopedService' by scoped service provider obtained from 'SteroidsDI.GenericScopeProvider`1[[SteroidsDI.Tests.Cases.UseFriendlyObjectDisposedExceptionTests, SteroidsDI.Tests, Version=1.0.3.0, Culture=neutral, PublicKeyToken=null]]'.
Most likely this happened because the scope and scoped provider were disposed BEFORE the actual completion of the user code.
Often, this is due to a forgotten 'await' operator somewhere in the user code. Make sure you await all the created tasks correctly.
You see this message because 'ServiceProviderAdvancedOptions.UseFriendlyObjectDisposedException' option is enabled.");
ex.InnerException.ShouldBeOfType<ObjectDisposedException>().Message.ShouldBe(@"Cannot access a disposed object.
Object name: 'IServiceProvider'.");
});

cts1.Token.WaitHandle.WaitOne();
outerScope.Dispose();
cts2.Cancel();
t.Wait();
}
}

[Test]
public void ObjectDisposedException_UseFriendlyObjectDisposedException_Disabled()
{
var services = new ServiceCollection()
.Configure<ServiceProviderAdvancedOptions>(opt => opt.UseFriendlyObjectDisposedException = false)
.AddDefer()
.AddScoped<ScopedService>()
.AddSingleton<SingletonService>()
.AddGenericScope<UseFriendlyObjectDisposedExceptionTests>();

using (var rootProvider = services.BuildServiceProvider())
{
var service = rootProvider.GetRequiredService<SingletonService>();
var outerScope = new Scoped<UseFriendlyObjectDisposedExceptionTests>(rootProvider.GetRequiredService<IScopeFactory>());

var cts1 = new CancellationTokenSource();
var cts2 = new CancellationTokenSource();

var t = Task.Run(() =>
{
cts1.Cancel();
cts2.Token.WaitHandle.WaitOne();
var ex = Should.Throw<ObjectDisposedException>(() => service.Scoped.Value);
ex.Message.ShouldBe(@"Cannot access a disposed object.
Object name: 'IServiceProvider'.");
ex.InnerException.ShouldBeNull();
});

cts1.Token.WaitHandle.WaitOne();
outerScope.Dispose();
cts2.Cancel();
t.Wait();
}
}

private sealed class ScopedService { }

private class SingletonService
{
public SingletonService(IDefer<ScopedService> scoped)
{
Scoped = scoped;
}

public IDefer<ScopedService> Scoped { get; }
}
}
5 changes: 3 additions & 2 deletions src/SteroidsDI.sln
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29519.181
# Visual Studio Version 17
VisualStudioVersion = 17.1.32228.430
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5507BC57-E2E4-4AA9-A385-6571387E1387}"
ProjectSection(SolutionItems) = preProject
Expand All @@ -26,6 +26,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Example", "Example\Example.
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{AE542B88-FDC2-4331-9D90-AB458D7BDFF9}"
ProjectSection(SolutionItems) = preProject
..\.github\codecov.yml = ..\.github\codecov.yml
..\.github\dependabot.yml = ..\.github\dependabot.yml
..\.github\FUNDING.yml = ..\.github\FUNDING.yml
..\.github\labeler.yml = ..\.github\labeler.yml
Expand Down
12 changes: 11 additions & 1 deletion src/SteroidsDI/Resolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,17 @@ internal static object Resolve(this IServiceProvider rootProvider, Type type, Se
}
}

return scopedServiceProvider.GetRequiredService(type);
try
{
return scopedServiceProvider.GetRequiredService(type);
}
catch (ObjectDisposedException e) when (options.UseFriendlyObjectDisposedException)
{
throw new ObjectDisposedException($@"ObjectDisposedException occurred while resolving service '{type.Name}' by scoped service provider obtained from '{scopeProvider.GetType().FullName}'.
Most likely this happened because the scope and scoped provider were disposed BEFORE the actual completion of the user code.
Often, this is due to a forgotten 'await' operator somewhere in the user code. Make sure you await all the created tasks correctly.
You see this message because 'ServiceProviderAdvancedOptions.UseFriendlyObjectDisposedException' option is enabled.", e);
}
}
}

Expand Down
23 changes: 18 additions & 5 deletions src/SteroidsDI/ServiceProviderAdvancedOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,28 @@ namespace SteroidsDI;
public sealed class ServiceProviderAdvancedOptions
{
/// <summary>
/// <see langword="true"/> to validate the situation with the presence of parallel
/// scopes from different providers. The situation is unlikely but may arise.
/// Set this option to <see langword="false"/> to disable wrapping original <see cref="ObjectDisposedException"/>
/// thrown from <see cref="IServiceProvider.GetService(Type)"/>. In most cases, the initial error message is in
/// little informative and the developer does not understand what is happening and what he should do to fix the error.
/// <br/>
/// Defaults to <see langword="true"/>.
/// </summary>
public bool UseFriendlyObjectDisposedException { get; set; } = true;

/// <summary>
/// Set this option to <see langword="true"/> to validate the situation with the presence
/// of parallel scopes from different providers. The situation is unlikely but may arise.
/// <br/>
/// Defaults to <see langword="false"/>.
/// </summary>
public bool ValidateParallelScopes { get; set; }

/// <summary>
/// Allows resolving objects through the root provider if the current scope is missing.
/// The object must have lifetime different from scoped. Getting scoped objects through
/// the root provider is ALWAYS FORBIDDEN. Defaults to <see langword="false"/>.
/// Set this option to <see langword="true"/> to allow resolving objects through the root
/// provider if the current scope is missing. The object must have lifetime different from
/// scoped. Getting scoped objects through the root provider is ALWAYS FORBIDDEN.
/// <br/>
/// Defaults to <see langword="false"/>.
/// </summary>
public bool AllowRootProviderResolve { get; set; }

Expand Down

0 comments on commit a8f4222

Please sign in to comment.