Browse Source

perf: improve allocations in `OwinEnvironment` (#58917)

Korolev Dmitry 9 months ago
parent
commit
fd3fe12cec

+ 2 - 0
AspNetCore.slnx

@@ -334,6 +334,7 @@
     <Project Path="src/Http/Metadata/src/Microsoft.AspNetCore.Metadata.csproj" />
   </Folder>
   <Folder Name="/src/Http/Owin/" Id="c6dae135-6509-c765-458b-3693a7b28e8c">
+    <Project Path="src/Http/Owin/benchmarks/Microsoft.AspNetCore.Owin.Microbenchmarks/Microsoft.AspNetCore.Owin.Microbenchmarks.csproj" />
     <Project Path="src/Http/Owin/src/Microsoft.AspNetCore.Owin.csproj" />
     <Project Path="src/Http/Owin/test/Microsoft.AspNetCore.Owin.Tests.csproj" />
   </Folder>
@@ -357,6 +358,7 @@
   <Folder Name="/src/Http/samples/" Id="0e46e96b-2613-2f61-4250-fc4a97d94f4c">
     <Project Path="src/Http/samples/MinimalSample/MinimalSample.csproj" />
     <Project Path="src/Http/samples/MinimalSampleFSharp/MinimalSampleFSharp.fsproj" />
+    <Project Path="src/Http/samples/MinimalSampleOwin/MinimalSampleOwin.csproj" />
     <Project Path="src/Http/samples/SampleApp/HttpAbstractions.SampleApp.csproj" />
   </Folder>
   <Folder Name="/src/Http/WebUtilities/" Id="1aadc95f-e3b5-447b-ddcf-108db21eb9ed">

+ 6 - 4
src/Http/HttpAbstractions.slnf

@@ -32,6 +32,7 @@
       "src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj",
       "src\\Http\\Http\\test\\Microsoft.AspNetCore.Http.Tests.csproj",
       "src\\Http\\Metadata\\src\\Microsoft.AspNetCore.Metadata.csproj",
+      "src\\Http\\Owin\\benchmarks\\Microsoft.AspNetCore.Owin.Microbenchmarks\\Microsoft.AspNetCore.Owin.Microbenchmarks.csproj",
       "src\\Http\\Owin\\src\\Microsoft.AspNetCore.Owin.csproj",
       "src\\Http\\Owin\\test\\Microsoft.AspNetCore.Owin.Tests.csproj",
       "src\\Http\\Routing.Abstractions\\src\\Microsoft.AspNetCore.Routing.Abstractions.csproj",
@@ -39,15 +40,16 @@
       "src\\Http\\Routing\\perf\\Microbenchmarks\\Microsoft.AspNetCore.Routing.Microbenchmarks.csproj",
       "src\\Http\\Routing\\src\\Microsoft.AspNetCore.Routing.csproj",
       "src\\Http\\Routing\\test\\FunctionalTests\\Microsoft.AspNetCore.Routing.FunctionalTests.csproj",
+      "src\\Http\\Routing\\test\\UnitTests\\Microsoft.AspNetCore.Routing.Tests.csproj",
       "src\\Http\\Routing\\test\\testassets\\Benchmarks\\Benchmarks.csproj",
       "src\\Http\\Routing\\test\\testassets\\RoutingSandbox\\RoutingSandbox.csproj",
       "src\\Http\\Routing\\test\\testassets\\RoutingWebSite\\RoutingWebSite.csproj",
-      "src\\Http\\Routing\\test\\UnitTests\\Microsoft.AspNetCore.Routing.Tests.csproj",
-      "src\\Http\\samples\\MinimalSample\\MinimalSample.csproj",
-      "src\\Http\\samples\\SampleApp\\HttpAbstractions.SampleApp.csproj",
       "src\\Http\\WebUtilities\\perf\\Microbenchmarks\\Microsoft.AspNetCore.WebUtilities.Microbenchmarks.csproj",
       "src\\Http\\WebUtilities\\src\\Microsoft.AspNetCore.WebUtilities.csproj",
       "src\\Http\\WebUtilities\\test\\Microsoft.AspNetCore.WebUtilities.Tests.csproj",
+      "src\\Http\\samples\\MinimalSampleOwin\\MinimalSampleOwin.csproj",
+      "src\\Http\\samples\\MinimalSample\\MinimalSample.csproj",
+      "src\\Http\\samples\\SampleApp\\HttpAbstractions.SampleApp.csproj",
       "src\\Middleware\\CORS\\src\\Microsoft.AspNetCore.Cors.csproj",
       "src\\Middleware\\Diagnostics.Abstractions\\src\\Microsoft.AspNetCore.Diagnostics.Abstractions.csproj",
       "src\\Middleware\\Diagnostics\\src\\Microsoft.AspNetCore.Diagnostics.csproj",
@@ -72,4 +74,4 @@
       "src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj"
     ]
   }
-}
+}

+ 4 - 0
src/Http/Owin/benchmarks/Microsoft.AspNetCore.Owin.Microbenchmarks/AssemblyInfo.cs

@@ -0,0 +1,4 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+[assembly: BenchmarkDotNet.Attributes.AspNetCoreBenchmark]

+ 107 - 0
src/Http/Owin/benchmarks/Microsoft.AspNetCore.Owin.Microbenchmarks/Benchmarks/OwinEnvironmentBenchmark.cs

