Skip to content

Commit bdea043

Browse files
committed
Fixes #3. Added AddAspNetCore overload that allows controlling behavior of accessing IServiceScopes for cross wiring.
1 parent 8099155 commit bdea043

8 files changed

+149
-17
lines changed

src/SimpleInjector.Integration.AspNetCore.Mvc.Core/SimpleInjector.Integration.AspNetCore.Mvc.Core.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
<ItemGroup>
3030
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.0.0" />
3131
<PackageReference Include="SimpleInjector" Version="5.0.0" />
32-
<PackageReference Include="SimpleInjector.Integration.ServiceCollection" Version="5.0.0" />
32+
<PackageReference Include="SimpleInjector.Integration.AspNetCore" Version="5.0.0" />
3333
</ItemGroup>
3434

3535
<ItemGroup Condition=" '$(TargetFramework)' == 'net451' ">
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) Simple Injector Contributors. All rights reserved.
2+
// Licensed under the MIT License. See LICENSE file in the project root for license information.
3+
4+
namespace SimpleInjector.Integration.AspNetCore
5+
{
6+
using System;
7+
using Microsoft.AspNetCore.Http;
8+
using SimpleInjector;
9+
using SimpleInjector.Integration.ServiceCollection;
10+
11+
internal sealed class OnePerNestedScopeServiceProviderAccessor : IServiceProviderAccessor
12+
{
13+
private readonly Container container;
14+
private readonly IServiceProviderAccessor decoratee;
15+
16+
internal OnePerNestedScopeServiceProviderAccessor(Container container, IServiceProviderAccessor decoratee)
17+
{
18+
this.decoratee = decoratee;
19+
this.container = container;
20+
}
21+
22+
public IServiceProvider Current =>
23+
this.RootScopeHttpContext?.RequestServices ?? this.decoratee.Current;
24+
25+
// Only the scope wrapping the request (the root scope) contains the HttpContext in its Items dictionary.
26+
// With nested scopes, this property returns null.
27+
private HttpContext? RootScopeHttpContext =>
28+
this.CurrentScope?.GetItem(RequestScopingStartupFilter.HttpContextKey) as HttpContext;
29+
30+
private Scope? CurrentScope => Lifestyle.Scoped.GetCurrentScope(this.container);
31+
}
32+
}

src/SimpleInjector.Integration.AspNetCore/AspNetCoreServiceProviderAccessor.cs renamed to src/SimpleInjector.Integration.AspNetCore/OnePerRequestServiceProviderAccessor.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ namespace SimpleInjector.Integration.AspNetCore
77
using Microsoft.AspNetCore.Http;
88
using SimpleInjector.Integration.ServiceCollection;
99

