Skip to content

Commit 9cad04d

Browse files
authored
Set current principal if available (#78)
1 parent 09bb0cf commit 9cad04d

File tree

9 files changed

+148
-7
lines changed

9 files changed

+148
-7
lines changed

docs/usage_guidance.md

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,6 @@ There are two ways to convert an `Microsoft.AspNetCore.Http.HttpContext` to a `S
1717

1818
**Recommendation**: For the most cases, implicit casting should be preferred as this will cache the created instance and ensure only a single `System.Web.HttpContext` per request.
1919

20-
## `System.Threading.Thread.CurrentPrincipal` not supported
21-
22-
In ASP.NET Framework, `System.Threading.Thread.CurrentPrincipal` would be set to the current user. This is not available on ASP.NET Core.
23-
24-
**Recommendation**: Use the property `HttpContext.User` instead.
25-
2620
## `CultureInfo.CurrentCulture` is not set by default
2721

2822
In ASP.NET Framework, `CultureInfo.Current` was set for a request, but this is not done automatically in ASP.NET Core. Instead, you must add the appropriate middleware to your pipeline.
@@ -35,12 +29,26 @@ Simplest way to enable this with similar behavior as ASP.NET Framework would be
3529
app.UseRequestLocalization();
3630
```
3731

32+
## `System.Threading.Thread.CurrentPrincipal`
33+
34+
In ASP.NET Framework, `System.Threading.Thread.CurrentPrincipal` and `System.Security.Claims.ClaimsPrincipal.Current` would be set to the current user. This is not available on ASP.NET Core out of the box. Support for this is available with these adapters by adding the `ISetThreadCurrentPrincipal` to the endpoint (available to controllers via the `SetThreadCurrentPrincipalAttribute`). However, it should only be used if the code cannot be refactored to remove usage.
35+
36+
**Recommendation**: If possible, use the property `HttpContext.User` instead by passing it through to the call site. If not possible, enable setting the current user and also consider setting the request to be a logical single thread (see below for details).
37+
3838
## Request thread does not exist in ASP.NET Core
3939

4040
In ASP.NET Framework, a request had thread-affinity and `HttpContext.Current` would only be available if on that thread. ASP.NET Core does not have this guarantee so `HttpContext.Current` will be available within the same async context, but no guarantees about threads are made.
4141

42-
**Recommendation**: If reading/writing to the `HttpContext`, you must ensure you are doing so in a single-threaded way.
42+
**Recommendation**: If reading/writing to the `HttpContext`, you must ensure you are doing so in a single-threaded way. You can force a request to never run concurrently on any async context by setting the `ISingleThreadedRequestMetadata`. This will have performance implications and should only be used if you can't refactor usage to ensure non-concurrent access. There is an implementation available to add to controllers with `SingleThreadedRequestAttribute`:
43+
4344

45+
```csharp
46+
[SingleThreadedRequest]
47+
public class SomeController : Controller
48+
{
49+
...
50+
}
51+
```
4452

4553
## `HttpContext.Request` may need to be prebuffered
4654

samples/MvcCoreApp/Program.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Security.Claims;
12
using Microsoft.AspNetCore.SystemWebAdapters;
23

34
var builder = WebApplication.CreateBuilder();
@@ -36,6 +37,23 @@ void ConfigureRemoteServiceOptions(RemoteServiceOptions options)
3637

3738
app.UseSystemWebAdapters();
3839

40+
app.MapGet("/current-principals-with-metadata", (HttpContext ctx) =>
41+
{
42+
var user1 = Thread.CurrentPrincipal;
43+
var user2 = ClaimsPrincipal.Current;
44+
45+
return "done";
46+
}).WithMetadata(new SetThreadCurrentPrincipalAttribute(), new SingleThreadedRequestAttribute());
47+
48+
49+
app.MapGet("/current-principals-no-metadata", (HttpContext ctx) =>
50+
{
51+
var user1 = Thread.CurrentPrincipal;
52+
var user2 = ClaimsPrincipal.Current;
53+
54+
return "done";
55+
});
56+
3957
app.UseEndpoints(endpoints =>
4058
{
4159
app.MapDefaultControllerRoute();
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using Microsoft.AspNetCore.Http;
7+
using Microsoft.Extensions.Logging;
8+
9+
namespace Microsoft.AspNetCore.SystemWebAdapters;
10+
11+
internal partial class CurrentPrincipalMiddleware
12+
{
13+
[LoggerMessage(0, LogLevel.Trace, "Thread.CurrentPrincipal has been set with the current user")]
14+
private partial void LogCurrentPrincipalUsage();
15+
16+
private readonly RequestDelegate _next;
17+
private readonly ILogger<CurrentPrincipalMiddleware> _logger;
18+
19+
public CurrentPrincipalMiddleware(RequestDelegate next, ILogger<CurrentPrincipalMiddleware> logger)
20+
{
21+
_next = next;
22+
_logger = logger;
23+
}
24+
25+
public Task InvokeAsync(HttpContext context)
26+
=> context.GetEndpoint()?.Metadata.GetMetadata<ISetThreadCurrentPrincipal>() is { IsEnabled: true } ? SetUserAsync(context) : _next(context);
27+
28+
private async Task SetUserAsync(HttpContext context)
29+
{
30+
LogCurrentPrincipalUsage();
31+
32+
var current = Thread.CurrentPrincipal;
33+
34+
try
35+
{
36+
Thread.CurrentPrincipal = context.User;
37+
38+
await _next(context);
39+
}
40+
finally
41+
{
42+
Thread.CurrentPrincipal = current;
43+
}
44+
}
45+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.SystemWebAdapters;
5+
6+
public interface ISetThreadCurrentPrincipal
7+
{
8+
bool IsEnabled { get; }
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.SystemWebAdapters;
5+
6+
public interface ISingleThreadedRequestMetadata
7+
{
8+
bool IsEnabled { get; }
9+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
6+
namespace Microsoft.AspNetCore.SystemWebAdapters;
7+
8+
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
9+
public sealed class SetThreadCurrentPrincipalAttribute : Attribute, ISetThreadCurrentPrincipal, ISingleThreadedRequestMetadata
10+
{
11+
public bool IsEnabled { get; set; } = true;
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
6+
namespace Microsoft.AspNetCore.SystemWebAdapters;
7+
8+
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
9+
public sealed class SingleThreadedRequestAttribute : Attribute, ISingleThreadedRequestMetadata
10+
{
11+
public bool IsEnabled { get; set; } = true;
12+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Threading.Tasks;
5+
using Microsoft.AspNetCore.Http;
6+
7+
namespace Microsoft.AspNetCore.SystemWebAdapters;
8+
9+
internal class SingleThreadedRequestMiddleware
10+
{
11+
private readonly RequestDelegate _next;
12+
13+
public SingleThreadedRequestMiddleware(RequestDelegate next) => _next = next;
14+
15+
public Task InvokeAsync(HttpContextCore context)
16+
=> context.GetEndpoint()?.Metadata.GetMetadata<ISingleThreadedRequestMetadata>() is { IsEnabled: true }
17+
? EnsureSingleThreaded(context)
18+
: _next(context);
19+
20+
private Task EnsureSingleThreaded(HttpContextCore context)
21+
{
22+
var schedule = new ConcurrentExclusiveSchedulerPair(TaskScheduler.Default, 1);
23+
24+
return Task.Factory.StartNew(() => _next(context), context.RequestAborted, TaskCreationOptions.DenyChildAttach, schedule.ExclusiveScheduler).Unwrap();
25+
}
26+
}

src/Microsoft.AspNetCore.SystemWebAdapters/Adapters/SystemWebAdaptersExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ public static void UseSystemWebAdapters(this IApplicationBuilder app)
3232
app.UseMiddleware<PreBufferRequestStreamMiddleware>();
3333
app.UseMiddleware<SessionMiddleware>();
3434
app.UseMiddleware<BufferResponseStreamMiddleware>();
35+
app.UseMiddleware<SingleThreadedRequestMiddleware>();
36+
app.UseMiddleware<CurrentPrincipalMiddleware>();
3537
}
3638

3739
/// <summary>

0 commit comments

Comments
 (0)