@@ -0,0 +1,107 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using BenchmarkDotNet.Attributes;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.AspNetCore.Owin.Microbenchmarks.Benchmarks;
+
+[MemoryDiagnoser]
+public class OwinEnvironmentBenchmark
+{
+    const int RequestCount = 10000;
+
+    RequestDelegate _noOperationRequestDelegate;
+    RequestDelegate _accessPortsRequestDelegate;
+    RequestDelegate _accessHeadersRequestDelegate;
+
+    HttpContext _defaultHttpContext;
+    HttpContext _httpContextWithHeaders;
+
+    [GlobalSetup]
+    public void GlobalSetup()
+    {
+        _noOperationRequestDelegate = BuildRequestDelegate();
+        _accessPortsRequestDelegate = BuildRequestDelegate(beforeOwinInvokeAction: env =>
+        {
+            _ = env.TryGetValue("server.LocalPort", out var localPort);
+            _ = env.TryGetValue("server.RemotePort", out var remotePort);
+        });
+        _accessHeadersRequestDelegate = BuildRequestDelegate(
+            beforeOwinInvokeAction: env =>
+            {
+                _ = env.TryGetValue("owin.RequestHeaders", out var requestHeaders);
+            },
+            afterOwinInvokeAction: env =>
+            {
+                _ = env.TryGetValue("owin.ResponseHeaders", out var responseHeaders);
+            }
+        );
+
+        _defaultHttpContext = new DefaultHttpContext();
+
+        _httpContextWithHeaders = new DefaultHttpContext();
+        _httpContextWithHeaders.Request.Headers["CustomRequestHeader1"] = "CustomRequestValue";
+        _httpContextWithHeaders.Request.Headers["CustomRequestHeader2"] = "CustomRequestValue";
+        _httpContextWithHeaders.Response.Headers["CustomResponseHeader1"] = "CustomResponseValue";
+        _httpContextWithHeaders.Response.Headers["CustomResponseHeader2"] = "CustomResponseValue";
+    }
+
+    [Benchmark]
+    public async Task OwinRequest_NoOperation()
+    {
+        foreach (var i in Enumerable.Range(0, RequestCount))
+        {
+            await _noOperationRequestDelegate(_defaultHttpContext);
+        }
+    }
+
+    [Benchmark]
+    public async Task OwinRequest_AccessPorts()
+    {
+        foreach (var i in Enumerable.Range(0, RequestCount))
+        {
+            await _accessPortsRequestDelegate(_defaultHttpContext);
+        }
+    }
+
+    [Benchmark]
+    public async Task OwinRequest_AccessHeaders()
+    {
+        foreach (var i in Enumerable.Range(0, RequestCount))
+        {
+            await _accessHeadersRequestDelegate(_httpContextWithHeaders);
+        }
+    }
+
+    private static RequestDelegate BuildRequestDelegate(
+        Action<IDictionary<string, object>> beforeOwinInvokeAction = null,
+        Action<IDictionary<string, object>> afterOwinInvokeAction = null)
+    {
+        var serviceProvider = new ServiceCollection().BuildServiceProvider();
+        var builder = new ApplicationBuilder(serviceProvider);
+
+        return builder.UseOwin(addToPipeline =>
+        {
+            addToPipeline(next =>
+            {
+                return async env =>
+                {
+                    if (beforeOwinInvokeAction is not null)
+                    {
+                        beforeOwinInvokeAction(env);
+                    }
+
+                    await next(env);
+
+                    if (afterOwinInvokeAction is not null)
+                    {
+                        afterOwinInvokeAction(env);
+                    }
+                };
+            });
+        }).Build();
+    }
+}

+ 17 - 0
src/Http/Owin/benchmarks/Microsoft.AspNetCore.Owin.Microbenchmarks/Microsoft.AspNetCore.Owin.Microbenchmarks.csproj

@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Reference Include="BenchmarkDotNet" />
+    <Reference Include="Microsoft.AspNetCore.Http" />
+    <Reference Include="Microsoft.AspNetCore.Owin" />
+    <Reference Include="Microsoft.Extensions.DependencyInjection" />
+
+    <Compile Include="$(SharedSourceRoot)BenchmarkRunner\*.cs" />
+  </ItemGroup>
+
+</Project>

+ 44 - 6
src/Http/Owin/src/DictionaryStringArrayWrapper.cs

@@ -10,16 +10,16 @@ namespace Microsoft.AspNetCore.Owin;
 
 internal sealed class DictionaryStringArrayWrapper : IDictionary<string, string[]>
 {
+    public readonly IHeaderDictionary Inner;
+
     public DictionaryStringArrayWrapper(IHeaderDictionary inner)
     {
         Inner = inner;
     }
 
-    public readonly IHeaderDictionary Inner;
-
-    private static KeyValuePair<string, StringValues> Convert(KeyValuePair<string, string[]> item) => new KeyValuePair<string, StringValues>(item.Key, item.Value);
+    private static KeyValuePair<string, StringValues> Convert(KeyValuePair<string, string[]> item) => new(item.Key, item.Value);
 
-    private static KeyValuePair<string, string[]> Convert(KeyValuePair<string, StringValues> item) => new KeyValuePair<string, string[]>(item.Key, item.Value);
+    private static KeyValuePair<string, string[]> Convert(KeyValuePair<string, StringValues> item) => new(item.Key, item.Value);
 
     private string[] Convert(StringValues item) => item;
 
@@ -55,9 +55,11 @@ internal sealed class DictionaryStringArrayWrapper : IDictionary<string, string[
         }
     }
 
-    IEnumerator IEnumerable.GetEnumerator() => Inner.Select(Convert).GetEnumerator();
+    public ConvertingEnumerator GetEnumerator() => new ConvertingEnumerator(Inner);
+
+    IEnumerator<KeyValuePair<string, string[]>> IEnumerable<KeyValuePair<string, string[]>>.GetEnumerator() => new ConvertingEnumerator(Inner);
 
-    IEnumerator<KeyValuePair<string, string[]>> IEnumerable<KeyValuePair<string, string[]>>.GetEnumerator() => Inner.Select(Convert).GetEnumerator();
+    IEnumerator IEnumerable.GetEnumerator() => new ConvertingEnumerator(Inner);
 
     bool ICollection<KeyValuePair<string, string[]>>.Remove(KeyValuePair<string, string[]> item) => Inner.Remove(Convert(item));
 