10-
internal class AspNetCoreServiceProviderAccessor : IServiceProviderAccessor
10+
internal sealed class OnePerRequestServiceProviderAccessor : IServiceProviderAccessor
1111
{
1212
private readonly IHttpContextAccessor accessor;
1313
private readonly IServiceProviderAccessor decoratee;
1414

15-
internal AspNetCoreServiceProviderAccessor(
15+
internal OnePerRequestServiceProviderAccessor(
1616
IHttpContextAccessor accessor,
1717
IServiceProviderAccessor decoratee)
1818
{

src/SimpleInjector.Integration.AspNetCore/RequestScopingStartupFilter.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ namespace SimpleInjector.Integration.AspNetCore
1111

1212
internal sealed class RequestScopingStartupFilter : IStartupFilter
1313
{
14+
internal static readonly object HttpContextKey = new object();
15+
1416
private readonly Container container;
1517

1618
public RequestScopingStartupFilter(Container container)
@@ -30,13 +32,13 @@ public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
3032

3133
private void ConfigureRequestScoping(IApplicationBuilder builder)
3234
{
33-
builder.Use(async (_, next) =>
35+
builder.Use(async (httpContext, next) =>
3436
{
35-
using (var scope = AsyncScopedLifestyle.BeginScope(this.container))
37+
await using (var scope = AsyncScopedLifestyle.BeginScope(this.container))
3638
{
37-
await next();
39+
scope.SetItem(HttpContextKey, httpContext);
3840

39-
await scope.DisposeAsync();
41+
await next();
4042
}
4143
});
4244
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright (c) Simple Injector Contributors. All rights reserved.
2+
// Licensed under the MIT License. See LICENSE file in the project root for license information.
3+
4+
using Microsoft.AspNetCore.Http;
5+
using Microsoft.Extensions.DependencyInjection;
6+
using SimpleInjector.Integration.ServiceCollection;
7+
8+
namespace SimpleInjector
9+
{
10+
/// <summary>
11+
/// Specifies the behavior of how ASP.NET Core <see cref="IServiceScope"/> instances are reused.
12+
/// </summary>
13+
public enum ServiceScopeReuseBehavior
14+
{
15+
/// <summary>
16+
/// Within the context of a web request (or SignalR connection), Simple Injector will reuse the same
17+
/// <see cref="IServiceScope"/> instance, irregardless of how many Simple Injector <see cref="Scope"/>
18+
/// instances are created. Outside the context of a (web) request (i.e.
19+
/// <see cref="IHttpContextAccessor.HttpContext"/> returns <c>null</c>), this behavior falls back to
20+
/// <see cref="Unchanged" />.
21+
/// </summary>
22+
OnePerRequest = 0,
23+
24+
/// <summary>
25+
/// Within the context of a web request (or SignalR connection), Simple Injector will use the request's
26+
/// <see cref="IServiceScope"/> within its root scope. Within a nested scope or outside the context of a (web)
27+
/// request, this behavior falls back to <see cref="Unchanged" />.
28+
/// </summary>
29+
OnePerNestedScope = 1,
30+
31+
/// <summary>
32+
/// This leaves the original configured <see cref="SimpleInjectorAddOptions.ServiceProviderAccessor"/> as-is.
33+
/// If <see cref="SimpleInjectorAddOptions.ServiceProviderAccessor">ServiceProviderAccessor</see> is not
34+
/// replaced, the default value, as returned by
35+
/// <see cref="SimpleInjectorServiceCollectionExtensions.AddSimpleInjector"/>, ensures the creation of a new
36+
/// .NET Core <see cref="IServiceScope"/> instance, for every Simple Injector <see cref="Scope"/>. The
37+
/// <see cref="IServiceScope"/> is <i>scoped</i> to that <see cref="Scope"/>. The ASP.NET Core's request
38+
/// <see cref="IServiceScope"/> will <b>NEVER</b> be used. Instead Simple Injector creates a new one for the
39+
/// request (and one for each nested scope). This disallows accessing ASP.NET Core services that depend on or
40+
/// return request-specific data.
41+
/// </summary>
42+
Unchanged = 2,
43+
}
44+
}

src/SimpleInjector.Integration.AspNetCore/SimpleInjector.Integration.AspNetCore.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
<Description>Integration library for ASP.NET Core for Simple Injector.</Description>
44
<AssemblyTitle>Simple Injector ASP.NET Core Integration</AssemblyTitle>
55
<NeutralLanguage>en-US</NeutralLanguage>
6-
<VersionPrefix>5.0.0</VersionPrefix>
7-
<PackageReleaseNotes>https://github.com/simpleinjector/SimpleInjector.Integration.AspNetCore/releases/tag/5.0.0</PackageReleaseNotes>
6+
<VersionPrefix>5.1.0</VersionPrefix>
7+
<PackageReleaseNotes>https://github.com/simpleinjector/SimpleInjector.Integration.AspNetCore/releases/tag/5.1.0</PackageReleaseNotes>
88
<AssemblyVersion>5.0.0.0</AssemblyVersion>
99
<Authors>Simple Injector Contributors</Authors>
1010
<TargetFrameworks>netstandard2.0</TargetFrameworks>

src/SimpleInjector.Integration.AspNetCore/SimpleInjectorAddOptionsAspNetCoreExtensions.cs

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,42 @@ public static class SimpleInjectorAddOptionsAspNetCoreExtensions
2222
/// additional integration options to be applied. These basic integrations includes wrapping each web
2323
/// request in an <see cref="AsyncScopedLifestyle"/> scope and making the nessesary changes that make
2424
/// it possible for enabling the injection of framework components in Simple Injector-constructed
25-
/// components when
25+
/// components when
2626
/// <see cref="SimpleInjectorServiceCollectionExtensions.UseSimpleInjector(IServiceProvider, Container)"/>
27-
/// is called.
27+
/// is called. This method uses the default <see cref="ServiceScopeReuseBehavior"/>, which is
28+
/// <see cref="ServiceScopeReuseBehavior.OnePerRequest"/>. This means that within a single web request, the same
29+
/// <see cref="IServiceScope"/> instance will be used, irregardless of the number of Simple Injector
30+
/// <see cref="Scope"/> instances you create. Outside the context of a web request, the ASP.NET Core
31+
/// integration falls back to the default behavior specified by the
32+
/// <see cref="SimpleInjectorAddOptions.ServiceProviderAccessor"/>. By default, a new
33+
/// <see cref="IServiceScope"/> instance will be created per Simple Injector <see cref="Scope"/>.
2834
/// </summary>
2935
/// <param name="options">The options to which the integration should be applied.</param>
3036
/// <returns>A new <see cref="SimpleInjectorAspNetCoreBuilder"/> instance that allows additional
3137
/// configurations to be made.</returns>
3238
/// <exception cref="ArgumentNullException">Thrown when <paramref name="options"/> is null.</exception>
3339
public static SimpleInjectorAspNetCoreBuilder AddAspNetCore(this SimpleInjectorAddOptions options)
40+
{
41+
return AddAspNetCore(options, ServiceScopeReuseBehavior.OnePerRequest);
42+
}
43+
44+
/// <summary>
45+
/// Adds basic Simple Injector integration for ASP.NET Core and returns a builder object that allow
46+
/// additional integration options to be applied. These basic integrations includes wrapping each web
47+
/// request in an <see cref="AsyncScopedLifestyle"/> scope and making the nessesary changes that make
48+
/// it possible for enabling the injection of framework components in Simple Injector-constructed
49+
/// components when
50+
/// <see cref="SimpleInjectorServiceCollectionExtensions.UseSimpleInjector(IServiceProvider, Container)"/>
51+
/// is called.
52+
/// </summary>
53+
/// <param name="options">The options to which the integration should be applied.</param>
54+
/// <param name="serviceScopeBehavior">Defines in which way Simple Injector should use and reuse the ASP.NET
55+
/// Core <see cref="IServiceScope"/>, which is used to resolve cross-wired dependencies from.</param>
56+
/// <returns>A new <see cref="SimpleInjectorAspNetCoreBuilder"/> instance that allows additional
57+
/// configurations to be made.</returns>
58+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="options"/> is null.</exception>
59+
public static SimpleInjectorAspNetCoreBuilder AddAspNetCore(
60+
this SimpleInjectorAddOptions options, ServiceScopeReuseBehavior serviceScopeBehavior)
3461
{
3562
if (options is null)
3663
{
@@ -44,15 +71,41 @@ public static SimpleInjectorAspNetCoreBuilder AddAspNetCore(this SimpleInjectorA
4471
// Add the IHttpContextAccessor to allow Simple Injector cross wiring to work in ASP.NET Core.
4572
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
4673

47-
// Replace the default IServiceProviderAccessor with on that can use IHttpContextAccessor to
48-
// resolve instances that are scoped inside the current request.
49-
options.ServiceProviderAccessor = new AspNetCoreServiceProviderAccessor(
50-
new HttpContextAccessor(),
51-
options.ServiceProviderAccessor);
74+
options.ServiceProviderAccessor = CreateServiceProviderAccessor(options, serviceScopeBehavior);
5275

5376
services.UseSimpleInjectorAspNetRequestScoping(container);
5477

5578
return new SimpleInjectorAspNetCoreBuilder(options);
5679
}
80+
81+
private static IServiceProviderAccessor CreateServiceProviderAccessor(
82+
SimpleInjectorAddOptions options, ServiceScopeReuseBehavior serviceScopeBehavior)
83+
{
84+
if (serviceScopeBehavior < ServiceScopeReuseBehavior.OnePerRequest
85+
|| serviceScopeBehavior > ServiceScopeReuseBehavior.Unchanged)
86+
{
87+
throw new ArgumentOutOfRangeException(nameof(serviceScopeBehavior));
88+
}
89+
90+
if (serviceScopeBehavior == ServiceScopeReuseBehavior.OnePerRequest)
91+
{
92+
// This IServiceProviderAccessor uses IHttpContextAccessor to resolve instances that are scoped inside
93+
// the current request.
94+
return new OnePerRequestServiceProviderAccessor(
95+
new HttpContextAccessor(),
96+
options.ServiceProviderAccessor);
97+
}
98+
else if (serviceScopeBehavior == ServiceScopeReuseBehavior.OnePerNestedScope)
99+
{
100+
// This IServiceProviderAccessor resolves cross-wired services from the request's IServiceScope, but
101+
// uses a new IServiceScope within a nested scope.
102+
return new OnePerNestedScopeServiceProviderAccessor(options.Container, options.ServiceProviderAccessor);
103+
}
104+
else
105+
{
106+
// This uses the default behavior.
107+
return options.ServiceProviderAccessor;
108+
}
109+
}
57110
}
58111
}

src/SimpleInjector.Integration.AspNetCore/SimpleInjectorAspNetCoreBuilder.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ namespace SimpleInjector.Integration.AspNetCore
77
using SimpleInjector.Integration.ServiceCollection;
88

99
/// <summary>
10-
/// Builder object returned by <see cref="SimpleInjectorAddOptionsAspNetCoreExtensions.AddAspNetCore"/>
10+
/// Builder object returned by
11+
/// <see cref="SimpleInjectorAddOptionsAspNetCoreExtensions.AddAspNetCore(SimpleInjectorAddOptions)"/>
1112
/// that allows additional integration options to be applied.
1213
/// </summary>
1314
public sealed class SimpleInjectorAspNetCoreBuilder

0 commit comments

Comments
 (0)