From c05d859eda7e008d1d5ccecfa89b0b52b707f31d Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Fri, 8 Nov 2024 13:46:38 +0100 Subject: [PATCH 01/12] reduce allocations for default usage in OwinEnvironment --- src/Http/Owin/src/OwinEnvironment.cs | 321 +++++++++++++-------- src/Http/Owin/test/OwinEnvironmentTests.cs | 48 +++ 2 files changed, 251 insertions(+), 118 deletions(-) diff --git a/src/Http/Owin/src/OwinEnvironment.cs b/src/Http/Owin/src/OwinEnvironment.cs index cc2995a10afe..9582dde288f9 100644 --- a/src/Http/Owin/src/OwinEnvironment.cs +++ b/src/Http/Owin/src/OwinEnvironment.cs @@ -28,8 +28,8 @@ namespace Microsoft.AspNetCore.Owin; /// public class OwinEnvironment : IDictionary { + private readonly OwinEntries _owinEntries = new(); private readonly HttpContext _context; - private readonly IDictionary _entries; /// /// Initializes a new instance of . @@ -47,65 +47,7 @@ public OwinEnvironment(HttpContext context) } _context = context; - _entries = new Dictionary() - { - { OwinConstants.RequestProtocol, new FeatureMap(feature => feature.Protocol, () => string.Empty, (feature, value) => feature.Protocol = Convert.ToString(value, CultureInfo.InvariantCulture)) }, - { OwinConstants.RequestScheme, new FeatureMap(feature => feature.Scheme, () => string.Empty, (feature, value) => feature.Scheme = Convert.ToString(value, CultureInfo.InvariantCulture)) }, - { OwinConstants.RequestMethod, new FeatureMap(feature => feature.Method, () => string.Empty, (feature, value) => feature.Method = Convert.ToString(value, CultureInfo.InvariantCulture)) }, - { OwinConstants.RequestPathBase, new FeatureMap(feature => feature.PathBase, () => string.Empty, (feature, value) => feature.PathBase = Convert.ToString(value, CultureInfo.InvariantCulture)) }, - { OwinConstants.RequestPath, new FeatureMap(feature => feature.Path, () => string.Empty, (feature, value) => feature.Path = Convert.ToString(value, CultureInfo.InvariantCulture)) }, - { OwinConstants.RequestQueryString, new FeatureMap(feature => Utilities.RemoveQuestionMark(feature.QueryString), () => string.Empty, - (feature, value) => feature.QueryString = Utilities.AddQuestionMark(Convert.ToString(value, CultureInfo.InvariantCulture))) }, - { OwinConstants.RequestHeaders, new FeatureMap(feature => Utilities.MakeDictionaryStringArray(feature.Headers), (feature, value) => feature.Headers = Utilities.MakeHeaderDictionary((IDictionary)value)) }, - { OwinConstants.RequestBody, new FeatureMap(feature => feature.Body, () => Stream.Null, (feature, value) => feature.Body = (Stream)value) }, - { OwinConstants.RequestUser, new FeatureMap(feature => feature.User, () => null, (feature, value) => feature.User = (ClaimsPrincipal)value) }, - - { OwinConstants.ResponseStatusCode, new FeatureMap(feature => feature.StatusCode, () => 200, (feature, value) => feature.StatusCode = Convert.ToInt32(value, CultureInfo.InvariantCulture)) }, - { OwinConstants.ResponseReasonPhrase, new FeatureMap(feature => feature.ReasonPhrase, (feature, value) => feature.ReasonPhrase = Convert.ToString(value, CultureInfo.InvariantCulture)) }, - { OwinConstants.ResponseHeaders, new FeatureMap(feature => Utilities.MakeDictionaryStringArray(feature.Headers), (feature, value) => feature.Headers = Utilities.MakeHeaderDictionary((IDictionary)value)) }, - { OwinConstants.ResponseBody, new FeatureMap(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( - feature => new Action, object>((cb, state) => { - feature.OnStarting(s => - { - cb(s); - return Task.CompletedTask; - }, state); - })) - }, - - { OwinConstants.CommonKeys.ConnectionId, new FeatureMap(feature => feature.ConnectionId, - (feature, value) => feature.ConnectionId = Convert.ToString(value, CultureInfo.InvariantCulture)) }, - - { OwinConstants.CommonKeys.LocalPort, new FeatureMap(feature => feature.LocalPort.ToString(CultureInfo.InvariantCulture), - (feature, value) => feature.LocalPort = Convert.ToInt32(value, CultureInfo.InvariantCulture)) }, - { OwinConstants.CommonKeys.RemotePort, new FeatureMap(feature => feature.RemotePort.ToString(CultureInfo.InvariantCulture), - (feature, value) => feature.RemotePort = Convert.ToInt32(value, CultureInfo.InvariantCulture)) }, - - { OwinConstants.CommonKeys.LocalIpAddress, new FeatureMap(feature => feature.LocalIpAddress.ToString(), - (feature, value) => feature.LocalIpAddress = IPAddress.Parse(Convert.ToString(value, CultureInfo.InvariantCulture))) }, - { OwinConstants.CommonKeys.RemoteIpAddress, new FeatureMap(feature => feature.RemoteIpAddress.ToString(), - (feature, value) => feature.RemoteIpAddress = IPAddress.Parse(Convert.ToString(value, CultureInfo.InvariantCulture))) }, - - { OwinConstants.SendFiles.SendAsync, new FeatureMap(feature => new SendFileFunc(feature.SendFileAsync)) }, - - { OwinConstants.Security.User, new FeatureMap(feature => feature.User, - ()=> null, (feature, value) => feature.User = Utilities.MakeClaimsPrincipal((IPrincipal)value), - () => new HttpAuthenticationFeature()) - }, - - { OwinConstants.RequestId, new FeatureMap(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() != null) - { - _entries[OwinConstants.CallCancelled] = new FeatureMap(feature => feature.RequestAborted); - } - else if (!_context.Items.ContainsKey(OwinConstants.CallCancelled)) + if (!_context.Items.ContainsKey(OwinConstants.CallCancelled)) { _context.Items[OwinConstants.CallCancelled] = CancellationToken.None; } @@ -116,19 +58,6 @@ public OwinEnvironment(HttpContext context) _context.Items[OwinConstants.OwinVersion] = "1.0"; } - if (context.Request.IsHttps) - { - _entries.Add(OwinConstants.CommonKeys.ClientCertificate, new FeatureMap(feature => feature.ClientCertificate, - (feature, value) => feature.ClientCertificate = (X509Certificate2)value)); - _entries.Add(OwinConstants.CommonKeys.LoadClientCertAsync, new FeatureMap( - feature => new Func(() => feature.GetClientCertificateAsync(CancellationToken.None)))); - } - - if (context.WebSockets.IsWebSocketRequest) - { - _entries.Add(OwinConstants.WebSocket.AcceptAlt, new FeatureMap(feature => new WebSocketAcceptAlt(feature.AcceptAsync))); - } - _context.Items[typeof(HttpContext).FullName] = _context; // Store for lookup when we transition back out of OWIN } @@ -136,37 +65,28 @@ public OwinEnvironment(HttpContext context) /// /// Get the environment's feature maps. /// - public IDictionary FeatureMaps - { - get { return _entries; } - } + public IDictionary FeatureMaps => _owinEntries.GetFeatureMaps(); void IDictionary.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.ContainsKey(string key) - { - return ((IDictionary)this).TryGetValue(key, out _); - } + bool IDictionary.ContainsKey(string key) => ((IDictionary)this).TryGetValue(key, out _); ICollection IDictionary.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.Remove(string key) { - if (_entries.Remove(key)) + if (_owinEntries.Remove(key)) { return true; } @@ -175,8 +95,7 @@ bool IDictionary.Remove(string key) bool IDictionary.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 +113,7 @@ object IDictionary.this[string key] { 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 +126,7 @@ object IDictionary.this[string key] set { FeatureMap entry; - if (_entries.TryGetValue(key, out entry)) + if (_owinEntries.TryGetValue(key, out entry)) { if (entry.CanSet) { @@ -215,7 +134,7 @@ object IDictionary.this[string key] } else { - _entries.Remove(key); + _owinEntries.Remove(key); if (value != null) { _context.Items[key] = value; @@ -243,7 +162,7 @@ void ICollection>.Add(KeyValuePair void ICollection>.Clear() { - _entries.Clear(); + _owinEntries.Clear(); _context.Items.Clear(); } @@ -256,7 +175,7 @@ void ICollection>.CopyTo(KeyValuePair array.Length) + if (arrayIndex + _owinEntries.Count + _context.Items.Count > array.Length) { throw new ArgumentException("Not enough available space in array", nameof(array)); } @@ -270,7 +189,7 @@ void ICollection>.CopyTo(KeyValuePair>.Count { - get { return _entries.Count + _context.Items.Count; } + get { return _owinEntries.Count + _context.Items.Count; } } bool ICollection>.IsReadOnly @@ -286,7 +205,7 @@ bool ICollection>.Remove(KeyValuePair public IEnumerator> GetEnumerator() { - foreach (var entryPair in _entries) + foreach (var entryPair in _owinEntries) { object value; if (entryPair.Value.TryGet(_context, out value)) @@ -315,7 +234,7 @@ public class FeatureMap /// /// The feature interface type. /// Value getter. - public FeatureMap(Type featureInterface, Func getter) + public FeatureMap(Type featureInterface, Func getter) : this(featureInterface, getter, defaultFactory: null) { } @@ -326,7 +245,7 @@ public FeatureMap(Type featureInterface, Func getter) /// The feature interface type. /// Value getter delegate. /// Default value factory delegate. - public FeatureMap(Type featureInterface, Func getter, Func defaultFactory) + public FeatureMap(Type featureInterface, Func getter, Func defaultFactory) : this(featureInterface, getter, defaultFactory, setter: null) { } @@ -337,7 +256,7 @@ public FeatureMap(Type featureInterface, Func getter, FuncThe feature interface type. /// Value getter delegate. /// Value setter delegate. - public FeatureMap(Type featureInterface, Func getter, Action setter) + public FeatureMap(Type featureInterface, Func getter, Action setter) : this(featureInterface, getter, defaultFactory: null, setter: setter) { } @@ -349,7 +268,7 @@ public FeatureMap(Type featureInterface, Func getter, ActionValue getter delegate. /// Default value factory delegate. /// Value setter delegate. - public FeatureMap(Type featureInterface, Func getter, Func defaultFactory, Action setter) + public FeatureMap(Type featureInterface, Func getter, Func defaultFactory, Action setter) : this(featureInterface, getter, defaultFactory, setter, featureFactory: null) { } @@ -362,7 +281,7 @@ public FeatureMap(Type featureInterface, Func getter, FuncDefault value factory delegate. /// Value setter delegate. /// Feature factory delegate. - public FeatureMap(Type featureInterface, Func getter, Func defaultFactory, Action setter, Func featureFactory) + public FeatureMap(Type featureInterface, Func getter, Func defaultFactory, Action setter, Func featureFactory) { FeatureInterface = featureInterface; Getter = getter; @@ -372,8 +291,8 @@ public FeatureMap(Type featureInterface, Func getter, Func Getter { get; set; } - private Action Setter { get; set; } + private Func Getter { get; set; } + private Action Setter { get; set; } private Func DefaultFactory { get; set; } private Func FeatureFactory { get; set; } @@ -393,7 +312,7 @@ internal bool TryGet(HttpContext context, out object value) value = null; return false; } - value = Getter(featureInstance); + value = Getter(featureInstance, context); if (value == null && DefaultFactory != null) { value = DefaultFactory(); @@ -416,7 +335,7 @@ internal void Set(HttpContext context, object value) context.Features[FeatureInterface] = feature; } } - Setter(feature, value); + Setter(feature, context, value); } } @@ -430,8 +349,8 @@ public class FeatureMap : FeatureMap /// Initializes a new instance of for the specified feature interface type. /// /// Value getter. - public FeatureMap(Func getter) - : base(typeof(TFeature), feature => getter((TFeature)feature)) + public FeatureMap(Func getter) + : base(typeof(TFeature), (feature, context) => getter((TFeature)feature, context)) { } @@ -440,8 +359,8 @@ public FeatureMap(Func getter) /// /// Value getter delegate. /// Default value factory delegate. - public FeatureMap(Func getter, Func defaultFactory) - : base(typeof(TFeature), feature => getter((TFeature)feature), defaultFactory) + public FeatureMap(Func getter, Func defaultFactory) + : base(typeof(TFeature), (feature, context) => getter((TFeature)feature, context), defaultFactory) { } @@ -450,8 +369,8 @@ public FeatureMap(Func getter, Func defaultFactory) /// /// Value getter delegate. /// Value setter delegate. - public FeatureMap(Func getter, Action setter) - : base(typeof(TFeature), feature => getter((TFeature)feature), (feature, value) => setter((TFeature)feature, value)) + public FeatureMap(Func getter, Action setter) + : base(typeof(TFeature), (feature, context) => getter((TFeature)feature, context), (feature, context, value) => setter((TFeature)feature, context, value)) { } @@ -461,8 +380,8 @@ public FeatureMap(Func getter, Action setter /// Value getter delegate. /// Default value factory delegate. /// Value setter delegate. - public FeatureMap(Func getter, Func defaultFactory, Action setter) - : base(typeof(TFeature), feature => getter((TFeature)feature), defaultFactory, (feature, value) => setter((TFeature)feature, value)) + public FeatureMap(Func getter, Func defaultFactory, Action setter) + : base(typeof(TFeature), (feature, context) => getter((TFeature)feature, context), defaultFactory, (feature, context, value) => setter((TFeature)feature, context, value)) { } @@ -473,9 +392,175 @@ public FeatureMap(Func getter, Func defaultFactory, Ac /// Default value factory delegate. /// Value setter delegate. /// Feature factory delegate. - public FeatureMap(Func getter, Func defaultFactory, Action setter, Func featureFactory) - : base(typeof(TFeature), feature => getter((TFeature)feature), defaultFactory, (feature, value) => setter((TFeature)feature, value), () => featureFactory()) + public FeatureMap(Func getter, Func defaultFactory, Action setter, Func featureFactory) + : base(typeof(TFeature), (feature, context) => getter((TFeature)feature, context), defaultFactory, (feature, context, value) => setter((TFeature)feature, context, value), () => featureFactory()) + { + } + } + + private class OwinEntries : IEnumerable> + { + private static readonly IDictionary _entries = new Dictionary + { + { OwinConstants.RequestProtocol, new FeatureMap((feature, _) => feature.Protocol, () => string.Empty, (feature, _, value) => feature.Protocol = Convert.ToString(value, CultureInfo.InvariantCulture)) }, + { OwinConstants.RequestScheme, new FeatureMap((feature, _) => feature.Scheme, () => string.Empty, (feature, _, value) => feature.Scheme = Convert.ToString(value, CultureInfo.InvariantCulture)) }, + { OwinConstants.RequestMethod, new FeatureMap((feature, _) => feature.Method, () => string.Empty, (feature, _, value) => feature.Method = Convert.ToString(value, CultureInfo.InvariantCulture)) }, + { OwinConstants.RequestPathBase, new FeatureMap((feature, _) => feature.PathBase, () => string.Empty, (feature, _, value) => feature.PathBase = Convert.ToString(value, CultureInfo.InvariantCulture)) }, + { OwinConstants.RequestPath, new FeatureMap((feature, _) => feature.Path, () => string.Empty, (feature, _, value) => feature.Path = Convert.ToString(value, CultureInfo.InvariantCulture)) }, + { OwinConstants.RequestQueryString, new FeatureMap((feature, _) => Utilities.RemoveQuestionMark(feature.QueryString), () => string.Empty, (feature, _, value) => feature.QueryString = Utilities.AddQuestionMark(Convert.ToString(value, CultureInfo.InvariantCulture))) }, + { OwinConstants.RequestHeaders, new FeatureMap((feature, _) => Utilities.MakeDictionaryStringArray(feature.Headers), (feature, _, value) => feature.Headers = Utilities.MakeHeaderDictionary((IDictionary)value)) }, + { OwinConstants.RequestBody, new FeatureMap((feature, _) => feature.Body, () => Stream.Null, (feature, _, value) => feature.Body = (Stream)value) }, + { OwinConstants.RequestUser, new FeatureMap((feature, _) => feature.User, () => null, (feature, _, value) => feature.User = (ClaimsPrincipal)value) }, + + { OwinConstants.ResponseStatusCode, new FeatureMap((feature, _) => feature.StatusCode, () => 200, (feature, _, value) => feature.StatusCode = Convert.ToInt32(value, CultureInfo.InvariantCulture)) }, + { OwinConstants.ResponseReasonPhrase, new FeatureMap((feature, _) => feature.ReasonPhrase, (feature, _, value) => feature.ReasonPhrase = Convert.ToString(value, CultureInfo.InvariantCulture)) }, + { OwinConstants.ResponseHeaders, new FeatureMap((feature, _) => Utilities.MakeDictionaryStringArray(feature.Headers), (feature, _, value) => feature.Headers = Utilities.MakeHeaderDictionary((IDictionary)value)) }, + { OwinConstants.ResponseBody, new FeatureMap((feature, _) => feature.Stream, () => Stream.Null, (feature, context, value) => context.Response.Body = (Stream)value) }, // DefaultHttpResponse.Body.Set has built in logic to handle replacing the feature. + { + OwinConstants.CommonKeys.OnSendingHeaders, new FeatureMap( + (feature, _) => new Action, object>((cb, state) => { + feature.OnStarting(s => + { + cb(s); + return Task.CompletedTask; + }, state); + })) + }, + + { OwinConstants.CommonKeys.ConnectionId, new FeatureMap((feature, _) => feature.ConnectionId, (feature, _, value) => feature.ConnectionId = Convert.ToString(value, CultureInfo.InvariantCulture)) }, + + { OwinConstants.CommonKeys.LocalPort, new FeatureMap((feature, _) => feature.LocalPort.ToString(CultureInfo.InvariantCulture), (feature, _, value) => feature.LocalPort = Convert.ToInt32(value, CultureInfo.InvariantCulture)) }, + { OwinConstants.CommonKeys.RemotePort, new FeatureMap((feature, _) => feature.RemotePort.ToString(CultureInfo.InvariantCulture), (feature, _, value) => feature.RemotePort = Convert.ToInt32(value, CultureInfo.InvariantCulture)) }, + + { OwinConstants.CommonKeys.LocalIpAddress, new FeatureMap((feature, _) => feature.LocalIpAddress.ToString(), (feature, _, value) => feature.LocalIpAddress = IPAddress.Parse(Convert.ToString(value, CultureInfo.InvariantCulture))) }, + { OwinConstants.CommonKeys.RemoteIpAddress, new FeatureMap((feature, _) => feature.RemoteIpAddress.ToString(), (feature, _, value) => feature.RemoteIpAddress = IPAddress.Parse(Convert.ToString(value, CultureInfo.InvariantCulture))) }, + + { OwinConstants.SendFiles.SendAsync, new FeatureMap((feature, _) => new SendFileFunc(feature.SendFileAsync)) }, + + { OwinConstants.Security.User, new FeatureMap((feature, _) => feature.User, () => null, (feature, _, value) => feature.User = Utilities.MakeClaimsPrincipal((IPrincipal)value), () => new HttpAuthenticationFeature()) }, + + { OwinConstants.RequestId, new FeatureMap((feature, _) => feature.TraceIdentifier, () => null, (feature, _, value) => feature.TraceIdentifier = (string)value, () => new HttpRequestIdentifierFeature()) }, + + { OwinConstants.CallCancelled, new FeatureMap((feature, _) => feature.RequestAborted) }, + + { OwinConstants.CommonKeys.ClientCertificate, new FeatureMap((feature, _) => feature.ClientCertificate, (feature, _, value) => feature.ClientCertificate = (X509Certificate2)value) }, + { OwinConstants.CommonKeys.LoadClientCertAsync, new FeatureMap((feature, _) => new Func(() => feature.GetClientCertificateAsync(CancellationToken.None))) }, + { OwinConstants.WebSocket.AcceptAlt, new FeatureMap((feature, _) => new WebSocketAcceptAlt(feature.AcceptAsync)) } + }; + + private IDictionary _contextEntries; + private HashSet _deletedKeys; + + public int Count + { + get + { + if (_contextEntries is not null) + { + return _contextEntries.Count; + } + return _entries.Count - (_deletedKeys?.Count ?? 0); + } + } + + public IDictionary 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; + } + return _entries.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); + } + + public bool Remove(string key) { + if (_contextEntries is not null) + { + return _contextEntries.Remove(key); + } + + if (_entries.ContainsKey(key)) + { + _deletedKeys ??= new HashSet(); + return _deletedKeys.Add(key); + } + + return false; + } + + public IEnumerator> 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; + } + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public void Clear() + { + InitializeContextEntries(emptyCollection: true); + } + + void InitializeContextEntries(bool emptyCollection = false) + { + if (emptyCollection) + { + _contextEntries = new Dictionary(); + return; + } + + _contextEntries = new Dictionary(_entries); + if (_deletedKeys is not null) + { + foreach (var key in _deletedKeys) + { + _contextEntries.Remove(key); + } + } } } } diff --git a/src/Http/Owin/test/OwinEnvironmentTests.cs b/src/Http/Owin/test/OwinEnvironmentTests.cs index 32eeba3d6768..acbe2b828e8c 100644 --- a/src/Http/Owin/test/OwinEnvironmentTests.cs +++ b/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,53 @@ public void OwinEnvironmentSupportsLinq() Assert.NotNull(orderedEnvironment); } + [Fact] + public void OwinEnvironmentRemoveCorrectlyRemovesEntryAndDoesNotImpactNextOwinEnvironment() + { + var httpContext1 = CreateContext(); + IDictionary 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 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 owinEnvironment1 = new OwinEnvironment(httpContext1); + owinEnvironment1.Clear(); + Assert.Empty(owinEnvironment1); + + var httpContext2 = CreateContext(); + IDictionary owinEnvironment2 = new OwinEnvironment(httpContext2); + Assert.True(owinEnvironment2.Count != 0); + } + private HttpContext CreateContext() { var context = new DefaultHttpContext(); From be3c7d470bed23f3dae85c638ac060343f855bc2 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Mon, 11 Nov 2024 12:03:07 +0100 Subject: [PATCH 02/12] init sample --- AspNetCore.sln | 19 +++++++++++++++ src/Http/HttpAbstractions.slnf | 7 +++--- .../MinimalSampleOwin.csproj | 24 +++++++++++++++++++ src/Http/samples/MinimalSampleOwin/Program.cs | 21 ++++++++++++++++ .../Properties/launchSettings.json | 12 ++++++++++ 5 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 src/Http/samples/MinimalSampleOwin/MinimalSampleOwin.csproj create mode 100644 src/Http/samples/MinimalSampleOwin/Program.cs create mode 100644 src/Http/samples/MinimalSampleOwin/Properties/launchSettings.json diff --git a/AspNetCore.sln b/AspNetCore.sln index 57a1bf9f0b3f..be53ce2bae67 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1816,6 +1816,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.OpenAp EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.App.SourceGenerators", "src\Framework\AspNetCoreAnalyzers\src\SourceGenerators\Microsoft.AspNetCore.App.SourceGenerators.csproj", "{C3928C15-1836-46DB-A09D-9EFBCCA33E08}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MinimalSampleOwin", "src\Http\samples\MinimalSampleOwin\MinimalSampleOwin.csproj", "{95032BC4-8BE5-4703-8075-93CB9DFF93EF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -10977,6 +10979,22 @@ Global {C3928C15-1836-46DB-A09D-9EFBCCA33E08}.Release|x64.Build.0 = Release|Any CPU {C3928C15-1836-46DB-A09D-9EFBCCA33E08}.Release|x86.ActiveCfg = Release|Any CPU {C3928C15-1836-46DB-A09D-9EFBCCA33E08}.Release|x86.Build.0 = Release|Any CPU + {95032BC4-8BE5-4703-8075-93CB9DFF93EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {95032BC4-8BE5-4703-8075-93CB9DFF93EF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {95032BC4-8BE5-4703-8075-93CB9DFF93EF}.Debug|arm64.ActiveCfg = Debug|Any CPU + {95032BC4-8BE5-4703-8075-93CB9DFF93EF}.Debug|arm64.Build.0 = Debug|Any CPU + {95032BC4-8BE5-4703-8075-93CB9DFF93EF}.Debug|x64.ActiveCfg = Debug|Any CPU + {95032BC4-8BE5-4703-8075-93CB9DFF93EF}.Debug|x64.Build.0 = Debug|Any CPU + {95032BC4-8BE5-4703-8075-93CB9DFF93EF}.Debug|x86.ActiveCfg = Debug|Any CPU + {95032BC4-8BE5-4703-8075-93CB9DFF93EF}.Debug|x86.Build.0 = Debug|Any CPU + {95032BC4-8BE5-4703-8075-93CB9DFF93EF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {95032BC4-8BE5-4703-8075-93CB9DFF93EF}.Release|Any CPU.Build.0 = Release|Any CPU + {95032BC4-8BE5-4703-8075-93CB9DFF93EF}.Release|arm64.ActiveCfg = Release|Any CPU + {95032BC4-8BE5-4703-8075-93CB9DFF93EF}.Release|arm64.Build.0 = Release|Any CPU + {95032BC4-8BE5-4703-8075-93CB9DFF93EF}.Release|x64.ActiveCfg = Release|Any CPU + {95032BC4-8BE5-4703-8075-93CB9DFF93EF}.Release|x64.Build.0 = Release|Any CPU + {95032BC4-8BE5-4703-8075-93CB9DFF93EF}.Release|x86.ActiveCfg = Release|Any CPU + {95032BC4-8BE5-4703-8075-93CB9DFF93EF}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -11874,6 +11892,7 @@ Global {B32FF7A7-9CB3-4DCD-AE97-3B2594DB9DAC} = {2299CCD8-8F9C-4F2B-A633-9BF4DA81022B} {B9BBC1A8-7F58-4F43-94C3-5F3CB125CEF7} = {B32FF7A7-9CB3-4DCD-AE97-3B2594DB9DAC} {C3928C15-1836-46DB-A09D-9EFBCCA33E08} = {B5D98AEB-9409-4280-8225-9C1EC6A791B2} + {95032BC4-8BE5-4703-8075-93CB9DFF93EF} = {EB5E294B-9ED5-43BF-AFA9-1CD2327F3DC1} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F} diff --git a/src/Http/HttpAbstractions.slnf b/src/Http/HttpAbstractions.slnf index dbf53c6c4c39..1b73a7939221 100644 --- a/src/Http/HttpAbstractions.slnf +++ b/src/Http/HttpAbstractions.slnf @@ -38,15 +38,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", diff --git a/src/Http/samples/MinimalSampleOwin/MinimalSampleOwin.csproj b/src/Http/samples/MinimalSampleOwin/MinimalSampleOwin.csproj new file mode 100644 index 000000000000..a3c5f0c8e261 --- /dev/null +++ b/src/Http/samples/MinimalSampleOwin/MinimalSampleOwin.csproj @@ -0,0 +1,24 @@ + + + + $(DefaultNetCoreTargetFramework) + enable + true + true + true + + + + + + + + + + + + + + + + diff --git a/src/Http/samples/MinimalSampleOwin/Program.cs b/src/Http/samples/MinimalSampleOwin/Program.cs new file mode 100644 index 000000000000..3e820afd0bef --- /dev/null +++ b/src/Http/samples/MinimalSampleOwin/Program.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +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(); + +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed +hostBuilder.Build().Run(); +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + +Func, Task> ConfigureOwinPipeline(Func, Task> next) => env => +{ + return next(env); +}; diff --git a/src/Http/samples/MinimalSampleOwin/Properties/launchSettings.json b/src/Http/samples/MinimalSampleOwin/Properties/launchSettings.json new file mode 100644 index 000000000000..e9dc44a7b3aa --- /dev/null +++ b/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" + } + } + } +} From 8cd24a21e4ae718b6cbd321a3a472bfdf546ff73 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Tue, 12 Nov 2024 21:11:15 +0100 Subject: [PATCH 03/12] init the owin sample --- src/Http/samples/MinimalSampleOwin/Program.cs | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/src/Http/samples/MinimalSampleOwin/Program.cs b/src/Http/samples/MinimalSampleOwin/Program.cs index 3e820afd0bef..210ff7e73508 100644 --- a/src/Http/samples/MinimalSampleOwin/Program.cs +++ b/src/Http/samples/MinimalSampleOwin/Program.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Reflection; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Http.Metadata; @@ -11,11 +10,34 @@ var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); -#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed -hostBuilder.Build().Run(); -#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed +string Plaintext() => "Hello, World!"; +app.MapGet("/plaintext", Plaintext); -Func, Task> ConfigureOwinPipeline(Func, Task> next) => env => +app.MapGet("/", () => $""" + Operating System: {Environment.OSVersion} + .NET version: {Environment.Version} + Username: {Environment.UserName} + Date and Time: {DateTime.Now} + """); + +app.UseOwin(pipeline => { - return next(env); -}; + 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(); From fb27a6e371e8f7cb7f3c7b84e313e2d48f924cdd Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Wed, 13 Nov 2024 13:34:36 +0100 Subject: [PATCH 04/12] no-public API change --- src/Http/Owin/src/OwinEnvironment.cs | 147 +++++++++++++-------- src/Http/Owin/test/OwinEnvironmentTests.cs | 10 ++ 2 files changed, 100 insertions(+), 57 deletions(-) diff --git a/src/Http/Owin/src/OwinEnvironment.cs b/src/Http/Owin/src/OwinEnvironment.cs index 9582dde288f9..2e25855d4f6f 100644 --- a/src/Http/Owin/src/OwinEnvironment.cs +++ b/src/Http/Owin/src/OwinEnvironment.cs @@ -28,7 +28,7 @@ namespace Microsoft.AspNetCore.Owin; /// public class OwinEnvironment : IDictionary { - private readonly OwinEntries _owinEntries = new(); + private readonly OwinEntries _owinEntries; private readonly HttpContext _context; /// @@ -59,6 +59,8 @@ public OwinEnvironment(HttpContext context) } _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. @@ -234,7 +236,7 @@ public class FeatureMap /// /// The feature interface type. /// Value getter. - public FeatureMap(Type featureInterface, Func getter) + public FeatureMap(Type featureInterface, Func getter) : this(featureInterface, getter, defaultFactory: null) { } @@ -245,7 +247,7 @@ public FeatureMap(Type featureInterface, Func gette /// The feature interface type. /// Value getter delegate. /// Default value factory delegate. - public FeatureMap(Type featureInterface, Func getter, Func defaultFactory) + public FeatureMap(Type featureInterface, Func getter, Func defaultFactory) : this(featureInterface, getter, defaultFactory, setter: null) { } @@ -256,7 +258,7 @@ public FeatureMap(Type featureInterface, Func gette /// The feature interface type. /// Value getter delegate. /// Value setter delegate. - public FeatureMap(Type featureInterface, Func getter, Action setter) + public FeatureMap(Type featureInterface, Func getter, Action setter) : this(featureInterface, getter, defaultFactory: null, setter: setter) { } @@ -268,7 +270,7 @@ public FeatureMap(Type featureInterface, Func gette /// Value getter delegate. /// Default value factory delegate. /// Value setter delegate. - public FeatureMap(Type featureInterface, Func getter, Func defaultFactory, Action setter) + public FeatureMap(Type featureInterface, Func getter, Func defaultFactory, Action setter) : this(featureInterface, getter, defaultFactory, setter, featureFactory: null) { } @@ -281,7 +283,7 @@ public FeatureMap(Type featureInterface, Func gette /// Default value factory delegate. /// Value setter delegate. /// Feature factory delegate. - public FeatureMap(Type featureInterface, Func getter, Func defaultFactory, Action setter, Func featureFactory) + public FeatureMap(Type featureInterface, Func getter, Func defaultFactory, Action setter, Func featureFactory) { FeatureInterface = featureInterface; Getter = getter; @@ -291,8 +293,8 @@ public FeatureMap(Type featureInterface, Func gette } private Type FeatureInterface { get; set; } - private Func Getter { get; set; } - private Action Setter { get; set; } + private Func Getter { get; set; } + private Action Setter { get; set; } private Func DefaultFactory { get; set; } private Func FeatureFactory { get; set; } @@ -312,7 +314,7 @@ internal bool TryGet(HttpContext context, out object value) value = null; return false; } - value = Getter(featureInstance, context); + value = Getter(featureInstance); if (value == null && DefaultFactory != null) { value = DefaultFactory(); @@ -335,7 +337,7 @@ internal void Set(HttpContext context, object value) context.Features[FeatureInterface] = feature; } } - Setter(feature, context, value); + Setter(feature, value); } } @@ -349,8 +351,8 @@ public class FeatureMap : FeatureMap /// Initializes a new instance of for the specified feature interface type. /// /// Value getter. - public FeatureMap(Func getter) - : base(typeof(TFeature), (feature, context) => getter((TFeature)feature, context)) + public FeatureMap(Func getter) + : base(typeof(TFeature), feature => getter((TFeature)feature)) { } @@ -359,8 +361,8 @@ public FeatureMap(Func getter) /// /// Value getter delegate. /// Default value factory delegate. - public FeatureMap(Func getter, Func defaultFactory) - : base(typeof(TFeature), (feature, context) => getter((TFeature)feature, context), defaultFactory) + public FeatureMap(Func getter, Func defaultFactory) + : base(typeof(TFeature), feature => getter((TFeature)feature), defaultFactory) { } @@ -369,8 +371,8 @@ public FeatureMap(Func getter, Func defau /// /// Value getter delegate. /// Value setter delegate. - public FeatureMap(Func getter, Action setter) - : base(typeof(TFeature), (feature, context) => getter((TFeature)feature, context), (feature, context, value) => setter((TFeature)feature, context, value)) + public FeatureMap(Func getter, Action setter) + : base(typeof(TFeature), feature => getter((TFeature)feature), (feature, value) => setter((TFeature)feature, value)) { } @@ -380,8 +382,8 @@ public FeatureMap(Func getter, ActionValue getter delegate. /// Default value factory delegate. /// Value setter delegate. - public FeatureMap(Func getter, Func defaultFactory, Action setter) - : base(typeof(TFeature), (feature, context) => getter((TFeature)feature, context), defaultFactory, (feature, context, value) => setter((TFeature)feature, context, value)) + public FeatureMap(Func getter, Func defaultFactory, Action setter) + : base(typeof(TFeature), feature => getter((TFeature)feature), defaultFactory, (feature, value) => setter((TFeature)feature, value)) { } @@ -392,8 +394,8 @@ public FeatureMap(Func getter, Func defau /// Default value factory delegate. /// Value setter delegate. /// Feature factory delegate. - public FeatureMap(Func getter, Func defaultFactory, Action setter, Func featureFactory) - : base(typeof(TFeature), (feature, context) => getter((TFeature)feature, context), defaultFactory, (feature, context, value) => setter((TFeature)feature, context, value), () => featureFactory()) + public FeatureMap(Func getter, Func defaultFactory, Action setter, Func featureFactory) + : base(typeof(TFeature), feature => getter((TFeature)feature), defaultFactory, (feature, value) => setter((TFeature)feature, value), () => featureFactory()) { } } @@ -402,23 +404,21 @@ private class OwinEntries : IEnumerable> { private static readonly IDictionary _entries = new Dictionary { - { OwinConstants.RequestProtocol, new FeatureMap((feature, _) => feature.Protocol, () => string.Empty, (feature, _, value) => feature.Protocol = Convert.ToString(value, CultureInfo.InvariantCulture)) }, - { OwinConstants.RequestScheme, new FeatureMap((feature, _) => feature.Scheme, () => string.Empty, (feature, _, value) => feature.Scheme = Convert.ToString(value, CultureInfo.InvariantCulture)) }, - { OwinConstants.RequestMethod, new FeatureMap((feature, _) => feature.Method, () => string.Empty, (feature, _, value) => feature.Method = Convert.ToString(value, CultureInfo.InvariantCulture)) }, - { OwinConstants.RequestPathBase, new FeatureMap((feature, _) => feature.PathBase, () => string.Empty, (feature, _, value) => feature.PathBase = Convert.ToString(value, CultureInfo.InvariantCulture)) }, - { OwinConstants.RequestPath, new FeatureMap((feature, _) => feature.Path, () => string.Empty, (feature, _, value) => feature.Path = Convert.ToString(value, CultureInfo.InvariantCulture)) }, - { OwinConstants.RequestQueryString, new FeatureMap((feature, _) => Utilities.RemoveQuestionMark(feature.QueryString), () => string.Empty, (feature, _, value) => feature.QueryString = Utilities.AddQuestionMark(Convert.ToString(value, CultureInfo.InvariantCulture))) }, - { OwinConstants.RequestHeaders, new FeatureMap((feature, _) => Utilities.MakeDictionaryStringArray(feature.Headers), (feature, _, value) => feature.Headers = Utilities.MakeHeaderDictionary((IDictionary)value)) }, - { OwinConstants.RequestBody, new FeatureMap((feature, _) => feature.Body, () => Stream.Null, (feature, _, value) => feature.Body = (Stream)value) }, - { OwinConstants.RequestUser, new FeatureMap((feature, _) => feature.User, () => null, (feature, _, value) => feature.User = (ClaimsPrincipal)value) }, - - { OwinConstants.ResponseStatusCode, new FeatureMap((feature, _) => feature.StatusCode, () => 200, (feature, _, value) => feature.StatusCode = Convert.ToInt32(value, CultureInfo.InvariantCulture)) }, - { OwinConstants.ResponseReasonPhrase, new FeatureMap((feature, _) => feature.ReasonPhrase, (feature, _, value) => feature.ReasonPhrase = Convert.ToString(value, CultureInfo.InvariantCulture)) }, - { OwinConstants.ResponseHeaders, new FeatureMap((feature, _) => Utilities.MakeDictionaryStringArray(feature.Headers), (feature, _, value) => feature.Headers = Utilities.MakeHeaderDictionary((IDictionary)value)) }, - { OwinConstants.ResponseBody, new FeatureMap((feature, _) => feature.Stream, () => Stream.Null, (feature, context, value) => context.Response.Body = (Stream)value) }, // DefaultHttpResponse.Body.Set has built in logic to handle replacing the feature. - { - OwinConstants.CommonKeys.OnSendingHeaders, new FeatureMap( - (feature, _) => new Action, object>((cb, state) => { + { OwinConstants.RequestProtocol, new FeatureMap(feature => feature.Protocol, () => string.Empty, (feature, value) => feature.Protocol = Convert.ToString(value, CultureInfo.InvariantCulture)) }, + { OwinConstants.RequestScheme, new FeatureMap(feature => feature.Scheme, () => string.Empty, (feature, value) => feature.Scheme = Convert.ToString(value, CultureInfo.InvariantCulture)) }, + { OwinConstants.RequestMethod, new FeatureMap(feature => feature.Method, () => string.Empty, (feature, value) => feature.Method = Convert.ToString(value, CultureInfo.InvariantCulture)) }, + { OwinConstants.RequestPathBase, new FeatureMap(feature => feature.PathBase, () => string.Empty, (feature, value) => feature.PathBase = Convert.ToString(value, CultureInfo.InvariantCulture)) }, + { OwinConstants.RequestPath, new FeatureMap(feature => feature.Path, () => string.Empty, (feature, value) => feature.Path = Convert.ToString(value, CultureInfo.InvariantCulture)) }, + { OwinConstants.RequestQueryString, new FeatureMap(feature => Utilities.RemoveQuestionMark(feature.QueryString), () => string.Empty, (feature, value) => feature.QueryString = Utilities.AddQuestionMark(Convert.ToString(value, CultureInfo.InvariantCulture))) }, + { OwinConstants.RequestHeaders, new FeatureMap(feature => Utilities.MakeDictionaryStringArray(feature.Headers), (feature, value) => feature.Headers = Utilities.MakeHeaderDictionary((IDictionary)value)) }, + { OwinConstants.RequestBody, new FeatureMap(feature => feature.Body, () => Stream.Null, (feature, value) => feature.Body = (Stream)value) }, + { OwinConstants.RequestUser, new FeatureMap(feature => feature.User, () => null, (feature, value) => feature.User = (ClaimsPrincipal)value) }, + + { OwinConstants.ResponseStatusCode, new FeatureMap(feature => feature.StatusCode, () => 200, (feature, value) => feature.StatusCode = Convert.ToInt32(value, CultureInfo.InvariantCulture)) }, + { OwinConstants.ResponseReasonPhrase, new FeatureMap(feature => feature.ReasonPhrase, (feature, value) => feature.ReasonPhrase = Convert.ToString(value, CultureInfo.InvariantCulture)) }, + { OwinConstants.ResponseHeaders, new FeatureMap(feature => Utilities.MakeDictionaryStringArray(feature.Headers), (feature, value) => feature.Headers = Utilities.MakeHeaderDictionary((IDictionary)value)) }, + { OwinConstants.CommonKeys.OnSendingHeaders, new FeatureMap( + feature => new Action, object>((cb, state) => { feature.OnStarting(s => { cb(s); @@ -427,30 +427,51 @@ private class OwinEntries : IEnumerable> })) }, - { OwinConstants.CommonKeys.ConnectionId, new FeatureMap((feature, _) => feature.ConnectionId, (feature, _, value) => feature.ConnectionId = Convert.ToString(value, CultureInfo.InvariantCulture)) }, - - { OwinConstants.CommonKeys.LocalPort, new FeatureMap((feature, _) => feature.LocalPort.ToString(CultureInfo.InvariantCulture), (feature, _, value) => feature.LocalPort = Convert.ToInt32(value, CultureInfo.InvariantCulture)) }, - { OwinConstants.CommonKeys.RemotePort, new FeatureMap((feature, _) => feature.RemotePort.ToString(CultureInfo.InvariantCulture), (feature, _, value) => feature.RemotePort = Convert.ToInt32(value, CultureInfo.InvariantCulture)) }, - - { OwinConstants.CommonKeys.LocalIpAddress, new FeatureMap((feature, _) => feature.LocalIpAddress.ToString(), (feature, _, value) => feature.LocalIpAddress = IPAddress.Parse(Convert.ToString(value, CultureInfo.InvariantCulture))) }, - { OwinConstants.CommonKeys.RemoteIpAddress, new FeatureMap((feature, _) => feature.RemoteIpAddress.ToString(), (feature, _, value) => feature.RemoteIpAddress = IPAddress.Parse(Convert.ToString(value, CultureInfo.InvariantCulture))) }, - - { OwinConstants.SendFiles.SendAsync, new FeatureMap((feature, _) => new SendFileFunc(feature.SendFileAsync)) }, + { OwinConstants.CommonKeys.ConnectionId, new FeatureMap(feature => feature.ConnectionId, (feature, value) => feature.ConnectionId = Convert.ToString(value, CultureInfo.InvariantCulture)) }, - { OwinConstants.Security.User, new FeatureMap((feature, _) => feature.User, () => null, (feature, _, value) => feature.User = Utilities.MakeClaimsPrincipal((IPrincipal)value), () => new HttpAuthenticationFeature()) }, + { OwinConstants.CommonKeys.LocalPort, new FeatureMap(feature => feature.LocalPort.ToString(CultureInfo.InvariantCulture), (feature, value) => feature.LocalPort = Convert.ToInt32(value, CultureInfo.InvariantCulture)) }, + { OwinConstants.CommonKeys.RemotePort, new FeatureMap(feature => feature.RemotePort.ToString(CultureInfo.InvariantCulture), (feature, value) => feature.RemotePort = Convert.ToInt32(value, CultureInfo.InvariantCulture)) }, - { OwinConstants.RequestId, new FeatureMap((feature, _) => feature.TraceIdentifier, () => null, (feature, _, value) => feature.TraceIdentifier = (string)value, () => new HttpRequestIdentifierFeature()) }, + { OwinConstants.CommonKeys.LocalIpAddress, new FeatureMap(feature => feature.LocalIpAddress.ToString(), (feature, value) => feature.LocalIpAddress = IPAddress.Parse(Convert.ToString(value, CultureInfo.InvariantCulture))) }, + { OwinConstants.CommonKeys.RemoteIpAddress, new FeatureMap(feature => feature.RemoteIpAddress.ToString(), (feature, value) => feature.RemoteIpAddress = IPAddress.Parse(Convert.ToString(value, CultureInfo.InvariantCulture))) }, - { OwinConstants.CallCancelled, new FeatureMap((feature, _) => feature.RequestAborted) }, + { OwinConstants.SendFiles.SendAsync, new FeatureMap(feature => new SendFileFunc(feature.SendFileAsync)) }, + { OwinConstants.Security.User, new FeatureMap(feature => feature.User, ()=> null, (feature, value) => feature.User = Utilities.MakeClaimsPrincipal((IPrincipal)value), () => new HttpAuthenticationFeature()) }, + { OwinConstants.RequestId, new FeatureMap(feature => feature.TraceIdentifier, ()=> null, (feature, value) => feature.TraceIdentifier = (string)value, () => new HttpRequestIdentifierFeature()) }, + { OwinConstants.CallCancelled, new FeatureMap(feature => feature.RequestAborted) }, - { OwinConstants.CommonKeys.ClientCertificate, new FeatureMap((feature, _) => feature.ClientCertificate, (feature, _, value) => feature.ClientCertificate = (X509Certificate2)value) }, - { OwinConstants.CommonKeys.LoadClientCertAsync, new FeatureMap((feature, _) => new Func(() => feature.GetClientCertificateAsync(CancellationToken.None))) }, - { OwinConstants.WebSocket.AcceptAlt, new FeatureMap((feature, _) => new WebSocketAcceptAlt(feature.AcceptAsync)) } + { OwinConstants.CommonKeys.ClientCertificate, new FeatureMap(feature => feature.ClientCertificate, (feature, value) => feature.ClientCertificate = (X509Certificate2)value) }, + { OwinConstants.CommonKeys.LoadClientCertAsync, new FeatureMap(feature => new Func(() => feature.GetClientCertificateAsync(CancellationToken.None))) }, + { OwinConstants.WebSocket.AcceptAlt, new FeatureMap(feature => new WebSocketAcceptAlt(feature.AcceptAsync)) } }; + /// + /// Will be used, only if or is called from user-code. + /// Is a deep-copy of the singleton + /// private IDictionary _contextEntries; + + /// + /// In order not to copy the whole dictionary of featureMaps per request, + /// and since OWIN allows the operation, + /// it's more lightweight to keep track of deleted keys. + /// private HashSet _deletedKeys; + /// + /// There are some entries that are context-dependent. + /// This dictionary is allocated per request, but does not contain as many entries as . + /// + private readonly IDictionary _contextDependentEntries; + + public OwinEntries(HttpContext context) + { + _contextDependentEntries = new Dictionary + { + { OwinConstants.ResponseBody, new FeatureMap(feature => feature.Stream, () => Stream.Null, (feature, value) => context.Response.Body = (Stream)value) }, // DefaultHttpResponse.Body.Set has built in logic to handle replacing the feature. + }; + } + public int Count { get @@ -459,7 +480,8 @@ public int Count { return _contextEntries.Count; } - return _entries.Count - (_deletedKeys?.Count ?? 0); + + return _entries.Count + _contextDependentEntries.Count - (_deletedKeys?.Count ?? 0); } } @@ -481,7 +503,13 @@ public bool TryGetValue(string key, out FeatureMap entry) entry = null; return false; } - return _entries.TryGetValue(key, out entry); + + if (_entries.TryGetValue(key, out entry)) + { + return true; + } + + return _contextDependentEntries.TryGetValue(key, out entry); } public bool ContainsKey(string key) @@ -496,7 +524,7 @@ public bool ContainsKey(string key) return false; } - return _entries.ContainsKey(key); + return _entries.ContainsKey(key) || _contextDependentEntries.ContainsKey(key); } public bool Remove(string key) @@ -506,7 +534,7 @@ public bool Remove(string key) return _contextEntries.Remove(key); } - if (_entries.ContainsKey(key)) + if (_entries.ContainsKey(key) || _contextDependentEntries.ContainsKey(key)) { _deletedKeys ??= new HashSet(); return _deletedKeys.Add(key); @@ -526,7 +554,7 @@ public IEnumerator> GetEnumerator() } else { - foreach (var entry in _entries) + foreach (var entry in _entries.Union(_contextDependentEntries)) { if (_deletedKeys?.Contains(entry.Key) == true) { @@ -554,6 +582,11 @@ void InitializeContextEntries(bool emptyCollection = false) } _contextEntries = new Dictionary(_entries); + foreach (var entry in _contextDependentEntries) + { + _contextEntries[entry.Key] = entry.Value; + } + if (_deletedKeys is not null) { foreach (var key in _deletedKeys) diff --git a/src/Http/Owin/test/OwinEnvironmentTests.cs b/src/Http/Owin/test/OwinEnvironmentTests.cs index acbe2b828e8c..272f61c257ee 100644 --- a/src/Http/Owin/test/OwinEnvironmentTests.cs +++ b/src/Http/Owin/test/OwinEnvironmentTests.cs @@ -207,6 +207,16 @@ public void OwinEnvironmentClearBehavesCorrectlyAndDoesNotImpactNextOwinEnvironm Assert.True(owinEnvironment2.Count != 0); } + [Fact] + public void OwinEnvironmentAccessContextDependentFeatureBehavesCorrectly() + { + var httpContext = CreateContext(); + IDictionary 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(); From a5f82643bd351b8f5198e7db90d322dc555db000 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Wed, 13 Nov 2024 13:55:42 +0100 Subject: [PATCH 05/12] remove dependency on sample --- src/Http/samples/MinimalSampleOwin/MinimalSampleOwin.csproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Http/samples/MinimalSampleOwin/MinimalSampleOwin.csproj b/src/Http/samples/MinimalSampleOwin/MinimalSampleOwin.csproj index a3c5f0c8e261..b9b4ede801fe 100644 --- a/src/Http/samples/MinimalSampleOwin/MinimalSampleOwin.csproj +++ b/src/Http/samples/MinimalSampleOwin/MinimalSampleOwin.csproj @@ -17,8 +17,4 @@ - - - - From 478c6db17a328d97b79e9f48fa24ed01dbe30063 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Wed, 13 Nov 2024 15:03:33 +0100 Subject: [PATCH 06/12] intro benchmark --- AspNetCore.sln | 19 ++++++++ src/Http/HttpAbstractions.slnf | 1 + .../Benchmarks/OwinEnvironmentBenchmark.cs | 47 +++++++++++++++++++ ...oft.AspNetCore.Owin.Microbenchmarks.csproj | 17 +++++++ .../Program.cs | 6 +++ 5 files changed, 90 insertions(+) create mode 100644 src/Http/Owin/benchmarks/Microsoft.AspNetCore.Owin.Microbenchmarks/Benchmarks/OwinEnvironmentBenchmark.cs create mode 100644 src/Http/Owin/benchmarks/Microsoft.AspNetCore.Owin.Microbenchmarks/Microsoft.AspNetCore.Owin.Microbenchmarks.csproj create mode 100644 src/Http/Owin/benchmarks/Microsoft.AspNetCore.Owin.Microbenchmarks/Program.cs diff --git a/AspNetCore.sln b/AspNetCore.sln index be53ce2bae67..ac7e4c3a854b 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1818,6 +1818,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.App.So EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MinimalSampleOwin", "src\Http\samples\MinimalSampleOwin\MinimalSampleOwin.csproj", "{95032BC4-8BE5-4703-8075-93CB9DFF93EF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Owin.Microbenchmarks", "src\Http\Owin\benchmarks\Microsoft.AspNetCore.Owin.Microbenchmarks\Microsoft.AspNetCore.Owin.Microbenchmarks.csproj", "{E0FBCBF6-030C-4F15-A58A-F0F33DCC26E9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -10995,6 +10997,22 @@ Global {95032BC4-8BE5-4703-8075-93CB9DFF93EF}.Release|x64.Build.0 = Release|Any CPU {95032BC4-8BE5-4703-8075-93CB9DFF93EF}.Release|x86.ActiveCfg = Release|Any CPU {95032BC4-8BE5-4703-8075-93CB9DFF93EF}.Release|x86.Build.0 = Release|Any CPU + {E0FBCBF6-030C-4F15-A58A-F0F33DCC26E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E0FBCBF6-030C-4F15-A58A-F0F33DCC26E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E0FBCBF6-030C-4F15-A58A-F0F33DCC26E9}.Debug|arm64.ActiveCfg = Debug|Any CPU + {E0FBCBF6-030C-4F15-A58A-F0F33DCC26E9}.Debug|arm64.Build.0 = Debug|Any CPU + {E0FBCBF6-030C-4F15-A58A-F0F33DCC26E9}.Debug|x64.ActiveCfg = Debug|Any CPU + {E0FBCBF6-030C-4F15-A58A-F0F33DCC26E9}.Debug|x64.Build.0 = Debug|Any CPU + {E0FBCBF6-030C-4F15-A58A-F0F33DCC26E9}.Debug|x86.ActiveCfg = Debug|Any CPU + {E0FBCBF6-030C-4F15-A58A-F0F33DCC26E9}.Debug|x86.Build.0 = Debug|Any CPU + {E0FBCBF6-030C-4F15-A58A-F0F33DCC26E9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E0FBCBF6-030C-4F15-A58A-F0F33DCC26E9}.Release|Any CPU.Build.0 = Release|Any CPU + {E0FBCBF6-030C-4F15-A58A-F0F33DCC26E9}.Release|arm64.ActiveCfg = Release|Any CPU + {E0FBCBF6-030C-4F15-A58A-F0F33DCC26E9}.Release|arm64.Build.0 = Release|Any CPU + {E0FBCBF6-030C-4F15-A58A-F0F33DCC26E9}.Release|x64.ActiveCfg = Release|Any CPU + {E0FBCBF6-030C-4F15-A58A-F0F33DCC26E9}.Release|x64.Build.0 = Release|Any CPU + {E0FBCBF6-030C-4F15-A58A-F0F33DCC26E9}.Release|x86.ActiveCfg = Release|Any CPU + {E0FBCBF6-030C-4F15-A58A-F0F33DCC26E9}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -11893,6 +11911,7 @@ Global {B9BBC1A8-7F58-4F43-94C3-5F3CB125CEF7} = {B32FF7A7-9CB3-4DCD-AE97-3B2594DB9DAC} {C3928C15-1836-46DB-A09D-9EFBCCA33E08} = {B5D98AEB-9409-4280-8225-9C1EC6A791B2} {95032BC4-8BE5-4703-8075-93CB9DFF93EF} = {EB5E294B-9ED5-43BF-AFA9-1CD2327F3DC1} + {E0FBCBF6-030C-4F15-A58A-F0F33DCC26E9} = {AA465A56-D9D0-4684-95B0-192F4436C582} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F} diff --git a/src/Http/HttpAbstractions.slnf b/src/Http/HttpAbstractions.slnf index 1b73a7939221..09e1b277cb68 100644 --- a/src/Http/HttpAbstractions.slnf +++ b/src/Http/HttpAbstractions.slnf @@ -31,6 +31,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", diff --git a/src/Http/Owin/benchmarks/Microsoft.AspNetCore.Owin.Microbenchmarks/Benchmarks/OwinEnvironmentBenchmark.cs b/src/Http/Owin/benchmarks/Microsoft.AspNetCore.Owin.Microbenchmarks/Benchmarks/OwinEnvironmentBenchmark.cs new file mode 100644 index 000000000000..cc8188908c6d --- /dev/null +++ b/src/Http/Owin/benchmarks/Microsoft.AspNetCore.Owin.Microbenchmarks/Benchmarks/OwinEnvironmentBenchmark.cs @@ -0,0 +1,47 @@ +// 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 +{ + private RequestDelegate _requestDelegate; + private readonly HttpContext _httpContext = new DefaultHttpContext(); + + [GlobalSetup] + public void GlobalSetup() + { + var serviceProvider = new ServiceCollection().BuildServiceProvider(); + var builder = new ApplicationBuilder(serviceProvider); + IDictionary environment = null; + + builder.UseOwin(addToPipeline => + { + addToPipeline(next => + { + return async env => + { + environment = env; + await next(env); + }; + }); + }); + + _requestDelegate = builder.Build(); + } + + [Benchmark] + public async Task ProcessMultipleRequests() + { + foreach (var i in Enumerable.Range(0, 10000)) + { + await _requestDelegate(_httpContext); + } + } +} diff --git a/src/Http/Owin/benchmarks/Microsoft.AspNetCore.Owin.Microbenchmarks/Microsoft.AspNetCore.Owin.Microbenchmarks.csproj b/src/Http/Owin/benchmarks/Microsoft.AspNetCore.Owin.Microbenchmarks/Microsoft.AspNetCore.Owin.Microbenchmarks.csproj new file mode 100644 index 000000000000..5ca0b8f6e5da --- /dev/null +++ b/src/Http/Owin/benchmarks/Microsoft.AspNetCore.Owin.Microbenchmarks/Microsoft.AspNetCore.Owin.Microbenchmarks.csproj @@ -0,0 +1,17 @@ + + + + Exe + $(DefaultNetCoreTargetFramework) + + + + + + + + + + + + diff --git a/src/Http/Owin/benchmarks/Microsoft.AspNetCore.Owin.Microbenchmarks/Program.cs b/src/Http/Owin/benchmarks/Microsoft.AspNetCore.Owin.Microbenchmarks/Program.cs new file mode 100644 index 000000000000..74471e67274a --- /dev/null +++ b/src/Http/Owin/benchmarks/Microsoft.AspNetCore.Owin.Microbenchmarks/Program.cs @@ -0,0 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using BenchmarkDotNet.Running; + +BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(); From a61aff4acbbebce9e0a40ccc8d2d62219b6b94a0 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Wed, 13 Nov 2024 15:39:43 +0100 Subject: [PATCH 07/12] make `OwinEntries` sealed --- src/Http/Owin/src/OwinEnvironment.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Owin/src/OwinEnvironment.cs b/src/Http/Owin/src/OwinEnvironment.cs index 2e25855d4f6f..c0f688216693 100644 --- a/src/Http/Owin/src/OwinEnvironment.cs +++ b/src/Http/Owin/src/OwinEnvironment.cs @@ -400,7 +400,7 @@ public FeatureMap(Func getter, Func defaultFactory, Ac } } - private class OwinEntries : IEnumerable> + private sealed class OwinEntries : IEnumerable> { private static readonly IDictionary _entries = new Dictionary { From 8ae8104c867ee3c8e78a570792e25639eec705e9 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Wed, 13 Nov 2024 21:14:10 +0100 Subject: [PATCH 08/12] improve port convert + enumerators for headers --- .../Benchmarks/OwinEnvironmentBenchmark.cs | 94 +++++++++++++++---- .../Owin/src/DictionaryStringArrayWrapper.cs | 50 ++++++++-- .../Owin/src/DictionaryStringValuesWrapper.cs | 50 ++++++++-- src/Http/Owin/src/OwinEnvironment.cs | 16 +++- 4 files changed, 179 insertions(+), 31 deletions(-) diff --git a/src/Http/Owin/benchmarks/Microsoft.AspNetCore.Owin.Microbenchmarks/Benchmarks/OwinEnvironmentBenchmark.cs b/src/Http/Owin/benchmarks/Microsoft.AspNetCore.Owin.Microbenchmarks/Benchmarks/OwinEnvironmentBenchmark.cs index cc8188908c6d..0e2eead9e260 100644 --- a/src/Http/Owin/benchmarks/Microsoft.AspNetCore.Owin.Microbenchmarks/Benchmarks/OwinEnvironmentBenchmark.cs +++ b/src/Http/Owin/benchmarks/Microsoft.AspNetCore.Owin.Microbenchmarks/Benchmarks/OwinEnvironmentBenchmark.cs @@ -11,37 +11,97 @@ namespace Microsoft.AspNetCore.Owin.Microbenchmarks.Benchmarks; [MemoryDiagnoser] public class OwinEnvironmentBenchmark { - private RequestDelegate _requestDelegate; - private readonly HttpContext _httpContext = new DefaultHttpContext(); + 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> beforeOwinInvokeAction = null, + Action> afterOwinInvokeAction = null) { var serviceProvider = new ServiceCollection().BuildServiceProvider(); var builder = new ApplicationBuilder(serviceProvider); - IDictionary environment = null; - builder.UseOwin(addToPipeline => + return builder.UseOwin(addToPipeline => { addToPipeline(next => { return async env => { - environment = env; + if (beforeOwinInvokeAction is not null) + { + beforeOwinInvokeAction(env); + } + await next(env); + + if (afterOwinInvokeAction is not null) + { + afterOwinInvokeAction(env); + } }; }); - }); - - _requestDelegate = builder.Build(); - } - - [Benchmark] - public async Task ProcessMultipleRequests() - { - foreach (var i in Enumerable.Range(0, 10000)) - { - await _requestDelegate(_httpContext); - } + }).Build(); } } diff --git a/src/Http/Owin/src/DictionaryStringArrayWrapper.cs b/src/Http/Owin/src/DictionaryStringArrayWrapper.cs index 00a726a6f682..a5f9ab8aa189 100644 --- a/src/Http/Owin/src/DictionaryStringArrayWrapper.cs +++ b/src/Http/Owin/src/DictionaryStringArrayWrapper.cs @@ -10,16 +10,16 @@ namespace Microsoft.AspNetCore.Owin; internal sealed class DictionaryStringArrayWrapper : IDictionary { + public readonly IHeaderDictionary Inner; + public DictionaryStringArrayWrapper(IHeaderDictionary inner) { Inner = inner; } - public readonly IHeaderDictionary Inner; - - private static KeyValuePair Convert(KeyValuePair item) => new KeyValuePair(item.Key, item.Value); + private static KeyValuePair Convert(KeyValuePair item) => new(item.Key, item.Value); - private static KeyValuePair Convert(KeyValuePair item) => new KeyValuePair(item.Key, item.Value); + private static KeyValuePair Convert(KeyValuePair item) => new(item.Key, item.Value); private string[] Convert(StringValues item) => item; @@ -55,9 +55,11 @@ void ICollection>.CopyTo(KeyValuePair Inner.Select(Convert).GetEnumerator(); + public Enumerator GetEnumerator() => new Enumerator(Inner); + + IEnumerator> IEnumerable>.GetEnumerator() => new Enumerator(Inner); - IEnumerator> IEnumerable>.GetEnumerator() => Inner.Select(Convert).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => new Enumerator(Inner); bool ICollection>.Remove(KeyValuePair item) => Inner.Remove(Convert(item)); @@ -74,4 +76,40 @@ bool IDictionary.TryGetValue(string key, out string[] value) value = default(StringValues); return false; } + + public struct Enumerator : IEnumerator>, IEnumerator + { + private IEnumerator> _inner; + private KeyValuePair _current; + + internal Enumerator(IDictionary 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 Current => _current; + + object? IEnumerator.Current => Current; + + void IEnumerator.Reset() => throw new NotImplementedException(); + } } diff --git a/src/Http/Owin/src/DictionaryStringValuesWrapper.cs b/src/Http/Owin/src/DictionaryStringValuesWrapper.cs index 50ab189ba5c2..a8c081ad5f8c 100644 --- a/src/Http/Owin/src/DictionaryStringValuesWrapper.cs +++ b/src/Http/Owin/src/DictionaryStringValuesWrapper.cs @@ -11,16 +11,16 @@ namespace Microsoft.AspNetCore.Owin; internal sealed class DictionaryStringValuesWrapper : IHeaderDictionary { + public readonly IDictionary Inner; + public DictionaryStringValuesWrapper(IDictionary inner) { Inner = inner; } - public readonly IDictionary Inner; - - private static KeyValuePair Convert(KeyValuePair item) => new KeyValuePair(item.Key, item.Value); + private static KeyValuePair Convert(KeyValuePair item) => new(item.Key, item.Value); - private static KeyValuePair Convert(KeyValuePair item) => new KeyValuePair(item.Key, item.Value); + private static KeyValuePair Convert(KeyValuePair item) => new(item.Key, item.Value); private StringValues Convert(string[] item) => item; @@ -100,9 +100,11 @@ void ICollection>.CopyTo(KeyValuePair Inner.Select(Convert).GetEnumerator(); + public Enumerator GetEnumerator() => new Enumerator(Inner); + + IEnumerator> IEnumerable>.GetEnumerator() => new Enumerator(Inner); - IEnumerator> IEnumerable>.GetEnumerator() => Inner.Select(Convert).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => new Enumerator(Inner); bool ICollection>.Remove(KeyValuePair item) => Inner.Remove(Convert(item)); @@ -119,4 +121,40 @@ bool IDictionary.TryGetValue(string key, out StringValues value = default(StringValues); return false; } + + public struct Enumerator : IEnumerator>, IEnumerator + { + private IEnumerator> _inner; + private KeyValuePair _current; + + internal Enumerator(IDictionary 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 Current => _current; + + object? IEnumerator.Current => Current; + + void IEnumerator.Reset() => throw new NotImplementedException(); + } } diff --git a/src/Http/Owin/src/OwinEnvironment.cs b/src/Http/Owin/src/OwinEnvironment.cs index c0f688216693..3cb22c3f82eb 100644 --- a/src/Http/Owin/src/OwinEnvironment.cs +++ b/src/Http/Owin/src/OwinEnvironment.cs @@ -429,8 +429,8 @@ private sealed class OwinEntries : IEnumerable> { OwinConstants.CommonKeys.ConnectionId, new FeatureMap(feature => feature.ConnectionId, (feature, value) => feature.ConnectionId = Convert.ToString(value, CultureInfo.InvariantCulture)) }, - { OwinConstants.CommonKeys.LocalPort, new FeatureMap(feature => feature.LocalPort.ToString(CultureInfo.InvariantCulture), (feature, value) => feature.LocalPort = Convert.ToInt32(value, CultureInfo.InvariantCulture)) }, - { OwinConstants.CommonKeys.RemotePort, new FeatureMap(feature => feature.RemotePort.ToString(CultureInfo.InvariantCulture), (feature, value) => feature.RemotePort = Convert.ToInt32(value, CultureInfo.InvariantCulture)) }, + { OwinConstants.CommonKeys.LocalPort, new FeatureMap(feature => PortToString(feature.LocalPort), (feature, value) => feature.LocalPort = Convert.ToInt32(value, CultureInfo.InvariantCulture)) }, + { OwinConstants.CommonKeys.RemotePort, new FeatureMap(feature => PortToString(feature.RemotePort), (feature, value) => feature.RemotePort = Convert.ToInt32(value, CultureInfo.InvariantCulture)) }, { OwinConstants.CommonKeys.LocalIpAddress, new FeatureMap(feature => feature.LocalIpAddress.ToString(), (feature, value) => feature.LocalIpAddress = IPAddress.Parse(Convert.ToString(value, CultureInfo.InvariantCulture))) }, { OwinConstants.CommonKeys.RemoteIpAddress, new FeatureMap(feature => feature.RemoteIpAddress.ToString(), (feature, value) => feature.RemoteIpAddress = IPAddress.Parse(Convert.ToString(value, CultureInfo.InvariantCulture))) }, @@ -472,6 +472,18 @@ public OwinEntries(HttpContext context) }; } + static readonly string[] PortStrings = CreatePortStrings(); + static string[] CreatePortStrings() + { + var ports = new string[65535]; // limit of ephemeral ports https://en.wikipedia.org/wiki/Ephemeral_port + for (var i = 0; i < ports.Length; i++) + { + ports[i] = (i + 1).ToString(CultureInfo.InvariantCulture); + } + return ports; + } + static string PortToString(int port) => PortStrings[port - 1]; + public int Count { get From ebc7e4d8aa70923182d69cd52bde986c3a70f9a8 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Thu, 14 Nov 2024 12:32:24 +0100 Subject: [PATCH 09/12] dont pre-allocate port buffer --- src/Http/Owin/src/OwinEnvironment.cs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/Http/Owin/src/OwinEnvironment.cs b/src/Http/Owin/src/OwinEnvironment.cs index 3cb22c3f82eb..01482e3811e4 100644 --- a/src/Http/Owin/src/OwinEnvironment.cs +++ b/src/Http/Owin/src/OwinEnvironment.cs @@ -472,17 +472,14 @@ public OwinEntries(HttpContext context) }; } - static readonly string[] PortStrings = CreatePortStrings(); - static string[] CreatePortStrings() + static string PortToString(int port) => port switch { - var ports = new string[65535]; // limit of ephemeral ports https://en.wikipedia.org/wiki/Ephemeral_port - for (var i = 0; i < ports.Length; i++) - { - ports[i] = (i + 1).ToString(CultureInfo.InvariantCulture); - } - return ports; - } - static string PortToString(int port) => PortStrings[port - 1]; + 80 => "80", + 443 => "443", + 8080 => "8080", + 8081 => "8081", + _ => port.ToString(CultureInfo.InvariantCulture), + }; public int Count { From 4ba14d48dc13ddd086076bef32c6718c51c8f32b Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Thu, 14 Nov 2024 12:48:18 +0100 Subject: [PATCH 10/12] fix build errors --- src/Http/Owin/src/DictionaryStringArrayWrapper.cs | 2 +- src/Http/Owin/src/DictionaryStringValuesWrapper.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Http/Owin/src/DictionaryStringArrayWrapper.cs b/src/Http/Owin/src/DictionaryStringArrayWrapper.cs index a5f9ab8aa189..52bc629051cf 100644 --- a/src/Http/Owin/src/DictionaryStringArrayWrapper.cs +++ b/src/Http/Owin/src/DictionaryStringArrayWrapper.cs @@ -108,7 +108,7 @@ public bool MoveNext() public KeyValuePair Current => _current; - object? IEnumerator.Current => Current; + object IEnumerator.Current => Current; void IEnumerator.Reset() => throw new NotImplementedException(); } diff --git a/src/Http/Owin/src/DictionaryStringValuesWrapper.cs b/src/Http/Owin/src/DictionaryStringValuesWrapper.cs index a8c081ad5f8c..ffc055779d05 100644 --- a/src/Http/Owin/src/DictionaryStringValuesWrapper.cs +++ b/src/Http/Owin/src/DictionaryStringValuesWrapper.cs @@ -153,7 +153,7 @@ public bool MoveNext() public KeyValuePair Current => _current; - object? IEnumerator.Current => Current; + object IEnumerator.Current => Current; void IEnumerator.Reset() => throw new NotImplementedException(); } From 7c236ddd95d677845700f971ae992a42e21abe2e Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Thu, 14 Nov 2024 13:25:37 +0100 Subject: [PATCH 11/12] address PR review --- src/Http/Owin/src/OwinEnvironment.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Http/Owin/src/OwinEnvironment.cs b/src/Http/Owin/src/OwinEnvironment.cs index 01482e3811e4..6cd67011c507 100644 --- a/src/Http/Owin/src/OwinEnvironment.cs +++ b/src/Http/Owin/src/OwinEnvironment.cs @@ -442,7 +442,17 @@ private sealed class OwinEntries : IEnumerable> { OwinConstants.CommonKeys.ClientCertificate, new FeatureMap(feature => feature.ClientCertificate, (feature, value) => feature.ClientCertificate = (X509Certificate2)value) }, { OwinConstants.CommonKeys.LoadClientCertAsync, new FeatureMap(feature => new Func(() => feature.GetClientCertificateAsync(CancellationToken.None))) }, - { OwinConstants.WebSocket.AcceptAlt, new FeatureMap(feature => new WebSocketAcceptAlt(feature.AcceptAsync)) } + { OwinConstants.WebSocket.AcceptAlt, new FeatureMap( + feature => + { + if (feature.IsWebSocketRequest) + { + return new WebSocketAcceptAlt(feature.AcceptAsync); + } + + return null; + }) + } }; /// @@ -476,8 +486,6 @@ public OwinEntries(HttpContext context) { 80 => "80", 443 => "443", - 8080 => "8080", - 8081 => "8081", _ => port.ToString(CultureInfo.InvariantCulture), }; From 32da5f8f839b2cb72af63a46cd07dca39ac294b7 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Thu, 14 Nov 2024 14:19:09 +0100 Subject: [PATCH 12/12] suppress NativeAOT warnings for Owin sample --- src/Http/samples/MinimalSampleOwin/Program.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Http/samples/MinimalSampleOwin/Program.cs b/src/Http/samples/MinimalSampleOwin/Program.cs index 210ff7e73508..ff2f86c8df30 100644 --- a/src/Http/samples/MinimalSampleOwin/Program.cs +++ b/src/Http/samples/MinimalSampleOwin/Program.cs @@ -10,15 +10,22 @@ 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 => {