@@ -74,4 +76,40 @@ internal sealed class DictionaryStringArrayWrapper : IDictionary<string, string[
         value = default(StringValues);
         return false;
     }
+
+    public struct ConvertingEnumerator : IEnumerator<KeyValuePair<string, string[]>>, IEnumerator
+    {
+        private IEnumerator<KeyValuePair<string, StringValues>> _inner;
+        private KeyValuePair<string, string[]> _current;
+
+        internal ConvertingEnumerator(IDictionary<string, StringValues> inner)
+        {
+            _inner = inner.GetEnumerator();
+            _current = default;
+        }
+
+        public void Dispose()
+        {
+            _inner?.Dispose();
+            _inner = null;
+        }
+
+        public bool MoveNext()
+        {
+            if (!_inner.MoveNext())
+            {
+                _current = default;
+                return false;
+            }
+
+            _current = Convert(_inner.Current);
+            return true;
+        }
+
+        public KeyValuePair<string, string[]> Current => _current;
+
+        object IEnumerator.Current => Current;
+
+        void IEnumerator.Reset() => throw new NotSupportedException();
+    }
 }

+ 44 - 6
src/Http/Owin/src/DictionaryStringValuesWrapper.cs

@@ -11,16 +11,16 @@ namespace Microsoft.AspNetCore.Owin;
 
 internal sealed class DictionaryStringValuesWrapper : IHeaderDictionary
 {
+    public readonly IDictionary<string, string[]> Inner;
+
     public DictionaryStringValuesWrapper(IDictionary<string, string[]> inner)
     {
         Inner = inner;
     }
 
-    public readonly IDictionary<string, string[]> Inner;
-
-    private static KeyValuePair<string, StringValues> Convert(KeyValuePair<string, string[]> item) => new KeyValuePair<string, StringValues>(item.Key, item.Value);
+    private static KeyValuePair<string, StringValues> Convert(KeyValuePair<string, string[]> item) => new(item.Key, item.Value);
 
-    private static KeyValuePair<string, string[]> Convert(KeyValuePair<string, StringValues> item) => new KeyValuePair<string, string[]>(item.Key, item.Value);
+    private static KeyValuePair<string, string[]> Convert(KeyValuePair<string, StringValues> item) => new(item.Key, item.Value);
 
     private StringValues Convert(string[] item) => item;
 
@@ -100,9 +100,11 @@ internal sealed class DictionaryStringValuesWrapper : IHeaderDictionary
         }
     }
 
-    IEnumerator IEnumerable.GetEnumerator() => Inner.Select(Convert).GetEnumerator();
+    public ConvertingEnumerator GetEnumerator() => new ConvertingEnumerator(Inner);
+
+    IEnumerator<KeyValuePair<string, StringValues>> IEnumerable<KeyValuePair<string, StringValues>>.GetEnumerator() => new ConvertingEnumerator(Inner);
 
-    IEnumerator<KeyValuePair<string, StringValues>> IEnumerable<KeyValuePair<string, StringValues>>.GetEnumerator() => Inner.Select(Convert).GetEnumerator();
+    IEnumerator IEnumerable.GetEnumerator() => new ConvertingEnumerator(Inner);
 
     bool ICollection<KeyValuePair<string, StringValues>>.Remove(KeyValuePair<string, StringValues> item) => Inner.Remove(Convert(item));
 
@@ -119,4 +121,40 @@ internal sealed class DictionaryStringValuesWrapper : IHeaderDictionary
         value = default(StringValues);
         return false;
     }
+
+    public struct ConvertingEnumerator : IEnumerator<KeyValuePair<string, StringValues>>, IEnumerator
+    {
+        private IEnumerator<KeyValuePair<string, string[]>> _inner;
+        private KeyValuePair<string, StringValues> _current;
+
+        internal ConvertingEnumerator(IDictionary<string, string[]> inner)
+        {
+            _inner = inner.GetEnumerator();
+            _current = default;
+        }
+
+        public void Dispose()
+        {
+            _inner?.Dispose();
+            _inner = null;
+        }
+
+        public bool MoveNext()
+        {
+            if (!_inner.MoveNext())
+            {
+                _current = default;
+                return false;
+            }
+
+            _current = Convert(_inner.Current);
+            return true;
+        }
+
+        public KeyValuePair<string, StringValues> Current => _current;
+
+        object IEnumerator.Current => Current;
+
+        void IEnumerator.Reset() => throw new NotSupportedException();
+    }
 }

+ 246 - 99
src/Http/Owin/src/OwinEnvironment.cs

@@ -2,6 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.Collections;
+using System.Collections.Immutable;
 using System.Globalization;
 using System.Linq;
 using System.Net;
@@ -28,8 +29,8 @@ using WebSocketAcceptAlt =
 /// </summary>
 public class OwinEnvironment : IDictionary<string, object>
 {
+    private readonly OwinEntries _owinEntries;
     private readonly HttpContext _context;
-    private readonly IDictionary<string, FeatureMap> _entries;
 
     /// <summary>
     /// Initializes a new instance of <see cref="OwinEnvironment"/>.
@@ -47,65 +48,7 @@ public class OwinEnvironment : IDictionary<string, object>
         }
 
         _context = context;
