diff --git a/AspNetCore.slnx b/AspNetCore.slnx index b4b5b19b9662..78284eb90b2c 100644 --- a/AspNetCore.slnx +++ b/AspNetCore.slnx @@ -334,6 +334,7 @@ + @@ -357,6 +358,7 @@ + diff --git a/src/Http/HttpAbstractions.slnf b/src/Http/HttpAbstractions.slnf index 2d4c235792cf..69a4e98a5599 100644 --- a/src/Http/HttpAbstractions.slnf +++ b/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" ] } -} +} \ No newline at end of file diff --git a/src/Http/Owin/benchmarks/Microsoft.AspNetCore.Owin.Microbenchmarks/AssemblyInfo.cs b/src/Http/Owin/benchmarks/Microsoft.AspNetCore.Owin.Microbenchmarks/AssemblyInfo.cs new file mode 100644 index 000000000000..09f49228e9e6 --- /dev/null +++ b/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] 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..0e2eead9e260 --- /dev/null +++ b/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> beforeOwinInvokeAction = null, + Action> 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(); + } +} 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/src/DictionaryStringArrayWrapper.cs b/src/Http/Owin/src/DictionaryStringArrayWrapper.cs index 00a726a6f682..e3b3e795458b 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 ConvertingEnumerator GetEnumerator() => new ConvertingEnumerator(Inner); + + IEnumerator> IEnumerable>.GetEnumerator() => new ConvertingEnumerator(Inner); - IEnumerator> IEnumerable>.GetEnumerator() => Inner.Select(Convert).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => new ConvertingEnumerator(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 ConvertingEnumerator : IEnumerator>, IEnumerator + { + private IEnumerator> _inner; + private KeyValuePair _current; + + internal ConvertingEnumerator(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 NotSupportedException(); + } } diff --git a/src/Http/Owin/src/DictionaryStringValuesWrapper.cs b/src/Http/Owin/src/DictionaryStringValuesWrapper.cs index 50ab189ba5c2..171680772949 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 ConvertingEnumerator GetEnumerator() => new ConvertingEnumerator(Inner); + + IEnumerator> IEnumerable>.GetEnumerator() => new ConvertingEnumerator(Inner); - IEnumerator> IEnumerable>.GetEnumerator() => Inner.Select(Convert).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => new ConvertingEnumerator(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 ConvertingEnumerator : IEnumerator>, IEnumerator + { + private IEnumerator> _inner; + private KeyValuePair _current; + + internal ConvertingEnumerator(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 NotSupportedException(); + } } diff --git a/src/Http/Owin/src/OwinEnvironment.cs b/src/Http/Owin/src/OwinEnvironment.cs index cc2995a10afe..32322fb4d0b2 100644 --- a/src/Http/Owin/src/OwinEnvironment.cs +++ b/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 @@ namespace Microsoft.AspNetCore.Owin; /// public class OwinEnvironment : IDictionary { + private readonly OwinEntries _owinEntries; private readonly HttpContext _context; - private readonly IDictionary _entries; /// /// Initializes a new instance of . @@ -47,65 +48,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,57 +59,37 @@ 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 + + _owinEntries = new(context); } // Public in case there's a new/custom feature interface that needs to be added. /// /// 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 +98,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 +116,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 +129,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 +137,7 @@ object IDictionary.this[string key] } else { - _entries.Remove(key); + _owinEntries.Remove(key); if (value != null) { _context.Items[key] = value; @@ -243,7 +165,7 @@ void ICollection>.Add(KeyValuePair void ICollection>.Clear() { - _entries.Clear(); + _owinEntries.Clear(); _context.Items.Clear(); } @@ -256,7 +178,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 +192,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 +208,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)) @@ -478,4 +400,229 @@ public FeatureMap(Func getter, Func defaultFactory, Ac { } } + + private sealed class OwinEntries : IEnumerable> + { + private static readonly IReadOnlyDictionary _entries = ImmutableDictionary.CreateRange( + 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.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 => 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))) }, + + { 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 => + { + if (feature.IsWebSocketRequest) + { + return new WebSocketAcceptAlt(feature.AcceptAsync); + } + + return null; + }) + } + }); + + /// + /// 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. + }; + } + + 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 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(StringComparer.Ordinal); + 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; + } + + 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(StringComparer.Ordinal); + return; + } + + _contextEntries = new Dictionary(_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); + } + } + } + } } diff --git a/src/Http/Owin/test/OwinEnvironmentTests.cs b/src/Http/Owin/test/OwinEnvironmentTests.cs index 32eeba3d6768..272f61c257ee 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,63 @@ 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); + } + + [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(); diff --git a/src/Http/samples/MinimalSampleOwin/MinimalSampleOwin.csproj b/src/Http/samples/MinimalSampleOwin/MinimalSampleOwin.csproj new file mode 100644 index 000000000000..b9b4ede801fe --- /dev/null +++ b/src/Http/samples/MinimalSampleOwin/MinimalSampleOwin.csproj @@ -0,0 +1,20 @@ + + + + $(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..62d500b974ea --- /dev/null +++ b/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(); 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" + } + } + } +}