-        _entries = new Dictionary<string, FeatureMap>()
-            {
-                { OwinConstants.RequestProtocol, new FeatureMap<IHttpRequestFeature>(feature => feature.Protocol, () => string.Empty, (feature, value) => feature.Protocol = Convert.ToString(value, CultureInfo.InvariantCulture)) },
-                { OwinConstants.RequestScheme, new FeatureMap<IHttpRequestFeature>(feature => feature.Scheme, () => string.Empty, (feature, value) => feature.Scheme = Convert.ToString(value, CultureInfo.InvariantCulture)) },
-                { OwinConstants.RequestMethod, new FeatureMap<IHttpRequestFeature>(feature => feature.Method, () => string.Empty, (feature, value) => feature.Method = Convert.ToString(value, CultureInfo.InvariantCulture)) },
-                { OwinConstants.RequestPathBase, new FeatureMap<IHttpRequestFeature>(feature => feature.PathBase, () => string.Empty, (feature, value) => feature.PathBase = Convert.ToString(value, CultureInfo.InvariantCulture)) },
-                { OwinConstants.RequestPath, new FeatureMap<IHttpRequestFeature>(feature => feature.Path, () => string.Empty, (feature, value) => feature.Path = Convert.ToString(value, CultureInfo.InvariantCulture)) },
-                { OwinConstants.RequestQueryString, new FeatureMap<IHttpRequestFeature>(feature => Utilities.RemoveQuestionMark(feature.QueryString), () => string.Empty,
-                    (feature, value) => feature.QueryString = Utilities.AddQuestionMark(Convert.ToString(value, CultureInfo.InvariantCulture))) },
-                { OwinConstants.RequestHeaders, new FeatureMap<IHttpRequestFeature>(feature => Utilities.MakeDictionaryStringArray(feature.Headers), (feature, value) => feature.Headers = Utilities.MakeHeaderDictionary((IDictionary<string, string[]>)value)) },
-                { OwinConstants.RequestBody, new FeatureMap<IHttpRequestFeature>(feature => feature.Body, () => Stream.Null, (feature, value) => feature.Body = (Stream)value) },
-                { OwinConstants.RequestUser, new FeatureMap<IHttpAuthenticationFeature>(feature => feature.User, () => null, (feature, value) => feature.User = (ClaimsPrincipal)value) },
-
-                { OwinConstants.ResponseStatusCode, new FeatureMap<IHttpResponseFeature>(feature => feature.StatusCode, () => 200, (feature, value) => feature.StatusCode = Convert.ToInt32(value, CultureInfo.InvariantCulture)) },
-                { OwinConstants.ResponseReasonPhrase, new FeatureMap<IHttpResponseFeature>(feature => feature.ReasonPhrase, (feature, value) => feature.ReasonPhrase = Convert.ToString(value, CultureInfo.InvariantCulture)) },
-                { OwinConstants.ResponseHeaders, new FeatureMap<IHttpResponseFeature>(feature => Utilities.MakeDictionaryStringArray(feature.Headers), (feature, value) => feature.Headers = Utilities.MakeHeaderDictionary((IDictionary<string, string[]>)value)) },
-                { OwinConstants.ResponseBody, new FeatureMap<IHttpResponseBodyFeature>(feature => feature.Stream, () => Stream.Null, (feature, value) => context.Response.Body = (Stream)value) }, // DefaultHttpResponse.Body.Set has built in logic to handle replacing the feature.
-                { OwinConstants.CommonKeys.OnSendingHeaders, new FeatureMap<IHttpResponseFeature>(
-                    feature => new Action<Action<object>, object>((cb, state) => {
-                        feature.OnStarting(s =>
-                        {
-                            cb(s);
-                            return Task.CompletedTask;
-                        }, state);
-                    }))
-                },
-
-                { OwinConstants.CommonKeys.ConnectionId, new FeatureMap<IHttpConnectionFeature>(feature => feature.ConnectionId,
-                    (feature, value) => feature.ConnectionId = Convert.ToString(value, CultureInfo.InvariantCulture)) },
-
-                { OwinConstants.CommonKeys.LocalPort, new FeatureMap<IHttpConnectionFeature>(feature => feature.LocalPort.ToString(CultureInfo.InvariantCulture),
-                    (feature, value) => feature.LocalPort = Convert.ToInt32(value, CultureInfo.InvariantCulture)) },
-                { OwinConstants.CommonKeys.RemotePort, new FeatureMap<IHttpConnectionFeature>(feature => feature.RemotePort.ToString(CultureInfo.InvariantCulture),
-                    (feature, value) => feature.RemotePort = Convert.ToInt32(value, CultureInfo.InvariantCulture)) },
-
-                { OwinConstants.CommonKeys.LocalIpAddress, new FeatureMap<IHttpConnectionFeature>(feature => feature.LocalIpAddress.ToString(),
-                    (feature, value) => feature.LocalIpAddress = IPAddress.Parse(Convert.ToString(value, CultureInfo.InvariantCulture))) },
-                { OwinConstants.CommonKeys.RemoteIpAddress, new FeatureMap<IHttpConnectionFeature>(feature => feature.RemoteIpAddress.ToString(),
-                    (feature, value) => feature.RemoteIpAddress = IPAddress.Parse(Convert.ToString(value, CultureInfo.InvariantCulture))) },
-
-                { OwinConstants.SendFiles.SendAsync, new FeatureMap<IHttpResponseBodyFeature>(feature => new SendFileFunc(feature.SendFileAsync)) },
-
-                { OwinConstants.Security.User, new FeatureMap<IHttpAuthenticationFeature>(feature => feature.User,
-                    ()=> null, (feature, value) => feature.User = Utilities.MakeClaimsPrincipal((IPrincipal)value),
-                    () => new HttpAuthenticationFeature())
-                },
-
-                { OwinConstants.RequestId, new FeatureMap<IHttpRequestIdentifierFeature>(feature => feature.TraceIdentifier,
-                    ()=> null, (feature, value) => feature.TraceIdentifier = (string)value,
-                    () => new HttpRequestIdentifierFeature())
-                }
-            };
-
-        // owin.CallCancelled is required but the feature may not be present.
-        if (context.Features.Get<IHttpRequestLifetimeFeature>() != null)
-        {
-            _entries[OwinConstants.CallCancelled] = new FeatureMap<IHttpRequestLifetimeFeature>(feature => feature.RequestAborted);
-        }
-        else if (!_context.Items.ContainsKey(OwinConstants.CallCancelled))
+        if (!_context.Items.ContainsKey(OwinConstants.CallCancelled))
         {
             _context.Items[OwinConstants.CallCancelled] = CancellationToken.None;
         }
@@ -116,57 +59,37 @@ public class OwinEnvironment : IDictionary<string, object>
             _context.Items[OwinConstants.OwinVersion] = "1.0";
         }
 
-        if (context.Request.IsHttps)
-        {
-            _entries.Add(OwinConstants.CommonKeys.ClientCertificate, new FeatureMap<ITlsConnectionFeature>(feature => feature.ClientCertificate,
-                (feature, value) => feature.ClientCertificate = (X509Certificate2)value));
-            _entries.Add(OwinConstants.CommonKeys.LoadClientCertAsync, new FeatureMap<ITlsConnectionFeature>(
-                feature => new Func<Task>(() => feature.GetClientCertificateAsync(CancellationToken.None))));
-        }
-
-        if (context.WebSockets.IsWebSocketRequest)
-        {
-            _entries.Add(OwinConstants.WebSocket.AcceptAlt, new FeatureMap<IHttpWebSocketFeature>(feature => new WebSocketAcceptAlt(feature.AcceptAsync)));
-        }
-
         _context.Items[typeof(HttpContext).FullName] = _context; // Store for lookup when we transition back out of OWIN
+
+        _owinEntries = new(context);
     }
 
     // Public in case there's a new/custom feature interface that needs to be added.
     /// <summary>
     /// Get the environment's feature maps.
     /// </summary>
-    public IDictionary<string, FeatureMap> FeatureMaps
-    {
-        get { return _entries; }
-    }
+    public IDictionary<string, FeatureMap> FeatureMaps => _owinEntries.GetFeatureMaps();
 
     void IDictionary<string, object>.Add(string key, object value)
     {
-        if (_entries.ContainsKey(key))
+        if (_owinEntries.ContainsKey(key))
         {
             throw new InvalidOperationException("Key already present");
         }
         _context.Items.Add(key, value);
     }
 
-    bool IDictionary<string, object>.ContainsKey(string key)
-    {
-        return ((IDictionary<string, object>)this).TryGetValue(key, out _);
-    }
+    bool IDictionary<string, object>.ContainsKey(string key) => ((IDictionary<string, object>)this).TryGetValue(key, out _);
 
     ICollection<string> IDictionary<string, object>.Keys
-    {
-        get
-        {
-            return _entries.Where(pair => pair.Value.TryGet(_context, out _))
-                .Select(pair => pair.Key).Concat(_context.Items.Keys.Select(key => Convert.ToString(key, CultureInfo.InvariantCulture))).ToList();
-        }
-    }
+        => _owinEntries
+            .Where(pair => pair.Value.TryGet(_context, out _)).Select(pair => pair.Key)
+            .Concat(_context.Items.Keys.Select(key => Convert.ToString(key, CultureInfo.InvariantCulture)))
+            .ToList();
 
     bool IDictionary<string, object>.Remove(string key)
     {
-        if (_entries.Remove(key))
+        if (_owinEntries.Remove(key))
         {
             return true;
         }
@@ -175,8 +98,7 @@ public class OwinEnvironment : IDictionary<string, object>
 
     bool IDictionary<string, object>.TryGetValue(string key, out object value)
     {
-        FeatureMap entry;
-        if (_entries.TryGetValue(key, out entry) && entry.TryGet(_context, out value))
+        if (_owinEntries.TryGetValue(key, out var entry) && entry.TryGet(_context, out value))
         {
             return true;
         }
@@ -194,7 +116,7 @@ public class OwinEnvironment : IDictionary<string, object>
         {
             FeatureMap entry;
             object value;
-            if (_entries.TryGetValue(key, out entry) && entry.TryGet(_context, out value))
+            if (_owinEntries.TryGetValue(key, out entry) && entry.TryGet(_context, out value))
             {
                 return value;
             }
@@ -207,7 +129,7 @@ public class OwinEnvironment : IDictionary<string, object>
         set
         {
             FeatureMap entry;
-            if (_entries.TryGetValue(key, out entry))
+            if (_owinEntries.TryGetValue(key, out entry))
             {
                 if (entry.CanSet)
                 {
@@ -215,7 +137,7 @@ public class OwinEnvironment : IDictionary<string, object>
                 }
                 else
                 {
-                    _entries.Remove(key);
+                    _owinEntries.Remove(key);
                     if (value != null)
                     {
                         _context.Items[key] = value;
@@ -243,7 +165,7 @@ public class OwinEnvironment : IDictionary<string, object>
 
     void ICollection<KeyValuePair<string, object>>.Clear()
     {
-        _entries.Clear();
+        _owinEntries.Clear();
         _context.Items.Clear();
     }
 
@@ -256,7 +178,7 @@ public class OwinEnvironment : IDictionary<string, object>
     {
         ArgumentNullException.ThrowIfNull(array);
         ArgumentOutOfRangeException.ThrowIfNegative(arrayIndex);
-        if (arrayIndex + _entries.Count + _context.Items.Count > array.Length)
+        if (arrayIndex + _owinEntries.Count + _context.Items.Count > array.Length)
         {
             throw new ArgumentException("Not enough available space in array", nameof(array));
         }
@@ -270,7 +192,7 @@ public class OwinEnvironment : IDictionary<string, object>
 
     int ICollection<KeyValuePair<string, object>>.Count
     {
-        get { return _entries.Count + _context.Items.Count; }
+        get { return _owinEntries.Count + _context.Items.Count; }
     }
 
     bool ICollection<KeyValuePair<string, object>>.IsReadOnly
@@ -286,7 +208,7 @@ public class OwinEnvironment : IDictionary<string, object>
     /// <inheritdoc />
     public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
     {
-        foreach (var entryPair in _entries)
+        foreach (var entryPair in _owinEntries)
         {
             object value;
             if (entryPair.Value.TryGet(_context, out value))
@@ -478,4 +400,229 @@ public class OwinEnvironment : IDictionary<string, object>
         {
         }
     }
+
+    private sealed class OwinEntries : IEnumerable<KeyValuePair<string, FeatureMap>>
+    {
+        private static readonly IReadOnlyDictionary<string, FeatureMap> _entries = ImmutableDictionary.CreateRange(
+            new Dictionary<string, FeatureMap>
+            {
+                { OwinConstants.RequestProtocol, new FeatureMap<IHttpRequestFeature>(feature => feature.Protocol, () => string.Empty, (feature, value) => feature.Protocol = Convert.ToString(value, CultureInfo.InvariantCulture)) },
+                { OwinConstants.RequestScheme, new FeatureMap<IHttpRequestFeature>(feature => feature.Scheme, () => string.Empty, (feature, value) => feature.Scheme = Convert.ToString(value, CultureInfo.InvariantCulture)) },
+                { OwinConstants.RequestMethod, new FeatureMap<IHttpRequestFeature>(feature => feature.Method, () => string.Empty, (feature, value) => feature.Method = Convert.ToString(value, CultureInfo.InvariantCulture)) },
+                { OwinConstants.RequestPathBase, new FeatureMap<IHttpRequestFeature>(feature => feature.PathBase, () => string.Empty, (feature, value) => feature.PathBase = Convert.ToString(value, CultureInfo.InvariantCulture)) },
+                { OwinConstants.RequestPath, new FeatureMap<IHttpRequestFeature>(feature => feature.Path, () => string.Empty, (feature, value) => feature.Path = Convert.ToString(value, CultureInfo.InvariantCulture)) },
+                { OwinConstants.RequestQueryString, new FeatureMap<IHttpRequestFeature>(feature => Utilities.RemoveQuestionMark(feature.QueryString), () => string.Empty, (feature, value) => feature.QueryString = Utilities.AddQuestionMark(Convert.ToString(value, CultureInfo.InvariantCulture))) },
+                { OwinConstants.RequestHeaders, new FeatureMap<IHttpRequestFeature>(feature => Utilities.MakeDictionaryStringArray(feature.Headers), (feature, value) => feature.Headers = Utilities.MakeHeaderDictionary((IDictionary<string, string[]>)value)) },
+                { OwinConstants.RequestBody, new FeatureMap<IHttpRequestFeature>(feature => feature.Body, () => Stream.Null, (feature, value) => feature.Body = (Stream)value) },
+                { OwinConstants.RequestUser, new FeatureMap<IHttpAuthenticationFeature>(feature => feature.User, () => null, (feature, value) => feature.User = (ClaimsPrincipal)value) },
+
+                { OwinConstants.ResponseStatusCode, new FeatureMap<IHttpResponseFeature>(feature => feature.StatusCode, () => 200, (feature, value) => feature.StatusCode = Convert.ToInt32(value, CultureInfo.InvariantCulture)) },
+                { OwinConstants.ResponseReasonPhrase, new FeatureMap<IHttpResponseFeature>(feature => feature.ReasonPhrase, (feature, value) => feature.ReasonPhrase = Convert.ToString(value, CultureInfo.InvariantCulture)) },
+                { OwinConstants.ResponseHeaders, new FeatureMap<IHttpResponseFeature>(feature => Utilities.MakeDictionaryStringArray(feature.Headers), (feature, value) => feature.Headers = Utilities.MakeHeaderDictionary((IDictionary<string, string[]>)value)) },
+                { OwinConstants.CommonKeys.OnSendingHeaders, new FeatureMap<IHttpResponseFeature>(
+                    feature => new Action<Action<object>, object>((cb, state) => {
+                        feature.OnStarting(s =>
+                        {
+                            cb(s);
+                            return Task.CompletedTask;
+                        }, state);
+                    }))
+                },
+
+                { OwinConstants.CommonKeys.ConnectionId, new FeatureMap<IHttpConnectionFeature>(feature => feature.ConnectionId, (feature, value) => feature.ConnectionId = Convert.ToString(value, CultureInfo.InvariantCulture)) },
+
+                { OwinConstants.CommonKeys.LocalPort, new FeatureMap<IHttpConnectionFeature>(feature => PortToString(feature.LocalPort), (feature, value) => feature.LocalPort = Convert.ToInt32(value, CultureInfo.InvariantCulture)) },
+                { OwinConstants.CommonKeys.RemotePort, new FeatureMap<IHttpConnectionFeature>(feature => PortToString(feature.RemotePort), (feature, value) => feature.RemotePort = Convert.ToInt32(value, CultureInfo.InvariantCulture)) },
+
+                { OwinConstants.CommonKeys.LocalIpAddress, new FeatureMap<IHttpConnectionFeature>(feature => feature.LocalIpAddress.ToString(), (feature, value) => feature.LocalIpAddress = IPAddress.Parse(Convert.ToString(value, CultureInfo.InvariantCulture))) },
+                { OwinConstants.CommonKeys.RemoteIpAddress, new FeatureMap<IHttpConnectionFeature>(feature => feature.RemoteIpAddress.ToString(), (feature, value) => feature.RemoteIpAddress = IPAddress.Parse(Convert.ToString(value, CultureInfo.InvariantCulture))) },
+
+                { OwinConstants.SendFiles.SendAsync, new FeatureMap<IHttpResponseBodyFeature>(feature => new SendFileFunc(feature.SendFileAsync)) },
+                { OwinConstants.Security.User, new FeatureMap<IHttpAuthenticationFeature>(feature => feature.User, ()=> null, (feature, value) => feature.User = Utilities.MakeClaimsPrincipal((IPrincipal)value), () => new HttpAuthenticationFeature()) },
+                { OwinConstants.RequestId, new FeatureMap<IHttpRequestIdentifierFeature>(feature => feature.TraceIdentifier, ()=> null, (feature, value) => feature.TraceIdentifier = (string)value, () => new HttpRequestIdentifierFeature()) },
+                { OwinConstants.CallCancelled, new FeatureMap<IHttpRequestLifetimeFeature>(feature => feature.RequestAborted) },
+
+                { OwinConstants.CommonKeys.ClientCertificate, new FeatureMap<ITlsConnectionFeature>(feature => feature.ClientCertificate, (feature, value) => feature.ClientCertificate = (X509Certificate2)value) },
+                { OwinConstants.CommonKeys.LoadClientCertAsync, new FeatureMap<ITlsConnectionFeature>(feature => new Func<Task>(() => feature.GetClientCertificateAsync(CancellationToken.None))) },
+                { OwinConstants.WebSocket.AcceptAlt, new FeatureMap<IHttpWebSocketFeature>(
+                    feature =>
+                    {
+                        if (feature.IsWebSocketRequest)
+                        {
+                            return new WebSocketAcceptAlt(feature.AcceptAsync);
+                        }
+
+                        return null;
+                    })
+                }
+            });
+
+        /// <remarks>
+        /// Will be used, only if <see cref="FeatureMaps"/> or <see cref="Clear"/> is called from user-code.
+        /// Is a deep-copy of the singleton <see cref="_entries"/>
+        /// </remarks>
+        private IDictionary<string, FeatureMap> _contextEntries;
+
+        /// <remarks>
+        /// In order not to copy the whole dictionary of featureMaps per request,
+        /// and since OWIN allows the <see cref="Remove(string)"/> operation,
+        /// it's more lightweight to keep track of deleted keys.
+        /// </remarks>
+        private HashSet<string> _deletedKeys;
+
+        /// <remarks>
+        /// There are some <see cref="FeatureMap"/> entries that are context-dependent.
+        /// This dictionary is allocated per request, but does not contain as many entries as <see cref="_entries"/>.
+        /// </remarks>
+        private readonly IDictionary<string, FeatureMap> _contextDependentEntries;
+
+        public OwinEntries(HttpContext context)
+        {
+            _contextDependentEntries = new Dictionary<string, FeatureMap>
+            {
+                { OwinConstants.ResponseBody, new FeatureMap<IHttpResponseBodyFeature>(feature => feature.Stream, () => Stream.Null, (feature, value) => context.Response.Body = (Stream)value) }, // DefaultHttpResponse.Body.Set has built in logic to handle replacing the feature.
+            };
+        }
+
+        static string PortToString(int port) => port switch
+        {
+            80 => "80",
+            443 => "443",
+            _ => port.ToString(CultureInfo.InvariantCulture),
+        };
+
+        public int Count
+        {
+            get
+            {
+                if (_contextEntries is not null)
+                {
+                    return _contextEntries.Count;
+                }
+
+                return _entries.Count + _contextDependentEntries.Count - (_deletedKeys?.Count ?? 0);
+            }
+        }
+
+        public IDictionary<string, FeatureMap> GetFeatureMaps()
+        {
+            InitializeContextEntries();
+            return _contextEntries;
+        }
+
+        public bool TryGetValue(string key, out FeatureMap entry)
+        {
+            if (_contextEntries is not null)
+            {
+                return _contextEntries.TryGetValue(key, out entry);
+            }
+
+            if (_deletedKeys?.Contains(key) == true)
+            {
+                entry = null;
+                return false;
+            }
+
+            if (_entries.TryGetValue(key, out entry))
+            {
+                return true;
+            }
+
+            return _contextDependentEntries.TryGetValue(key, out entry);
+        }
+
+        public bool ContainsKey(string key)
+        {
+            if (_contextEntries is not null)
+            {
+                return _contextEntries.ContainsKey(key);
+            }
+
+            if (_deletedKeys?.Contains(key) == true)
+            {
+                return false;
+            }
+
+            return _entries.ContainsKey(key) || _contextDependentEntries.ContainsKey(key);
+        }
+
+        public bool Remove(string key)
+        {
+            if (_contextEntries is not null)
+            {
+                return _contextEntries.Remove(key);
+            }
+
+            if (_entries.ContainsKey(key) || _contextDependentEntries.ContainsKey(key))
+            {
+                _deletedKeys ??= new HashSet<string>(StringComparer.Ordinal);
+                return _deletedKeys.Add(key);
+            }
+
+            return false;
+        }
+
+        public IEnumerator<KeyValuePair<string, FeatureMap>> GetEnumerator()
+        {
+            if (_contextEntries is not null)
+            {
+                foreach (var entry in _contextEntries)
+                {
+                    yield return entry;
+                }
+            }
+            else
+            {
+                foreach (var entry in _entries)
+                {
+                    if (_deletedKeys?.Contains(entry.Key) == true)
+                    {
+                        continue;
+                    }
+
+                    yield return entry;
+                }
+
+                foreach (var entry in _contextDependentEntries)
+                {
+                    if (_deletedKeys?.Contains(entry.Key) == true)
+                    {
+                        continue;
+                    }
+
+                    yield return entry;
+                }
+            }
+        }
+
+        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+        public void Clear()
+        {
+            InitializeContextEntries(emptyCollection: true);
+        }
+
+        void InitializeContextEntries(bool emptyCollection = false)
+        {
+            if (emptyCollection)
+            {
+                _contextEntries = new Dictionary<string, FeatureMap>(StringComparer.Ordinal);
+                return;
+            }
+
+            _contextEntries = new Dictionary<string, FeatureMap>(_entries, StringComparer.Ordinal);
+            foreach (var entry in _contextDependentEntries)
+            {
+                _contextEntries[entry.Key] = entry.Value;
+            }
+
+            if (_deletedKeys is not null)
+            {
+                foreach (var key in _deletedKeys)
+                {
+                    _contextEntries.Remove(key);
+                }
+            }
+        }
+    }
 }

+ 58 - 0
src/Http/Owin/test/OwinEnvironmentTests.cs

@@ -2,6 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.Collections;
+using System.Net.Http;
 using System.Security.Claims;
 using Microsoft.AspNetCore.Http;
 
@@ -159,6 +160,63 @@ public class OwinEnvironmentTests
         Assert.NotNull(orderedEnvironment);
     }
 
+    [Fact]
+    public void OwinEnvironmentRemoveCorrectlyRemovesEntryAndDoesNotImpactNextOwinEnvironment()
+    {
+        var httpContext1 = CreateContext();
+        IDictionary<string, object> env1 = new OwinEnvironment(httpContext1);
+        var initialEnv1Count = env1.Count;
+
+        Assert.True(env1.Remove("owin.RequestProtocol"));
+        Assert.Equal(initialEnv1Count, env1.Count + 1);
+        Assert.False(env1.ContainsKey("owin.RequestProtocol"));
+        foreach (var key in env1.Keys)
+        {
+            Assert.NotEqual("owin.RequestProtocol", key);
+        }
+
+        var httpContext2 = CreateContext();
+        IDictionary<string, object> env2 = new OwinEnvironment(httpContext2);
+        Assert.True(env2.ContainsKey("owin.RequestProtocol"));
+    }
+
+    [Fact]
+    public void OwinEnvironmentFeatureMapsRemoveDoesNotImpactNextOwinEnvironment()
+    {
+        var httpContext1 = CreateContext();
+        var httpContext2 = CreateContext();
+
+        var owinEnvironment1 = new OwinEnvironment(httpContext1);
+        owinEnvironment1.FeatureMaps.Remove("owin.RequestProtocol");
+
+        var owinEnvironment2 = new OwinEnvironment(httpContext2);
+        Assert.True(owinEnvironment2.FeatureMaps.ContainsKey("owin.RequestProtocol"));
+        Assert.Equal(owinEnvironment1.ToList().Count + 1, owinEnvironment2.ToList().Count);
+    }
+
+    [Fact]
+    public void OwinEnvironmentClearBehavesCorrectlyAndDoesNotImpactNextOwinEnvironment()
+    {
+        var httpContext1 = CreateContext();
+        IDictionary<string, object> owinEnvironment1 = new OwinEnvironment(httpContext1);
+        owinEnvironment1.Clear();
+        Assert.Empty(owinEnvironment1);
+
+        var httpContext2 = CreateContext();
+        IDictionary<string, object> owinEnvironment2 = new OwinEnvironment(httpContext2);
+        Assert.True(owinEnvironment2.Count != 0);
+    }
+
+    [Fact]
+    public void OwinEnvironmentAccessContextDependentFeatureBehavesCorrectly()
+    {
+        var httpContext = CreateContext();
+        IDictionary<string, object> owinEnvironment = new OwinEnvironment(httpContext);
+
+        Assert.True(owinEnvironment.TryGetValue("owin.ResponseBody", out var responseBody));
+        responseBody.Equals(httpContext.Response.Body);
+    }
+
     private HttpContext CreateContext()
     {
         var context = new DefaultHttpContext();

+ 20 - 0
src/Http/samples/MinimalSampleOwin/MinimalSampleOwin.csproj

@@ -0,0 +1,20 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+  <PropertyGroup>
+    <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
+    <Nullable>enable</Nullable>
+    <EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator>
+    <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
+    <IsTrimmable>true</IsTrimmable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Reference Include="Microsoft.AspNetCore" />
+    <Reference Include="Microsoft.AspNetCore.Diagnostics" />
+    <Reference Include="Microsoft.AspNetCore.Hosting" />
+    <Reference Include="Microsoft.AspNetCore.Http" />
+    <Reference Include="Microsoft.AspNetCore.Http.Results" />
+    <Reference Include="Microsoft.AspNetCore.Owin" />
+  </ItemGroup>
+
+</Project>

+ 49 - 0
src/Http/samples/MinimalSampleOwin/Program.cs

@@ -0,0 +1,49 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore;
+using Microsoft.AspNetCore.Http.HttpResults;
+using Microsoft.AspNetCore.Http.Metadata;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Owin;
+
+var builder = WebApplication.CreateBuilder(args);
+var app = builder.Build();
+
+app.Logger.LogInformation($"Current process ID: {Environment.ProcessId}");
+
+#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
+#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
+string Plaintext() => "Hello, World!";
+app.MapGet("/plaintext", Plaintext);
+
+app.MapGet("/", () => $"""
+    Operating System: {Environment.OSVersion}
+    .NET version: {Environment.Version}
+    Username: {Environment.UserName}
+    Date and Time: {DateTime.Now}
+    """);
+#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
+#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
+
+app.UseOwin(pipeline =>
+{
+    pipeline(next =>
+    {
+        return async environment =>
+        {
+            // if you want to get OWIN environment properties
+            //if (environment is OwinEnvironment owin)
+            //{
+            //    foreach (var prop in owin)
+            //    {
+            //        app.Logger.LogInformation($"{prop.Key} - {prop.Value}");
+            //    }
+            //}
+
+            await next(environment);
+        };
+    });
+});
+
+app.Run();

+ 12 - 0
src/Http/samples/MinimalSampleOwin/Properties/launchSettings.json

@@ -0,0 +1,12 @@
+{
+  "profiles": {
+    "MinimalSampleOwin": {
+      "commandName": "Project",
+      "launchBrowser": true,
+      "applicationUrl": "https://localhost:5001;http://localhost:5000",
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development"
+      }
+    }
+  }
+}