diff --git a/.github/workflows/inter-branch-merge-flow.yml b/.github/workflows/inter-branch-merge-flow.yml index daff3d8a9ec4..135609265f53 100644 --- a/.github/workflows/inter-branch-merge-flow.yml +++ b/.github/workflows/inter-branch-merge-flow.yml @@ -10,4 +10,4 @@ permissions: jobs: Merge: - uses: dotnet/arcade/.github/workflows/backport-base.yml@fac534d85b77789bd4daf2b4c916117f1ca381e7 # 2024-06-24 + uses: dotnet/arcade/.github/workflows/inter-branch-merge-base.yml@fac534d85b77789bd4daf2b4c916117f1ca381e7 # 2024-06-24 diff --git a/src/Hosting/Hosting/src/Internal/HostingApplication.cs b/src/Hosting/Hosting/src/Internal/HostingApplication.cs index b610173be531..37996905bdf1 100644 --- a/src/Hosting/Hosting/src/Internal/HostingApplication.cs +++ b/src/Hosting/Hosting/src/Internal/HostingApplication.cs @@ -17,6 +17,13 @@ internal sealed class HostingApplication : IHttpApplication _diagnostics.SuppressActivityOpenTelemetryData; + set => _diagnostics.SuppressActivityOpenTelemetryData = value; + } + public HostingApplication( RequestDelegate application, ILogger logger, diff --git a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs index 1a344203727c..67f584462e6e 100644 --- a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs +++ b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs @@ -34,6 +34,9 @@ internal sealed class HostingApplicationDiagnostics private readonly HostingMetrics _metrics; private readonly ILogger _logger; + // Internal for testing purposes only + internal bool SuppressActivityOpenTelemetryData { get; set; } + public HostingApplicationDiagnostics( ILogger logger, DiagnosticListener diagnosticListener, @@ -48,6 +51,19 @@ public HostingApplicationDiagnostics( _propagator = propagator; _eventSource = eventSource; _metrics = metrics; + + SuppressActivityOpenTelemetryData = GetSuppressActivityOpenTelemetryData(); + } + + private static bool GetSuppressActivityOpenTelemetryData() + { + // Default to true if the switch isn't set. + if (!AppContext.TryGetSwitch("Microsoft.AspNetCore.Hosting.SuppressActivityOpenTelemetryData", out var enabled)) + { + return true; + } + + return enabled; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -88,9 +104,9 @@ public void BeginRequest(HttpContext httpContext, HostingApplication.Context con var diagnosticListenerActivityCreationEnabled = (diagnosticListenerEnabled && _diagnosticListener.IsEnabled(ActivityName, httpContext)); var loggingEnabled = _logger.IsEnabled(LogLevel.Critical); - if (loggingEnabled || diagnosticListenerActivityCreationEnabled || _activitySource.HasListeners()) + if (ActivityCreator.IsActivityCreated(_activitySource, loggingEnabled || diagnosticListenerActivityCreationEnabled)) { - context.Activity = StartActivity(httpContext, loggingEnabled, diagnosticListenerActivityCreationEnabled, out var hasDiagnosticListener); + context.Activity = StartActivity(httpContext, loggingEnabled || diagnosticListenerActivityCreationEnabled, out var hasDiagnosticListener); context.HasDiagnosticListener = hasDiagnosticListener; if (context.Activity != null) @@ -385,10 +401,18 @@ private void RecordRequestStartMetrics(HttpContext httpContext) } [MethodImpl(MethodImplOptions.NoInlining)] - private Activity? StartActivity(HttpContext httpContext, bool loggingEnabled, bool diagnosticListenerActivityCreationEnabled, out bool hasDiagnosticListener) + private Activity? StartActivity(HttpContext httpContext, bool diagnosticsOrLoggingEnabled, out bool hasDiagnosticListener) { + // StartActivity is only called if an Activity is already verified to be created. + Debug.Assert(ActivityCreator.IsActivityCreated(_activitySource, diagnosticsOrLoggingEnabled), + "Activity should only be created if diagnostics or logging is enabled."); + hasDiagnosticListener = false; + var initializeTags = !SuppressActivityOpenTelemetryData + ? CreateInitializeActivityTags(httpContext) + : (TagList?)null; + var headers = httpContext.Request.Headers; var activity = ActivityCreator.CreateFromRemote( _activitySource, @@ -402,9 +426,9 @@ private void RecordRequestStartMetrics(HttpContext httpContext) }, ActivityName, ActivityKind.Server, - tags: null, + tags: initializeTags, links: null, - loggingEnabled || diagnosticListenerActivityCreationEnabled); + diagnosticsOrLoggingEnabled); if (activity is null) { return null; @@ -425,6 +449,47 @@ private void RecordRequestStartMetrics(HttpContext httpContext) return activity; } + private static TagList CreateInitializeActivityTags(HttpContext httpContext) + { + // The tags here are set when the activity is created. They can be used in sampling decisions. + // Most values in semantic conventions that are present at creation are specified: + // https://github.com/open-telemetry/semantic-conventions/blob/27735ccca3746d7bb7fa061dfb73d93bcbae2b6e/docs/http/http-spans.md#L581-L592 + // Missing values recommended by the spec are: + // - url.query (need configuration around redaction to do properly) + // - http.request.header. + + var request = httpContext.Request; + var creationTags = new TagList(); + + if (request.Host.HasValue) + { + creationTags.Add(HostingTelemetryHelpers.AttributeServerAddress, request.Host.Host); + + if (HostingTelemetryHelpers.TryGetServerPort(request.Host, request.Scheme, out var port)) + { + creationTags.Add(HostingTelemetryHelpers.AttributeServerPort, port); + } + } + + HostingTelemetryHelpers.SetActivityHttpMethodTags(ref creationTags, request.Method); + + if (request.Headers.TryGetValue("User-Agent", out var values)) + { + var userAgent = values.Count > 0 ? values[0] : null; + if (!string.IsNullOrEmpty(userAgent)) + { + creationTags.Add(HostingTelemetryHelpers.AttributeUserAgentOriginal, userAgent); + } + } + + creationTags.Add(HostingTelemetryHelpers.AttributeUrlScheme, request.Scheme); + + var path = (request.PathBase.HasValue || request.Path.HasValue) ? (request.PathBase + request.Path).ToString() : "/"; + creationTags.Add(HostingTelemetryHelpers.AttributeUrlPath, path); + + return creationTags; + } + [MethodImpl(MethodImplOptions.NoInlining)] private void StopActivity(HttpContext httpContext, Activity activity, bool hasDiagnosticListener) { diff --git a/src/Hosting/Hosting/src/Internal/HostingMetrics.cs b/src/Hosting/Hosting/src/Internal/HostingMetrics.cs index 129542fec15a..2615bf5608f7 100644 --- a/src/Hosting/Hosting/src/Internal/HostingMetrics.cs +++ b/src/Hosting/Hosting/src/Internal/HostingMetrics.cs @@ -1,11 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Frozen; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Metrics; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Shared; namespace Microsoft.AspNetCore.Hosting; @@ -55,7 +54,7 @@ public void RequestEnd(string protocol, string scheme, string method, string? ro if (!disableHttpRequestDurationMetric && _requestDuration.Enabled) { - if (TryGetHttpVersion(protocol, out var httpVersion)) + if (HostingTelemetryHelpers.TryGetHttpVersion(protocol, out var httpVersion)) { tags.Add("network.protocol.version", httpVersion); } @@ -65,10 +64,10 @@ public void RequestEnd(string protocol, string scheme, string method, string? ro } // Add information gathered during request. - tags.Add("http.response.status_code", GetBoxedStatusCode(statusCode)); + tags.Add("http.response.status_code", HostingTelemetryHelpers.GetBoxedStatusCode(statusCode)); if (route != null) { - tags.Add("http.route", route); + tags.Add("http.route", RouteDiagnosticsHelpers.ResolveHttpRoute(route)); } // Add before some built in tags so custom tags are prioritized when dealing with duplicates. @@ -104,73 +103,6 @@ public void Dispose() private static void InitializeRequestTags(ref TagList tags, string scheme, string method) { tags.Add("url.scheme", scheme); - tags.Add("http.request.method", ResolveHttpMethod(method)); - } - - private static readonly object[] BoxedStatusCodes = new object[512]; - - private static object GetBoxedStatusCode(int statusCode) - { - object[] boxes = BoxedStatusCodes; - return (uint)statusCode < (uint)boxes.Length - ? boxes[statusCode] ??= statusCode - : statusCode; - } - - private static readonly FrozenDictionary KnownMethods = FrozenDictionary.ToFrozenDictionary(new[] - { - KeyValuePair.Create(HttpMethods.Connect, HttpMethods.Connect), - KeyValuePair.Create(HttpMethods.Delete, HttpMethods.Delete), - KeyValuePair.Create(HttpMethods.Get, HttpMethods.Get), - KeyValuePair.Create(HttpMethods.Head, HttpMethods.Head), - KeyValuePair.Create(HttpMethods.Options, HttpMethods.Options), - KeyValuePair.Create(HttpMethods.Patch, HttpMethods.Patch), - KeyValuePair.Create(HttpMethods.Post, HttpMethods.Post), - KeyValuePair.Create(HttpMethods.Put, HttpMethods.Put), - KeyValuePair.Create(HttpMethods.Trace, HttpMethods.Trace) - }, StringComparer.OrdinalIgnoreCase); - - private static string ResolveHttpMethod(string method) - { - // TODO: Support configuration for configuring known methods - if (KnownMethods.TryGetValue(method, out var result)) - { - // KnownMethods ignores case. Use the value returned by the dictionary to have a consistent case. - return result; - } - return "_OTHER"; - } - - private static bool TryGetHttpVersion(string protocol, [NotNullWhen(true)] out string? version) - { - if (HttpProtocol.IsHttp11(protocol)) - { - version = "1.1"; - return true; - } - if (HttpProtocol.IsHttp2(protocol)) - { - // HTTP/2 only has one version. - version = "2"; - return true; - } - if (HttpProtocol.IsHttp3(protocol)) - { - // HTTP/3 only has one version. - version = "3"; - return true; - } - if (HttpProtocol.IsHttp10(protocol)) - { - version = "1.0"; - return true; - } - if (HttpProtocol.IsHttp09(protocol)) - { - version = "0.9"; - return true; - } - version = null; - return false; + tags.Add("http.request.method", HostingTelemetryHelpers.GetNormalizedHttpMethod(method)); } } diff --git a/src/Hosting/Hosting/src/Internal/HostingTelemetryHelpers.cs b/src/Hosting/Hosting/src/Internal/HostingTelemetryHelpers.cs new file mode 100644 index 000000000000..e415869e9cff --- /dev/null +++ b/src/Hosting/Hosting/src/Internal/HostingTelemetryHelpers.cs @@ -0,0 +1,132 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Frozen; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Hosting; + +internal static class HostingTelemetryHelpers +{ + // Semantic Conventions for HTTP. + // Note: Not all telemetry code is using these const attribute names yet. + public const string AttributeHttpRequestMethod = "http.request.method"; + public const string AttributeHttpRequestMethodOriginal = "http.request.method_original"; + public const string AttributeUrlScheme = "url.scheme"; + public const string AttributeUrlPath = "url.path"; + public const string AttributeServerAddress = "server.address"; + public const string AttributeServerPort = "server.port"; + public const string AttributeUserAgentOriginal = "user_agent.original"; + + // The value "_OTHER" is used for non-standard HTTP methods. + // https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-spans.md#common-attributes + private const string OtherHttpMethod = "_OTHER"; + + private static readonly object[] BoxedStatusCodes = new object[512]; + + private static readonly FrozenDictionary KnownHttpMethods = FrozenDictionary.ToFrozenDictionary([ + KeyValuePair.Create(HttpMethods.Connect, HttpMethods.Connect), + KeyValuePair.Create(HttpMethods.Delete, HttpMethods.Delete), + KeyValuePair.Create(HttpMethods.Get, HttpMethods.Get), + KeyValuePair.Create(HttpMethods.Head, HttpMethods.Head), + KeyValuePair.Create(HttpMethods.Options, HttpMethods.Options), + KeyValuePair.Create(HttpMethods.Patch, HttpMethods.Patch), + KeyValuePair.Create(HttpMethods.Post, HttpMethods.Post), + KeyValuePair.Create(HttpMethods.Put, HttpMethods.Put), + KeyValuePair.Create(HttpMethods.Trace, HttpMethods.Trace) + ], StringComparer.OrdinalIgnoreCase); + + // Boxed port values for HTTP and HTTPS. + private static readonly object HttpPort = 80; + private static readonly object HttpsPort = 443; + + public static bool TryGetServerPort(HostString host, string scheme, [NotNullWhen(true)] out object? port) + { + if (host.Port.HasValue) + { + port = host.Port.Value; + return true; + } + + // If the port is not specified, use the default port for the scheme. + if (string.Equals(scheme, "http", StringComparison.OrdinalIgnoreCase)) + { + port = HttpPort; + return true; + } + else if (string.Equals(scheme, "https", StringComparison.OrdinalIgnoreCase)) + { + port = HttpsPort; + return true; + } + + // Unknown scheme, no default port. + port = null; + return false; + } + + public static object GetBoxedStatusCode(int statusCode) + { + object[] boxes = BoxedStatusCodes; + return (uint)statusCode < (uint)boxes.Length + ? boxes[statusCode] ??= statusCode + : statusCode; + } + + public static string GetNormalizedHttpMethod(string method) + { + // TODO: Support configuration for configuring known methods + if (method != null && KnownHttpMethods.TryGetValue(method, out var result)) + { + // KnownHttpMethods ignores case. Use the value returned by the dictionary to have a consistent case. + return result; + } + return OtherHttpMethod; + } + + public static bool TryGetHttpVersion(string protocol, [NotNullWhen(true)] out string? version) + { + if (HttpProtocol.IsHttp11(protocol)) + { + version = "1.1"; + return true; + } + if (HttpProtocol.IsHttp2(protocol)) + { + // HTTP/2 only has one version. + version = "2"; + return true; + } + if (HttpProtocol.IsHttp3(protocol)) + { + // HTTP/3 only has one version. + version = "3"; + return true; + } + if (HttpProtocol.IsHttp10(protocol)) + { + version = "1.0"; + return true; + } + if (HttpProtocol.IsHttp09(protocol)) + { + version = "0.9"; + return true; + } + version = null; + return false; + } + + public static void SetActivityHttpMethodTags(ref TagList tags, string originalHttpMethod) + { + var normalizedHttpMethod = GetNormalizedHttpMethod(originalHttpMethod); + tags.Add(AttributeHttpRequestMethod, normalizedHttpMethod); + + if (originalHttpMethod != normalizedHttpMethod) + { + tags.Add(AttributeHttpRequestMethodOriginal, originalHttpMethod); + } + } +} diff --git a/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj b/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj index e91f7124f54d..fb5a2e295272 100644 --- a/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj +++ b/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj @@ -19,6 +19,7 @@ + diff --git a/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs b/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs index ff6735d1b5e3..032eab752397 100644 --- a/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs +++ b/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs @@ -352,6 +352,71 @@ public void Metrics_Route_RouteTagReported() }); } + private sealed class EmptyRouteDiagnosticsMetadata : IRouteDiagnosticsMetadata + { + public string Route { get; } = ""; + } + + [Fact] + public void Metrics_Route_RouteTagIsRootWhenEmpty() + { + // Arrange + var hostingEventSource = new HostingEventSource(Guid.NewGuid().ToString()); + + var testMeterFactory = new TestMeterFactory(); + using var activeRequestsCollector = new MetricCollector(testMeterFactory, HostingMetrics.MeterName, "http.server.active_requests"); + using var requestDurationCollector = new MetricCollector(testMeterFactory, HostingMetrics.MeterName, "http.server.request.duration"); + + // Act + var hostingApplication = CreateApplication(out var features, eventSource: hostingEventSource, meterFactory: testMeterFactory, configure: c => + { + c.Request.Protocol = "1.1"; + c.Request.Scheme = "http"; + c.Request.Method = "POST"; + c.Request.Host = new HostString("localhost"); + c.Request.Path = ""; + c.Request.ContentType = "text/plain"; + c.Request.ContentLength = 1024; + }); + var context = hostingApplication.CreateContext(features); + + Assert.Collection(activeRequestsCollector.GetMeasurementSnapshot(), + m => + { + Assert.Equal(1, m.Value); + Assert.Equal("http", m.Tags["url.scheme"]); + Assert.Equal("POST", m.Tags["http.request.method"]); + }); + + context.HttpContext.SetEndpoint(new Endpoint( + c => Task.CompletedTask, + new EndpointMetadataCollection(new EmptyRouteDiagnosticsMetadata()), + "Test empty endpoint")); + + hostingApplication.DisposeContext(context, null); + + // Assert + Assert.Collection(activeRequestsCollector.GetMeasurementSnapshot(), + m => + { + Assert.Equal(1, m.Value); + Assert.Equal("http", m.Tags["url.scheme"]); + Assert.Equal("POST", m.Tags["http.request.method"]); + }, + m => + { + Assert.Equal(-1, m.Value); + Assert.Equal("http", m.Tags["url.scheme"]); + Assert.Equal("POST", m.Tags["http.request.method"]); + }); + Assert.Collection(requestDurationCollector.GetMeasurementSnapshot(), + m => + { + Assert.True(m.Value > 0); + Assert.Equal("/", m.Tags["http.route"]); + }); + } + [Fact] public void Metrics_DisableHttpMetricsWithMetadata_NoMetrics() { @@ -1021,6 +1086,7 @@ public void ActivityListenersAreCalled() var testSource = new ActivitySource(Path.GetRandomFileName()); var hostingApplication = CreateApplication(out var features, activitySource: testSource); var parentSpanId = ""; + var tags = new List>(); using var listener = new ActivityListener { ShouldListenTo = activitySource => ReferenceEquals(activitySource, testSource), @@ -1028,6 +1094,7 @@ public void ActivityListenersAreCalled() ActivityStarted = activity => { parentSpanId = Activity.Current.ParentSpanId.ToHexString(); + tags = Activity.Current.TagObjects.OrderBy(t => t.Key).ToList(); } }; @@ -1039,12 +1106,112 @@ public void ActivityListenersAreCalled() { {"traceparent", "00-0123456789abcdef0123456789abcdef-0123456789abcdef-01"}, {"tracestate", "TraceState1"}, - {"baggage", "Key1=value1, Key2=value2"} + {"baggage", "Key1=value1, Key2=value2"}, + {"host", "localhost:8080" } + }, + PathBase = "/path_base", + Path = "/path", + Scheme = "http", + Method = "CUSTOM_METHOD", + Protocol = "HTTP/1.1" + }); + + hostingApplication.CreateContext(features); + Assert.Equal("0123456789abcdef", parentSpanId); + + Assert.Empty(tags); + } + + [Fact] + public void ActivityListeners_DontSuppressActivityTags_TagsAdded() + { + var testSource = new ActivitySource(Path.GetRandomFileName()); + var hostingApplication = CreateApplication(out var features, activitySource: testSource, suppressActivityOpenTelemetryData: false); + var parentSpanId = ""; + var tags = new List>(); + using var listener = new ActivityListener + { + ShouldListenTo = activitySource => ReferenceEquals(activitySource, testSource), + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData, + ActivityStarted = activity => + { + parentSpanId = Activity.Current.ParentSpanId.ToHexString(); + tags = Activity.Current.TagObjects.OrderBy(t => t.Key).ToList(); } + }; + + ActivitySource.AddActivityListener(listener); + + features.Set(new HttpRequestFeature() + { + Headers = new HeaderDictionary() + { + {"traceparent", "00-0123456789abcdef0123456789abcdef-0123456789abcdef-01"}, + {"tracestate", "TraceState1"}, + {"baggage", "Key1=value1, Key2=value2"}, + {"host", "localhost:8080" } + }, + PathBase = "/path_base", + Path = "/path", + Scheme = "http", + Method = "CUSTOM_METHOD", + Protocol = "HTTP/1.1" }); hostingApplication.CreateContext(features); Assert.Equal("0123456789abcdef", parentSpanId); + + Assert.Collection(tags, + kvp => AssertKeyValuePair(kvp, "http.request.method", "_OTHER"), + kvp => AssertKeyValuePair(kvp, "http.request.method_original", "CUSTOM_METHOD"), + kvp => AssertKeyValuePair(kvp, "server.address", "localhost"), + kvp => AssertKeyValuePair(kvp, "server.port", 8080), + kvp => AssertKeyValuePair(kvp, "url.path", "/path_base/path"), + kvp => AssertKeyValuePair(kvp, "url.scheme", "http")); + + static void AssertKeyValuePair(KeyValuePair pair, string key, T value) + { + Assert.Equal(key, pair.Key); + Assert.Equal(value, pair.Value); + } + } + + [Theory] + [InlineData("http", 80)] + [InlineData("HTTP", 80)] + [InlineData("https", 443)] + [InlineData("HTTPS", 443)] + [InlineData("other", null)] + public void ActivityListeners_DefaultPorts(string scheme, int? expectedPort) + { + var testSource = new ActivitySource(Path.GetRandomFileName()); + var hostingApplication = CreateApplication(out var features, activitySource: testSource, suppressActivityOpenTelemetryData: false); + var tags = new Dictionary(); + using var listener = new ActivityListener + { + ShouldListenTo = activitySource => ReferenceEquals(activitySource, testSource), + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData, + ActivityStarted = activity => + { + tags = Activity.Current.TagObjects.ToDictionary(); + } + }; + + ActivitySource.AddActivityListener(listener); + + features.Set(new HttpRequestFeature() + { + Headers = new HeaderDictionary() + { + {"host", "localhost" } + }, + Scheme = scheme, + }); + + hostingApplication.CreateContext(features); + + Assert.Equal(expectedPort != null, tags.TryGetValue("server.port", out var actualPort)); + Assert.Equal(expectedPort, (int?)actualPort); } [Fact] @@ -1092,7 +1259,8 @@ private static void AssertProperty(object o, string name) private static HostingApplication CreateApplication(out FeatureCollection features, DiagnosticListener diagnosticListener = null, ActivitySource activitySource = null, ILogger logger = null, - Action configure = null, HostingEventSource eventSource = null, IMeterFactory meterFactory = null) + Action configure = null, HostingEventSource eventSource = null, IMeterFactory meterFactory = null, + bool? suppressActivityOpenTelemetryData = null) { var httpContextFactory = new Mock(); @@ -1114,6 +1282,11 @@ private static HostingApplication CreateApplication(out FeatureCollection featur eventSource ?? HostingEventSource.Log, new HostingMetrics(meterFactory ?? new TestMeterFactory())); + if (suppressActivityOpenTelemetryData is { } suppress) + { + hostingApplication.SuppressActivityOpenTelemetryData = suppress; + } + return hostingApplication; } diff --git a/src/Hosting/TestHost/src/PublicAPI.Unshipped.txt b/src/Hosting/TestHost/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..38d66595d1b9 100644 --- a/src/Hosting/TestHost/src/PublicAPI.Unshipped.txt +++ b/src/Hosting/TestHost/src/PublicAPI.Unshipped.txt @@ -1 +1,8 @@ #nullable enable +*REMOVED*static Microsoft.AspNetCore.TestHost.WebHostBuilderExtensions.UseSolutionRelativeContentRoot(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder, string! solutionRelativePath, string! applicationBasePath, string! solutionName = "*.sln") -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! +*REMOVED*static Microsoft.AspNetCore.TestHost.WebHostBuilderExtensions.UseSolutionRelativeContentRoot(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder, string! solutionRelativePath, string! solutionName = "*.sln") -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! +static Microsoft.AspNetCore.TestHost.WebHostBuilderExtensions.UseSolutionRelativeContentRoot(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder, string! solutionRelativePath) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! +static Microsoft.AspNetCore.TestHost.WebHostBuilderExtensions.UseSolutionRelativeContentRoot(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder, string! solutionRelativePath, string! applicationBasePath, string! solutionName) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! +static Microsoft.AspNetCore.TestHost.WebHostBuilderExtensions.UseSolutionRelativeContentRoot(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder, string! solutionRelativePath, string! applicationBasePath, System.ReadOnlySpan solutionNames = default(System.ReadOnlySpan)) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! +static Microsoft.AspNetCore.TestHost.WebHostBuilderExtensions.UseSolutionRelativeContentRoot(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder, string! solutionRelativePath, string! solutionName) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! + diff --git a/src/Hosting/TestHost/src/WebHostBuilderExtensions.cs b/src/Hosting/TestHost/src/WebHostBuilderExtensions.cs index 351b568d575a..b2e6b0a9619e 100644 --- a/src/Hosting/TestHost/src/WebHostBuilderExtensions.cs +++ b/src/Hosting/TestHost/src/WebHostBuilderExtensions.cs @@ -16,6 +16,8 @@ namespace Microsoft.AspNetCore.TestHost; /// public static class WebHostBuilderExtensions { + private static readonly string[] _defaultSolutionNames = ["*.sln", "*.slnx"]; + /// /// Enables the service. /// @@ -117,6 +119,19 @@ public static IWebHostBuilder ConfigureTestContainer(this IWebHostBu return webHostBuilder; } + /// + /// Sets the content root of relative to the . + /// + /// The . + /// The directory of the solution file. + /// The . + public static IWebHostBuilder UseSolutionRelativeContentRoot( + this IWebHostBuilder builder, + string solutionRelativePath) + { + return builder.UseSolutionRelativeContentRoot(solutionRelativePath, AppContext.BaseDirectory, _defaultSolutionNames); + } + /// /// Sets the content root of relative to the . /// @@ -124,13 +139,12 @@ public static IWebHostBuilder ConfigureTestContainer(this IWebHostBu /// The directory of the solution file. /// The name of the solution file to make the content root relative to. /// The . - [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] public static IWebHostBuilder UseSolutionRelativeContentRoot( this IWebHostBuilder builder, string solutionRelativePath, - string solutionName = "*.sln") + string solutionName) { - return builder.UseSolutionRelativeContentRoot(solutionRelativePath, AppContext.BaseDirectory, solutionName); + return builder.UseSolutionRelativeContentRoot(solutionRelativePath, AppContext.BaseDirectory, [solutionName]); } /// @@ -141,24 +155,49 @@ public static IWebHostBuilder UseSolutionRelativeContentRoot( /// The root of the app's directory. /// The name of the solution file to make the content root relative to. /// The . - [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] public static IWebHostBuilder UseSolutionRelativeContentRoot( this IWebHostBuilder builder, string solutionRelativePath, string applicationBasePath, - string solutionName = "*.sln") + string solutionName) + { + return builder.UseSolutionRelativeContentRoot(solutionRelativePath, applicationBasePath, [solutionName]); + } + + /// + /// Sets the content root of relative to the . + /// + /// The . + /// The directory of the solution file. + /// The root of the app's directory. + /// The names of the solution files to make the content root relative to. If empty, defaults to *.sln and *.slnx. + /// The . + [SuppressMessage("ApiDesign", "RS0027:Public API with optional parameter(s) should have the most parameters amongst its public overloads", Justification = "Required to maintain compatibility")] + public static IWebHostBuilder UseSolutionRelativeContentRoot( + this IWebHostBuilder builder, + string solutionRelativePath, + string applicationBasePath, + ReadOnlySpan solutionNames = default) { ArgumentNullException.ThrowIfNull(solutionRelativePath); ArgumentNullException.ThrowIfNull(applicationBasePath); + if (solutionNames.IsEmpty) + { + solutionNames = _defaultSolutionNames; + } + var directoryInfo = new DirectoryInfo(applicationBasePath); do { - var solutionPath = Directory.EnumerateFiles(directoryInfo.FullName, solutionName).FirstOrDefault(); - if (solutionPath != null) + foreach (var solutionName in solutionNames) { - builder.UseContentRoot(Path.GetFullPath(Path.Combine(directoryInfo.FullName, solutionRelativePath))); - return builder; + var solutionPath = Directory.EnumerateFiles(directoryInfo.FullName, solutionName).FirstOrDefault(); + if (solutionPath != null) + { + builder.UseContentRoot(Path.GetFullPath(Path.Combine(directoryInfo.FullName, solutionRelativePath))); + return builder; + } } directoryInfo = directoryInfo.Parent; diff --git a/src/Hosting/TestHost/test/UseSolutionRelativeContentRootTests.cs b/src/Hosting/TestHost/test/UseSolutionRelativeContentRootTests.cs new file mode 100644 index 000000000000..96a6466b72ad --- /dev/null +++ b/src/Hosting/TestHost/test/UseSolutionRelativeContentRootTests.cs @@ -0,0 +1,200 @@ +// 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.Hosting; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.TestHost; + +#pragma warning disable ASPDEPR004 // WebHostBuilder is obsolete +#pragma warning disable ASPDEPR008 // WebHost is obsolete +public class UseSolutionRelativeContentRootTests : IDisposable +{ + private readonly string _tempDirectory; + private readonly string _contentDirectory; + + public UseSolutionRelativeContentRootTests() + { + _tempDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")[..8]); + _contentDirectory = Path.Combine(_tempDirectory, "src"); + Directory.CreateDirectory(_contentDirectory); + } + + [Fact] + public void UseSolutionRelativeContentRoot_FindsSlnFile() + { + var solutionFile = Path.Combine(_tempDirectory, "TestApp.sln"); + File.WriteAllText(solutionFile, "Microsoft Visual Studio Solution File, Format Version 12.00"); + + var builder = new WebHostBuilder() + .UseTestServer() + .Configure(app => { }); + + builder.UseSolutionRelativeContentRoot("src", applicationBasePath: _tempDirectory); + + using var host = builder.Build(); + var environment = host.Services.GetRequiredService(); + + Assert.Equal(_contentDirectory, environment.ContentRootPath); + } + + [Fact] + public void UseSolutionRelativeContentRoot_FindsSlnxFile() + { + var solutionFile = Path.Combine(_tempDirectory, "TestApp.slnx"); + File.WriteAllText(solutionFile, """ + + + + + + + """); + + var builder = new WebHostBuilder() + .UseTestServer() + .Configure(app => { }); + + builder.UseSolutionRelativeContentRoot("src", applicationBasePath: _tempDirectory); + + using var host = builder.Build(); + var environment = host.Services.GetRequiredService(); + + Assert.Equal(_contentDirectory, environment.ContentRootPath); + } + + [Fact] + public void UseSolutionRelativeContentRoot_WithSolutionName_FindsSpecifiedFile() + { + var subDirectory = Path.Combine(_tempDirectory, "sub"); + Directory.CreateDirectory(subDirectory); + + var slnFile = Path.Combine(subDirectory, "TestApp.sln"); + var slnxFile = Path.Combine(_tempDirectory, "TestApp.slnx"); + File.WriteAllText(slnFile, "Microsoft Visual Studio Solution File, Format Version 12.00"); + File.WriteAllText(slnxFile, """ + + + + + + """); + + var builder = new WebHostBuilder() + .UseTestServer() + .Configure(app => { }); + + builder.UseSolutionRelativeContentRoot("src", _tempDirectory, "*.slnx"); + + using var host = builder.Build(); + var environment = host.Services.GetRequiredService(); + + Assert.Equal(_contentDirectory, environment.ContentRootPath); + } + + [Fact] + public void UseSolutionRelativeContentRoot_WithMultipleSolutionNames_FindsInCurrentDirectoryFirst() + { + var expectedPath = Path.Combine(_contentDirectory, "sub"); + Directory.CreateDirectory(expectedPath); + + var slnFile = Path.Combine(_tempDirectory, "TestApp.sln"); + var slnxFile = Path.Combine(_contentDirectory, "TestApp.slnx"); + File.WriteAllText(slnFile, "Microsoft Visual Studio Solution File, Format Version 12.00"); + File.WriteAllText(slnxFile, """ + + + + + + """); + + var builder = new WebHostBuilder() + .UseTestServer() + .Configure(app => { }); + + builder.UseSolutionRelativeContentRoot("sub", _contentDirectory, ["*.sln", "*.slnx"]); + + using var host = builder.Build(); + var environment = host.Services.GetRequiredService(); + + Assert.Equal(expectedPath, environment.ContentRootPath); + } + + [Fact] + public void UseSolutionRelativeContentRoot_WithMultipleSolutionNames_WorksWithMultipleFiles() + { + var slnFile = Path.Combine(_tempDirectory, "TestApp.sln"); + var slnxFile = Path.Combine(_tempDirectory, "TestApp.slnx"); + File.WriteAllText(slnFile, "Microsoft Visual Studio Solution File, Format Version 12.00"); + File.WriteAllText(slnxFile, """ + + + + + + """); + + var builder = new WebHostBuilder() + .UseTestServer() + .Configure(app => { }); + + builder.UseSolutionRelativeContentRoot("src", applicationBasePath: _tempDirectory, solutionNames: ["*.sln", "*.slnx"]); + + using var host = builder.Build(); + var environment = host.Services.GetRequiredService(); + + Assert.Equal(_contentDirectory, environment.ContentRootPath); + } + + [Fact] + public void UseSolutionRelativeContentRoot_ThrowsWhenSolutionNotFound() + { + var builder = new WebHostBuilder() + .UseTestServer() + .Configure(app => { }); + + var exception = Assert.Throws(() => + builder.UseSolutionRelativeContentRoot("src", applicationBasePath: _tempDirectory)); + + Assert.Contains("Solution root could not be located", exception.Message); + Assert.Contains(_tempDirectory, exception.Message); + } + + [Fact] + public void UseSolutionRelativeContentRoot_WithSolutionName_SearchesParentDirectories() + { + var subDirectory = Path.Combine(_tempDirectory, "sub", "folder"); + Directory.CreateDirectory(subDirectory); + + var solutionFile = Path.Combine(_tempDirectory, "TestApp.slnx"); + File.WriteAllText(solutionFile, """ + + + + + + """); + + var builder = new WebHostBuilder() + .UseTestServer() + .Configure(app => { }); + + builder.UseSolutionRelativeContentRoot("src", subDirectory, "*.slnx"); + + using var host = builder.Build(); + var environment = host.Services.GetRequiredService(); + + Assert.Equal(_contentDirectory, environment.ContentRootPath); + } + + public void Dispose() + { + if (Directory.Exists(_tempDirectory)) + { + Directory.Delete(_tempDirectory, recursive: true); + } + } +} +#pragma warning restore ASPDEPR008 // WebHost is obsolete +#pragma warning disable ASPDEPR004 // WebHostBuilder is obsolete diff --git a/src/Http/Http.Abstractions/src/Metadata/ApiEndpointMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/ApiEndpointMetadata.cs deleted file mode 100644 index 815bf2c834c9..000000000000 --- a/src/Http/Http.Abstractions/src/Metadata/ApiEndpointMetadata.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.Http.Metadata; - -/// -/// Metadata that indicates the endpoint is intended for API clients. -/// When present, authentication handlers should prefer returning status codes over browser redirects. -/// -internal sealed class ApiEndpointMetadata : IApiEndpointMetadata -{ - /// - /// Singleton instance of . - /// - public static readonly ApiEndpointMetadata Instance = new(); - - private ApiEndpointMetadata() - { - } -} diff --git a/src/Http/Http.Abstractions/src/Metadata/DisableCookieRedirectMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/DisableCookieRedirectMetadata.cs new file mode 100644 index 000000000000..d7aad7f5c217 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Metadata/DisableCookieRedirectMetadata.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Http.Metadata; + +/// +/// Metadata that indicates the endpoint should disable cookie-based authentication redirects. +/// When present, authentication handlers should prefer returning status codes over browser redirects. +/// +internal sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata +{ + /// + /// Singleton instance of . + /// + public static readonly DisableCookieRedirectMetadata Instance = new(); + + private DisableCookieRedirectMetadata() + { + } +} diff --git a/src/Http/Http.Abstractions/src/Metadata/IAllowCookieRedirectMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IAllowCookieRedirectMetadata.cs new file mode 100644 index 000000000000..63c0de5859e3 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Metadata/IAllowCookieRedirectMetadata.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Http.Metadata; + +/// +/// Metadata that indicates the endpoint should allow cookie-based authentication redirects. +/// This is normally the default behavior, but it exists to override no matter the order. +/// When present, the cookie authentication handler will prefer browser login or access denied redirects over 401 and 403 status codes. +/// +public interface IAllowCookieRedirectMetadata +{ +} diff --git a/src/Http/Http.Abstractions/src/Metadata/IApiEndpointMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IApiEndpointMetadata.cs deleted file mode 100644 index cadaafcae1a9..000000000000 --- a/src/Http/Http.Abstractions/src/Metadata/IApiEndpointMetadata.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.Http.Metadata; - -/// -/// Metadata that indicates the endpoint is an API intended for programmatic access rather than direct browser navigation. -/// When present, authentication handlers should prefer returning status codes over browser redirects. -/// -public interface IApiEndpointMetadata -{ -} diff --git a/src/Http/Http.Abstractions/src/Metadata/IDisableCookieRedirectMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IDisableCookieRedirectMetadata.cs new file mode 100644 index 000000000000..c9651833552a --- /dev/null +++ b/src/Http/Http.Abstractions/src/Metadata/IDisableCookieRedirectMetadata.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Http.Metadata; + +/// +/// Metadata that indicates the endpoint should disable cookie-based authentication redirects +/// typically because it is intended for API clients rather than direct browser navigation. +/// +/// overrides this no matter the order. +/// +/// When present and not overridden, the cookie authentication handler will prefer using +/// 401 and 403 status codes over redirecting to the login or access denied paths. +/// +public interface IDisableCookieRedirectMetadata +{ +} diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index d0fc0872dd90..6ccd592237b8 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -1,5 +1,6 @@ #nullable enable -Microsoft.AspNetCore.Http.Metadata.IApiEndpointMetadata +Microsoft.AspNetCore.Http.Metadata.IAllowCookieRedirectMetadata +Microsoft.AspNetCore.Http.Metadata.IDisableCookieRedirectMetadata Microsoft.AspNetCore.Http.Metadata.IDisableValidationMetadata Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata.Description.get -> string? Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata.Description.set -> void diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.RequestDelegateGenerator/RequestDelegateGenerator.cs b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.RequestDelegateGenerator/RequestDelegateGenerator.cs index f5e2abacaaf1..6823c8746f02 100644 --- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.RequestDelegateGenerator/RequestDelegateGenerator.cs +++ b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.RequestDelegateGenerator/RequestDelegateGenerator.cs @@ -253,7 +253,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) if (hasJsonBody || hasResponseMetadata) { - codeWriter.WriteLine(RequestDelegateGeneratorSources.ApiEndpointMetadataClass); + codeWriter.WriteLine(RequestDelegateGeneratorSources.DisableCookieRedirectMetadataClass); } if (hasFormBody || hasJsonBody || hasResponseMetadata) diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.RequestDelegateGenerator/RequestDelegateGeneratorSources.cs b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.RequestDelegateGenerator/RequestDelegateGeneratorSources.cs index b626db274124..4c0e6609382f 100644 --- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.RequestDelegateGenerator/RequestDelegateGeneratorSources.cs +++ b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.RequestDelegateGenerator/RequestDelegateGeneratorSources.cs @@ -493,18 +493,18 @@ public AntiforgeryMetadata(bool requiresValidation) } """; - public static string ApiEndpointMetadataClass = """ - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + public static string DisableCookieRedirectMetadataClass = """ + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.RequestDelegateGenerator/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.RequestDelegateGenerator/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs index 97fe2dc990ff..e4a35735b65b 100644 --- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.RequestDelegateGenerator/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs +++ b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.RequestDelegateGenerator/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs @@ -218,7 +218,7 @@ private static void EmitBuiltinResponseTypeMetadata(this Endpoint endpoint, Code else if (response.ResponseType is { } responseType) { codeWriter.WriteLine($$"""options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof({{responseType.ToDisplayString(EmitterConstants.DisplayFormatWithoutNullability)}}), contentTypes: GeneratedMetadataConstants.JsonContentType));"""); - codeWriter.WriteLine("ApiEndpointMetadata.AddApiEndpointMetadataIfMissing(options.EndpointBuilder);"); + codeWriter.WriteLine("DisableCookieRedirectMetadata.AddMetadataIfMissing(options.EndpointBuilder);"); } } @@ -336,7 +336,7 @@ public static void EmitJsonAcceptsMetadata(this Endpoint endpoint, CodeWriter co codeWriter.WriteLine("if (!serviceProviderIsService.IsService(type))"); codeWriter.StartBlock(); codeWriter.WriteLine("options.EndpointBuilder.Metadata.Add(new AcceptsMetadata(type: type, isOptional: isOptional, contentTypes: GeneratedMetadataConstants.JsonContentType));"); - codeWriter.WriteLine("options.EndpointBuilder.Metadata.Add(ApiEndpointMetadata.Instance);"); + codeWriter.WriteLine("options.EndpointBuilder.Metadata.Add(DisableCookieRedirectMetadata.Instance);"); codeWriter.WriteLine("break;"); codeWriter.EndBlock(); codeWriter.EndBlock(); @@ -344,7 +344,7 @@ public static void EmitJsonAcceptsMetadata(this Endpoint endpoint, CodeWriter co else { codeWriter.WriteLine("options.EndpointBuilder.Metadata.Add(new AcceptsMetadata(contentTypes: GeneratedMetadataConstants.JsonContentType));"); - codeWriter.WriteLine("options.EndpointBuilder.Metadata.Add(ApiEndpointMetadata.Instance);"); + codeWriter.WriteLine("options.EndpointBuilder.Metadata.Add(DisableCookieRedirectMetadata.Instance);"); } } diff --git a/src/Http/Http.Extensions/src/AllowCookieRedirectAttribute.cs b/src/Http/Http.Extensions/src/AllowCookieRedirectAttribute.cs new file mode 100644 index 000000000000..b559f3b1081f --- /dev/null +++ b/src/Http/Http.Extensions/src/AllowCookieRedirectAttribute.cs @@ -0,0 +1,16 @@ +// 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.Http.Metadata; + +namespace Microsoft.AspNetCore.Http; + +/// +/// Specifies that cookie-based authentication redirects are allowed for an endpoint. +/// This is normally the default behavior, but it exists to override no matter the order. +/// When present, the cookie authentication handler will prefer browser login or access denied redirects over 401 and 403 status codes. +/// +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] +public sealed class AllowCookieRedirectAttribute : Attribute, IAllowCookieRedirectMetadata +{ +} diff --git a/src/Http/Http.Extensions/src/CookieRedirectEndpointConventionBuilderExtensions.cs b/src/Http/Http.Extensions/src/CookieRedirectEndpointConventionBuilderExtensions.cs new file mode 100644 index 000000000000..c7e045448d46 --- /dev/null +++ b/src/Http/Http.Extensions/src/CookieRedirectEndpointConventionBuilderExtensions.cs @@ -0,0 +1,43 @@ +// 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.Http; +using Microsoft.AspNetCore.Http.Metadata; + +namespace Microsoft.AspNetCore.Builder; + +/// +/// Cookie redirect extension methods for . +/// +public static class CookieRedirectEndpointConventionBuilderExtensions +{ + private static readonly AllowCookieRedirectAttribute _allowCookieRedirectAttribute = new(); + + /// + /// Specifies that cookie-based authentication redirects are disabled for an endpoint using . + /// When present and not overridden by or , + /// the cookie authentication handler will prefer using 401 and 403 status codes over redirecting to the login or access denied paths. + /// + /// The type of endpoint convention builder. + /// The endpoint convention builder. + /// The original convention builder parameter. + public static TBuilder DisableCookieRedirect(this TBuilder builder) where TBuilder : IEndpointConventionBuilder + { + builder.Add(b => b.Metadata.Add(DisableCookieRedirectMetadata.Instance)); + return builder; + } + + /// + /// Specifies that cookie-based authentication redirects are allowed for an endpoint using . + /// This is normally the default behavior, but it exists to override no matter the order. + /// When present, the cookie authentication handler will prefer browser login or access denied redirects over 401 and 403 status codes. + /// + /// The type of endpoint convention builder. + /// The endpoint convention builder. + /// The original convention builder parameter. + public static TBuilder AllowCookieRedirect(this TBuilder builder) where TBuilder : IEndpointConventionBuilder + { + builder.Add(b => b.Metadata.Add(_allowCookieRedirectAttribute)); + return builder; + } +} diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..dda4ec8ff207 100644 --- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt @@ -1 +1,6 @@ #nullable enable +Microsoft.AspNetCore.Builder.CookieRedirectEndpointConventionBuilderExtensions +Microsoft.AspNetCore.Http.AllowCookieRedirectAttribute +Microsoft.AspNetCore.Http.AllowCookieRedirectAttribute.AllowCookieRedirectAttribute() -> void +static Microsoft.AspNetCore.Builder.CookieRedirectEndpointConventionBuilderExtensions.AllowCookieRedirect(this TBuilder builder) -> TBuilder +static Microsoft.AspNetCore.Builder.CookieRedirectEndpointConventionBuilderExtensions.DisableCookieRedirect(this TBuilder builder) -> TBuilder diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index f5182f47c1f5..ee76afb27f69 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -405,7 +405,7 @@ private static Expression[] CreateArgumentsAndInferMetadata(MethodInfo methodInf // When present, authentication handlers should prefer returning status codes over browser redirects. if (factoryContext.JsonRequestBodyParameter is not null) { - factoryContext.EndpointBuilder.Metadata.Add(ApiEndpointMetadata.Instance); + factoryContext.EndpointBuilder.Metadata.Add(DisableCookieRedirectMetadata.Instance); } PopulateBuiltInResponseTypeMetadata(methodInfo.ReturnType, factoryContext); @@ -1062,7 +1062,7 @@ private static void PopulateBuiltInResponseTypeMetadata(Type returnType, Request { // Since this endpoint responds with JSON, we assume its an API endpoint not intended for browser navigation, // but we don't want to bother adding this metadata twice if we've already inferred it based on the expected JSON request body. - builder.Metadata.Add(ApiEndpointMetadata.Instance); + builder.Metadata.Add(DisableCookieRedirectMetadata.Instance); } } } diff --git a/src/Http/Http.Extensions/test/CookieRedirectEndpointConventionBuilderExtensionsTests.cs b/src/Http/Http.Extensions/test/CookieRedirectEndpointConventionBuilderExtensionsTests.cs new file mode 100644 index 000000000000..18610cdca905 --- /dev/null +++ b/src/Http/Http.Extensions/test/CookieRedirectEndpointConventionBuilderExtensionsTests.cs @@ -0,0 +1,72 @@ +// 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.Builder; +using Microsoft.AspNetCore.Http.Metadata; + +namespace Microsoft.AspNetCore.Http.Extensions.Tests; + +public class CookieRedirectEndpointConventionBuilderExtensionsTests +{ + [Fact] + public void DisableCookieRedirect_AddsMetadata() + { + // Arrange + var builder = new TestEndpointConventionBuilder(); + + // Act + builder.DisableCookieRedirect(); + + // Assert + Assert.IsAssignableFrom(Assert.Single(builder.Metadata)); + } + + [Fact] + public void AllowCookieRedirect_AddsMetadata() + { + // Arrange + var builder = new TestEndpointConventionBuilder(); + + // Act + builder.AllowCookieRedirect(); + + // Assert + Assert.IsAssignableFrom(Assert.Single(builder.Metadata)); + } + + [Fact] + public void DisableCookieRedirect_ReturnsBuilder() + { + // Arrange + var builder = new TestEndpointConventionBuilder(); + + // Act + var result = builder.DisableCookieRedirect(); + + // Assert + Assert.Same(builder, result); + } + + [Fact] + public void AllowCookieRedirect_ReturnsBuilder() + { + // Arrange + var builder = new TestEndpointConventionBuilder(); + + // Act + var result = builder.AllowCookieRedirect(); + + // Assert + Assert.Same(builder, result); + } + + private sealed class TestEndpointConventionBuilder : EndpointBuilder, IEndpointConventionBuilder + { + public void Add(Action convention) + { + convention(this); + } + + public override Endpoint Build() => throw new NotImplementedException(); + } +} diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 7cee04fb8a0f..8ace889b526b 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -2825,8 +2825,8 @@ public void Create_CombinesAllMetadata_InCorrectOrder() m => Assert.True(m is AcceptsMetadata am && am.RequestType == typeof(AddsCustomParameterMetadata)), // Inferred ParameterBinding metadata m => Assert.True(m is IParameterBindingMetadata { Name: "param1" }), - // Inferred IApiEndpointMetadata from RDF for complex request and response type - m => Assert.True(m is IApiEndpointMetadata), + // Inferred IDisableCookieRedirectMetadata from RDF for complex request and response type + m => Assert.True(m is IDisableCookieRedirectMetadata), // Inferred ProducesResponseTypeMetadata from RDF for complex type m => Assert.Equal(typeof(CountsDefaultEndpointMetadataPoco), ((IProducesResponseTypeMetadata)m).Type), // Metadata provided by parameters implementing IEndpointParameterMetadataProvider diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/HandlesEndpointsWithAndWithoutDiagnostics.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/HandlesEndpointsWithAndWithoutDiagnostics.generated.txt index 18cf2ce8010b..62465758d43e 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/HandlesEndpointsWithAndWithoutDiagnostics.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/HandlesEndpointsWithAndWithoutDiagnostics.generated.txt @@ -225,17 +225,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_BindAsync_NullableReturn.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_BindAsync_NullableReturn.generated.txt index 04b1dc6c3bbe..2011bb74d023 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_BindAsync_NullableReturn.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_BindAsync_NullableReturn.generated.txt @@ -363,17 +363,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_BindAsync_Snapshot.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_BindAsync_Snapshot.generated.txt index 6c1240e6b8f3..d4bed32cff50 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_BindAsync_Snapshot.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_BindAsync_Snapshot.generated.txt @@ -2225,17 +2225,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitBodyParam_ComplexReturn_Snapshot.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitBodyParam_ComplexReturn_Snapshot.generated.txt index 830c81d1309b..6159dd40f846 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitBodyParam_ComplexReturn_Snapshot.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitBodyParam_ComplexReturn_Snapshot.generated.txt @@ -412,17 +412,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_ComplexTypeArrayParam.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_ComplexTypeArrayParam.generated.txt index bd0ceac25240..2547e7535558 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_ComplexTypeArrayParam.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_ComplexTypeArrayParam.generated.txt @@ -72,7 +72,7 @@ namespace Microsoft.AspNetCore.Http.Generated var parameters = methodInfo.GetParameters(); options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: true, hasBindAsync: false, isOptional: false)); options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.Int32), contentTypes: GeneratedMetadataConstants.JsonContentType)); - ApiEndpointMetadata.AddApiEndpointMetadataIfMissing(options.EndpointBuilder); + DisableCookieRedirectMetadata.AddMetadataIfMissing(options.EndpointBuilder); return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; }; RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => @@ -260,17 +260,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_NullableStringArrayParam.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_NullableStringArrayParam.generated.txt index e2c866e2c2bb..b2d3d5182a98 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_NullableStringArrayParam.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_NullableStringArrayParam.generated.txt @@ -72,7 +72,7 @@ namespace Microsoft.AspNetCore.Http.Generated var parameters = methodInfo.GetParameters(); options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: false, hasBindAsync: false, isOptional: false)); options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.Int32), contentTypes: GeneratedMetadataConstants.JsonContentType)); - ApiEndpointMetadata.AddApiEndpointMetadataIfMissing(options.EndpointBuilder); + DisableCookieRedirectMetadata.AddMetadataIfMissing(options.EndpointBuilder); return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; }; RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => @@ -232,17 +232,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_StringArrayParam.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_StringArrayParam.generated.txt index b2fc71b9158a..3b6dfaa17a49 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_StringArrayParam.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_StringArrayParam.generated.txt @@ -72,7 +72,7 @@ namespace Microsoft.AspNetCore.Http.Generated var parameters = methodInfo.GetParameters(); options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: false, hasBindAsync: false, isOptional: false)); options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.Int32), contentTypes: GeneratedMetadataConstants.JsonContentType)); - ApiEndpointMetadata.AddApiEndpointMetadataIfMissing(options.EndpointBuilder); + DisableCookieRedirectMetadata.AddMetadataIfMissing(options.EndpointBuilder); return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; }; RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => @@ -232,17 +232,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_ComplexTypeArrayParam.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_ComplexTypeArrayParam.generated.txt index 9aee726d2055..e4de33218466 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_ComplexTypeArrayParam.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_ComplexTypeArrayParam.generated.txt @@ -72,7 +72,7 @@ namespace Microsoft.AspNetCore.Http.Generated var parameters = methodInfo.GetParameters(); options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: true, hasBindAsync: false, isOptional: false)); options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.Int32), contentTypes: GeneratedMetadataConstants.JsonContentType)); - ApiEndpointMetadata.AddApiEndpointMetadataIfMissing(options.EndpointBuilder); + DisableCookieRedirectMetadata.AddMetadataIfMissing(options.EndpointBuilder); return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; }; RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => @@ -260,17 +260,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_IntArrayParam_Optional.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_IntArrayParam_Optional.generated.txt index 01d6808eaf6e..598d6ecd2a8d 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_IntArrayParam_Optional.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_IntArrayParam_Optional.generated.txt @@ -72,7 +72,7 @@ namespace Microsoft.AspNetCore.Http.Generated var parameters = methodInfo.GetParameters(); options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: true, hasBindAsync: false, isOptional: true)); options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.Int32[]), contentTypes: GeneratedMetadataConstants.JsonContentType)); - ApiEndpointMetadata.AddApiEndpointMetadataIfMissing(options.EndpointBuilder); + DisableCookieRedirectMetadata.AddMetadataIfMissing(options.EndpointBuilder); return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; }; RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => @@ -260,17 +260,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_IntArrayParam_Optional_QueryNotPresent.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_IntArrayParam_Optional_QueryNotPresent.generated.txt index 01d6808eaf6e..598d6ecd2a8d 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_IntArrayParam_Optional_QueryNotPresent.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_IntArrayParam_Optional_QueryNotPresent.generated.txt @@ -72,7 +72,7 @@ namespace Microsoft.AspNetCore.Http.Generated var parameters = methodInfo.GetParameters(); options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: true, hasBindAsync: false, isOptional: true)); options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.Int32[]), contentTypes: GeneratedMetadataConstants.JsonContentType)); - ApiEndpointMetadata.AddApiEndpointMetadataIfMissing(options.EndpointBuilder); + DisableCookieRedirectMetadata.AddMetadataIfMissing(options.EndpointBuilder); return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; }; RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => @@ -260,17 +260,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_NullableIntArrayParam_Optional.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_NullableIntArrayParam_Optional.generated.txt index 40a1b29bb120..6bfc8e54f428 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_NullableIntArrayParam_Optional.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_NullableIntArrayParam_Optional.generated.txt @@ -72,7 +72,7 @@ namespace Microsoft.AspNetCore.Http.Generated var parameters = methodInfo.GetParameters(); options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: true, hasBindAsync: false, isOptional: true)); options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.Int32?[]), contentTypes: GeneratedMetadataConstants.JsonContentType)); - ApiEndpointMetadata.AddApiEndpointMetadataIfMissing(options.EndpointBuilder); + DisableCookieRedirectMetadata.AddMetadataIfMissing(options.EndpointBuilder); return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; }; RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => @@ -260,17 +260,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_NullableIntArrayParam_Optional_QueryNotPresent.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_NullableIntArrayParam_Optional_QueryNotPresent.generated.txt index 40a1b29bb120..6bfc8e54f428 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_NullableIntArrayParam_Optional_QueryNotPresent.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_NullableIntArrayParam_Optional_QueryNotPresent.generated.txt @@ -72,7 +72,7 @@ namespace Microsoft.AspNetCore.Http.Generated var parameters = methodInfo.GetParameters(); options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: true, hasBindAsync: false, isOptional: true)); options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.Int32?[]), contentTypes: GeneratedMetadataConstants.JsonContentType)); - ApiEndpointMetadata.AddApiEndpointMetadataIfMissing(options.EndpointBuilder); + DisableCookieRedirectMetadata.AddMetadataIfMissing(options.EndpointBuilder); return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; }; RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => @@ -260,17 +260,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_NullableStringArrayParam.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_NullableStringArrayParam.generated.txt index 9bebb6eea5ee..a26bf2b75f56 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_NullableStringArrayParam.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_NullableStringArrayParam.generated.txt @@ -72,7 +72,7 @@ namespace Microsoft.AspNetCore.Http.Generated var parameters = methodInfo.GetParameters(); options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: false, hasBindAsync: false, isOptional: false)); options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.Int32), contentTypes: GeneratedMetadataConstants.JsonContentType)); - ApiEndpointMetadata.AddApiEndpointMetadataIfMissing(options.EndpointBuilder); + DisableCookieRedirectMetadata.AddMetadataIfMissing(options.EndpointBuilder); return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; }; RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => @@ -231,17 +231,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_StringArrayParam.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_StringArrayParam.generated.txt index 7931fdc136d0..0b1e00050add 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_StringArrayParam.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_StringArrayParam.generated.txt @@ -72,7 +72,7 @@ namespace Microsoft.AspNetCore.Http.Generated var parameters = methodInfo.GetParameters(); options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: false, hasBindAsync: false, isOptional: false)); options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.Int32), contentTypes: GeneratedMetadataConstants.JsonContentType)); - ApiEndpointMetadata.AddApiEndpointMetadataIfMissing(options.EndpointBuilder); + DisableCookieRedirectMetadata.AddMetadataIfMissing(options.EndpointBuilder); return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; }; RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => @@ -231,17 +231,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_StringArrayParam_Optional.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_StringArrayParam_Optional.generated.txt index 757eb5f81f8d..fa538f0fe524 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_StringArrayParam_Optional.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_StringArrayParam_Optional.generated.txt @@ -72,7 +72,7 @@ namespace Microsoft.AspNetCore.Http.Generated var parameters = methodInfo.GetParameters(); options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: false, hasBindAsync: false, isOptional: true)); options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.String[]), contentTypes: GeneratedMetadataConstants.JsonContentType)); - ApiEndpointMetadata.AddApiEndpointMetadataIfMissing(options.EndpointBuilder); + DisableCookieRedirectMetadata.AddMetadataIfMissing(options.EndpointBuilder); return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; }; RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => @@ -231,17 +231,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_StringArrayParam_Optional_QueryNotPresent.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_StringArrayParam_Optional_QueryNotPresent.generated.txt index 757eb5f81f8d..fa538f0fe524 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_StringArrayParam_Optional_QueryNotPresent.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitQuery_StringArrayParam_Optional_QueryNotPresent.generated.txt @@ -72,7 +72,7 @@ namespace Microsoft.AspNetCore.Http.Generated var parameters = methodInfo.GetParameters(); options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: false, hasBindAsync: false, isOptional: true)); options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.String[]), contentTypes: GeneratedMetadataConstants.JsonContentType)); - ApiEndpointMetadata.AddApiEndpointMetadataIfMissing(options.EndpointBuilder); + DisableCookieRedirectMetadata.AddMetadataIfMissing(options.EndpointBuilder); return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; }; RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => @@ -231,17 +231,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitServiceParam_SimpleReturn_Snapshot.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitServiceParam_SimpleReturn_Snapshot.generated.txt index 2e18c631a26a..73f90510b5f0 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitServiceParam_SimpleReturn_Snapshot.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitServiceParam_SimpleReturn_Snapshot.generated.txt @@ -439,17 +439,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitSource_SimpleReturn_Snapshot.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitSource_SimpleReturn_Snapshot.generated.txt index 3663350edf78..9b10d9642c06 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitSource_SimpleReturn_Snapshot.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitSource_SimpleReturn_Snapshot.generated.txt @@ -606,17 +606,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_ComplexTypeArrayParam.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_ComplexTypeArrayParam.generated.txt index 07e79ab29820..718e7346565c 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_ComplexTypeArrayParam.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_ComplexTypeArrayParam.generated.txt @@ -72,7 +72,7 @@ namespace Microsoft.AspNetCore.Http.Generated var parameters = methodInfo.GetParameters(); options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: true, hasBindAsync: false, isOptional: false)); options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.Int32), contentTypes: GeneratedMetadataConstants.JsonContentType)); - ApiEndpointMetadata.AddApiEndpointMetadataIfMissing(options.EndpointBuilder); + DisableCookieRedirectMetadata.AddMetadataIfMissing(options.EndpointBuilder); return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; }; RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => @@ -267,17 +267,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_IntArrayParam_Optional.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_IntArrayParam_Optional.generated.txt index e51b39e6f806..93b03a43f10b 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_IntArrayParam_Optional.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_IntArrayParam_Optional.generated.txt @@ -72,7 +72,7 @@ namespace Microsoft.AspNetCore.Http.Generated var parameters = methodInfo.GetParameters(); options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: true, hasBindAsync: false, isOptional: true)); options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.Int32[]), contentTypes: GeneratedMetadataConstants.JsonContentType)); - ApiEndpointMetadata.AddApiEndpointMetadataIfMissing(options.EndpointBuilder); + DisableCookieRedirectMetadata.AddMetadataIfMissing(options.EndpointBuilder); return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; }; RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => @@ -267,17 +267,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_IntArrayParam_Optional_QueryNotPresent.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_IntArrayParam_Optional_QueryNotPresent.generated.txt index e51b39e6f806..93b03a43f10b 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_IntArrayParam_Optional_QueryNotPresent.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_IntArrayParam_Optional_QueryNotPresent.generated.txt @@ -72,7 +72,7 @@ namespace Microsoft.AspNetCore.Http.Generated var parameters = methodInfo.GetParameters(); options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: true, hasBindAsync: false, isOptional: true)); options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.Int32[]), contentTypes: GeneratedMetadataConstants.JsonContentType)); - ApiEndpointMetadata.AddApiEndpointMetadataIfMissing(options.EndpointBuilder); + DisableCookieRedirectMetadata.AddMetadataIfMissing(options.EndpointBuilder); return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; }; RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => @@ -267,17 +267,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableIntArrayParam_Optional.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableIntArrayParam_Optional.generated.txt index 2e4b0d343e08..a4534206e6cd 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableIntArrayParam_Optional.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableIntArrayParam_Optional.generated.txt @@ -72,7 +72,7 @@ namespace Microsoft.AspNetCore.Http.Generated var parameters = methodInfo.GetParameters(); options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: true, hasBindAsync: false, isOptional: true)); options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.Int32?[]), contentTypes: GeneratedMetadataConstants.JsonContentType)); - ApiEndpointMetadata.AddApiEndpointMetadataIfMissing(options.EndpointBuilder); + DisableCookieRedirectMetadata.AddMetadataIfMissing(options.EndpointBuilder); return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; }; RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => @@ -267,17 +267,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableIntArrayParam_Optional_QueryNotPresent.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableIntArrayParam_Optional_QueryNotPresent.generated.txt index 2e4b0d343e08..a4534206e6cd 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableIntArrayParam_Optional_QueryNotPresent.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableIntArrayParam_Optional_QueryNotPresent.generated.txt @@ -72,7 +72,7 @@ namespace Microsoft.AspNetCore.Http.Generated var parameters = methodInfo.GetParameters(); options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: true, hasBindAsync: false, isOptional: true)); options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.Int32?[]), contentTypes: GeneratedMetadataConstants.JsonContentType)); - ApiEndpointMetadata.AddApiEndpointMetadataIfMissing(options.EndpointBuilder); + DisableCookieRedirectMetadata.AddMetadataIfMissing(options.EndpointBuilder); return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; }; RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => @@ -267,17 +267,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam.generated.txt index 857501669994..15f5ee3f3a31 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam.generated.txt @@ -81,14 +81,14 @@ namespace Microsoft.AspNetCore.Http.Generated if (!serviceProviderIsService.IsService(type)) { options.EndpointBuilder.Metadata.Add(new AcceptsMetadata(type: type, isOptional: isOptional, contentTypes: GeneratedMetadataConstants.JsonContentType)); - options.EndpointBuilder.Metadata.Add(ApiEndpointMetadata.Instance); + options.EndpointBuilder.Metadata.Add(DisableCookieRedirectMetadata.Instance); break; } } var parameters = methodInfo.GetParameters(); options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: false, hasBindAsync: false, isOptional: false)); options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.Int32), contentTypes: GeneratedMetadataConstants.JsonContentType)); - ApiEndpointMetadata.AddApiEndpointMetadataIfMissing(options.EndpointBuilder); + DisableCookieRedirectMetadata.AddMetadataIfMissing(options.EndpointBuilder); return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; }; RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => @@ -329,17 +329,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam_EmptyQueryValues.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam_EmptyQueryValues.generated.txt index 857501669994..15f5ee3f3a31 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam_EmptyQueryValues.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam_EmptyQueryValues.generated.txt @@ -81,14 +81,14 @@ namespace Microsoft.AspNetCore.Http.Generated if (!serviceProviderIsService.IsService(type)) { options.EndpointBuilder.Metadata.Add(new AcceptsMetadata(type: type, isOptional: isOptional, contentTypes: GeneratedMetadataConstants.JsonContentType)); - options.EndpointBuilder.Metadata.Add(ApiEndpointMetadata.Instance); + options.EndpointBuilder.Metadata.Add(DisableCookieRedirectMetadata.Instance); break; } } var parameters = methodInfo.GetParameters(); options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: false, hasBindAsync: false, isOptional: false)); options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.Int32), contentTypes: GeneratedMetadataConstants.JsonContentType)); - ApiEndpointMetadata.AddApiEndpointMetadataIfMissing(options.EndpointBuilder); + DisableCookieRedirectMetadata.AddMetadataIfMissing(options.EndpointBuilder); return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; }; RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => @@ -329,17 +329,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam_QueryNotPresent.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam_QueryNotPresent.generated.txt index 857501669994..15f5ee3f3a31 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam_QueryNotPresent.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam_QueryNotPresent.generated.txt @@ -81,14 +81,14 @@ namespace Microsoft.AspNetCore.Http.Generated if (!serviceProviderIsService.IsService(type)) { options.EndpointBuilder.Metadata.Add(new AcceptsMetadata(type: type, isOptional: isOptional, contentTypes: GeneratedMetadataConstants.JsonContentType)); - options.EndpointBuilder.Metadata.Add(ApiEndpointMetadata.Instance); + options.EndpointBuilder.Metadata.Add(DisableCookieRedirectMetadata.Instance); break; } } var parameters = methodInfo.GetParameters(); options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: false, hasBindAsync: false, isOptional: false)); options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.Int32), contentTypes: GeneratedMetadataConstants.JsonContentType)); - ApiEndpointMetadata.AddApiEndpointMetadataIfMissing(options.EndpointBuilder); + DisableCookieRedirectMetadata.AddMetadataIfMissing(options.EndpointBuilder); return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; }; RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => @@ -329,17 +329,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_StringArrayParam.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_StringArrayParam.generated.txt index 9c1fe26e5902..1bfa6b37f0a0 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_StringArrayParam.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_StringArrayParam.generated.txt @@ -81,14 +81,14 @@ namespace Microsoft.AspNetCore.Http.Generated if (!serviceProviderIsService.IsService(type)) { options.EndpointBuilder.Metadata.Add(new AcceptsMetadata(type: type, isOptional: isOptional, contentTypes: GeneratedMetadataConstants.JsonContentType)); - options.EndpointBuilder.Metadata.Add(ApiEndpointMetadata.Instance); + options.EndpointBuilder.Metadata.Add(DisableCookieRedirectMetadata.Instance); break; } } var parameters = methodInfo.GetParameters(); options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: false, hasBindAsync: false, isOptional: false)); options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.Int32), contentTypes: GeneratedMetadataConstants.JsonContentType)); - ApiEndpointMetadata.AddApiEndpointMetadataIfMissing(options.EndpointBuilder); + DisableCookieRedirectMetadata.AddMetadataIfMissing(options.EndpointBuilder); return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; }; RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => @@ -329,17 +329,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_StringArrayParam_Optional.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_StringArrayParam_Optional.generated.txt index 65ddf7109200..18b9123305b6 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_StringArrayParam_Optional.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_StringArrayParam_Optional.generated.txt @@ -81,14 +81,14 @@ namespace Microsoft.AspNetCore.Http.Generated if (!serviceProviderIsService.IsService(type)) { options.EndpointBuilder.Metadata.Add(new AcceptsMetadata(type: type, isOptional: isOptional, contentTypes: GeneratedMetadataConstants.JsonContentType)); - options.EndpointBuilder.Metadata.Add(ApiEndpointMetadata.Instance); + options.EndpointBuilder.Metadata.Add(DisableCookieRedirectMetadata.Instance); break; } } var parameters = methodInfo.GetParameters(); options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: false, hasBindAsync: false, isOptional: true)); options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.String[]), contentTypes: GeneratedMetadataConstants.JsonContentType)); - ApiEndpointMetadata.AddApiEndpointMetadataIfMissing(options.EndpointBuilder); + DisableCookieRedirectMetadata.AddMetadataIfMissing(options.EndpointBuilder); return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; }; RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => @@ -329,17 +329,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_StringArrayParam_Optional_QueryNotPresent.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_StringArrayParam_Optional_QueryNotPresent.generated.txt index 65ddf7109200..18b9123305b6 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_StringArrayParam_Optional_QueryNotPresent.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_StringArrayParam_Optional_QueryNotPresent.generated.txt @@ -81,14 +81,14 @@ namespace Microsoft.AspNetCore.Http.Generated if (!serviceProviderIsService.IsService(type)) { options.EndpointBuilder.Metadata.Add(new AcceptsMetadata(type: type, isOptional: isOptional, contentTypes: GeneratedMetadataConstants.JsonContentType)); - options.EndpointBuilder.Metadata.Add(ApiEndpointMetadata.Instance); + options.EndpointBuilder.Metadata.Add(DisableCookieRedirectMetadata.Instance); break; } } var parameters = methodInfo.GetParameters(); options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: false, hasBindAsync: false, isOptional: true)); options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.String[]), contentTypes: GeneratedMetadataConstants.JsonContentType)); - ApiEndpointMetadata.AddApiEndpointMetadataIfMissing(options.EndpointBuilder); + DisableCookieRedirectMetadata.AddMetadataIfMissing(options.EndpointBuilder); return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; }; RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => @@ -329,17 +329,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_JsonBodyOrService_HandlesBothJsonAndService.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_JsonBodyOrService_HandlesBothJsonAndService.generated.txt index bc9c6a59d734..2eedb8ebb4e1 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_JsonBodyOrService_HandlesBothJsonAndService.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_JsonBodyOrService_HandlesBothJsonAndService.generated.txt @@ -82,7 +82,7 @@ namespace Microsoft.AspNetCore.Http.Generated if (!serviceProviderIsService.IsService(type)) { options.EndpointBuilder.Metadata.Add(new AcceptsMetadata(type: type, isOptional: isOptional, contentTypes: GeneratedMetadataConstants.JsonContentType)); - options.EndpointBuilder.Metadata.Add(ApiEndpointMetadata.Instance); + options.EndpointBuilder.Metadata.Add(DisableCookieRedirectMetadata.Instance); break; } } @@ -346,17 +346,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_MultipleSpecialTypeParam_StringReturn.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_MultipleSpecialTypeParam_StringReturn.generated.txt index 560f0ec1f94a..5f19b3e84abc 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_MultipleSpecialTypeParam_StringReturn.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_MultipleSpecialTypeParam_StringReturn.generated.txt @@ -234,17 +234,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_MultipleStringParam_StringReturn.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_MultipleStringParam_StringReturn.generated.txt index 0c26ae021468..df4453e888a8 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_MultipleStringParam_StringReturn.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_MultipleStringParam_StringReturn.generated.txt @@ -267,17 +267,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_NoParam_StringReturn_WithFilter.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_NoParam_StringReturn_WithFilter.generated.txt index 18cf2ce8010b..62465758d43e 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_NoParam_StringReturn_WithFilter.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_NoParam_StringReturn_WithFilter.generated.txt @@ -225,17 +225,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ReturnsString_Has_Metadata.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ReturnsString_Has_Metadata.generated.txt index 18cf2ce8010b..62465758d43e 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ReturnsString_Has_Metadata.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ReturnsString_Has_Metadata.generated.txt @@ -225,17 +225,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ReturnsTodo_Has_Metadata.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ReturnsTodo_Has_Metadata.generated.txt index 808d55a96981..c13dd9830c01 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ReturnsTodo_Has_Metadata.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ReturnsTodo_Has_Metadata.generated.txt @@ -70,7 +70,7 @@ namespace Microsoft.AspNetCore.Http.Generated Debug.Assert(options.EndpointBuilder != null, "EndpointBuilder not found."); options.EndpointBuilder.Metadata.Add(new System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.RequestDelegateGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")); options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::Microsoft.AspNetCore.Http.Generators.Tests.Todo), contentTypes: GeneratedMetadataConstants.JsonContentType)); - ApiEndpointMetadata.AddApiEndpointMetadataIfMissing(options.EndpointBuilder); + DisableCookieRedirectMetadata.AddMetadataIfMissing(options.EndpointBuilder); return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; }; RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => @@ -219,17 +219,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_SingleComplexTypeParam_StringReturn.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_SingleComplexTypeParam_StringReturn.generated.txt index 509930131549..1fcb2d9a7010 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_SingleComplexTypeParam_StringReturn.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_SingleComplexTypeParam_StringReturn.generated.txt @@ -266,17 +266,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_SingleEnumParam_StringReturn.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_SingleEnumParam_StringReturn.generated.txt index b1b71f8eecd5..dca5ca2932c2 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_SingleEnumParam_StringReturn.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_SingleEnumParam_StringReturn.generated.txt @@ -266,17 +266,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_SingleNullableStringParam_WithEmptyQueryStringValueProvided_StringReturn.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_SingleNullableStringParam_WithEmptyQueryStringValueProvided_StringReturn.generated.txt index b56621cb8c50..44bf443feb7f 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_SingleNullableStringParam_WithEmptyQueryStringValueProvided_StringReturn.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_SingleNullableStringParam_WithEmptyQueryStringValueProvided_StringReturn.generated.txt @@ -238,17 +238,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_TakesCustomMetadataEmitter_Has_Metadata.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_TakesCustomMetadataEmitter_Has_Metadata.generated.txt index 72e03e06b74e..f034206dfb59 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_TakesCustomMetadataEmitter_Has_Metadata.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_TakesCustomMetadataEmitter_Has_Metadata.generated.txt @@ -81,7 +81,7 @@ namespace Microsoft.AspNetCore.Http.Generated if (!serviceProviderIsService.IsService(type)) { options.EndpointBuilder.Metadata.Add(new AcceptsMetadata(type: type, isOptional: isOptional, contentTypes: GeneratedMetadataConstants.JsonContentType)); - options.EndpointBuilder.Metadata.Add(ApiEndpointMetadata.Instance); + options.EndpointBuilder.Metadata.Add(DisableCookieRedirectMetadata.Instance); break; } } @@ -334,17 +334,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapMethods_Get_WithArrayQueryString_AndBody_ShouldUseQueryString.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapMethods_Get_WithArrayQueryString_AndBody_ShouldUseQueryString.generated.txt index 7409065d2ce6..48eafa3d6687 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapMethods_Get_WithArrayQueryString_AndBody_ShouldUseQueryString.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapMethods_Get_WithArrayQueryString_AndBody_ShouldUseQueryString.generated.txt @@ -81,7 +81,7 @@ namespace Microsoft.AspNetCore.Http.Generated if (!serviceProviderIsService.IsService(type)) { options.EndpointBuilder.Metadata.Add(new AcceptsMetadata(type: type, isOptional: isOptional, contentTypes: GeneratedMetadataConstants.JsonContentType)); - options.EndpointBuilder.Metadata.Add(ApiEndpointMetadata.Instance); + options.EndpointBuilder.Metadata.Add(DisableCookieRedirectMetadata.Instance); break; } } @@ -335,17 +335,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapMethods_PostAndGet_WithArrayQueryString_AndBody_ShouldUseQueryString.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapMethods_PostAndGet_WithArrayQueryString_AndBody_ShouldUseQueryString.generated.txt index 7409065d2ce6..48eafa3d6687 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapMethods_PostAndGet_WithArrayQueryString_AndBody_ShouldUseQueryString.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapMethods_PostAndGet_WithArrayQueryString_AndBody_ShouldUseQueryString.generated.txt @@ -81,7 +81,7 @@ namespace Microsoft.AspNetCore.Http.Generated if (!serviceProviderIsService.IsService(type)) { options.EndpointBuilder.Metadata.Add(new AcceptsMetadata(type: type, isOptional: isOptional, contentTypes: GeneratedMetadataConstants.JsonContentType)); - options.EndpointBuilder.Metadata.Add(ApiEndpointMetadata.Instance); + options.EndpointBuilder.Metadata.Add(DisableCookieRedirectMetadata.Instance); break; } } @@ -335,17 +335,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapMethods_PostAndPut_WithArrayQueryString_AndBody_ShouldUseBody.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapMethods_PostAndPut_WithArrayQueryString_AndBody_ShouldUseBody.generated.txt index 7409065d2ce6..48eafa3d6687 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapMethods_PostAndPut_WithArrayQueryString_AndBody_ShouldUseBody.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapMethods_PostAndPut_WithArrayQueryString_AndBody_ShouldUseBody.generated.txt @@ -81,7 +81,7 @@ namespace Microsoft.AspNetCore.Http.Generated if (!serviceProviderIsService.IsService(type)) { options.EndpointBuilder.Metadata.Add(new AcceptsMetadata(type: type, isOptional: isOptional, contentTypes: GeneratedMetadataConstants.JsonContentType)); - options.EndpointBuilder.Metadata.Add(ApiEndpointMetadata.Instance); + options.EndpointBuilder.Metadata.Add(DisableCookieRedirectMetadata.Instance); break; } } @@ -335,17 +335,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapMethods_Post_WithArrayQueryString_AndBody_ShouldUseBody.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapMethods_Post_WithArrayQueryString_AndBody_ShouldUseBody.generated.txt index 7409065d2ce6..48eafa3d6687 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapMethods_Post_WithArrayQueryString_AndBody_ShouldUseBody.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapMethods_Post_WithArrayQueryString_AndBody_ShouldUseBody.generated.txt @@ -81,7 +81,7 @@ namespace Microsoft.AspNetCore.Http.Generated if (!serviceProviderIsService.IsService(type)) { options.EndpointBuilder.Metadata.Add(new AcceptsMetadata(type: type, isOptional: isOptional, contentTypes: GeneratedMetadataConstants.JsonContentType)); - options.EndpointBuilder.Metadata.Add(ApiEndpointMetadata.Instance); + options.EndpointBuilder.Metadata.Add(DisableCookieRedirectMetadata.Instance); break; } } @@ -335,17 +335,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapPost_WithArrayQueryString_AndBody_ShouldUseBody.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapPost_WithArrayQueryString_AndBody_ShouldUseBody.generated.txt index 3fb5a66501a7..d919734fe2bc 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapPost_WithArrayQueryString_AndBody_ShouldUseBody.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapPost_WithArrayQueryString_AndBody_ShouldUseBody.generated.txt @@ -81,7 +81,7 @@ namespace Microsoft.AspNetCore.Http.Generated if (!serviceProviderIsService.IsService(type)) { options.EndpointBuilder.Metadata.Add(new AcceptsMetadata(type: type, isOptional: isOptional, contentTypes: GeneratedMetadataConstants.JsonContentType)); - options.EndpointBuilder.Metadata.Add(ApiEndpointMetadata.Instance); + options.EndpointBuilder.Metadata.Add(DisableCookieRedirectMetadata.Instance); break; } } @@ -335,17 +335,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapPost_WithArrayQueryString_ShouldFail.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapPost_WithArrayQueryString_ShouldFail.generated.txt index f34ec56593e5..feb0157220ce 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapPost_WithArrayQueryString_ShouldFail.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapPost_WithArrayQueryString_ShouldFail.generated.txt @@ -81,14 +81,14 @@ namespace Microsoft.AspNetCore.Http.Generated if (!serviceProviderIsService.IsService(type)) { options.EndpointBuilder.Metadata.Add(new AcceptsMetadata(type: type, isOptional: isOptional, contentTypes: GeneratedMetadataConstants.JsonContentType)); - options.EndpointBuilder.Metadata.Add(ApiEndpointMetadata.Instance); + options.EndpointBuilder.Metadata.Add(DisableCookieRedirectMetadata.Instance); break; } } var parameters = methodInfo.GetParameters(); options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("p", parameters[0], hasTryParse: false, hasBindAsync: false, isOptional: false)); options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(global::System.Int32), contentTypes: GeneratedMetadataConstants.JsonContentType)); - ApiEndpointMetadata.AddApiEndpointMetadataIfMissing(options.EndpointBuilder); + DisableCookieRedirectMetadata.AddMetadataIfMissing(options.EndpointBuilder); return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; }; RequestDelegateFactoryFunc createRequestDelegate = (del, options, inferredMetadataResult) => @@ -329,17 +329,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/Multiple_MapAction_NoParam_StringReturn.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/Multiple_MapAction_NoParam_StringReturn.generated.txt index bf51a39927f5..93d79c0abaec 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/Multiple_MapAction_NoParam_StringReturn.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/Multiple_MapAction_NoParam_StringReturn.generated.txt @@ -414,17 +414,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/Multiple_MapAction_WithParams_StringReturn.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/Multiple_MapAction_WithParams_StringReturn.generated.txt index ffb90a91f7fc..e2b916aea081 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/Multiple_MapAction_WithParams_StringReturn.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/Multiple_MapAction_WithParams_StringReturn.generated.txt @@ -428,17 +428,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/SupportsDifferentInterceptorsFromSameLocation.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/SupportsDifferentInterceptorsFromSameLocation.generated.txt index fea0e359a92f..2211c3a0291f 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/SupportsDifferentInterceptorsFromSameLocation.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/SupportsDifferentInterceptorsFromSameLocation.generated.txt @@ -388,17 +388,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/SupportsSameInterceptorsFromDifferentFiles.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/SupportsSameInterceptorsFromDifferentFiles.generated.txt index cafc9280c930..df7e24a9eebc 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/SupportsSameInterceptorsFromDifferentFiles.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/SupportsSameInterceptorsFromDifferentFiles.generated.txt @@ -257,17 +257,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/VerifyAsParametersBaseline.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/VerifyAsParametersBaseline.generated.txt index 489a8b16b73a..02fc8baf1ac0 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/VerifyAsParametersBaseline.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/VerifyAsParametersBaseline.generated.txt @@ -446,7 +446,7 @@ namespace Microsoft.AspNetCore.Http.Generated Debug.Assert(options.EndpointBuilder != null, "EndpointBuilder not found."); options.EndpointBuilder.Metadata.Add(new System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.RequestDelegateGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")); options.EndpointBuilder.Metadata.Add(new AcceptsMetadata(contentTypes: GeneratedMetadataConstants.JsonContentType)); - options.EndpointBuilder.Metadata.Add(ApiEndpointMetadata.Instance); + options.EndpointBuilder.Metadata.Add(DisableCookieRedirectMetadata.Instance); options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("HttpContext", new PropertyAsParameterInfo(false, typeof(Microsoft.AspNetCore.Http.Generators.Tests.ParametersListWithImplicitFromBody)!.GetProperty("HttpContext")!, typeof(Microsoft.AspNetCore.Http.Generators.Tests.ParametersListWithImplicitFromBody).GetConstructor(new[] { typeof(Microsoft.AspNetCore.Http.HttpContext), typeof(Microsoft.AspNetCore.Http.Generators.Tests.TodoStruct) })?.GetParameters()[0]), hasTryParse: false, hasBindAsync: false, isOptional: false)); options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("Todo", new PropertyAsParameterInfo(false, typeof(Microsoft.AspNetCore.Http.Generators.Tests.ParametersListWithImplicitFromBody)!.GetProperty("Todo")!, typeof(Microsoft.AspNetCore.Http.Generators.Tests.ParametersListWithImplicitFromBody).GetConstructor(new[] { typeof(Microsoft.AspNetCore.Http.HttpContext), typeof(Microsoft.AspNetCore.Http.Generators.Tests.TodoStruct) })?.GetParameters()[1]), hasTryParse: false, hasBindAsync: false, isOptional: false)); options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(string), contentTypes: GeneratedMetadataConstants.PlaintextContentType)); @@ -568,7 +568,7 @@ namespace Microsoft.AspNetCore.Http.Generated Debug.Assert(options.EndpointBuilder != null, "EndpointBuilder not found."); options.EndpointBuilder.Metadata.Add(new System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.RequestDelegateGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")); options.EndpointBuilder.Metadata.Add(new AcceptsMetadata(contentTypes: GeneratedMetadataConstants.JsonContentType)); - options.EndpointBuilder.Metadata.Add(ApiEndpointMetadata.Instance); + options.EndpointBuilder.Metadata.Add(DisableCookieRedirectMetadata.Instance); options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("HttpContext", new PropertyAsParameterInfo(false, typeof(Microsoft.AspNetCore.Http.Generators.Tests.ParametersListWithMetadataType)!.GetProperty("HttpContext")!, typeof(Microsoft.AspNetCore.Http.Generators.Tests.ParametersListWithMetadataType).GetConstructor(new[] { typeof(Microsoft.AspNetCore.Http.HttpContext), typeof(Microsoft.AspNetCore.Http.Generators.Tests.AddsCustomParameterMetadataAsProperty) })?.GetParameters()[0]), hasTryParse: false, hasBindAsync: false, isOptional: false)); options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("Value", new PropertyAsParameterInfo(false, typeof(Microsoft.AspNetCore.Http.Generators.Tests.ParametersListWithMetadataType)!.GetProperty("Value")!, typeof(Microsoft.AspNetCore.Http.Generators.Tests.ParametersListWithMetadataType).GetConstructor(new[] { typeof(Microsoft.AspNetCore.Http.HttpContext), typeof(Microsoft.AspNetCore.Http.Generators.Tests.AddsCustomParameterMetadataAsProperty) })?.GetParameters()[1]), hasTryParse: false, hasBindAsync: false, isOptional: false)); var parameterInfos = methodInfo.GetParameters(); @@ -685,7 +685,7 @@ namespace Microsoft.AspNetCore.Http.Generated Debug.Assert(options.EndpointBuilder != null, "EndpointBuilder not found."); options.EndpointBuilder.Metadata.Add(new System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.RequestDelegateGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")); options.EndpointBuilder.Metadata.Add(new AcceptsMetadata(contentTypes: GeneratedMetadataConstants.JsonContentType)); - options.EndpointBuilder.Metadata.Add(ApiEndpointMetadata.Instance); + options.EndpointBuilder.Metadata.Add(DisableCookieRedirectMetadata.Instance); options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("Todo", new PropertyAsParameterInfo(false, typeof(Microsoft.AspNetCore.Http.Generators.Tests.ParameterRecordStructWithJsonBodyOrService)!.GetProperty("Todo")!), hasTryParse: false, hasBindAsync: false, isOptional: false)); options.EndpointBuilder.Metadata.Add(new ParameterBindingMetadata("Service", new PropertyAsParameterInfo(false, typeof(Microsoft.AspNetCore.Http.Generators.Tests.ParameterRecordStructWithJsonBodyOrService)!.GetProperty("Service")!), hasTryParse: false, hasBindAsync: false, isOptional: false)); options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof(string), contentTypes: GeneratedMetadataConstants.PlaintextContentType)); @@ -969,17 +969,17 @@ namespace Microsoft.AspNetCore.Http.Generated } - file sealed class ApiEndpointMetadata : IApiEndpointMetadata + file sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static readonly ApiEndpointMetadata Instance = new(); + public static readonly DisableCookieRedirectMetadata Instance = new(); - private ApiEndpointMetadata() + private DisableCookieRedirectMetadata() { } - public static void AddApiEndpointMetadataIfMissing(EndpointBuilder builder) + public static void AddMetadataIfMissing(EndpointBuilder builder) { - if (!builder.Metadata.Any(m => m is IApiEndpointMetadata)) + if (!builder.Metadata.Any(m => m is IDisableCookieRedirectMetadata)) { builder.Metadata.Add(Instance); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Metadata.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Metadata.cs index 1f3412e54391..2814a136e55f 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Metadata.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Metadata.cs @@ -506,10 +506,10 @@ public async Task InferMetadata_ThenCreate_CombinesAllMetadata_InCorrectOrder() // Act var endpoint = GetEndpointFromCompilation(compilation); - // IApiEndpointMetadata is tricky to order consistently because it depends on whether AddsCustomParameterMetadata is registered - // as a service at runtime. However, the order of IApiEndpointMetadata is not significant since there's no way to override it + // IDisableCookieRedirectMetadata is tricky to order consistently because it depends on whether AddsCustomParameterMetadata is registered + // as a service at runtime. However, the order of IDisableCookieRedirectMetadata is not significant since there's no way to override it // other than removing it. - Assert.Single(endpoint.Metadata, m => m is IApiEndpointMetadata); + Assert.Single(endpoint.Metadata, m => m is IDisableCookieRedirectMetadata); // Assert // NOTE: Depending on whether we are running under RDG or RDG, there are some generated types which @@ -525,7 +525,7 @@ m is not HttpMethodMetadata && m is not Attribute1 && m is not Attribute2 && m is not IRouteDiagnosticsMetadata && - m is not IApiEndpointMetadata); + m is not IDisableCookieRedirectMetadata); Assert.Collection(filteredMetadata, // Inferred AcceptsMetadata from RDF for complex type diff --git a/src/Http/Http.Results/src/Accepted.cs b/src/Http/Http.Results/src/Accepted.cs index d71fe9072c7e..b6b890960848 100644 --- a/src/Http/Http.Results/src/Accepted.cs +++ b/src/Http/Http.Results/src/Accepted.cs @@ -82,6 +82,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(builder); builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status202Accepted, typeof(void))); - builder.Metadata.Add(ApiEndpointMetadata.Instance); + builder.Metadata.Add(DisableCookieRedirectMetadata.Instance); } } diff --git a/src/Http/Http.Results/src/AcceptedAtRoute.cs b/src/Http/Http.Results/src/AcceptedAtRoute.cs index 900ee507d9cf..8c5d5a7c7b04 100644 --- a/src/Http/Http.Results/src/AcceptedAtRoute.cs +++ b/src/Http/Http.Results/src/AcceptedAtRoute.cs @@ -109,6 +109,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(builder); builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status202Accepted, typeof(void))); - builder.Metadata.Add(ApiEndpointMetadata.Instance); + builder.Metadata.Add(DisableCookieRedirectMetadata.Instance); } } diff --git a/src/Http/Http.Results/src/AcceptedAtRouteOfT.cs b/src/Http/Http.Results/src/AcceptedAtRouteOfT.cs index b8e4304a7237..026a040fc35c 100644 --- a/src/Http/Http.Results/src/AcceptedAtRouteOfT.cs +++ b/src/Http/Http.Results/src/AcceptedAtRouteOfT.cs @@ -123,6 +123,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(builder); builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status202Accepted, ContentTypeConstants.ApplicationJsonContentTypes)); - builder.Metadata.Add(ApiEndpointMetadata.Instance); + builder.Metadata.Add(DisableCookieRedirectMetadata.Instance); } } diff --git a/src/Http/Http.Results/src/AcceptedOfT.cs b/src/Http/Http.Results/src/AcceptedOfT.cs index 346070714b43..4fdc346ebabd 100644 --- a/src/Http/Http.Results/src/AcceptedOfT.cs +++ b/src/Http/Http.Results/src/AcceptedOfT.cs @@ -101,6 +101,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(builder); builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status202Accepted, ContentTypeConstants.ApplicationJsonContentTypes)); - builder.Metadata.Add(ApiEndpointMetadata.Instance); + builder.Metadata.Add(DisableCookieRedirectMetadata.Instance); } } diff --git a/src/Http/Http.Results/src/BadRequest.cs b/src/Http/Http.Results/src/BadRequest.cs index 0356afedc2c7..70a6eba3199f 100644 --- a/src/Http/Http.Results/src/BadRequest.cs +++ b/src/Http/Http.Results/src/BadRequest.cs @@ -52,6 +52,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(builder); builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status400BadRequest, typeof(void))); - builder.Metadata.Add(ApiEndpointMetadata.Instance); + builder.Metadata.Add(DisableCookieRedirectMetadata.Instance); } } diff --git a/src/Http/Http.Results/src/BadRequestOfT.cs b/src/Http/Http.Results/src/BadRequestOfT.cs index b8fa51423d7e..32bb7a0ca613 100644 --- a/src/Http/Http.Results/src/BadRequestOfT.cs +++ b/src/Http/Http.Results/src/BadRequestOfT.cs @@ -66,6 +66,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(builder); builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status400BadRequest, ContentTypeConstants.ApplicationJsonContentTypes)); - builder.Metadata.Add(ApiEndpointMetadata.Instance); + builder.Metadata.Add(DisableCookieRedirectMetadata.Instance); } } diff --git a/src/Http/Http.Results/src/Conflict.cs b/src/Http/Http.Results/src/Conflict.cs index f0067ba4aea1..1de48f75a455 100644 --- a/src/Http/Http.Results/src/Conflict.cs +++ b/src/Http/Http.Results/src/Conflict.cs @@ -52,6 +52,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(builder); builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status409Conflict, typeof(void))); - builder.Metadata.Add(ApiEndpointMetadata.Instance); + builder.Metadata.Add(DisableCookieRedirectMetadata.Instance); } } diff --git a/src/Http/Http.Results/src/ConflictOfT.cs b/src/Http/Http.Results/src/ConflictOfT.cs index dda54d9dd5cc..32241e2b9424 100644 --- a/src/Http/Http.Results/src/ConflictOfT.cs +++ b/src/Http/Http.Results/src/ConflictOfT.cs @@ -66,6 +66,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(builder); builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status409Conflict, ContentTypeConstants.ApplicationJsonContentTypes)); - builder.Metadata.Add(ApiEndpointMetadata.Instance); + builder.Metadata.Add(DisableCookieRedirectMetadata.Instance); } } diff --git a/src/Http/Http.Results/src/Created.cs b/src/Http/Http.Results/src/Created.cs index 1959556280c9..e9eb9fa96655 100644 --- a/src/Http/Http.Results/src/Created.cs +++ b/src/Http/Http.Results/src/Created.cs @@ -82,6 +82,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(builder); builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status201Created, typeof(void))); - builder.Metadata.Add(ApiEndpointMetadata.Instance); + builder.Metadata.Add(DisableCookieRedirectMetadata.Instance); } } diff --git a/src/Http/Http.Results/src/CreatedAtRoute.cs b/src/Http/Http.Results/src/CreatedAtRoute.cs index 2ff79596ba36..e194030ee327 100644 --- a/src/Http/Http.Results/src/CreatedAtRoute.cs +++ b/src/Http/Http.Results/src/CreatedAtRoute.cs @@ -109,6 +109,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(builder); builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status201Created, typeof(void))); - builder.Metadata.Add(ApiEndpointMetadata.Instance); + builder.Metadata.Add(DisableCookieRedirectMetadata.Instance); } } diff --git a/src/Http/Http.Results/src/CreatedAtRouteOfT.cs b/src/Http/Http.Results/src/CreatedAtRouteOfT.cs index a2868afdc0a1..9b972b29c044 100644 --- a/src/Http/Http.Results/src/CreatedAtRouteOfT.cs +++ b/src/Http/Http.Results/src/CreatedAtRouteOfT.cs @@ -126,6 +126,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(builder); builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status201Created, ContentTypeConstants.ApplicationJsonContentTypes)); - builder.Metadata.Add(ApiEndpointMetadata.Instance); + builder.Metadata.Add(DisableCookieRedirectMetadata.Instance); } } diff --git a/src/Http/Http.Results/src/CreatedOfT.cs b/src/Http/Http.Results/src/CreatedOfT.cs index a12e52b9f69f..267f25fae39a 100644 --- a/src/Http/Http.Results/src/CreatedOfT.cs +++ b/src/Http/Http.Results/src/CreatedOfT.cs @@ -100,6 +100,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(builder); builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status201Created, ContentTypeConstants.ApplicationJsonContentTypes)); - builder.Metadata.Add(ApiEndpointMetadata.Instance); + builder.Metadata.Add(DisableCookieRedirectMetadata.Instance); } } diff --git a/src/Http/Http.Results/src/InternalServerErrorOfT.cs b/src/Http/Http.Results/src/InternalServerErrorOfT.cs index c726debf67c0..1a813205de25 100644 --- a/src/Http/Http.Results/src/InternalServerErrorOfT.cs +++ b/src/Http/Http.Results/src/InternalServerErrorOfT.cs @@ -66,6 +66,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(builder); builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status500InternalServerError, ContentTypeConstants.ApplicationJsonContentTypes)); - builder.Metadata.Add(ApiEndpointMetadata.Instance); + builder.Metadata.Add(DisableCookieRedirectMetadata.Instance); } } diff --git a/src/Http/Http.Results/src/JsonHttpResultOfT.cs b/src/Http/Http.Results/src/JsonHttpResultOfT.cs index fea230609a20..d016be7f97e0 100644 --- a/src/Http/Http.Results/src/JsonHttpResultOfT.cs +++ b/src/Http/Http.Results/src/JsonHttpResultOfT.cs @@ -137,6 +137,6 @@ public Task ExecuteAsync(HttpContext httpContext) static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder) { - builder.Metadata.Add(ApiEndpointMetadata.Instance); + builder.Metadata.Add(DisableCookieRedirectMetadata.Instance); } } diff --git a/src/Http/Http.Results/src/NoContent.cs b/src/Http/Http.Results/src/NoContent.cs index 35ffb3c56701..e76f8cc0f68c 100644 --- a/src/Http/Http.Results/src/NoContent.cs +++ b/src/Http/Http.Results/src/NoContent.cs @@ -52,6 +52,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(builder); builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status204NoContent, typeof(void))); - builder.Metadata.Add(ApiEndpointMetadata.Instance); + builder.Metadata.Add(DisableCookieRedirectMetadata.Instance); } } diff --git a/src/Http/Http.Results/src/NotFoundOfT.cs b/src/Http/Http.Results/src/NotFoundOfT.cs index d37a4ee9707c..6c27054e81bd 100644 --- a/src/Http/Http.Results/src/NotFoundOfT.cs +++ b/src/Http/Http.Results/src/NotFoundOfT.cs @@ -65,6 +65,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(builder); builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status404NotFound, ContentTypeConstants.ApplicationJsonContentTypes)); - builder.Metadata.Add(ApiEndpointMetadata.Instance); + builder.Metadata.Add(DisableCookieRedirectMetadata.Instance); } } diff --git a/src/Http/Http.Results/src/Ok.cs b/src/Http/Http.Results/src/Ok.cs index 837a0fa7513c..c348934d1523 100644 --- a/src/Http/Http.Results/src/Ok.cs +++ b/src/Http/Http.Results/src/Ok.cs @@ -51,6 +51,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(builder); builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status200OK, typeof(void))); - builder.Metadata.Add(ApiEndpointMetadata.Instance); + builder.Metadata.Add(DisableCookieRedirectMetadata.Instance); } } diff --git a/src/Http/Http.Results/src/OkOfT.cs b/src/Http/Http.Results/src/OkOfT.cs index 7f19598effe9..6939fb290f19 100644 --- a/src/Http/Http.Results/src/OkOfT.cs +++ b/src/Http/Http.Results/src/OkOfT.cs @@ -65,6 +65,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(builder); builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status200OK, ContentTypeConstants.ApplicationJsonContentTypes)); - builder.Metadata.Add(ApiEndpointMetadata.Instance); + builder.Metadata.Add(DisableCookieRedirectMetadata.Instance); } } diff --git a/src/Http/Http.Results/src/ProblemHttpResult.cs b/src/Http/Http.Results/src/ProblemHttpResult.cs index 8ac29336f053..2aa3a7418653 100644 --- a/src/Http/Http.Results/src/ProblemHttpResult.cs +++ b/src/Http/Http.Results/src/ProblemHttpResult.cs @@ -79,6 +79,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(method); ArgumentNullException.ThrowIfNull(builder); - builder.Metadata.Add(ApiEndpointMetadata.Instance); + builder.Metadata.Add(DisableCookieRedirectMetadata.Instance); } } diff --git a/src/Http/Http.Results/src/ServerSentEventsResult.cs b/src/Http/Http.Results/src/ServerSentEventsResult.cs index a7951acd1554..5cbb9d871f60 100644 --- a/src/Http/Http.Results/src/ServerSentEventsResult.cs +++ b/src/Http/Http.Results/src/ServerSentEventsResult.cs @@ -105,6 +105,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(builder); builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status200OK, typeof(SseItem), contentTypes: ["text/event-stream"])); - builder.Metadata.Add(ApiEndpointMetadata.Instance); + builder.Metadata.Add(DisableCookieRedirectMetadata.Instance); } } diff --git a/src/Http/Http.Results/src/UnprocessableEntity.cs b/src/Http/Http.Results/src/UnprocessableEntity.cs index 49d3982720b8..298e162c7ae0 100644 --- a/src/Http/Http.Results/src/UnprocessableEntity.cs +++ b/src/Http/Http.Results/src/UnprocessableEntity.cs @@ -52,6 +52,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(builder); builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status422UnprocessableEntity, typeof(void))); - builder.Metadata.Add(ApiEndpointMetadata.Instance); + builder.Metadata.Add(DisableCookieRedirectMetadata.Instance); } } diff --git a/src/Http/Http.Results/src/UnprocessableEntityOfT.cs b/src/Http/Http.Results/src/UnprocessableEntityOfT.cs index 2d1ee494a271..c59c519ec0fd 100644 --- a/src/Http/Http.Results/src/UnprocessableEntityOfT.cs +++ b/src/Http/Http.Results/src/UnprocessableEntityOfT.cs @@ -66,6 +66,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(builder); builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status422UnprocessableEntity, ContentTypeConstants.ApplicationJsonContentTypes)); - builder.Metadata.Add(ApiEndpointMetadata.Instance); + builder.Metadata.Add(DisableCookieRedirectMetadata.Instance); } } diff --git a/src/Http/Http.Results/src/ValidationProblem.cs b/src/Http/Http.Results/src/ValidationProblem.cs index c4a539c9cd5f..bd4c75dda703 100644 --- a/src/Http/Http.Results/src/ValidationProblem.cs +++ b/src/Http/Http.Results/src/ValidationProblem.cs @@ -77,6 +77,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(builder); builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(HttpValidationProblemDetails), StatusCodes.Status400BadRequest, ContentTypeConstants.ProblemDetailsContentTypes)); - builder.Metadata.Add(ApiEndpointMetadata.Instance); + builder.Metadata.Add(DisableCookieRedirectMetadata.Instance); } } diff --git a/src/Http/Http.Results/test/AcceptedAtRouteOfTResultTests.cs b/src/Http/Http.Results/test/AcceptedAtRouteOfTResultTests.cs index 1d53452fa65e..9423fb06142d 100644 --- a/src/Http/Http.Results/test/AcceptedAtRouteOfTResultTests.cs +++ b/src/Http/Http.Results/test/AcceptedAtRouteOfTResultTests.cs @@ -135,7 +135,7 @@ public void PopulateMetadata_AddsResponseTypeMetadata() Assert.Equal(typeof(Todo), producesResponseTypeMetadata.Type); Assert.Single(producesResponseTypeMetadata.ContentTypes, "application/json"); - Assert.Contains(builder.Metadata, m => m is IApiEndpointMetadata); + Assert.Contains(builder.Metadata, m => m is IDisableCookieRedirectMetadata); } [Fact] diff --git a/src/Http/Http.Results/test/AcceptedAtRouteResultTests.cs b/src/Http/Http.Results/test/AcceptedAtRouteResultTests.cs index 15a93310e80c..67c2657fb42c 100644 --- a/src/Http/Http.Results/test/AcceptedAtRouteResultTests.cs +++ b/src/Http/Http.Results/test/AcceptedAtRouteResultTests.cs @@ -88,7 +88,7 @@ public void PopulateMetadata_AddsResponseTypeMetadata() Assert.Equal(StatusCodes.Status202Accepted, producesResponseTypeMetadata.StatusCode); Assert.Equal(typeof(void), producesResponseTypeMetadata.Type); - Assert.Contains(builder.Metadata, m => m is IApiEndpointMetadata); + Assert.Contains(builder.Metadata, m => m is IDisableCookieRedirectMetadata); } [Fact] diff --git a/src/Http/Http.Results/test/AcceptedOfTResultTests.cs b/src/Http/Http.Results/test/AcceptedOfTResultTests.cs index 2ee6e03193d4..b2d16737c4b6 100644 --- a/src/Http/Http.Results/test/AcceptedOfTResultTests.cs +++ b/src/Http/Http.Results/test/AcceptedOfTResultTests.cs @@ -76,7 +76,7 @@ public void PopulateMetadata_AddsResponseTypeMetadata() Assert.Equal(typeof(Todo), producesResponseTypeMetadata.Type); Assert.Single(producesResponseTypeMetadata.ContentTypes, "application/json"); - Assert.Contains(builder.Metadata, m => m is IApiEndpointMetadata); + Assert.Contains(builder.Metadata, m => m is IDisableCookieRedirectMetadata); } [Fact] diff --git a/src/Http/Http.Results/test/AcceptedResultTests.cs b/src/Http/Http.Results/test/AcceptedResultTests.cs index 171b45112174..fdb97dcc9e88 100644 --- a/src/Http/Http.Results/test/AcceptedResultTests.cs +++ b/src/Http/Http.Results/test/AcceptedResultTests.cs @@ -45,7 +45,7 @@ public void PopulateMetadata_AddsResponseTypeMetadata() Assert.Equal(StatusCodes.Status202Accepted, producesResponseTypeMetadata.StatusCode); Assert.Equal(typeof(void), producesResponseTypeMetadata.Type); - Assert.Contains(builder.Metadata, m => m is IApiEndpointMetadata); + Assert.Contains(builder.Metadata, m => m is IDisableCookieRedirectMetadata); } [Fact] diff --git a/src/Http/Http.Results/test/BadRequestOfTResultTests.cs b/src/Http/Http.Results/test/BadRequestOfTResultTests.cs index c930db6ea80a..c58041b203d8 100644 --- a/src/Http/Http.Results/test/BadRequestOfTResultTests.cs +++ b/src/Http/Http.Results/test/BadRequestOfTResultTests.cs @@ -119,7 +119,7 @@ public void PopulateMetadata_AddsResponseTypeMetadata() Assert.Equal(typeof(Todo), producesResponseTypeMetadata.Type); Assert.Single(producesResponseTypeMetadata.ContentTypes, "application/json"); - Assert.Contains(builder.Metadata, m => m is IApiEndpointMetadata); + Assert.Contains(builder.Metadata, m => m is IDisableCookieRedirectMetadata); } [Fact] diff --git a/src/Http/Http.Results/test/BadRequestResultTests.cs b/src/Http/Http.Results/test/BadRequestResultTests.cs index cca2a4d139ee..c8bac8d0282a 100644 --- a/src/Http/Http.Results/test/BadRequestResultTests.cs +++ b/src/Http/Http.Results/test/BadRequestResultTests.cs @@ -58,7 +58,7 @@ public void PopulateMetadata_AddsResponseTypeMetadata() Assert.Equal(StatusCodes.Status400BadRequest, producesResponseTypeMetadata.StatusCode); Assert.Equal(typeof(void), producesResponseTypeMetadata.Type); - Assert.Contains(builder.Metadata, m => m is IApiEndpointMetadata); + Assert.Contains(builder.Metadata, m => m is IDisableCookieRedirectMetadata); } [Fact] diff --git a/src/Http/Http.Results/test/ConflictOfTResultTests.cs b/src/Http/Http.Results/test/ConflictOfTResultTests.cs index be4e43d441df..fb2a0b655915 100644 --- a/src/Http/Http.Results/test/ConflictOfTResultTests.cs +++ b/src/Http/Http.Results/test/ConflictOfTResultTests.cs @@ -97,7 +97,7 @@ public void PopulateMetadata_AddsResponseTypeMetadata() Assert.Equal(typeof(Todo), producesResponseTypeMetadata.Type); Assert.Single(producesResponseTypeMetadata.ContentTypes, "application/json"); - Assert.Contains(builder.Metadata, m => m is IApiEndpointMetadata); + Assert.Contains(builder.Metadata, m => m is IDisableCookieRedirectMetadata); } [Fact] diff --git a/src/Http/Http.Results/test/ConflictResultTests.cs b/src/Http/Http.Results/test/ConflictResultTests.cs index 2f639948eac6..449eb7665b88 100644 --- a/src/Http/Http.Results/test/ConflictResultTests.cs +++ b/src/Http/Http.Results/test/ConflictResultTests.cs @@ -59,7 +59,7 @@ public void PopulateMetadata_AddsResponseTypeMetadata() Assert.Equal(StatusCodes.Status409Conflict, producesResponseTypeMetadata.StatusCode); Assert.Equal(typeof(void), producesResponseTypeMetadata.Type); - Assert.Contains(builder.Metadata, m => m is IApiEndpointMetadata); + Assert.Contains(builder.Metadata, m => m is IDisableCookieRedirectMetadata); } [Fact] diff --git a/src/Http/Http.Results/test/CreatedAtRouteOfTResultTests.cs b/src/Http/Http.Results/test/CreatedAtRouteOfTResultTests.cs index a55f7dd6ef9c..82591bc7fc27 100644 --- a/src/Http/Http.Results/test/CreatedAtRouteOfTResultTests.cs +++ b/src/Http/Http.Results/test/CreatedAtRouteOfTResultTests.cs @@ -105,7 +105,7 @@ public void PopulateMetadata_AddsResponseTypeMetadata() Assert.Equal(typeof(Todo), producesResponseTypeMetadata.Type); Assert.Single(producesResponseTypeMetadata.ContentTypes, "application/json"); - Assert.Contains(builder.Metadata, m => m is IApiEndpointMetadata); + Assert.Contains(builder.Metadata, m => m is IDisableCookieRedirectMetadata); } [Fact] diff --git a/src/Http/Http.Results/test/CreatedAtRouteResultTests.cs b/src/Http/Http.Results/test/CreatedAtRouteResultTests.cs index 5a36fdcce055..077bd18bfd7a 100644 --- a/src/Http/Http.Results/test/CreatedAtRouteResultTests.cs +++ b/src/Http/Http.Results/test/CreatedAtRouteResultTests.cs @@ -86,7 +86,7 @@ public void PopulateMetadata_AddsResponseTypeMetadata() Assert.Equal(StatusCodes.Status201Created, producesResponseTypeMetadata.StatusCode); Assert.Equal(typeof(void), producesResponseTypeMetadata.Type); - Assert.Contains(builder.Metadata, m => m is IApiEndpointMetadata); + Assert.Contains(builder.Metadata, m => m is IDisableCookieRedirectMetadata); } [Fact] diff --git a/src/Http/Http.Results/test/CreatedOfTResultTests.cs b/src/Http/Http.Results/test/CreatedOfTResultTests.cs index b4b5747745a1..600370f60386 100644 --- a/src/Http/Http.Results/test/CreatedOfTResultTests.cs +++ b/src/Http/Http.Results/test/CreatedOfTResultTests.cs @@ -111,7 +111,7 @@ public void PopulateMetadata_AddsResponseTypeMetadata() Assert.Equal(typeof(Todo), producesResponseTypeMetadata.Type); Assert.Single(producesResponseTypeMetadata.ContentTypes, "application/json"); - Assert.Contains(builder.Metadata, m => m is IApiEndpointMetadata); + Assert.Contains(builder.Metadata, m => m is IDisableCookieRedirectMetadata); } [Fact] diff --git a/src/Http/Http.Results/test/CreatedResultTests.cs b/src/Http/Http.Results/test/CreatedResultTests.cs index af1c8a1179b8..fb5e5e7f9056 100644 --- a/src/Http/Http.Results/test/CreatedResultTests.cs +++ b/src/Http/Http.Results/test/CreatedResultTests.cs @@ -76,7 +76,7 @@ public void PopulateMetadata_AddsResponseTypeMetadata() Assert.Equal(StatusCodes.Status201Created, producesResponseTypeMetadata.StatusCode); Assert.Equal(typeof(void), producesResponseTypeMetadata.Type); - Assert.Contains(builder.Metadata, m => m is IApiEndpointMetadata); + Assert.Contains(builder.Metadata, m => m is IDisableCookieRedirectMetadata); } [Fact] diff --git a/src/Http/Http.Results/test/InternalServerErrorOfTResultTests.cs b/src/Http/Http.Results/test/InternalServerErrorOfTResultTests.cs index 1ee5bd751053..900cfa3e3a36 100644 --- a/src/Http/Http.Results/test/InternalServerErrorOfTResultTests.cs +++ b/src/Http/Http.Results/test/InternalServerErrorOfTResultTests.cs @@ -119,7 +119,7 @@ public void PopulateMetadata_AddsResponseTypeMetadata() Assert.Equal(typeof(Todo), producesResponseTypeMetadata.Type); Assert.Single(producesResponseTypeMetadata.ContentTypes, "application/json"); - Assert.Contains(builder.Metadata, m => m is IApiEndpointMetadata); + Assert.Contains(builder.Metadata, m => m is IDisableCookieRedirectMetadata); } [Fact] diff --git a/src/Http/Http.Results/test/JsonResultTests.cs b/src/Http/Http.Results/test/JsonResultTests.cs index 52d76c0a03c9..6e2b05dbcc19 100644 --- a/src/Http/Http.Results/test/JsonResultTests.cs +++ b/src/Http/Http.Results/test/JsonResultTests.cs @@ -327,7 +327,7 @@ public void PopulateMetadata_AddsNonBrowserEndpointMetadata() PopulateMetadata>(((Delegate)MyApi).GetMethodInfo(), builder); // Assert - Assert.Contains(builder.Metadata, m => m is IApiEndpointMetadata); + Assert.Contains(builder.Metadata, m => m is IDisableCookieRedirectMetadata); } private static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) diff --git a/src/Http/Http.Results/test/NoContentResultTests.cs b/src/Http/Http.Results/test/NoContentResultTests.cs index fc1cae4a95aa..ce6047dd25d7 100644 --- a/src/Http/Http.Results/test/NoContentResultTests.cs +++ b/src/Http/Http.Results/test/NoContentResultTests.cs @@ -56,7 +56,7 @@ public void PopulateMetadata_AddsResponseTypeMetadata() Assert.Equal(typeof(void), producesResponseTypeMetadata.Type); // Assert ApiEndpointMetadata is added - Assert.Contains(builder.Metadata, m => m is IApiEndpointMetadata); + Assert.Contains(builder.Metadata, m => m is IDisableCookieRedirectMetadata); } [Fact] diff --git a/src/Http/Http.Results/test/NotFoundOfTResultTests.cs b/src/Http/Http.Results/test/NotFoundOfTResultTests.cs index ec15a5c8a7e8..4ee3c6a6eed7 100644 --- a/src/Http/Http.Results/test/NotFoundOfTResultTests.cs +++ b/src/Http/Http.Results/test/NotFoundOfTResultTests.cs @@ -79,7 +79,7 @@ public void PopulateMetadata_AddsResponseTypeMetadata() Assert.Equal(typeof(Todo), producesResponseTypeMetadata.Type); Assert.Single(producesResponseTypeMetadata.ContentTypes, "application/json"); - Assert.Contains(builder.Metadata, m => m is IApiEndpointMetadata); + Assert.Contains(builder.Metadata, m => m is IDisableCookieRedirectMetadata); } [Fact] diff --git a/src/Http/Http.Results/test/OkOfTResultTests.cs b/src/Http/Http.Results/test/OkOfTResultTests.cs index 85693056570b..0fbd02fa3cd8 100644 --- a/src/Http/Http.Results/test/OkOfTResultTests.cs +++ b/src/Http/Http.Results/test/OkOfTResultTests.cs @@ -96,7 +96,7 @@ public void PopulateMetadata_AddsResponseTypeMetadata() Assert.Equal(typeof(Todo), producesResponseTypeMetadata.Type); Assert.Single(producesResponseTypeMetadata.ContentTypes, "application/json"); - Assert.Contains(builder.Metadata, m => m is IApiEndpointMetadata); + Assert.Contains(builder.Metadata, m => m is IDisableCookieRedirectMetadata); } [Fact] diff --git a/src/Http/Http.Results/test/OkResultTests.cs b/src/Http/Http.Results/test/OkResultTests.cs index 98cac07cbb60..26a777a7c5e1 100644 --- a/src/Http/Http.Results/test/OkResultTests.cs +++ b/src/Http/Http.Results/test/OkResultTests.cs @@ -57,7 +57,7 @@ public void PopulateMetadata_AddsResponseTypeMetadata() Assert.Equal(StatusCodes.Status200OK, producesResponseTypeMetadata.StatusCode); Assert.Equal(typeof(void), producesResponseTypeMetadata.Type); - Assert.Contains(builder.Metadata, m => m is IApiEndpointMetadata); + Assert.Contains(builder.Metadata, m => m is IDisableCookieRedirectMetadata); } [Fact] diff --git a/src/Http/Http.Results/test/UnprocessableEntityOfTResultTests.cs b/src/Http/Http.Results/test/UnprocessableEntityOfTResultTests.cs index 8c8161f9955c..16a0dfa3e14e 100644 --- a/src/Http/Http.Results/test/UnprocessableEntityOfTResultTests.cs +++ b/src/Http/Http.Results/test/UnprocessableEntityOfTResultTests.cs @@ -96,7 +96,7 @@ public void PopulateMetadata_AddsResponseTypeMetadata() Assert.Equal(typeof(Todo), producesResponseTypeMetadata.Type); Assert.Single(producesResponseTypeMetadata.ContentTypes, "application/json"); - Assert.Contains(builder.Metadata, m => m is IApiEndpointMetadata); + Assert.Contains(builder.Metadata, m => m is IDisableCookieRedirectMetadata); } [Fact] diff --git a/src/Http/Http.Results/test/UnprocessableEntityResultTests.cs b/src/Http/Http.Results/test/UnprocessableEntityResultTests.cs index 7931b226c065..ea6eba2d668d 100644 --- a/src/Http/Http.Results/test/UnprocessableEntityResultTests.cs +++ b/src/Http/Http.Results/test/UnprocessableEntityResultTests.cs @@ -58,7 +58,7 @@ public void PopulateMetadata_AddsResponseTypeMetadata() Assert.Equal(StatusCodes.Status422UnprocessableEntity, producesResponseTypeMetadata.StatusCode); Assert.Equal(typeof(void), producesResponseTypeMetadata.Type); - Assert.Contains(builder.Metadata, m => m is IApiEndpointMetadata); + Assert.Contains(builder.Metadata, m => m is IDisableCookieRedirectMetadata); } [Fact] diff --git a/src/Http/Http/perf/Microbenchmarks/ValidatableTypesBenchmark.cs b/src/Http/Http/perf/Microbenchmarks/ValidatableTypesBenchmark.cs index 6af879569e80..1d182e89c823 100644 --- a/src/Http/Http/perf/Microbenchmarks/ValidatableTypesBenchmark.cs +++ b/src/Http/Http/perf/Microbenchmarks/ValidatableTypesBenchmark.cs @@ -244,6 +244,7 @@ public IEnumerable Validate(ValidationContext validationContex private class MockValidatableTypeInfo(Type type, ValidatablePropertyInfo[] members) : ValidatableTypeInfo(type, members) { + protected override ValidationAttribute[] GetValidationAttributes() => []; } private class MockValidatablePropertyInfo( diff --git a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj index 61036f37c6d3..fc7303fe15dd 100644 --- a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj +++ b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj @@ -38,6 +38,7 @@ + diff --git a/src/Http/Routing/src/RoutingMetrics.cs b/src/Http/Routing/src/RoutingMetrics.cs index 7b1388b235bf..9024f41c850e 100644 --- a/src/Http/Routing/src/RoutingMetrics.cs +++ b/src/Http/Routing/src/RoutingMetrics.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.Metrics; +using Microsoft.AspNetCore.Shared; namespace Microsoft.AspNetCore.Routing; @@ -31,7 +32,7 @@ public RoutingMetrics(IMeterFactory meterFactory) public void MatchSuccess(string route, bool isFallback) { _matchAttemptsCounter.Add(1, - new KeyValuePair("http.route", route), + new KeyValuePair("http.route", RouteDiagnosticsHelpers.ResolveHttpRoute(route)), new KeyValuePair("aspnetcore.routing.match_status", "success"), new KeyValuePair("aspnetcore.routing.is_fallback", isFallback ? BoxedTrue : BoxedFalse)); } diff --git a/src/Http/Routing/test/UnitTests/RoutingMetricsTests.cs b/src/Http/Routing/test/UnitTests/RoutingMetricsTests.cs index fb3ac0b30fc2..778f114404d4 100644 --- a/src/Http/Routing/test/UnitTests/RoutingMetricsTests.cs +++ b/src/Http/Routing/test/UnitTests/RoutingMetricsTests.cs @@ -48,6 +48,34 @@ public async Task Match_Success() m => AssertSuccess(m, "/{hi}", fallback: false)); } + [Fact] + public async Task Match_EmptyRoute_ResolveForwardSlash() + { + // Arrange + var routeEndpointBuilder = new RouteEndpointBuilder(c => Task.CompletedTask, RoutePatternFactory.Parse(string.Empty), order: 0); + var meterFactory = new TestMeterFactory(); + var middleware = CreateMiddleware( + matcherFactory: new TestMatcherFactory(true, c => + { + c.SetEndpoint(routeEndpointBuilder.Build()); + }), + meterFactory: meterFactory); + var httpContext = CreateHttpContext(); + var meter = meterFactory.Meters.Single(); + + using var routingMatchAttemptsCollector = new MetricCollector(meterFactory, RoutingMetrics.MeterName, "aspnetcore.routing.match_attempts"); + + // Act + await middleware.Invoke(httpContext); + + // Assert + Assert.Equal(RoutingMetrics.MeterName, meter.Name); + Assert.Null(meter.Version); + + Assert.Collection(routingMatchAttemptsCollector.GetMeasurementSnapshot(), + m => AssertSuccess(m, "/", fallback: false)); + } + [Theory] [InlineData(true)] [InlineData(false)] diff --git a/src/Mvc/Mvc.Core/src/ApiControllerAttribute.cs b/src/Mvc/Mvc.Core/src/ApiControllerAttribute.cs index f7179b0b447d..cde73404a399 100644 --- a/src/Mvc/Mvc.Core/src/ApiControllerAttribute.cs +++ b/src/Mvc/Mvc.Core/src/ApiControllerAttribute.cs @@ -18,6 +18,6 @@ namespace Microsoft.AspNetCore.Mvc; /// /// [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class, AllowMultiple = false, Inherited = true)] -public class ApiControllerAttribute : ControllerAttribute, IApiBehaviorMetadata, IApiEndpointMetadata +public class ApiControllerAttribute : ControllerAttribute, IApiBehaviorMetadata, IDisableCookieRedirectMetadata { } diff --git a/src/Mvc/Mvc.Core/test/ApplicationModels/DefaultApplicationModelProviderTest.cs b/src/Mvc/Mvc.Core/test/ApplicationModels/DefaultApplicationModelProviderTest.cs index c8c6368fe7e8..082b7e498b42 100644 --- a/src/Mvc/Mvc.Core/test/ApplicationModels/DefaultApplicationModelProviderTest.cs +++ b/src/Mvc/Mvc.Core/test/ApplicationModels/DefaultApplicationModelProviderTest.cs @@ -1280,7 +1280,7 @@ public void AddReturnTypeMetadata_ExtractsMetadataFromReturnType() Assert.NotNull(selector.EndpointMetadata); Assert.Equal(2, selector.EndpointMetadata.Count); Assert.Single(selector.EndpointMetadata.OfType()); - Assert.Single(selector.EndpointMetadata.OfType()); + Assert.Single(selector.EndpointMetadata.OfType()); Assert.Equal(200, ((ProducesResponseTypeMetadata)selector.EndpointMetadata[0]).StatusCode); } diff --git a/src/Mvc/test/Mvc.FunctionalTests/WebApplicationFactorySlnxTests.cs b/src/Mvc/test/Mvc.FunctionalTests/WebApplicationFactorySlnxTests.cs new file mode 100644 index 000000000000..60de72aa2e55 --- /dev/null +++ b/src/Mvc/test/Mvc.FunctionalTests/WebApplicationFactorySlnxTests.cs @@ -0,0 +1,74 @@ +// 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.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using BasicWebSite; + +namespace Microsoft.AspNetCore.Mvc.FunctionalTests; + +public class WebApplicationFactorySlnxTests : IClassFixture>, IDisposable +{ + private readonly string _tempDirectory; + private readonly string _contentDirectory; + + public WebApplicationFactorySlnxTests(WebApplicationFactory factory) + { + Factory = factory; + _tempDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")[..8]); + _contentDirectory = Path.Combine(_tempDirectory, "BasicWebSite"); + + Directory.CreateDirectory(_tempDirectory); + Directory.CreateDirectory(_contentDirectory); + + // Create a minimal wwwroot directory to satisfy content root expectations + var wwwrootDir = Path.Combine(_contentDirectory, "wwwroot"); + Directory.CreateDirectory(wwwrootDir); + } + + public WebApplicationFactory Factory { get; } + + [Fact] + public async Task WebApplicationFactory_UsesSlnxForSolutionRelativeContentRoot() + { + // Create .slnx file in temp directory + var slnxFile = Path.Combine(_tempDirectory, "TestSolution.slnx"); + File.WriteAllText(slnxFile, """ + + + + + + + + + + """); + + var factory = Factory.WithWebHostBuilder(builder => + { + builder.UseSolutionRelativeContentRoot("BasicWebSite", _tempDirectory, "TestSolution.slnx"); + }); + + using var client = factory.CreateClient(); + + // Verify that the content root was set correctly by accessing the environment + var environment = factory.Services.GetRequiredService(); + Assert.Equal(_contentDirectory, environment.ContentRootPath); + Assert.True(Directory.Exists(environment.ContentRootPath)); + + // Verify the factory is functional with the .slnx-resolved content root + var response = await client.GetAsync("/"); + Assert.True(response.IsSuccessStatusCode); + } + + public void Dispose() + { + if (Directory.Exists(_tempDirectory)) + { + Directory.Delete(_tempDirectory, recursive: true); + } + } +} diff --git a/src/OpenApi/sample/Endpoints/MapSchemasEndpoints.cs b/src/OpenApi/sample/Endpoints/MapSchemasEndpoints.cs index 23591e8efb18..958a2fd9c47c 100644 --- a/src/OpenApi/sample/Endpoints/MapSchemasEndpoints.cs +++ b/src/OpenApi/sample/Endpoints/MapSchemasEndpoints.cs @@ -46,6 +46,32 @@ public static IEndpointRouteBuilder MapSchemasEndpoints(this IEndpointRouteBuild schemas.MapPost("/config-with-generic-lists", (Config config) => Results.Ok(config)); schemas.MapPost("/project-response", (ProjectResponse project) => Results.Ok(project)); schemas.MapPost("/subscription", (Subscription subscription) => Results.Ok(subscription)); + + // Tests for oneOf nullable behavior on responses and request bodies + schemas.MapGet("/nullable-response", () => TypedResults.Ok(new NullableResponseModel + { + RequiredProperty = "required", + NullableProperty = null, + NullableComplexProperty = null + })); + schemas.MapGet("/nullable-return-type", NullableResponseModel? () => new NullableResponseModel + { + RequiredProperty = "required", + NullableProperty = null, + NullableComplexProperty = null + }); + schemas.MapPost("/nullable-request", (NullableRequestModel? request) => Results.Ok(request)); + schemas.MapPost("/complex-nullable-hierarchy", (ComplexHierarchyModel model) => Results.Ok(model)); + + // Additional edge cases for nullable testing + schemas.MapPost("/nullable-array-elements", (NullableArrayModel model) => Results.Ok(model)); + schemas.MapGet("/optional-with-default", () => TypedResults.Ok(new ModelWithDefaults())); + schemas.MapGet("/nullable-enum-response", () => TypedResults.Ok(new EnumNullableModel + { + RequiredEnum = TestEnum.Value1, + NullableEnum = null + })); + return endpointRouteBuilder; } @@ -173,4 +199,73 @@ public sealed class RefUser public string Name { get; set; } = ""; public string Email { get; set; } = ""; } + + // Models for testing oneOf nullable behavior + public sealed class NullableResponseModel + { + public required string RequiredProperty { get; set; } + public string? NullableProperty { get; set; } + public ComplexType? NullableComplexProperty { get; set; } + } + + public sealed class NullableRequestModel + { + public required string RequiredField { get; set; } + public string? OptionalField { get; set; } + public List? NullableList { get; set; } + public Dictionary? NullableDictionary { get; set; } + } + + // Complex hierarchy model for testing nested nullable properties + public sealed class ComplexHierarchyModel + { + public required string Id { get; set; } + public NestedModel? OptionalNested { get; set; } + public required NestedModel RequiredNested { get; set; } + public List? NullableListWithNullableItems { get; set; } + } + + public sealed class NestedModel + { + public required string Name { get; set; } + public int? OptionalValue { get; set; } + public ComplexType? DeepNested { get; set; } + } + + public sealed class ComplexType + { + public string? Description { get; set; } + public DateTime? Timestamp { get; set; } + } + + // Additional models for edge case testing + public sealed class NullableArrayModel + { + public string[]? NullableArray { get; set; } + public List ListWithNullableElements { get; set; } = []; + public Dictionary? NullableDictionaryWithNullableValues { get; set; } + } + + public sealed class ModelWithDefaults + { + public string PropertyWithDefault { get; set; } = "default"; + public string? NullableWithNull { get; set; } + public int NumberWithDefault { get; set; } = 42; + public bool BoolWithDefault { get; set; } = true; + } + + // Enum testing with nullable + public enum TestEnum + { + Value1, + Value2, + Value3 + } + + public sealed class EnumNullableModel + { + public required TestEnum RequiredEnum { get; set; } + public TestEnum? NullableEnum { get; set; } + public List ListOfNullableEnums { get; set; } = []; + } } diff --git a/src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs b/src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs index feb32d33b8e0..553c87643557 100644 --- a/src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs +++ b/src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs @@ -195,11 +195,6 @@ internal static void ApplyDefaultValue(this JsonNode schema, object? defaultValu /// underlying schema generator does not support this, we need to manually apply the /// supported formats to the schemas associated with the generated type. /// - /// Whereas JsonSchema represents nullable types via `type: ["string", "null"]`, OpenAPI - /// v3 exposes a nullable property on the schema. This method will set the nullable property - /// based on whether the underlying schema generator returned an array type containing "null" to - /// represent a nullable type or if the type was denoted as nullable from our lookup cache. - /// /// Note that this method targets and not because /// it is is designed to be invoked via the `OnGenerated` callback in the underlying schema generator as /// opposed to after the generated schemas have been mapped to OpenAPI schemas. @@ -349,8 +344,6 @@ internal static void ApplyParameterInfo(this JsonNode schema, ApiParameterDescri { schema.ApplyValidationAttributes(validationAttributes); } - - schema.ApplyNullabilityContextInfo(parameterInfo); } // Route constraints are only defined on parameters that are sourced from the path. Since // they are encoded in the route template, and not in the type information based to the underlying @@ -451,42 +444,49 @@ private static bool IsNonAbstractTypeWithoutDerivedTypeReference(JsonSchemaExpor } /// - /// Support applying nullability status for reference types provided as a parameter. + /// Support applying nullability status for reference types provided as a property or field. /// /// The produced by the underlying schema generator. - /// The associated with the schema. - internal static void ApplyNullabilityContextInfo(this JsonNode schema, ParameterInfo parameterInfo) + /// The associated with the schema. + internal static void ApplyNullabilityContextInfo(this JsonNode schema, JsonPropertyInfo propertyInfo) { - if (parameterInfo.ParameterType.IsValueType) + // Avoid setting explicit nullability annotations for `object` types so they continue to match on the catch + // all schema (no type, no format, no constraints). + if (propertyInfo.PropertyType != typeof(object) && (propertyInfo.IsGetNullable || propertyInfo.IsSetNullable)) { - return; + if (MapJsonNodeToSchemaType(schema[OpenApiSchemaKeywords.TypeKeyword]) is { } schemaTypes && + !schemaTypes.HasFlag(JsonSchemaType.Null)) + { + schema[OpenApiSchemaKeywords.TypeKeyword] = (schemaTypes | JsonSchemaType.Null).ToString(); + } } - - var nullabilityInfoContext = new NullabilityInfoContext(); - var nullabilityInfo = nullabilityInfoContext.Create(parameterInfo); - if (nullabilityInfo.WriteState == NullabilityState.Nullable - && MapJsonNodeToSchemaType(schema[OpenApiSchemaKeywords.TypeKeyword]) is { } schemaTypes - && !schemaTypes.HasFlag(JsonSchemaType.Null)) + if (schema[OpenApiConstants.SchemaId] is not null && + propertyInfo.PropertyType != typeof(object) && propertyInfo.ShouldApplyNullablePropertySchema()) { - schema[OpenApiSchemaKeywords.TypeKeyword] = (schemaTypes | JsonSchemaType.Null).ToString(); + schema[OpenApiConstants.NullableProperty] = true; } } /// - /// Support applying nullability status for reference types provided as a property or field. + /// Prunes the "null" type from the schema for types that are componentized. These + /// types should represent their nullability using oneOf with null instead. /// /// The produced by the underlying schema generator. - /// The associated with the schema. - internal static void ApplyNullabilityContextInfo(this JsonNode schema, JsonPropertyInfo propertyInfo) + internal static void PruneNullTypeForComponentizedTypes(this JsonNode schema) { - // Avoid setting explicit nullability annotations for `object` types so they continue to match on the catch - // all schema (no type, no format, no constraints). - if (propertyInfo.PropertyType != typeof(object) && (propertyInfo.IsGetNullable || propertyInfo.IsSetNullable)) + if (schema[OpenApiConstants.SchemaId] is not null && + schema[OpenApiSchemaKeywords.TypeKeyword] is JsonArray typeArray) { - if (MapJsonNodeToSchemaType(schema[OpenApiSchemaKeywords.TypeKeyword]) is { } schemaTypes && - !schemaTypes.HasFlag(JsonSchemaType.Null)) + for (var i = typeArray.Count - 1; i >= 0; i--) { - schema[OpenApiSchemaKeywords.TypeKeyword] = (schemaTypes | JsonSchemaType.Null).ToString(); + if (typeArray[i]?.GetValue() == "null") + { + typeArray.RemoveAt(i); + } + } + if (typeArray.Count == 1) + { + schema[OpenApiSchemaKeywords.TypeKeyword] = typeArray[0]?.GetValue(); } } } diff --git a/src/OpenApi/src/Extensions/JsonTypeInfoExtensions.cs b/src/OpenApi/src/Extensions/JsonTypeInfoExtensions.cs index be025a61b529..a97a2226d5cf 100644 --- a/src/OpenApi/src/Extensions/JsonTypeInfoExtensions.cs +++ b/src/OpenApi/src/Extensions/JsonTypeInfoExtensions.cs @@ -108,6 +108,12 @@ internal static string GetSchemaReferenceId(this Type type, JsonSerializerOption return $"{typeName}Of{propertyNames}"; } + // Special handling for nullable value types + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + return type.GetGenericArguments()[0].GetSchemaReferenceId(options); + } + // Special handling for generic types that are collections // Generic types become a concatenation of the generic type name and the type arguments if (type.IsGenericType) diff --git a/src/OpenApi/src/Extensions/OpenApiSchemaExtensions.cs b/src/OpenApi/src/Extensions/OpenApiSchemaExtensions.cs new file mode 100644 index 000000000000..f394445850fe --- /dev/null +++ b/src/OpenApi/src/Extensions/OpenApiSchemaExtensions.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. + +namespace Microsoft.AspNetCore.OpenApi; + +internal static class OpenApiSchemaExtensions +{ + private static readonly OpenApiSchema _nullSchema = new() { Type = JsonSchemaType.Null }; + + public static IOpenApiSchema CreateOneOfNullableWrapper(this IOpenApiSchema originalSchema) + { + return new OpenApiSchema + { + OneOf = + [ + _nullSchema, + originalSchema + ] + }; + } +} diff --git a/src/OpenApi/src/Extensions/TypeExtensions.cs b/src/OpenApi/src/Extensions/TypeExtensions.cs index e2ba5f500c63..0636347bf5bc 100644 --- a/src/OpenApi/src/Extensions/TypeExtensions.cs +++ b/src/OpenApi/src/Extensions/TypeExtensions.cs @@ -1,6 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Linq; +using System.Reflection; +using System.Text.Json.Serialization.Metadata; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; + namespace Microsoft.AspNetCore.OpenApi; internal static class TypeExtensions @@ -30,4 +37,73 @@ public static bool IsJsonPatchDocument(this Type type) return false; } + + public static bool ShouldApplyNullableResponseSchema(this ApiResponseType apiResponseType, ApiDescription apiDescription) + { + // Get the MethodInfo from the ActionDescriptor + var responseType = apiResponseType.Type; + var methodInfo = apiDescription.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor + ? controllerActionDescriptor.MethodInfo + : apiDescription.ActionDescriptor.EndpointMetadata.OfType().SingleOrDefault(); + + if (methodInfo is null) + { + return false; + } + + var returnType = methodInfo.ReturnType; + if (returnType.IsGenericType && + (returnType.GetGenericTypeDefinition() == typeof(Task<>) || returnType.GetGenericTypeDefinition() == typeof(ValueTask<>))) + { + returnType = returnType.GetGenericArguments()[0]; + } + if (returnType != responseType) + { + return false; + } + + if (returnType.IsValueType) + { + return apiResponseType.ModelMetadata?.IsNullableValueType ?? false; + } + + var nullabilityInfoContext = new NullabilityInfoContext(); + var nullabilityInfo = nullabilityInfoContext.Create(methodInfo.ReturnParameter); + return nullabilityInfo.WriteState == NullabilityState.Nullable; + } + + public static bool ShouldApplyNullableRequestSchema(this ApiParameterDescription apiParameterDescription) + { + var parameterType = apiParameterDescription.Type; + if (parameterType is null) + { + return false; + } + + if (apiParameterDescription.ParameterDescriptor is not IParameterInfoParameterDescriptor { ParameterInfo: { } parameterInfo }) + { + return false; + } + + if (parameterType.IsValueType) + { + return apiParameterDescription.ModelMetadata?.IsNullableValueType ?? false; + } + + var nullabilityInfoContext = new NullabilityInfoContext(); + var nullabilityInfo = nullabilityInfoContext.Create(parameterInfo); + return nullabilityInfo.WriteState == NullabilityState.Nullable; + } + + public static bool ShouldApplyNullablePropertySchema(this JsonPropertyInfo jsonPropertyInfo) + { + if (jsonPropertyInfo.AttributeProvider is not PropertyInfo propertyInfo) + { + return false; + } + + var nullabilityInfoContext = new NullabilityInfoContext(); + var nullabilityInfo = nullabilityInfoContext.Create(propertyInfo); + return nullabilityInfo.WriteState == NullabilityState.Nullable; + } } diff --git a/src/OpenApi/src/Schemas/OpenApiJsonSchema.Helpers.cs b/src/OpenApi/src/Schemas/OpenApiJsonSchema.Helpers.cs index 28b8de3eaa89..877ac70010db 100644 --- a/src/OpenApi/src/Schemas/OpenApiJsonSchema.Helpers.cs +++ b/src/OpenApi/src/Schemas/OpenApiJsonSchema.Helpers.cs @@ -333,6 +333,11 @@ public static void ReadProperty(ref Utf8JsonReader reader, string propertyName, schema.Metadata ??= new Dictionary(); schema.Metadata.Add(OpenApiConstants.SchemaId, reader.GetString() ?? string.Empty); break; + case OpenApiConstants.NullableProperty: + reader.Read(); + schema.Metadata ??= new Dictionary(); + schema.Metadata.Add(OpenApiConstants.NullableProperty, reader.GetBoolean()); + break; // OpenAPI does not support the `const` keyword in its schema implementation, so // we map it to its closest approximation, an enum with a single value, here. case OpenApiSchemaKeywords.ConstKeyword: diff --git a/src/OpenApi/src/Services/OpenApiConstants.cs b/src/OpenApi/src/Services/OpenApiConstants.cs index 0bb51a9bbd32..df4228633556 100644 --- a/src/OpenApi/src/Services/OpenApiConstants.cs +++ b/src/OpenApi/src/Services/OpenApiConstants.cs @@ -17,6 +17,7 @@ internal static class OpenApiConstants internal const string RefExampleAnnotation = "x-ref-example"; internal const string RefKeyword = "$ref"; internal const string RefPrefix = "#"; + internal const string NullableProperty = "x-is-nullable-property"; internal const string DefaultOpenApiResponseKey = "default"; // Since there's a finite set of HTTP methods that can be included in a given // OpenApiPaths, we can pre-allocate an array of these methods and use a direct diff --git a/src/OpenApi/src/Services/OpenApiDocumentService.cs b/src/OpenApi/src/Services/OpenApiDocumentService.cs index 5308dc3792a1..f342f5b7943b 100644 --- a/src/OpenApi/src/Services/OpenApiDocumentService.cs +++ b/src/OpenApi/src/Services/OpenApiDocumentService.cs @@ -423,8 +423,15 @@ private async Task GetResponseAsync( .Select(responseFormat => responseFormat.MediaType); foreach (var contentType in apiResponseFormatContentTypes) { - var schema = apiResponseType.Type is { } type ? await _componentService.GetOrCreateSchemaAsync(document, type, scopedServiceProvider, schemaTransformers, null, cancellationToken) : new OpenApiSchema(); - response.Content[contentType] = new OpenApiMediaType { Schema = schema }; + IOpenApiSchema? schema = null; + if (apiResponseType.Type is { } responseType) + { + schema = await _componentService.GetOrCreateSchemaAsync(document, responseType, scopedServiceProvider, schemaTransformers, null, cancellationToken); + schema = apiResponseType.ShouldApplyNullableResponseSchema(apiDescription) + ? schema.CreateOneOfNullableWrapper() + : schema; + } + response.Content[contentType] = new OpenApiMediaType { Schema = schema ?? new OpenApiSchema() }; } // MVC's `ProducesAttribute` doesn't implement the produces metadata that the ApiExplorer @@ -744,7 +751,11 @@ private async Task GetJsonRequestBody( foreach (var requestFormat in supportedRequestFormats) { var contentType = requestFormat.MediaType; - requestBody.Content[contentType] = new OpenApiMediaType { Schema = await _componentService.GetOrCreateSchemaAsync(document, bodyParameter.Type, scopedServiceProvider, schemaTransformers, bodyParameter, cancellationToken: cancellationToken) }; + var schema = await _componentService.GetOrCreateSchemaAsync(document, bodyParameter.Type, scopedServiceProvider, schemaTransformers, bodyParameter, cancellationToken: cancellationToken); + schema = bodyParameter.ShouldApplyNullableRequestSchema() + ? schema.CreateOneOfNullableWrapper() + : schema; + requestBody.Content[contentType] = new OpenApiMediaType { Schema = schema }; } return requestBody; diff --git a/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs b/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs index d35ab8bb449e..12b0c1ed996c 100644 --- a/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs +++ b/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs @@ -132,7 +132,7 @@ internal sealed class OpenApiSchemaService( } } } - + schema.PruneNullTypeForComponentizedTypes(); return schema; } }; @@ -281,7 +281,17 @@ internal static IOpenApiSchema ResolveReferenceForSchema(OpenApiDocument documen { foreach (var property in schema.Properties) { - schema.Properties[property.Key] = ResolveReferenceForSchema(document, property.Value, rootSchemaId); + var resolvedProperty = ResolveReferenceForSchema(document, property.Value, rootSchemaId); + if (property.Value is OpenApiSchema targetSchema && + targetSchema.Metadata?.TryGetValue(OpenApiConstants.NullableProperty, out var isNullableProperty) == true && + isNullableProperty is true) + { + schema.Properties[property.Key] = resolvedProperty.CreateOneOfNullableWrapper(); + } + else + { + schema.Properties[property.Key] = resolvedProperty; + } } } diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_0/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=schemas-by-ref.verified.txt b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_0/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=schemas-by-ref.verified.txt index 86d6b730e372..648510972ee6 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_0/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=schemas-by-ref.verified.txt +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_0/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=schemas-by-ref.verified.txt @@ -636,6 +636,161 @@ } } } + }, + "/schemas-by-ref/nullable-response": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NullableResponseModel" + } + } + } + } + } + } + }, + "/schemas-by-ref/nullable-return-type": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "nullable": true + }, + { + "$ref": "#/components/schemas/NullableResponseModel" + } + ] + } + } + } + } + } + } + }, + "/schemas-by-ref/nullable-request": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "nullable": true + }, + { + "$ref": "#/components/schemas/NullableRequestModel" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/complex-nullable-hierarchy": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ComplexHierarchyModel" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/nullable-array-elements": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NullableArrayModel" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/optional-with-default": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelWithDefaults" + } + } + } + } + } + } + }, + "/schemas-by-ref/nullable-enum-response": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EnumNullableModel" + } + } + } + } + } + } } }, "components": { @@ -707,6 +862,52 @@ } } }, + "ComplexHierarchyModel": { + "required": [ + "id", + "requiredNested" + ], + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "optionalNested": { + "oneOf": [ + { + "nullable": true + }, + { + "$ref": "#/components/schemas/NestedModel" + } + ] + }, + "requiredNested": { + "$ref": "#/components/schemas/NestedModel" + }, + "nullableListWithNullableItems": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NestedModel" + }, + "nullable": true + } + } + }, + "ComplexType": { + "type": "object", + "properties": { + "description": { + "type": "string", + "nullable": true + }, + "timestamp": { + "type": "string", + "format": "date-time", + "nullable": true + } + } + }, "Config": { "type": "object", "properties": { @@ -787,6 +988,33 @@ } } }, + "EnumNullableModel": { + "required": [ + "requiredEnum" + ], + "type": "object", + "properties": { + "requiredEnum": { + "$ref": "#/components/schemas/TestEnum" + }, + "nullableEnum": { + "oneOf": [ + { + "nullable": true + }, + { + "$ref": "#/components/schemas/TestEnum" + } + ] + }, + "listOfNullableEnums": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TestEnum" + } + } + } + }, "Item": { "type": "object", "properties": { @@ -898,6 +1126,130 @@ } } }, + "ModelWithDefaults": { + "type": "object", + "properties": { + "propertyWithDefault": { + "type": "string" + }, + "nullableWithNull": { + "type": "string", + "nullable": true + }, + "numberWithDefault": { + "type": "integer", + "format": "int32" + }, + "boolWithDefault": { + "type": "boolean" + } + } + }, + "NestedModel": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "optionalValue": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "deepNested": { + "oneOf": [ + { + "nullable": true + }, + { + "$ref": "#/components/schemas/ComplexType" + } + ] + } + } + }, + "NullableArrayModel": { + "type": "object", + "properties": { + "nullableArray": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "listWithNullableElements": { + "type": "array", + "items": { + "type": "string" + } + }, + "nullableDictionaryWithNullableValues": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "nullable": true + } + } + }, + "NullableRequestModel": { + "required": [ + "requiredField" + ], + "type": "object", + "properties": { + "requiredField": { + "type": "string" + }, + "optionalField": { + "type": "string", + "nullable": true + }, + "nullableList": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "nullableDictionary": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "nullable": true + } + } + }, + "NullableResponseModel": { + "required": [ + "requiredProperty" + ], + "type": "object", + "properties": { + "requiredProperty": { + "type": "string" + }, + "nullableProperty": { + "type": "string", + "nullable": true + }, + "nullableComplexProperty": { + "oneOf": [ + { + "nullable": true + }, + { + "$ref": "#/components/schemas/ComplexType" + } + ] + } + } + }, "ParentObject": { "type": "object", "properties": { @@ -1022,8 +1374,7 @@ "user": { "$ref": "#/components/schemas/RefUser" } - }, - "nullable": true + } }, "RefUser": { "type": "object", @@ -1124,7 +1475,14 @@ "$ref": "#/components/schemas/RefProfile" }, "secondaryUser": { - "$ref": "#/components/schemas/RefProfile" + "oneOf": [ + { + "nullable": true + }, + { + "$ref": "#/components/schemas/RefProfile" + } + ] } } }, @@ -1139,6 +1497,9 @@ } } }, + "TestEnum": { + "type": "integer" + }, "Triangle": { "type": "object", "properties": { diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_1/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=schemas-by-ref.verified.txt b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_1/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=schemas-by-ref.verified.txt index 00e055a69541..61d0527bc80f 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_1/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=schemas-by-ref.verified.txt +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_1/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=schemas-by-ref.verified.txt @@ -636,6 +636,161 @@ } } } + }, + "/schemas-by-ref/nullable-response": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NullableResponseModel" + } + } + } + } + } + } + }, + "/schemas-by-ref/nullable-return-type": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/NullableResponseModel" + } + ] + } + } + } + } + } + } + }, + "/schemas-by-ref/nullable-request": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/NullableRequestModel" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/complex-nullable-hierarchy": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ComplexHierarchyModel" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/nullable-array-elements": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NullableArrayModel" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/optional-with-default": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelWithDefaults" + } + } + } + } + } + } + }, + "/schemas-by-ref/nullable-enum-response": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EnumNullableModel" + } + } + } + } + } + } } }, "components": { @@ -707,6 +862,58 @@ } } }, + "ComplexHierarchyModel": { + "required": [ + "id", + "requiredNested" + ], + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "optionalNested": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/NestedModel" + } + ] + }, + "requiredNested": { + "$ref": "#/components/schemas/NestedModel" + }, + "nullableListWithNullableItems": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/NestedModel" + } + } + } + }, + "ComplexType": { + "type": "object", + "properties": { + "description": { + "type": [ + "null", + "string" + ] + }, + "timestamp": { + "type": [ + "null", + "string" + ], + "format": "date-time" + } + } + }, "Config": { "type": "object", "properties": { @@ -797,6 +1004,33 @@ } } }, + "EnumNullableModel": { + "required": [ + "requiredEnum" + ], + "type": "object", + "properties": { + "requiredEnum": { + "$ref": "#/components/schemas/TestEnum" + }, + "nullableEnum": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/TestEnum" + } + ] + }, + "listOfNullableEnums": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TestEnum" + } + } + } + }, "Item": { "type": "object", "properties": { @@ -908,6 +1142,146 @@ } } }, + "ModelWithDefaults": { + "type": "object", + "properties": { + "propertyWithDefault": { + "type": "string" + }, + "nullableWithNull": { + "type": [ + "null", + "string" + ] + }, + "numberWithDefault": { + "type": "integer", + "format": "int32" + }, + "boolWithDefault": { + "type": "boolean" + } + } + }, + "NestedModel": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "optionalValue": { + "type": [ + "null", + "integer" + ], + "format": "int32" + }, + "deepNested": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ComplexType" + } + ] + } + } + }, + "NullableArrayModel": { + "type": "object", + "properties": { + "nullableArray": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + } + }, + "listWithNullableElements": { + "type": "array", + "items": { + "type": "string" + } + }, + "nullableDictionaryWithNullableValues": { + "type": [ + "null", + "object" + ], + "additionalProperties": { + "type": "string" + } + } + } + }, + "NullableRequestModel": { + "required": [ + "requiredField" + ], + "type": "object", + "properties": { + "requiredField": { + "type": "string" + }, + "optionalField": { + "type": [ + "null", + "string" + ] + }, + "nullableList": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + } + }, + "nullableDictionary": { + "type": [ + "null", + "object" + ], + "additionalProperties": { + "type": "string" + } + } + } + }, + "NullableResponseModel": { + "required": [ + "requiredProperty" + ], + "type": "object", + "properties": { + "requiredProperty": { + "type": "string" + }, + "nullableProperty": { + "type": [ + "null", + "string" + ] + }, + "nullableComplexProperty": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ComplexType" + } + ] + } + } + }, "ParentObject": { "type": "object", "properties": { @@ -1027,10 +1401,7 @@ "required": [ "user" ], - "type": [ - "null", - "object" - ], + "type": "object", "properties": { "user": { "$ref": "#/components/schemas/RefUser" @@ -1136,7 +1507,14 @@ "$ref": "#/components/schemas/RefProfile" }, "secondaryUser": { - "$ref": "#/components/schemas/RefProfile" + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/RefProfile" + } + ] } } }, @@ -1151,6 +1529,9 @@ } } }, + "TestEnum": { + "type": "integer" + }, "Triangle": { "type": "object", "properties": { diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentLocalizationTests.VerifyOpenApiDocumentIsInvariant.verified.txt b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentLocalizationTests.VerifyOpenApiDocumentIsInvariant.verified.txt index d70cdd320341..eec2cfe16702 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentLocalizationTests.VerifyOpenApiDocumentIsInvariant.verified.txt +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentLocalizationTests.VerifyOpenApiDocumentIsInvariant.verified.txt @@ -1162,6 +1162,161 @@ } } }, + "/schemas-by-ref/nullable-response": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NullableResponseModel" + } + } + } + } + } + } + }, + "/schemas-by-ref/nullable-return-type": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/NullableResponseModel" + } + ] + } + } + } + } + } + } + }, + "/schemas-by-ref/nullable-request": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/NullableRequestModel" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/complex-nullable-hierarchy": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ComplexHierarchyModel" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/nullable-array-elements": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NullableArrayModel" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/optional-with-default": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelWithDefaults" + } + } + } + } + } + } + }, + "/schemas-by-ref/nullable-enum-response": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EnumNullableModel" + } + } + } + } + } + } + }, "/responses/200-add-xml": { "get": { "tags": [ @@ -1445,6 +1600,58 @@ } } }, + "ComplexHierarchyModel": { + "required": [ + "id", + "requiredNested" + ], + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "optionalNested": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/NestedModel" + } + ] + }, + "requiredNested": { + "$ref": "#/components/schemas/NestedModel" + }, + "nullableListWithNullableItems": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/NestedModel" + } + } + } + }, + "ComplexType": { + "type": "object", + "properties": { + "description": { + "type": [ + "null", + "string" + ] + }, + "timestamp": { + "type": [ + "null", + "string" + ], + "format": "date-time" + } + } + }, "Config": { "type": "object", "properties": { @@ -1547,6 +1754,33 @@ } } }, + "EnumNullableModel": { + "required": [ + "requiredEnum" + ], + "type": "object", + "properties": { + "requiredEnum": { + "$ref": "#/components/schemas/TestEnum" + }, + "nullableEnum": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/TestEnum" + } + ] + }, + "listOfNullableEnums": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TestEnum" + } + } + } + }, "IFormFile": { "type": "string", "format": "binary" @@ -1668,6 +1902,27 @@ } } }, + "ModelWithDefaults": { + "type": "object", + "properties": { + "propertyWithDefault": { + "type": "string" + }, + "nullableWithNull": { + "type": [ + "null", + "string" + ] + }, + "numberWithDefault": { + "type": "integer", + "format": "int32" + }, + "boolWithDefault": { + "type": "boolean" + } + } + }, "MvcTodo": { "required": [ "title", @@ -1687,6 +1942,125 @@ } } }, + "NestedModel": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "optionalValue": { + "type": [ + "null", + "integer" + ], + "format": "int32" + }, + "deepNested": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ComplexType" + } + ] + } + } + }, + "NullableArrayModel": { + "type": "object", + "properties": { + "nullableArray": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + } + }, + "listWithNullableElements": { + "type": "array", + "items": { + "type": "string" + } + }, + "nullableDictionaryWithNullableValues": { + "type": [ + "null", + "object" + ], + "additionalProperties": { + "type": "string" + } + } + } + }, + "NullableRequestModel": { + "required": [ + "requiredField" + ], + "type": "object", + "properties": { + "requiredField": { + "type": "string" + }, + "optionalField": { + "type": [ + "null", + "string" + ] + }, + "nullableList": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + } + }, + "nullableDictionary": { + "type": [ + "null", + "object" + ], + "additionalProperties": { + "type": "string" + } + } + } + }, + "NullableResponseModel": { + "required": [ + "requiredProperty" + ], + "type": "object", + "properties": { + "requiredProperty": { + "type": "string" + }, + "nullableProperty": { + "type": [ + "null", + "string" + ] + }, + "nullableComplexProperty": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ComplexType" + } + ] + } + } + }, "ParentObject": { "type": "object", "properties": { @@ -1840,10 +2214,7 @@ "required": [ "user" ], - "type": [ - "null", - "object" - ], + "type": "object", "properties": { "user": { "$ref": "#/components/schemas/RefUser" @@ -1949,7 +2320,14 @@ "$ref": "#/components/schemas/RefProfile" }, "secondaryUser": { - "$ref": "#/components/schemas/RefProfile" + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/RefProfile" + } + ] } } }, @@ -1964,6 +2342,9 @@ } } }, + "TestEnum": { + "type": "integer" + }, "Todo": { "required": [ "id", diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.ParameterSchemas.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.ParameterSchemas.cs index ede9a0b3d1b2..2903064bf31c 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.ParameterSchemas.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.ParameterSchemas.cs @@ -451,18 +451,25 @@ await VerifyOpenApiDocument(builder, document => }); } +#nullable enable public static object[][] ArrayBasedQueryParameters => [ [(int[] id) => { }, JsonSchemaType.Integer, false], [(int?[] id) => { }, JsonSchemaType.Integer, true], [(Guid[] id) => { }, JsonSchemaType.String, false], [(Guid?[] id) => { }, JsonSchemaType.String, true], + [(string[] id) => { }, JsonSchemaType.String, false], + // Due to runtime restrictions, we can't resolve nullability + // info for reference types as element types so this will still + // encode as non-nullable. + [(string?[] id) => { }, JsonSchemaType.String, false], [(DateTime[] id) => { }, JsonSchemaType.String, false], [(DateTime?[] id) => { }, JsonSchemaType.String, true], [(DateTimeOffset[] id) => { }, JsonSchemaType.String, false], [(DateTimeOffset?[] id) => { }, JsonSchemaType.String, true], [(Uri[] id) => { }, JsonSchemaType.String, false], ]; +#nullable restore [Theory] [MemberData(nameof(ArrayBasedQueryParameters))] @@ -890,6 +897,86 @@ public override void Write(Utf8JsonWriter writer, EnumArrayType value, JsonSeria } } + [Fact] + public async Task GetOpenApiParameters_HandlesNullableComplexTypesWithNullInType() + { + // Arrange + var builder = CreateBuilder(); + + // Act +#nullable enable + builder.MapPost("/api/nullable-todo", (Todo? todo) => { }); + builder.MapPost("/api/nullable-account", (Account? account) => { }); +#nullable restore + + // Assert + await VerifyOpenApiDocument(builder, document => + { + // Verify nullable Todo parameter uses null in type directly + var todoOperation = document.Paths["/api/nullable-todo"].Operations[HttpMethod.Post]; + var todoRequestBody = todoOperation.RequestBody; + var todoContent = Assert.Single(todoRequestBody.Content); + Assert.Equal("application/json", todoContent.Key); + var todoSchema = todoContent.Value.Schema; + + // For complex types, check if it has both null and the reference type + if (todoSchema.OneOf != null) + { + // If it now uses oneOf, verify the structure + Assert.Equal(2, todoSchema.OneOf.Count); + Assert.Collection(todoSchema.OneOf, + item => + { + Assert.NotNull(item); + Assert.Equal(JsonSchemaType.Null, item.Type); + }, + item => + { + Assert.NotNull(item); + Assert.Equal("Todo", ((OpenApiSchemaReference)item).Reference.Id); + }); + } + else + { + // If it uses direct type, verify null is included + Assert.True(todoSchema.Type?.HasFlag(JsonSchemaType.Null)); + } + + // Verify nullable Account parameter + var accountOperation = document.Paths["/api/nullable-account"].Operations[HttpMethod.Post]; + var accountRequestBody = accountOperation.RequestBody; + var accountContent = Assert.Single(accountRequestBody.Content); + Assert.Equal("application/json", accountContent.Key); + var accountSchema = accountContent.Value.Schema; + + if (accountSchema.OneOf != null) + { + // If it now uses oneOf, verify the structure + Assert.Equal(2, accountSchema.OneOf.Count); + Assert.Collection(accountSchema.OneOf, + item => + { + Assert.NotNull(item); + Assert.Equal(JsonSchemaType.Null, item.Type); + }, + item => + { + Assert.NotNull(item); + Assert.Equal("Account", ((OpenApiSchemaReference)item).Reference.Id); + }); + } + else + { + // If it uses direct type, verify null is included + Assert.True(accountSchema.Type?.HasFlag(JsonSchemaType.Null)); + } + + // Verify component schemas are created for Todo and Account + Assert.Contains("Todo", document.Components.Schemas.Keys); + Assert.Contains("Account", document.Components.Schemas.Keys); + }); + } + [ApiController] [Route("[controller]/[action]")] private class TestFromQueryController : ControllerBase @@ -913,4 +1000,15 @@ private record FromQueryModel [DefaultValue(20)] public int Limit { get; set; } } + +#nullable enable + private record NullableParamsModel + { + [FromQuery(Name = "name")] + public string? Name { get; set; } + + [FromQuery(Name = "id")] + public int? Id { get; set; } + } +#nullable restore } diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.PropertySchemas.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.PropertySchemas.cs new file mode 100644 index 000000000000..0773249e195d --- /dev/null +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.PropertySchemas.cs @@ -0,0 +1,456 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Net.Http; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +public partial class OpenApiSchemaServiceTests : OpenApiDocumentServiceTestBase +{ + [Fact] + public async Task GetOpenApiSchema_HandlesNullablePropertiesWithNullInType() + { + // Arrange + var builder = CreateBuilder(); + + // Act + builder.MapPost("/api", (NullablePropertiesTestModel model) => { }); + + // Assert + await VerifyOpenApiDocument(builder, document => + { + var operation = document.Paths["/api"].Operations[HttpMethod.Post]; + var requestBody = operation.RequestBody; + var content = Assert.Single(requestBody.Content); + var schema = content.Value.Schema; + + Assert.Equal(JsonSchemaType.Object, schema.Type); + + // Check nullable int property has null in type directly or uses oneOf + var nullableIntProperty = schema.Properties["nullableInt"]; + if (nullableIntProperty.OneOf != null) + { + // If still uses oneOf, verify structure + Assert.Equal(2, nullableIntProperty.OneOf.Count); + Assert.Collection(nullableIntProperty.OneOf, + item => Assert.Equal(JsonSchemaType.Null, item.Type), + item => + { + Assert.Equal(JsonSchemaType.Integer, item.Type); + Assert.Equal("int32", item.Format); + }); + } + else + { + // If uses direct type, verify null is included + Assert.True(nullableIntProperty.Type?.HasFlag(JsonSchemaType.Integer)); + Assert.True(nullableIntProperty.Type?.HasFlag(JsonSchemaType.Null)); + Assert.Equal("int32", nullableIntProperty.Format); + } + + // Check nullable string property has null in type directly or uses oneOf + var nullableStringProperty = schema.Properties["nullableString"]; + if (nullableStringProperty.OneOf != null) + { + // If still uses oneOf, verify structure + Assert.Equal(2, nullableStringProperty.OneOf.Count); + Assert.Collection(nullableStringProperty.OneOf, + item => Assert.Equal(JsonSchemaType.Null, item.Type), + item => Assert.Equal(JsonSchemaType.String, item.Type)); + } + else + { + // If uses direct type, verify null is included + Assert.True(nullableStringProperty.Type?.HasFlag(JsonSchemaType.String)); + Assert.True(nullableStringProperty.Type?.HasFlag(JsonSchemaType.Null)); + } + + // Check nullable bool property has null in type directly or uses oneOf + var nullableBoolProperty = schema.Properties["nullableBool"]; + if (nullableBoolProperty.OneOf != null) + { + // If still uses oneOf, verify structure + Assert.Equal(2, nullableBoolProperty.OneOf.Count); + Assert.Collection(nullableBoolProperty.OneOf, + item => Assert.Equal(JsonSchemaType.Null, item.Type), + item => Assert.Equal(JsonSchemaType.Boolean, item.Type)); + } + else + { + // If uses direct type, verify null is included + Assert.True(nullableBoolProperty.Type?.HasFlag(JsonSchemaType.Boolean)); + Assert.True(nullableBoolProperty.Type?.HasFlag(JsonSchemaType.Null)); + } + + // Check nullable DateTime property has null in type directly or uses oneOf + var nullableDateTimeProperty = schema.Properties["nullableDateTime"]; + if (nullableDateTimeProperty.OneOf != null) + { + // If still uses oneOf, verify structure + Assert.Equal(2, nullableDateTimeProperty.OneOf.Count); + Assert.Collection(nullableDateTimeProperty.OneOf, + item => Assert.Equal(JsonSchemaType.Null, item.Type), + item => + { + Assert.Equal(JsonSchemaType.String, item.Type); + Assert.Equal("date-time", item.Format); + }); + } + else + { + // If uses direct type, verify null is included + Assert.True(nullableDateTimeProperty.Type?.HasFlag(JsonSchemaType.String)); + Assert.True(nullableDateTimeProperty.Type?.HasFlag(JsonSchemaType.Null)); + Assert.Equal("date-time", nullableDateTimeProperty.Format); + } + + // Check nullable Guid property has null in type directly or uses oneOf + var nullableGuidProperty = schema.Properties["nullableGuid"]; + if (nullableGuidProperty.OneOf != null) + { + // If still uses oneOf, verify structure + Assert.Equal(2, nullableGuidProperty.OneOf.Count); + Assert.Collection(nullableGuidProperty.OneOf, + item => Assert.Equal(JsonSchemaType.Null, item.Type), + item => + { + Assert.Equal(JsonSchemaType.String, item.Type); + Assert.Equal("uuid", item.Format); + }); + } + else + { + // If uses direct type, verify null is included + Assert.True(nullableGuidProperty.Type?.HasFlag(JsonSchemaType.String)); + Assert.True(nullableGuidProperty.Type?.HasFlag(JsonSchemaType.Null)); + Assert.Equal("uuid", nullableGuidProperty.Format); + } + + // Check nullable Uri property has null in type directly or uses oneOf + var nullableUriProperty = schema.Properties["nullableUri"]; + if (nullableUriProperty.OneOf != null) + { + // If still uses oneOf, verify structure + Assert.Equal(2, nullableUriProperty.OneOf.Count); + Assert.Collection(nullableUriProperty.OneOf, + item => Assert.Equal(JsonSchemaType.Null, item.Type), + item => + { + Assert.Equal(JsonSchemaType.String, item.Type); + Assert.Equal("uri", item.Format); + }); + } + else + { + // If uses direct type, verify null is included + Assert.True(nullableUriProperty.Type?.HasFlag(JsonSchemaType.String)); + Assert.True(nullableUriProperty.Type?.HasFlag(JsonSchemaType.Null)); + Assert.Equal("uri", nullableUriProperty.Format); + } + }); + } + + [Fact] + public async Task GetOpenApiSchema_HandlesNullableComplexTypesInPropertiesWithOneOf() + { + // Arrange + var builder = CreateBuilder(); + + // Act + builder.MapPost("/api", (ComplexNullablePropertiesModel model) => { }); + + // Assert + await VerifyOpenApiDocument(builder, document => + { + var operation = document.Paths["/api"].Operations[HttpMethod.Post]; + var requestBody = operation.RequestBody; + var content = Assert.Single(requestBody.Content); + var schema = content.Value.Schema; + + Assert.Equal(JsonSchemaType.Object, schema.Type); + + // Check nullable Todo property uses oneOf with reference + var nullableTodoProperty = schema.Properties["nullableTodo"]; + Assert.NotNull(nullableTodoProperty.OneOf); + Assert.Equal(2, nullableTodoProperty.OneOf.Count); + Assert.Collection(nullableTodoProperty.OneOf, + item => Assert.Equal(JsonSchemaType.Null, item.Type), + item => Assert.Equal("Todo", ((OpenApiSchemaReference)item).Reference.Id)); + + // Check nullable Account property uses oneOf with reference + var nullableAccountProperty = schema.Properties["nullableAccount"]; + Assert.NotNull(nullableAccountProperty.OneOf); + Assert.Equal(2, nullableAccountProperty.OneOf.Count); + Assert.Collection(nullableAccountProperty.OneOf, + item => Assert.Equal(JsonSchemaType.Null, item.Type), + item => Assert.Equal("Account", ((OpenApiSchemaReference)item).Reference.Id)); + + // Verify component schemas are created + Assert.Contains("Todo", document.Components.Schemas.Keys); + Assert.Contains("Account", document.Components.Schemas.Keys); + }); + } + + [Fact] + public async Task GetOpenApiSchema_HandlesNullableCollectionPropertiesWithNullInType() + { + // Arrange + var builder = CreateBuilder(); + + // Act + builder.MapPost("/api", (NullableCollectionPropertiesModel model) => { }); + + // Assert + await VerifyOpenApiDocument(builder, document => + { + var operation = document.Paths["/api"].Operations[HttpMethod.Post]; + var requestBody = operation.RequestBody; + var content = Assert.Single(requestBody.Content); + var schema = content.Value.Schema; + + Assert.Equal(JsonSchemaType.Object, schema.Type); + + // Check nullable List property has null in type or uses oneOf + var nullableTodoListProperty = schema.Properties["nullableTodoList"]; + if (nullableTodoListProperty.OneOf != null) + { + // If still uses oneOf, verify structure + Assert.Equal(2, nullableTodoListProperty.OneOf.Count); + Assert.Collection(nullableTodoListProperty.OneOf, + item => Assert.Equal(JsonSchemaType.Null, item.Type), + item => + { + Assert.Equal(JsonSchemaType.Array, item.Type); + Assert.NotNull(item.Items); + Assert.Equal("Todo", ((OpenApiSchemaReference)item.Items).Reference.Id); + }); + } + else + { + // If uses direct type, verify null is included + Assert.True(nullableTodoListProperty.Type?.HasFlag(JsonSchemaType.Array)); + Assert.True(nullableTodoListProperty.Type?.HasFlag(JsonSchemaType.Null)); + } + + // Check nullable Todo[] property has null in type or uses oneOf + var nullableTodoArrayProperty = schema.Properties["nullableTodoArray"]; + if (nullableTodoArrayProperty.OneOf != null) + { + // If still uses oneOf, verify structure + Assert.Equal(2, nullableTodoArrayProperty.OneOf.Count); + Assert.Collection(nullableTodoArrayProperty.OneOf, + item => Assert.Equal(JsonSchemaType.Null, item.Type), + item => + { + Assert.Equal(JsonSchemaType.Array, item.Type); + Assert.NotNull(item.Items); + Assert.Equal("Todo", ((OpenApiSchemaReference)item.Items).Reference.Id); + }); + } + else + { + // If uses direct type, verify null is included + Assert.True(nullableTodoArrayProperty.Type?.HasFlag(JsonSchemaType.Array)); + Assert.True(nullableTodoArrayProperty.Type?.HasFlag(JsonSchemaType.Null)); + } + + // Check nullable Dictionary property has null in type or uses oneOf + var nullableDictionaryProperty = schema.Properties["nullableDictionary"]; + if (nullableDictionaryProperty.OneOf != null) + { + // If still uses oneOf, verify structure + Assert.Equal(2, nullableDictionaryProperty.OneOf.Count); + Assert.Collection(nullableDictionaryProperty.OneOf, + item => Assert.Equal(JsonSchemaType.Null, item.Type), + item => + { + Assert.Equal(JsonSchemaType.Object, item.Type); + Assert.NotNull(item.AdditionalProperties); + Assert.Equal("Todo", ((OpenApiSchemaReference)item.AdditionalProperties).Reference.Id); + }); + } + else + { + // If uses direct type, verify null is included + Assert.True(nullableDictionaryProperty.Type?.HasFlag(JsonSchemaType.Object)); + Assert.True(nullableDictionaryProperty.Type?.HasFlag(JsonSchemaType.Null)); + } + }); + } + + [Fact] + public async Task GetOpenApiSchema_HandlesNullableEnumPropertiesWithOneOf() + { + // Arrange + var builder = CreateBuilder(); + + // Act + builder.MapPost("/api", (NullableEnumPropertiesModel model) => { }); + + // Assert + await VerifyOpenApiDocument(builder, document => + { + var operation = document.Paths["/api"].Operations[HttpMethod.Post]; + var requestBody = operation.RequestBody; + var content = Assert.Single(requestBody.Content); + var schema = content.Value.Schema; + + Assert.Equal(JsonSchemaType.Object, schema.Type); + + // Check nullable Status (with string converter) property uses oneOf with reference + var nullableStatusProperty = schema.Properties["nullableStatus"]; + Assert.NotNull(nullableStatusProperty.OneOf); + Assert.Equal(2, nullableStatusProperty.OneOf.Count); + Assert.Collection(nullableStatusProperty.OneOf, + item => Assert.Equal(JsonSchemaType.Null, item.Type), + item => Assert.Equal("Status", ((OpenApiSchemaReference)item).Reference.Id)); + + // Check nullable TaskStatus (without converter) property uses oneOf + var nullableTaskStatusProperty = schema.Properties["nullableTaskStatus"]; + Assert.NotNull(nullableTaskStatusProperty.OneOf); + Assert.Equal(2, nullableTaskStatusProperty.OneOf.Count); + Assert.Collection(nullableTaskStatusProperty.OneOf, + item => Assert.Equal(JsonSchemaType.Null, item.Type), + item => Assert.Equal(JsonSchemaType.Integer, item.Type)); + }); + } + + [Fact] + public async Task GetOpenApiSchema_HandlesNullablePropertiesWithValidationAttributesAndNullInType() + { + // Arrange + var builder = CreateBuilder(); + + // Act + builder.MapPost("/api", (NullablePropertiesWithValidationModel model) => { }); + + // Assert + await VerifyOpenApiDocument(builder, document => + { + var operation = document.Paths["/api"].Operations[HttpMethod.Post]; + var requestBody = operation.RequestBody; + var content = Assert.Single(requestBody.Content); + var schema = content.Value.Schema; + + Assert.Equal(JsonSchemaType.Object, schema.Type); + + // Check nullable string with validation attributes has null in type or uses oneOf + var nullableNameProperty = schema.Properties["nullableName"]; + if (nullableNameProperty.OneOf != null) + { + // If still uses oneOf for properties with validation, verify structure + Assert.Equal(2, nullableNameProperty.OneOf.Count); + Assert.Collection(nullableNameProperty.OneOf, + item => Assert.Equal(JsonSchemaType.Null, item.Type), + item => + { + Assert.Equal(JsonSchemaType.String, item.Type); + Assert.Equal(3, item.MinLength); + Assert.Equal(50, item.MaxLength); + }); + } + else + { + // If uses direct type, verify null is included and validation attributes + Assert.True(nullableNameProperty.Type?.HasFlag(JsonSchemaType.String)); + Assert.True(nullableNameProperty.Type?.HasFlag(JsonSchemaType.Null)); + Assert.Equal(3, nullableNameProperty.MinLength); + Assert.Equal(50, nullableNameProperty.MaxLength); + } + + // Check nullable int with range validation has null in type or uses oneOf + var nullableAgeProperty = schema.Properties["nullableAge"]; + if (nullableAgeProperty.OneOf != null) + { + // If still uses oneOf for properties with validation, verify structure + Assert.Equal(2, nullableAgeProperty.OneOf.Count); + Assert.Collection(nullableAgeProperty.OneOf, + item => Assert.Equal(JsonSchemaType.Null, item.Type), + item => + { + Assert.Equal(JsonSchemaType.Integer, item.Type); + Assert.Equal("int32", item.Format); + Assert.Equal("18", item.Minimum); + Assert.Equal("120", item.Maximum); + }); + } + else + { + // If uses direct type, verify null is included and validation attributes + Assert.True(nullableAgeProperty.Type?.HasFlag(JsonSchemaType.Integer)); + Assert.True(nullableAgeProperty.Type?.HasFlag(JsonSchemaType.Null)); + Assert.Equal("int32", nullableAgeProperty.Format); + Assert.Equal("18", nullableAgeProperty.Minimum); + Assert.Equal("120", nullableAgeProperty.Maximum); + } + + // Check nullable string with description has null in type or uses oneOf + var nullableDescriptionProperty = schema.Properties["nullableDescription"]; + if (nullableDescriptionProperty.OneOf != null) + { + // If still uses oneOf for properties with description, verify structure + Assert.Equal(2, nullableDescriptionProperty.OneOf.Count); + Assert.Collection(nullableDescriptionProperty.OneOf, + item => Assert.Equal(JsonSchemaType.Null, item.Type), + item => + { + Assert.Equal(JsonSchemaType.String, item.Type); + Assert.Equal("A description field", item.Description); + }); + } + else + { + // If uses direct type, verify null is included and description + Assert.True(nullableDescriptionProperty.Type?.HasFlag(JsonSchemaType.String)); + Assert.True(nullableDescriptionProperty.Type?.HasFlag(JsonSchemaType.Null)); + Assert.Equal("A description field", nullableDescriptionProperty.Description); + } + }); + } + +#nullable enable + private class NullablePropertiesTestModel + { + public int? NullableInt { get; set; } + public string? NullableString { get; set; } + public bool? NullableBool { get; set; } + public DateTime? NullableDateTime { get; set; } + public Guid? NullableGuid { get; set; } + public Uri? NullableUri { get; set; } + } + + private class ComplexNullablePropertiesModel + { + public Todo? NullableTodo { get; set; } + public Account? NullableAccount { get; set; } + } + + private class NullableCollectionPropertiesModel + { + public List? NullableTodoList { get; set; } + public Todo[]? NullableTodoArray { get; set; } + public Dictionary? NullableDictionary { get; set; } + } + + private class NullableEnumPropertiesModel + { + public Status? NullableStatus { get; set; } + public TaskStatus? NullableTaskStatus { get; set; } + } + + private class NullablePropertiesWithValidationModel + { + [StringLength(50, MinimumLength = 3)] + public string? NullableName { get; set; } + + [Range(18, 120)] + public int? NullableAge { get; set; } + + [Description("A description field")] + public string? NullableDescription { get; set; } + } +#nullable restore +} diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.RequestBodySchemas.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.RequestBodySchemas.cs index 016ea0663edc..2c368e559c49 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.RequestBodySchemas.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.RequestBodySchemas.cs @@ -464,6 +464,188 @@ await VerifyOpenApiDocument(builder, document => }); } + [Fact] + public async Task GetOpenApiRequestBody_HandlesNullableParameterWithOneOf() + { + // Arrange + var builder = CreateBuilder(); + + // Act +#nullable enable + builder.MapPost("/api/nullable-todo", (Todo? todo) => { }); + builder.MapPost("/api/nullable-point", (Point? point) => { }); +#nullable restore + + // Assert + await VerifyOpenApiDocument(builder, document => + { + // Verify nullable Todo parameter + var todoOperation = document.Paths["/api/nullable-todo"].Operations[HttpMethod.Post]; + var todoRequestBody = todoOperation.RequestBody; + var todoContent = Assert.Single(todoRequestBody.Content); + Assert.Equal("application/json", todoContent.Key); + var todoSchema = todoContent.Value.Schema; + Assert.NotNull(todoSchema.OneOf); + Assert.Equal(2, todoSchema.OneOf.Count); + Assert.Collection(todoSchema.OneOf, + item => + { + Assert.NotNull(item); + Assert.Equal(JsonSchemaType.Null, item.Type); + }, + item => + { + Assert.NotNull(item); + Assert.Equal(JsonSchemaType.Object, item.Type); + Assert.Collection(item.Properties, + property => + { + Assert.Equal("id", property.Key); + Assert.Equal(JsonSchemaType.Integer, property.Value.Type); + Assert.Equal("int32", property.Value.Format); + }, + property => + { + Assert.Equal("title", property.Key); + Assert.Equal(JsonSchemaType.String | JsonSchemaType.Null, property.Value.Type); + }, + property => + { + Assert.Equal("completed", property.Key); + Assert.Equal(JsonSchemaType.Boolean, property.Value.Type); + }, + property => + { + Assert.Equal("createdAt", property.Key); + Assert.Equal(JsonSchemaType.String, property.Value.Type); + Assert.Equal("date-time", property.Value.Format); + }); + }); + + // Verify nullable Point parameter + var pointOperation = document.Paths["/api/nullable-point"].Operations[HttpMethod.Post]; + var pointRequestBody = pointOperation.RequestBody; + var pointContent = Assert.Single(pointRequestBody.Content); + Assert.Equal("application/json", pointContent.Key); + var pointSchema = pointContent.Value.Schema; + Assert.NotNull(pointSchema.OneOf); + Assert.Equal(2, pointSchema.OneOf.Count); + Assert.Collection(pointSchema.OneOf, + item => + { + Assert.NotNull(item); + Assert.Equal(JsonSchemaType.Null, item.Type); + }, + item => + { + Assert.NotNull(item); + Assert.Equal(JsonSchemaType.Object, item.Type); + Assert.Collection(item.Properties, + property => + { + Assert.Equal("x", property.Key); + Assert.Equal(JsonSchemaType.Integer, property.Value.Type); + Assert.Equal("int32", property.Value.Format); + }, + property => + { + Assert.Equal("y", property.Key); + Assert.Equal(JsonSchemaType.Integer, property.Value.Type); + Assert.Equal("int32", property.Value.Format); + }); + }); + + Assert.Equal(["Point", "Todo"], [.. document.Components.Schemas.Keys]); + Assert.Collection(document.Components.Schemas.Values, + item => Assert.Equal(JsonSchemaType.Object, item.Type), + item => Assert.Equal(JsonSchemaType.Object, item.Type)); + }); + } + + [Fact] + public async Task GetOpenApiRequestBody_HandlesNullableCollectionParametersWithOneOf() + { + // Arrange + var builder = CreateBuilder(); + + // Act +#nullable enable + builder.MapPost("/api/nullable-array", (Todo[]? todos) => { }); + builder.MapPost("/api/nullable-list", (List? todoList) => { }); + builder.MapPost("/api/nullable-enumerable", (IEnumerable? todoEnumerable) => { }); +#nullable restore + + // Assert + await VerifyOpenApiDocument(builder, document => + { + // Verify nullable array parameter - verify actual behavior with OneOf + var arrayOperation = document.Paths["/api/nullable-array"].Operations[HttpMethod.Post]; + var arrayRequestBody = arrayOperation.RequestBody; + var arrayContent = Assert.Single(arrayRequestBody.Content); + Assert.Equal("application/json", arrayContent.Key); + var arraySchema = arrayContent.Value.Schema; + Assert.NotNull(arraySchema.OneOf); // OneOf IS used for nullable collections + Assert.Equal(2, arraySchema.OneOf.Count); + Assert.Collection(arraySchema.OneOf, + item => + { + Assert.NotNull(item); + Assert.Equal(JsonSchemaType.Null, item.Type); + }, + item => + { + Assert.NotNull(item); + Assert.Equal(JsonSchemaType.Array, item.Type); + Assert.NotNull(item.Items); + Assert.Equal("Todo", ((OpenApiSchemaReference)item.Items).Reference.Id); + }); + + // Verify nullable List parameter - verify actual behavior with OneOf + var listOperation = document.Paths["/api/nullable-list"].Operations[HttpMethod.Post]; + var listRequestBody = listOperation.RequestBody; + var listContent = Assert.Single(listRequestBody.Content); + Assert.Equal("application/json", listContent.Key); + var listSchema = listContent.Value.Schema; + Assert.NotNull(listSchema.OneOf); // OneOf IS used for nullable collections + Assert.Equal(2, listSchema.OneOf.Count); + Assert.Collection(listSchema.OneOf, + item => + { + Assert.NotNull(item); + Assert.Equal(JsonSchemaType.Null, item.Type); + }, + item => + { + Assert.NotNull(item); + Assert.Equal(JsonSchemaType.Array, item.Type); + Assert.NotNull(item.Items); + Assert.Equal("Todo", ((OpenApiSchemaReference)item.Items).Reference.Id); + }); + + // Verify nullable IEnumerable parameter - verify actual behavior with OneOf + var enumerableOperation = document.Paths["/api/nullable-enumerable"].Operations[HttpMethod.Post]; + var enumerableRequestBody = enumerableOperation.RequestBody; + var enumerableContent = Assert.Single(enumerableRequestBody.Content); + Assert.Equal("application/json", enumerableContent.Key); + var enumerableSchema = enumerableContent.Value.Schema; + Assert.NotNull(enumerableSchema.OneOf); // OneOf IS used for nullable collections + Assert.Equal(2, enumerableSchema.OneOf.Count); + Assert.Collection(enumerableSchema.OneOf, + item => + { + Assert.NotNull(item); + Assert.Equal(JsonSchemaType.Null, item.Type); + }, + item => + { + Assert.NotNull(item); + Assert.Equal(JsonSchemaType.Array, item.Type); + Assert.NotNull(item.Items); + Assert.Equal("Todo", ((OpenApiSchemaReference)item.Items).Reference.Id); + }); + }); + } + [Fact] public async Task SupportsNestedTypes() { @@ -670,6 +852,67 @@ private class ExampleWithSkippedUnmappedMembers public int Number { get; init; } } + [Fact] + public async Task GetOpenApiRequestBody_HandlesNullableGenericTypesWithOneOf() + { + // Arrange + var builder = CreateBuilder(); + + // Act +#nullable enable + builder.MapPost("/api/nullable-result", (Result? result) => { }); + builder.MapPost("/api/nullable-list", (List? todos) => { }); + builder.MapPost("/api/nullable-dictionary", (Dictionary? todoDict) => { }); +#nullable restore + + // Assert + await VerifyOpenApiDocument(builder, document => + { + // Verify nullable Result uses allOf + var resultOperation = document.Paths["/api/nullable-result"].Operations[HttpMethod.Post]; + var resultRequestBody = resultOperation.RequestBody; + var resultContent = Assert.Single(resultRequestBody.Content); + var resultSchema = resultContent.Value.Schema; + Assert.NotNull(resultSchema.OneOf); + Assert.Equal(2, resultSchema.OneOf.Count); + Assert.Collection(resultSchema.OneOf, + item => Assert.Equal(JsonSchemaType.Null, item.Type), + item => Assert.Equal(JsonSchemaType.Object, item.Type)); + + // Verify nullable List uses allOf + var listOperation = document.Paths["/api/nullable-list"].Operations[HttpMethod.Post]; + var listRequestBody = listOperation.RequestBody; + var listContent = Assert.Single(listRequestBody.Content); + var listSchema = listContent.Value.Schema; + Assert.NotNull(listSchema.OneOf); + Assert.Equal(2, listSchema.OneOf.Count); + Assert.Collection(listSchema.OneOf, + item => Assert.Equal(JsonSchemaType.Null, item.Type), + item => + { + Assert.Equal(JsonSchemaType.Array, item.Type); + Assert.NotNull(item.Items); + Assert.Equal("Todo", ((OpenApiSchemaReference)item.Items).Reference.Id); + }); + + // Verify nullable Dictionary uses allOf + var dictOperation = document.Paths["/api/nullable-dictionary"].Operations[HttpMethod.Post]; + var dictRequestBody = dictOperation.RequestBody; + var dictContent = Assert.Single(dictRequestBody.Content); + var dictSchema = dictContent.Value.Schema; + Assert.NotNull(dictSchema.OneOf); + Assert.Equal(2, dictSchema.OneOf.Count); + Assert.Collection(dictSchema.OneOf, + item => Assert.Equal(JsonSchemaType.Null, item.Type), + item => + { + Assert.Equal(JsonSchemaType.Object, item.Type); + Assert.NotNull(item.AdditionalProperties); + Assert.Equal("Todo", ((OpenApiSchemaReference)item.AdditionalProperties).Reference.Id); + }); + }); + } + [Fact] public async Task SupportsTypesWithSelfReferencedProperties() { diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.ResponseSchemas.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.ResponseSchemas.cs index cabbd30d0c08..c0a38a43997b 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.ResponseSchemas.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.ResponseSchemas.cs @@ -174,6 +174,114 @@ public async Task GetOpenApiResponse_HandlesNullablePocoResponse() builder.MapGet("/api", GetTodo); #nullable restore + // Assert + await VerifyOpenApiDocument(builder, document => + { + var operation = document.Paths["/api"].Operations[HttpMethod.Get]; + var responses = Assert.Single(operation.Responses); + var response = responses.Value; + Assert.True(response.Content.TryGetValue("application/json", out var mediaType)); + var schema = mediaType.Schema; + Assert.NotNull(schema.OneOf); + Assert.Equal(2, schema.OneOf.Count); + // Check that the oneOf consists of a nullable schema and the GetTodo schema + Assert.Collection(schema.OneOf, + item => + { + Assert.NotNull(item); + Assert.Equal(JsonSchemaType.Null, item.Type); + }, + item => + { + Assert.NotNull(item); + Assert.Equal(JsonSchemaType.Object, item.Type); + Assert.Collection(item.Properties, + property => + { + Assert.Equal("id", property.Key); + Assert.Equal(JsonSchemaType.Integer, property.Value.Type); + Assert.Equal("int32", property.Value.Format); + }, + property => + { + Assert.Equal("title", property.Key); + Assert.Equal(JsonSchemaType.String | JsonSchemaType.Null, property.Value.Type); + }, + property => + { + Assert.Equal("completed", property.Key); + Assert.Equal(JsonSchemaType.Boolean, property.Value.Type); + }, + property => + { + Assert.Equal("createdAt", property.Key); + Assert.Equal(JsonSchemaType.String, property.Value.Type); + Assert.Equal("date-time", property.Value.Format); + }); + }); + + }); + } + + [Fact] + public async Task GetOpenApiResponse_HandlesNullablePocoTaskResponse() + { + // Arrange + var builder = CreateBuilder(); + + // Act +#nullable enable + static Task GetTodoAsync() => Task.FromResult(Random.Shared.Next() < 0.5 ? new Todo(1, "Test Title", true, DateTime.Now) : null); + builder.MapGet("/api", GetTodoAsync); +#nullable restore + + // Assert + await VerifyOpenApiDocument(builder, document => + { + var operation = document.Paths["/api"].Operations[HttpMethod.Get]; + var responses = Assert.Single(operation.Responses); + var response = responses.Value; + Assert.True(response.Content.TryGetValue("application/json", out var mediaType)); + var schema = mediaType.Schema; + Assert.Equal(JsonSchemaType.Object, schema.Type); + Assert.Collection(schema.Properties, + property => + { + Assert.Equal("id", property.Key); + Assert.Equal(JsonSchemaType.Integer, property.Value.Type); + Assert.Equal("int32", property.Value.Format); + }, + property => + { + Assert.Equal("title", property.Key); + Assert.Equal(JsonSchemaType.String | JsonSchemaType.Null, property.Value.Type); + }, + property => + { + Assert.Equal("completed", property.Key); + Assert.Equal(JsonSchemaType.Boolean, property.Value.Type); + }, + property => + { + Assert.Equal("createdAt", property.Key); + Assert.Equal(JsonSchemaType.String, property.Value.Type); + Assert.Equal("date-time", property.Value.Format); + }); + }); + } + + [Fact] + public async Task GetOpenApiResponse_HandlesNullablePocoValueTaskResponse() + { + // Arrange + var builder = CreateBuilder(); + + // Act +#nullable enable + static ValueTask GetTodoValueTaskAsync() => ValueTask.FromResult(Random.Shared.Next() < 0.5 ? new Todo(1, "Test Title", true, DateTime.Now) : null); + builder.MapGet("/api", GetTodoValueTaskAsync); +#nullable restore + // Assert await VerifyOpenApiDocument(builder, document => { @@ -231,6 +339,214 @@ await VerifyOpenApiDocument(builder, document => }); } + [Fact] + public async Task GetOpenApiResponse_HandlesNullableValueTypeResponse() + { + // Arrange + var builder = CreateBuilder(); + + // Act +#nullable enable + static Point? GetNullablePoint() => Random.Shared.Next() < 0.5 ? new Point { X = 10, Y = 20 } : null; + builder.MapGet("/api/nullable-point", GetNullablePoint); + + static Coordinate? GetNullableCoordinate() => Random.Shared.Next() < 0.5 ? new Coordinate(1.5, 2.5) : null; + builder.MapGet("/api/nullable-coordinate", GetNullableCoordinate); +#nullable restore + + // Assert + await VerifyOpenApiDocument(builder, document => + { + // Verify nullable Point response + var pointOperation = document.Paths["/api/nullable-point"].Operations[HttpMethod.Get]; + var pointResponses = Assert.Single(pointOperation.Responses); + var pointResponse = pointResponses.Value; + Assert.True(pointResponse.Content.TryGetValue("application/json", out var pointMediaType)); + var pointSchema = pointMediaType.Schema; + Assert.NotNull(pointSchema.OneOf); + Assert.Equal(2, pointSchema.OneOf.Count); + Assert.Collection(pointSchema.OneOf, + item => + { + Assert.NotNull(item); + Assert.Equal(JsonSchemaType.Null, item.Type); + }, + item => + { + Assert.NotNull(item); + Assert.Equal(JsonSchemaType.Object, item.Type); + Assert.Collection(item.Properties, + property => + { + Assert.Equal("x", property.Key); + Assert.Equal(JsonSchemaType.Integer, property.Value.Type); + Assert.Equal("int32", property.Value.Format); + }, + property => + { + Assert.Equal("y", property.Key); + Assert.Equal(JsonSchemaType.Integer, property.Value.Type); + Assert.Equal("int32", property.Value.Format); + }); + }); + + // Verify nullable Coordinate response + var coordinateOperation = document.Paths["/api/nullable-coordinate"].Operations[HttpMethod.Get]; + var coordinateResponses = Assert.Single(coordinateOperation.Responses); + var coordinateResponse = coordinateResponses.Value; + Assert.True(coordinateResponse.Content.TryGetValue("application/json", out var coordinateMediaType)); + var coordinateSchema = coordinateMediaType.Schema; + Assert.NotNull(coordinateSchema.OneOf); + Assert.Equal(2, coordinateSchema.OneOf.Count); + Assert.Collection(coordinateSchema.OneOf, + item => + { + Assert.NotNull(item); + Assert.Equal(JsonSchemaType.Null, item.Type); + }, + item => + { + Assert.NotNull(item); + Assert.Equal(JsonSchemaType.Object, item.Type); + Assert.Collection(item.Properties, + property => + { + Assert.Equal("latitude", property.Key); + Assert.Equal(JsonSchemaType.Number, property.Value.Type); + Assert.Equal("double", property.Value.Format); + }, + property => + { + Assert.Equal("longitude", property.Key); + Assert.Equal(JsonSchemaType.Number, property.Value.Type); + Assert.Equal("double", property.Value.Format); + }); + }); + + // Assert that Point and Coordinates are the only schemas defined at the top-level + Assert.Equal(["Coordinate", "Point"], [.. document.Components.Schemas.Keys]); + }); + } + + [Fact] + public async Task GetOpenApiResponse_HandlesNullableCollectionResponsesWithOneOf() + { + // Arrange + var builder = CreateBuilder(); + + // Act +#nullable enable + static List? GetNullableTodos() => Random.Shared.Next() < 0.5 ? + [new Todo(1, "Test", true, DateTime.Now)] : null; + static Todo[]? GetNullableTodoArray() => Random.Shared.Next() < 0.5 ? + [new Todo(1, "Test", true, DateTime.Now)] : null; + static IEnumerable? GetNullableTodoEnumerable() => Random.Shared.Next() < 0.5 ? + [new Todo(1, "Test", true, DateTime.Now)] : null; + + builder.MapGet("/api/nullable-list", GetNullableTodos); + builder.MapGet("/api/nullable-array", GetNullableTodoArray); + builder.MapGet("/api/nullable-enumerable", GetNullableTodoEnumerable); +#nullable restore + + // Assert + await VerifyOpenApiDocument(builder, document => + { + // Verify nullable List response uses oneOf + var listOperation = document.Paths["/api/nullable-list"].Operations[HttpMethod.Get]; + var listResponse = Assert.Single(listOperation.Responses).Value; + Assert.True(listResponse.Content.TryGetValue("application/json", out var listMediaType)); + var listSchema = listMediaType.Schema; + Assert.NotNull(listSchema.OneOf); + Assert.Equal(2, listSchema.OneOf.Count); + Assert.Collection(listSchema.OneOf, + item => Assert.Equal(JsonSchemaType.Null, item.Type), + item => + { + Assert.Equal(JsonSchemaType.Array, item.Type); + Assert.NotNull(item.Items); + Assert.Equal("Todo", ((OpenApiSchemaReference)item.Items).Reference.Id); + }); + + // Verify nullable Todo[] response uses oneOf + var arrayOperation = document.Paths["/api/nullable-array"].Operations[HttpMethod.Get]; + var arrayResponse = Assert.Single(arrayOperation.Responses).Value; + Assert.True(arrayResponse.Content.TryGetValue("application/json", out var arrayMediaType)); + var arraySchema = arrayMediaType.Schema; + Assert.NotNull(arraySchema.OneOf); + Assert.Equal(2, arraySchema.OneOf.Count); + Assert.Collection(arraySchema.OneOf, + item => Assert.Equal(JsonSchemaType.Null, item.Type), + item => + { + Assert.Equal(JsonSchemaType.Array, item.Type); + Assert.NotNull(item.Items); + Assert.Equal("Todo", ((OpenApiSchemaReference)item.Items).Reference.Id); + }); + + // Verify nullable IEnumerable response uses oneOf + var enumerableOperation = document.Paths["/api/nullable-enumerable"].Operations[HttpMethod.Get]; + var enumerableResponse = Assert.Single(enumerableOperation.Responses).Value; + Assert.True(enumerableResponse.Content.TryGetValue("application/json", out var enumerableMediaType)); + var enumerableSchema = enumerableMediaType.Schema; + Assert.NotNull(enumerableSchema.OneOf); + Assert.Equal(2, enumerableSchema.OneOf.Count); + Assert.Collection(enumerableSchema.OneOf, + item => Assert.Equal(JsonSchemaType.Null, item.Type), + item => + { + Assert.Equal(JsonSchemaType.Array, item.Type); + Assert.NotNull(item.Items); + Assert.Equal("Todo", ((OpenApiSchemaReference)item.Items).Reference.Id); + }); + }); + } + + [Fact] + public async Task GetOpenApiResponse_HandlesNullableEnumResponsesWithOneOf() + { + // Arrange + var builder = CreateBuilder(); + + // Act +#nullable enable + static Status? GetNullableStatus() => Random.Shared.Next() < 0.5 ? Status.Approved : null; + static TaskStatus? GetNullableTaskStatus() => Random.Shared.Next() < 0.5 ? TaskStatus.Running : null; + + builder.MapGet("/api/nullable-status", GetNullableStatus); + builder.MapGet("/api/nullable-task-status", GetNullableTaskStatus); +#nullable restore + + // Assert + await VerifyOpenApiDocument(builder, document => + { + // Verify nullable Status (with string converter) response uses oneOf + var statusOperation = document.Paths["/api/nullable-status"].Operations[HttpMethod.Get]; + var statusResponse = Assert.Single(statusOperation.Responses).Value; + Assert.True(statusResponse.Content.TryGetValue("application/json", out var statusMediaType)); + var statusSchema = statusMediaType.Schema; + Assert.NotNull(statusSchema.OneOf); + Assert.Equal(2, statusSchema.OneOf.Count); + Assert.Collection(statusSchema.OneOf, + item => Assert.Equal(JsonSchemaType.Null, item.Type), + item => + { + // Status has string enum converter, so it should be a reference to the enum schema + Assert.Equal("Status", ((OpenApiSchemaReference)item).Reference.Id); + }); + + // Verify nullable TaskStatus (without converter) response uses oneOf + var taskStatusOperation = document.Paths["/api/nullable-task-status"].Operations[HttpMethod.Get]; + var taskStatusResponse = Assert.Single(taskStatusOperation.Responses).Value; + Assert.True(taskStatusResponse.Content.TryGetValue("application/json", out var taskStatusMediaType)); + var taskStatusSchema = taskStatusMediaType.Schema; + Assert.NotNull(taskStatusSchema.OneOf); + Assert.Equal(2, taskStatusSchema.OneOf.Count); + Assert.Collection(taskStatusSchema.OneOf, + item => Assert.Equal(JsonSchemaType.Null, item.Type), + item => Assert.Equal(JsonSchemaType.Integer, item.Type)); + }); + } + [Fact] public async Task GetOpenApiResponse_HandlesInheritedTypeResponse() { @@ -313,7 +629,7 @@ await VerifyOpenApiDocument(builder, document => { Assert.Equal("value", property.Key); var propertyValue = property.Value; - Assert.Equal(JsonSchemaType.Null | JsonSchemaType.Object, propertyValue.Type); + Assert.Equal(JsonSchemaType.Object, propertyValue.Type); Assert.Collection(propertyValue.Properties, property => { @@ -339,7 +655,7 @@ await VerifyOpenApiDocument(builder, document => { Assert.Equal("error", property.Key); var propertyValue = property.Value; - Assert.Equal(JsonSchemaType.Null | JsonSchemaType.Object, propertyValue.Type); + Assert.Equal(JsonSchemaType.Object, propertyValue.Type); Assert.Collection(propertyValue.Properties, property => { Assert.Equal("code", property.Key); @@ -427,7 +743,7 @@ await VerifyOpenApiDocument(builder, document => { Assert.Equal("todo", property.Key); var propertyValue = property.Value; - Assert.Equal(JsonSchemaType.Null | JsonSchemaType.Object, propertyValue.Type); + Assert.Equal(JsonSchemaType.Object, propertyValue.Type); Assert.Collection(propertyValue.Properties, property => { @@ -732,4 +1048,22 @@ private class ClassWithObjectProperty [DefaultValue(32)] public object AnotherObject { get; set; } } + + private struct Point + { + public int X { get; set; } + public int Y { get; set; } + } + + private readonly struct Coordinate + { + public double Latitude { get; } + public double Longitude { get; } + + public Coordinate(double latitude, double longitude) + { + Latitude = latitude; + Longitude = longitude; + } + } } diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/Implementations/OpenApiSchemaReferenceTransformerTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/Implementations/OpenApiSchemaReferenceTransformerTests.cs index 67d4bf32160d..1d49c03970b5 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/Implementations/OpenApiSchemaReferenceTransformerTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/Implementations/OpenApiSchemaReferenceTransformerTests.cs @@ -501,10 +501,7 @@ await VerifyOpenApiDocument(builder, document => serializedSchema = writer.ToString(); Assert.Equal(""" { - "type": [ - "null", - "object" - ], + "type": "object", "properties": { "address": { "$ref": "#/components/schemas/AddressDto" @@ -519,10 +516,7 @@ await VerifyOpenApiDocument(builder, document => serializedSchema = writer.ToString(); Assert.Equal(""" { - "type": [ - "null", - "object" - ], + "type": "object", "properties": { "relatedLocation": { "$ref": "#/components/schemas/LocationDto" @@ -985,7 +979,10 @@ await VerifyOpenApiDocument(builder, document => // Check secondaryUser property (nullable RefProfile) var secondaryUserSchema = requestSchema.Properties!["secondaryUser"]; - Assert.Equal("RefProfile", ((OpenApiSchemaReference)secondaryUserSchema).Reference.Id); + Assert.NotNull(secondaryUserSchema.OneOf); + Assert.Collection(secondaryUserSchema.OneOf, + item => Assert.Equal(JsonSchemaType.Null, item.Type), + item => Assert.Equal("RefProfile", ((OpenApiSchemaReference)item).Reference.Id)); // Verify the RefProfile schema has a User property that references RefUser var userPropertySchema = primaryUserSchema.Properties!["user"]; @@ -998,10 +995,12 @@ await VerifyOpenApiDocument(builder, document => Assert.Contains("email", userSchemaContent.Properties?.Keys ?? []); // Both properties should reference the same RefProfile schema + var secondaryUserSchemaRef = secondaryUserSchema.OneOf.Last(); Assert.Equal(((OpenApiSchemaReference)primaryUserSchema).Reference.Id, - ((OpenApiSchemaReference)secondaryUserSchema).Reference.Id); + ((OpenApiSchemaReference)secondaryUserSchemaRef).Reference.Id); Assert.Equal(["RefProfile", "RefUser", "Subscription"], document.Components!.Schemas!.Keys.OrderBy(x => x)); + Assert.All(document.Components.Schemas.Values, item => Assert.False(item.Type?.HasFlag(JsonSchemaType.Null))); }); } diff --git a/src/Security/Authentication/Cookies/src/CookieAuthenticationEvents.cs b/src/Security/Authentication/Cookies/src/CookieAuthenticationEvents.cs index 994d999aaf52..35a73901c06b 100644 --- a/src/Security/Authentication/Cookies/src/CookieAuthenticationEvents.cs +++ b/src/Security/Authentication/Cookies/src/CookieAuthenticationEvents.cs @@ -12,6 +12,8 @@ namespace Microsoft.AspNetCore.Authentication.Cookies; /// public class CookieAuthenticationEvents { + private static readonly bool _ignoreCookieRedirectMetadata = AppContext.TryGetSwitch("Microsoft.AspNetCore.Authentication.Cookies.IgnoreRedirectMetadata", out var isEnabled) && isEnabled; + /// /// Invoked to validate the principal. /// @@ -42,7 +44,7 @@ public class CookieAuthenticationEvents /// public Func, Task> OnRedirectToLogin { get; set; } = context => { - if (IsAjaxRequest(context.Request) || IsApiEndpoint(context.HttpContext)) + if (IsAjaxRequest(context.Request) || IsCookieRedirectDisabledByMetadata(context.HttpContext)) { context.Response.Headers.Location = context.RedirectUri; context.Response.StatusCode = 401; @@ -59,7 +61,7 @@ public class CookieAuthenticationEvents /// public Func, Task> OnRedirectToAccessDenied { get; set; } = context => { - if (IsAjaxRequest(context.Request) || IsApiEndpoint(context.HttpContext)) + if (IsAjaxRequest(context.Request) || IsCookieRedirectDisabledByMetadata(context.HttpContext)) { context.Response.Headers.Location = context.RedirectUri; context.Response.StatusCode = 403; @@ -109,10 +111,16 @@ private static bool IsAjaxRequest(HttpRequest request) string.Equals(request.Headers.XRequestedWith, "XMLHttpRequest", StringComparison.Ordinal); } - private static bool IsApiEndpoint(HttpContext context) + private static bool IsCookieRedirectDisabledByMetadata(HttpContext context) { + if (_ignoreCookieRedirectMetadata) + { + return false; + } + var endpoint = context.GetEndpoint(); - return endpoint?.Metadata.GetMetadata() is not null; + return endpoint?.Metadata.GetMetadata() is not null && + endpoint?.Metadata.GetMetadata() is null; } /// diff --git a/src/Security/Authentication/test/CookieTests.cs b/src/Security/Authentication/test/CookieTests.cs index 3dc0d638b8cf..9f209173fa35 100644 --- a/src/Security/Authentication/test/CookieTests.cs +++ b/src/Security/Authentication/test/CookieTests.cs @@ -18,6 +18,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using Microsoft.Extensions.Time.Testing; +using Microsoft.AspNetCore.Routing; namespace Microsoft.AspNetCore.Authentication.Cookies; @@ -98,7 +99,7 @@ public async Task AjaxChallengeRedirectTurnsInto200WithLocationHeader() } [Fact] - public async Task ApiEndpointChallengeReturns401WithLocationHeader() + public async Task CanConfigure401ChallengeInsteadOfRedirectWithMetadata() { using var host = await CreateHost(s => { }); using var server = host.GetTestServer(); @@ -110,7 +111,7 @@ public async Task ApiEndpointChallengeReturns401WithLocationHeader() } [Fact] - public async Task ApiEndpointForbidReturns403WithLocationHeader() + public async Task CanConfigure403ForbiddenInsteadOfRedirectWithMetadata() { using var host = await CreateHost(s => { }); using var server = host.GetTestServer(); @@ -121,6 +122,30 @@ public async Task ApiEndpointForbidReturns403WithLocationHeader() Assert.StartsWith("http://example.com/Account/AccessDenied", responded.Single()); } + [Fact] + public async Task CanReenableLoginRedirectWithMetadata() + { + using var host = await CreateHost(s => { }); + using var server = host.GetTestServer(); + var transaction = await SendAsync(server, "http://example.com/api/jk/challenge"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + var responded = transaction.Response.Headers.GetValues("Location"); + Assert.Single(responded); + Assert.StartsWith("http://example.com/Account/Login", responded.Single()); + } + + [Fact] + public async Task CanReenableAccessDeniedRedirectWithMetadata() + { + using var host = await CreateHost(s => { }); + using var server = host.GetTestServer(); + var transaction = await SendAsync(server, "http://example.com/api/jk/forbid"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + var responded = transaction.Response.Headers.GetValues("Location"); + Assert.Single(responded); + Assert.StartsWith("http://example.com/Account/AccessDenied", responded.Single()); + } + [Fact] public async Task ProtectedCustomRequestShouldRedirectToCustomRedirectUri() { @@ -1908,17 +1933,25 @@ private static async Task CreateHostWithServices(Action { - var apiRouteGroup = endpoints.MapGroup("/api").WithMetadata(new TestApiEndpointMetadata()); - // Add endpoints with IApiEndpointMetadata - apiRouteGroup.MapGet("/challenge", async context => + void AddChallengeAndForbidEndpoints(IEndpointRouteBuilder routeGroup) { - await context.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme); - }); + routeGroup.MapGet("/challenge", async context => + { + await context.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme); + }); - apiRouteGroup.MapGet("/forbid", async context => - { - await context.ForbidAsync(CookieAuthenticationDefaults.AuthenticationScheme); - }); + routeGroup.MapGet("/forbid", async context => + { + await context.ForbidAsync(CookieAuthenticationDefaults.AuthenticationScheme); + }); + } + + var apiRouteGroup = endpoints.MapGroup("/api").DisableCookieRedirect(); + AddChallengeAndForbidEndpoints(apiRouteGroup); + + // IAllowCookieRedirect always wins if present. Adding IDisableCookieRedirect before and afterwards does not override it. + var overriddenRouteGroup = apiRouteGroup.MapGroup("/jk").AllowCookieRedirect().DisableCookieRedirect(); + AddChallengeAndForbidEndpoints(overriddenRouteGroup); }); }) .ConfigureServices(services => @@ -1994,8 +2027,4 @@ private class Transaction public string ResponseText { get; set; } public XElement ResponseElement { get; set; } } - - private class TestApiEndpointMetadata : IApiEndpointMetadata - { - } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs index 664e466ccefb..094ba0058b87 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs @@ -407,21 +407,20 @@ public void Reset() _manuallySetRequestAbortToken = null; - // Lock to prevent CancelRequestAbortedToken from attempting to cancel a disposed CTS. - CancellationTokenSource? localAbortCts = null; - lock (_abortLock) { _preventRequestAbortedCancellation = false; - if (_abortedCts?.TryReset() == false) + + // If the connection has already been aborted, allow that to be observed during the next request. + if (!_connectionAborted && _abortedCts is not null) { - localAbortCts = _abortedCts; - _abortedCts = null; + // _connectionAborted is terminal and only set inside the _abortLock, so if it isn't set here, + // _abortedCts has not been canceled yet. + var resetSuccess = _abortedCts.TryReset(); + Debug.Assert(resetSuccess); } } - localAbortCts?.Dispose(); - Output?.Reset(); _requestHeadersParsed = 0; @@ -760,7 +759,7 @@ private async Task ProcessRequests(IHttpApplication applicat } else if (!HasResponseStarted) { - // If the request was aborted and no response was sent, we use status code 499 for logging + // If the request was aborted and no response was sent, we use status code 499 for logging StatusCode = StatusCodes.Status499ClientClosedRequest; } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs index da2b547c03a7..8524b35f3947 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs @@ -129,7 +129,6 @@ public bool ReceivedEmptyRequestBody protected override void OnReset() { _keepAlive = true; - _connectionAborted = false; _userTrailers = null; // Reset Http2 Features diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs index 20ec37eb3cc7..c2db22acd8bd 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs @@ -602,15 +602,16 @@ private async Task CreateHttp3Stream(ConnectionContext streamContext, // Check whether there is an existing HTTP/3 stream on the transport stream. // A stream will only be cached if the transport stream itself is reused. - if (!persistentStateFeature.State.TryGetValue(StreamPersistentStateKey, out var s)) + if (!persistentStateFeature.State.TryGetValue(StreamPersistentStateKey, out var s) || + s is not Http3Stream { CanReuse: true } reusableStream) { stream = new Http3Stream(application, CreateHttpStreamContext(streamContext)); - persistentStateFeature.State.Add(StreamPersistentStateKey, stream); + persistentStateFeature.State[StreamPersistentStateKey] = stream; } else { - stream = (Http3Stream)s!; - stream.InitializeWithExistingContext(streamContext.Transport); + stream = reusableStream; + reusableStream.InitializeWithExistingContext(streamContext.Transport); } _streamLifetimeHandler.OnStreamCreated(stream); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs index 672c566b7538..7ada6890f867 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs @@ -66,6 +66,8 @@ internal abstract partial class Http3Stream : HttpProtocol, IHttp3Stream, IHttpS private bool IsAbortedRead => (_completionState & StreamCompletionFlags.AbortedRead) == StreamCompletionFlags.AbortedRead; public bool IsCompleted => (_completionState & StreamCompletionFlags.Completed) == StreamCompletionFlags.Completed; + public bool CanReuse => !_connectionAborted && HasResponseCompleted; + public bool ReceivedEmptyRequestBody { get @@ -957,7 +959,6 @@ private Task ProcessDataFrameAsync(in ReadOnlySequence payload) protected override void OnReset() { _keepAlive = true; - _connectionAborted = false; _userTrailers = null; _isWebTransportSessionAccepted = false; _isMethodConnect = false; diff --git a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs index f76cd141bb89..e0840b525f6d 100644 --- a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs +++ b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs @@ -879,6 +879,160 @@ public async Task GET_MultipleRequestsInSequence_ReusedState() } } + [ConditionalFact] + [MsQuicSupported] + public async Task GET_RequestAbortedByClient_StateNotReused() + { + // Arrange + object persistedState = null; + var requestCount = 0; + var abortedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var requestStartedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var builder = CreateHostBuilder(async context => + { + requestCount++; + var persistentStateCollection = context.Features.Get().State; + if (persistentStateCollection.TryGetValue("Counter", out var value)) + { + persistedState = value; + } + persistentStateCollection["Counter"] = requestCount; + + if (requestCount == 1) + { + // For the first request, wait for RequestAborted to fire before returning + context.RequestAborted.Register(() => + { + Logger.LogInformation("Server received cancellation"); + abortedTcs.SetResult(); + }); + + // Signal that the request has started and is ready to be cancelled + requestStartedTcs.SetResult(); + + // Wait for the request to be aborted + await abortedTcs.Task; + } + }); + + using (var host = builder.Build()) + using (var client = HttpHelpers.CreateClient()) + { + await host.StartAsync(); + + // Act - Send first request and cancel it + var cts1 = new CancellationTokenSource(); + var request1 = new HttpRequestMessage(HttpMethod.Get, $"https://127.0.0.1:{host.GetPort()}/"); + request1.Version = HttpVersion.Version30; + request1.VersionPolicy = HttpVersionPolicy.RequestVersionExact; + + var responseTask1 = client.SendAsync(request1, cts1.Token); + + // Wait for the server to start processing the request + await requestStartedTcs.Task.DefaultTimeout(); + + // Cancel the first request + cts1.Cancel(); + await Assert.ThrowsAnyAsync(() => responseTask1).DefaultTimeout(); + + // Wait for the server to process the abort + await abortedTcs.Task.DefaultTimeout(); + + // Store the state from the first (aborted) request + var firstRequestState = persistedState; + + // Delay to ensure the stream has enough time to return to pool + await Task.Delay(100); + + // Send second request (should not reuse state from aborted request) + var request2 = new HttpRequestMessage(HttpMethod.Get, $"https://127.0.0.1:{host.GetPort()}/"); + request2.Version = HttpVersion.Version30; + request2.VersionPolicy = HttpVersionPolicy.RequestVersionExact; + + var response2 = await client.SendAsync(request2, CancellationToken.None); + response2.EnsureSuccessStatusCode(); + var secondRequestState = persistedState; + + // Assert + // First request has no persisted state (it was aborted) + Assert.Null(firstRequestState); + + // Second request should also have no persisted state since the first request was aborted + // and state should not be reused from aborted requests + Assert.Null(secondRequestState); + + await host.StopAsync(); + } + } + + [ConditionalFact] + [MsQuicSupported] + public async Task GET_RequestAbortedByServer_StateNotReused() + { + // Arrange + object persistedState = null; + var requestCount = 0; + + var builder = CreateHostBuilder(context => + { + requestCount++; + var persistentStateCollection = context.Features.Get().State; + if (persistentStateCollection.TryGetValue("Counter", out var value)) + { + persistedState = value; + } + persistentStateCollection["Counter"] = requestCount; + + if (requestCount == 1) + { + context.Abort(); + } + + return Task.CompletedTask; + }); + + using (var host = builder.Build()) + using (var client = HttpHelpers.CreateClient()) + { + await host.StartAsync(); + + var request1 = new HttpRequestMessage(HttpMethod.Get, $"https://127.0.0.1:{host.GetPort()}/"); + request1.Version = HttpVersion.Version30; + request1.VersionPolicy = HttpVersionPolicy.RequestVersionExact; + + var responseTask1 = client.SendAsync(request1, CancellationToken.None); + var ex = await Assert.ThrowsAnyAsync(() => responseTask1).DefaultTimeout(); + var innerEx = Assert.IsType(ex.InnerException); + Assert.Equal(Http3ErrorCode.InternalError, (Http3ErrorCode)innerEx.ErrorCode); + + // Store the state from the first (aborted) request + var firstRequestState = persistedState; + + // Delay to ensure the stream has enough time to return to pool + await Task.Delay(100); + + // Send second request (should not reuse state from aborted request) + var request2 = new HttpRequestMessage(HttpMethod.Get, $"https://127.0.0.1:{host.GetPort()}/"); + request2.Version = HttpVersion.Version30; + request2.VersionPolicy = HttpVersionPolicy.RequestVersionExact; + + var response2 = await client.SendAsync(request2, CancellationToken.None); + response2.EnsureSuccessStatusCode(); + var secondRequestState = persistedState; + + // Assert + // First request has no persisted state (it was aborted) + Assert.Null(firstRequestState); + + // Second request should also have no persisted state since the first request was aborted + // and state should not be reused from aborted requests + Assert.Null(secondRequestState); + + await host.StopAsync(); + } + } + [ConditionalFact] [MsQuicSupported] public async Task GET_MultipleRequests_RequestVersionOrHigher_UpgradeToHttp3() diff --git a/src/Shared/Diagnostics/ActivityCreator.cs b/src/Shared/Diagnostics/ActivityCreator.cs index 170e9cff267d..9960ff91be84 100644 --- a/src/Shared/Diagnostics/ActivityCreator.cs +++ b/src/Shared/Diagnostics/ActivityCreator.cs @@ -7,6 +7,11 @@ namespace Microsoft.AspNetCore.Shared; internal static class ActivityCreator { + public static bool IsActivityCreated(ActivitySource activitySource, bool diagnosticsOrLoggingEnabled) + { + return activitySource.HasListeners() || diagnosticsOrLoggingEnabled; + } + /// /// Create an activity with details received from a remote source. /// diff --git a/src/Shared/Diagnostics/RouteDiagnosticsHelpers.cs b/src/Shared/Diagnostics/RouteDiagnosticsHelpers.cs new file mode 100644 index 000000000000..3084f3caf57d --- /dev/null +++ b/src/Shared/Diagnostics/RouteDiagnosticsHelpers.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Shared; + +internal static class RouteDiagnosticsHelpers +{ + public static string ResolveHttpRoute(string route) + { + // A route that matches the root of the website could be an empty string. This is problematic. + // 1. It is potentially confusing, "What does empty string mean?" + // 2. Some telemetry tools have problems with empty string values, e.g. https://github.com/dotnet/aspnetcore/pull/62432 + // + // The fix is to resolve empty string route to "/" in metrics. + return string.IsNullOrEmpty(route) ? "/" : route; + } +} diff --git a/src/SignalR/clients/ts/signalr/src/HubConnection.ts b/src/SignalR/clients/ts/signalr/src/HubConnection.ts index 2487d0f0b558..53fc6fafa069 100644 --- a/src/SignalR/clients/ts/signalr/src/HubConnection.ts +++ b/src/SignalR/clients/ts/signalr/src/HubConnection.ts @@ -721,10 +721,19 @@ export class HubConnection { // Set the timeout timer this._timeoutHandle = setTimeout(() => this.serverTimeout(), this.serverTimeoutInMilliseconds); + // Immediately fire Keep-Alive ping if nextPing is overdue to avoid dependency on JS timers + let nextPing = this._nextKeepAlive - new Date().getTime(); + if (nextPing < 0) { + if (this._connectionState === HubConnectionState.Connected) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this._trySendPingMessage(); + } + return; + } + // Set keepAlive timer if there isn't one if (this._pingServerHandle === undefined) { - let nextPing = this._nextKeepAlive - new Date().getTime(); if (nextPing < 0) { nextPing = 0; } @@ -732,13 +741,7 @@ export class HubConnection { // The timer needs to be set from a networking callback to avoid Chrome timer throttling from causing timers to run once a minute this._pingServerHandle = setTimeout(async () => { if (this._connectionState === HubConnectionState.Connected) { - try { - await this._sendMessage(this._cachedPingMessage); - } catch { - // We don't care about the error. It should be seen elsewhere in the client. - // The connection is probably in a bad or closed state now, cleanup the timer so it stops triggering - this._cleanupPingTimer(); - } + await this._trySendPingMessage(); } }, nextPing); } @@ -1149,4 +1152,14 @@ export class HubConnection { private _createCloseMessage(): CloseMessage { return { type: MessageType.Close }; } + + private async _trySendPingMessage(): Promise { + try { + await this._sendMessage(this._cachedPingMessage); + } catch { + // We don't care about the error. It should be seen elsewhere in the client. + // The connection is probably in a bad or closed state now, cleanup the timer so it stops triggering + this._cleanupPingTimer(); + } + } } diff --git a/src/SignalR/common/Http.Connections/src/ConnectionEndpointRouteBuilderExtensions.cs b/src/SignalR/common/Http.Connections/src/ConnectionEndpointRouteBuilderExtensions.cs index f1aad8649e82..cd094cbf300e 100644 --- a/src/SignalR/common/Http.Connections/src/ConnectionEndpointRouteBuilderExtensions.cs +++ b/src/SignalR/common/Http.Connections/src/ConnectionEndpointRouteBuilderExtensions.cs @@ -127,8 +127,8 @@ public static ConnectionEndpointRouteBuilder MapConnections(this IEndpointRouteB e.Metadata.Add(data); } - // Add IApiEndpointMetadata to indicate this is a non-browser endpoint (SignalR) - e.Metadata.Add(ApiEndpointMetadata.Instance); + // Add IDisableCookieRedirectMetadata to indicate this is a non-browser endpoint (SignalR) + e.Metadata.Add(DisableCookieRedirectMetadata.Instance); }); return new ConnectionEndpointRouteBuilder(compositeConventionBuilder); @@ -160,8 +160,8 @@ public void Finally(Action finalConvention) } } - private sealed class ApiEndpointMetadata : IApiEndpointMetadata + private sealed class DisableCookieRedirectMetadata : IDisableCookieRedirectMetadata { - public static ApiEndpointMetadata Instance { get; } = new(); + public static DisableCookieRedirectMetadata Instance { get; } = new(); } } diff --git a/src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs b/src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs index 45b1426a1955..513e376aee81 100644 --- a/src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs +++ b/src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs @@ -72,7 +72,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => ValidationAttributeCache.GetPropertyValidationAttributes(ContainingType, Name); } {{GeneratedCodeAttribute}} @@ -81,7 +81,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } {{GeneratedCodeAttribute}} @@ -128,15 +137,17 @@ private sealed record CacheKey( [property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type ContainingType, string PropertyName); - private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _propertyCache = new(); + private static readonly global::System.Lazy> _lazyTypeCache = new (() => new ()); + private static global::System.Collections.Concurrent.ConcurrentDictionary TypeCache => _lazyTypeCache.Value; - public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetPropertyValidationAttributes( [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type containingType, string propertyName) { var key = new CacheKey(containingType, propertyName); - return _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); @@ -173,6 +184,20 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } """; diff --git a/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs b/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs index f0f453c7fec0..7544c9d72feb 100644 --- a/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs +++ b/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs @@ -82,6 +82,8 @@ internal bool TryExtractValidatableType(ITypeSymbol incomingTypeSymbol, WellKnow visitedTypes.Add(typeSymbol); + var hasValidationAttributes = HasValidationAttributes(typeSymbol, wellKnownTypes); + // Extract validatable types discovered in base types of this type and add them to the top-level list. var current = typeSymbol.BaseType; var hasValidatableBaseType = false; @@ -107,7 +109,7 @@ internal bool TryExtractValidatableType(ITypeSymbol incomingTypeSymbol, WellKnow } // No validatable members or derived types found, so we don't need to add this type. - if (members.IsDefaultOrEmpty && !hasValidatableBaseType && !hasValidatableDerivedTypes) + if (members.IsDefaultOrEmpty && !hasValidationAttributes && !hasValidatableBaseType && !hasValidatableDerivedTypes) { return false; } @@ -283,4 +285,20 @@ internal static ImmutableArray ExtractValidationAttributes( NamedArguments: attribute.NamedArguments.ToDictionary(namedArgument => namedArgument.Key, namedArgument => namedArgument.Value.ToCSharpString()), IsCustomValidationAttribute: SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_ComponentModel_DataAnnotations_CustomValidationAttribute))))]; } + + internal static bool HasValidationAttributes(ISymbol symbol, WellKnownTypes wellKnownTypes) + { + var validationAttributeSymbol = wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_ComponentModel_DataAnnotations_ValidationAttribute); + + foreach (var attribute in symbol.GetAttributes()) + { + if (attribute.AttributeClass is not null && + attribute.AttributeClass.ImplementsValidationAttribute(validationAttributeSymbol)) + { + return true; + } + } + + return false; + } } diff --git a/src/Validation/src/PublicAPI.Unshipped.txt b/src/Validation/src/PublicAPI.Unshipped.txt index e2e20423b5ea..e67bfe44dd6d 100644 --- a/src/Validation/src/PublicAPI.Unshipped.txt +++ b/src/Validation/src/PublicAPI.Unshipped.txt @@ -1,4 +1,5 @@ #nullable enable +abstract Microsoft.Extensions.Validation.ValidatableTypeInfo.GetValidationAttributes() -> System.ComponentModel.DataAnnotations.ValidationAttribute![]! Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions Microsoft.Extensions.Validation.IValidatableInfo Microsoft.Extensions.Validation.IValidatableInfo.ValidateAsync(object? value, Microsoft.Extensions.Validation.ValidateContext! context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! diff --git a/src/Validation/src/ValidatableTypeInfo.cs b/src/Validation/src/ValidatableTypeInfo.cs index 8852f674a7e0..e409778f1f1d 100644 --- a/src/Validation/src/ValidatableTypeInfo.cs +++ b/src/Validation/src/ValidatableTypeInfo.cs @@ -14,7 +14,7 @@ namespace Microsoft.Extensions.Validation; public abstract class ValidatableTypeInfo : IValidatableInfo { private readonly int _membersCount; - private readonly List _subTypes; + private readonly List _superTypes; /// /// Creates a new instance of . @@ -28,9 +28,15 @@ protected ValidatableTypeInfo( Type = type; Members = members; _membersCount = members.Count; - _subTypes = type.GetAllImplementedTypes(); + _superTypes = type.GetAllImplementedTypes(); } + /// + /// Gets the validation attributes for this member. + /// + /// An array of validation attributes to apply to this member. + protected abstract ValidationAttribute[] GetValidationAttributes(); + /// /// The type being validated. /// @@ -59,75 +65,139 @@ public virtual async Task ValidateAsync(object? value, ValidateContext context, } var originalPrefix = context.CurrentValidationPath; + var originalErrorCount = context.ValidationErrors?.Count ?? 0; try { + // First validate direct members + await ValidateMembersAsync(value, context, cancellationToken); + var actualType = value.GetType(); - // First validate members - for (var i = 0; i < _membersCount; i++) + // Then validate inherited members + foreach (var superTypeInfo in GetSuperTypeInfos(actualType, context)) + { + await superTypeInfo.ValidateMembersAsync(value, context, cancellationToken); + } + + // If any property-level validation errors were found, return early + if (context.ValidationErrors is not null && context.ValidationErrors.Count > originalErrorCount) + { + return; + } + + // Validate type-level attributes + ValidateTypeAttributes(value, context); + + // If any type-level attribute errors were found, return early + if (context.ValidationErrors is not null && context.ValidationErrors.Count > originalErrorCount) + { + return; + } + + // Finally validate IValidatableObject if implemented + ValidateValidatableObjectInterface(value, context); + } + finally + { + context.CurrentValidationPath = originalPrefix; + } + } + + private async Task ValidateMembersAsync(object? value, ValidateContext context, CancellationToken cancellationToken) + { + var originalPrefix = context.CurrentValidationPath; + + for (var i = 0; i < _membersCount; i++) + { + try { await Members[i].ValidateAsync(value, context, cancellationToken); + + } + finally + { context.CurrentValidationPath = originalPrefix; } + } + } + + private void ValidateTypeAttributes(object? value, ValidateContext context) + { + var validationAttributes = GetValidationAttributes(); + var errorPrefix = context.CurrentValidationPath; - // Then validate sub-types if any - foreach (var subType in _subTypes) + for (var i = 0; i < validationAttributes.Length; i++) + { + var attribute = validationAttributes[i]; + var result = attribute.GetValidationResult(value, context.ValidationContext); + if (result is not null && result != ValidationResult.Success && result.ErrorMessage is not null) { - // Check if the actual type is assignable to the sub-type - // and validate it if it is - if (subType.IsAssignableFrom(actualType)) + // Create a validation error for each member name that is provided + foreach (var memberName in result.MemberNames) { - if (context.ValidationOptions.TryGetValidatableTypeInfo(subType, out var subTypeInfo)) - { - await subTypeInfo.ValidateAsync(value, context, cancellationToken); - context.CurrentValidationPath = originalPrefix; - } + var key = string.IsNullOrEmpty(errorPrefix) ? memberName : $"{errorPrefix}.{memberName}"; + context.AddOrExtendValidationError(memberName, key, result.ErrorMessage, value); + } + + if (!result.MemberNames.Any()) + { + // If no member names are specified, then treat this as a top-level error + context.AddOrExtendValidationError(string.Empty, errorPrefix, result.ErrorMessage, value); } } + } + } - // Finally validate IValidatableObject if implemented - if (Type.ImplementsInterface(typeof(IValidatableObject)) && value is IValidatableObject validatable) + private void ValidateValidatableObjectInterface(object? value, ValidateContext context) + { + if (Type.ImplementsInterface(typeof(IValidatableObject)) && value is IValidatableObject validatable) + { + // Important: Set the DisplayName to the type name for top-level validations + // and restore the original validation context properties + var originalDisplayName = context.ValidationContext.DisplayName; + var originalMemberName = context.ValidationContext.MemberName; + var errorPrefix = context.CurrentValidationPath; + + // Set the display name to the class name for IValidatableObject validation + context.ValidationContext.DisplayName = Type.Name; + context.ValidationContext.MemberName = null; + + var validationResults = validatable.Validate(context.ValidationContext); + foreach (var validationResult in validationResults) { - // Important: Set the DisplayName to the type name for top-level validations - // and restore the original validation context properties - var originalDisplayName = context.ValidationContext.DisplayName; - var originalMemberName = context.ValidationContext.MemberName; - - // Set the display name to the class name for IValidatableObject validation - context.ValidationContext.DisplayName = Type.Name; - context.ValidationContext.MemberName = null; - - var validationResults = validatable.Validate(context.ValidationContext); - foreach (var validationResult in validationResults) + if (validationResult != ValidationResult.Success && validationResult.ErrorMessage is not null) { - if (validationResult != ValidationResult.Success && validationResult.ErrorMessage is not null) + // Create a validation error for each member name that is provided + foreach (var memberName in validationResult.MemberNames) { - // Create a validation error for each member name that is provided - foreach (var memberName in validationResult.MemberNames) - { - var key = string.IsNullOrEmpty(originalPrefix) ? - memberName : - $"{originalPrefix}.{memberName}"; - context.AddOrExtendValidationError(memberName, key, validationResult.ErrorMessage, value); - } - - if (!validationResult.MemberNames.Any()) - { - // If no member names are specified, then treat this as a top-level error - context.AddOrExtendValidationError(string.Empty, string.Empty, validationResult.ErrorMessage, value); - } + var key = string.IsNullOrEmpty(errorPrefix) ? memberName : $"{errorPrefix}.{memberName}"; + context.AddOrExtendValidationError(memberName, key, validationResult.ErrorMessage, value); } - } - // Restore the original validation context properties - context.ValidationContext.DisplayName = originalDisplayName; - context.ValidationContext.MemberName = originalMemberName; + if (!validationResult.MemberNames.Any()) + { + // If no member names are specified, then treat this as a top-level error + context.AddOrExtendValidationError(string.Empty, string.Empty, validationResult.ErrorMessage, value); + } + } } + + // Restore the original validation context properties + context.ValidationContext.DisplayName = originalDisplayName; + context.ValidationContext.MemberName = originalMemberName; } - finally + } + + private IEnumerable GetSuperTypeInfos(Type actualType, ValidateContext context) + { + foreach (var superType in _superTypes.Where(t => t.IsAssignableFrom(actualType))) { - context.CurrentValidationPath = originalPrefix; + if (context.ValidationOptions.TryGetValidatableTypeInfo(superType, out var found) + && found is ValidatableTypeInfo superTypeInfo) + { + yield return superTypeInfo; + } } } } diff --git a/src/Validation/startvscode.sh b/src/Validation/startvscode.sh old mode 100644 new mode 100755 diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ClassAttributes.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ClassAttributes.cs new file mode 100644 index 000000000000..ad2c5ab14b6f --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ClassAttributes.cs @@ -0,0 +1,170 @@ +#pragma warning disable ASP0029 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Validation; + +namespace Microsoft.Extensions.Validation.GeneratorTests; + +public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase +{ + [Fact] + public async Task CanValidateValidationAttributesOnClasses() + { + var source = """ +#pragma warning disable ASP0029 + +using System; +using System.ComponentModel.DataAnnotations; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Validation; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(); + +builder.Services.AddValidation(); + +var app = builder.Build(); + +app.Run(); + +[ValidatableType] +[SumLimit] +public class ComplexType : IPoint +{ + [Range(0, 15)] + public int X { get; set; } = 10; + + [Range(0, 15)] + public int Y { get; set; } = 10; + + public NestedType ObjectProperty { get; set; } = new NestedType(); +} + +// This class does not have any property-level validation attributes, but it has a class-level validation attribute. +// Therefore, its type info should still be emitted in the generator output. +[SumLimit] +public class NestedType : IPoint +{ + public int X { get; set; } = 10; + + public int Y { get; set; } = 10; +} + +public interface IPoint +{ + int X { get; } + int Y { get; } +} + +public class SumLimitAttribute : ValidationAttribute +{ + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) + { + if (value is IPoint point) + { + if (point.X + point.Y > 20) + { + return new ValidationResult($"Sum is too high"); + } + } + return ValidationResult.Success; + } +} +"""; + await Verify(source, out var compilation); + await VerifyValidatableType(compilation, "ComplexType", async (validationOptions, type) => + { + Assert.True(validationOptions.TryGetValidatableTypeInfo(type, out var validatableTypeInfo)); + + await InvalidPropertyAttributeCheck_ProducesError_AndShortCircuits(validatableTypeInfo); + await ValidClassAttributeCheck_DoesNotProduceError(validatableTypeInfo); + await InvalidClassAttributeCheck_ProducesError(validatableTypeInfo); + await InvalidNestedClassAttributeCheck_ProducesError_AndShortCircuits(validatableTypeInfo); + + async Task InvalidPropertyAttributeCheck_ProducesError_AndShortCircuits(IValidatableInfo validatableInfo) + { + var instance = Activator.CreateInstance(type); + type.GetProperty("X")?.SetValue(instance, 16); + type.GetProperty("Y")?.SetValue(instance, 0); + + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = new ValidationContext(instance) + }; + + await validatableTypeInfo.ValidateAsync(instance, context, CancellationToken.None); + + Assert.NotNull(context.ValidationErrors); + var propertyAttributeError = Assert.Single(context.ValidationErrors); + Assert.Equal("X", propertyAttributeError.Key); + Assert.Equal("The field X must be between 0 and 15.", propertyAttributeError.Value.Single()); + } + + async Task ValidClassAttributeCheck_DoesNotProduceError(IValidatableInfo validatableInfo) + { + var instance = Activator.CreateInstance(type); + + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = new ValidationContext(instance) + }; + + await validatableTypeInfo.ValidateAsync(instance, context, CancellationToken.None); + + Assert.Null(context.ValidationErrors); + } + + async Task InvalidClassAttributeCheck_ProducesError(IValidatableInfo validatableInfo) + { + var instance = Activator.CreateInstance(type); + type.GetProperty("X")?.SetValue(instance, 11); + type.GetProperty("Y")?.SetValue(instance, 12); + + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = new ValidationContext(instance) + }; + + await validatableTypeInfo.ValidateAsync(instance, context, CancellationToken.None); + + Assert.NotNull(context.ValidationErrors); + var classAttributeError = Assert.Single(context.ValidationErrors); + Assert.Equal(string.Empty, classAttributeError.Key); + Assert.Equal("Sum is too high", classAttributeError.Value.Single()); + } + + async Task InvalidNestedClassAttributeCheck_ProducesError_AndShortCircuits(IValidatableInfo validatableInfo) + { + var instance = Activator.CreateInstance(type); + var objectPropertyInstance = type.GetProperty("ObjectProperty").GetValue(instance); + objectPropertyInstance.GetType().GetProperty("X")?.SetValue(objectPropertyInstance, 11); + objectPropertyInstance.GetType().GetProperty("Y")?.SetValue(objectPropertyInstance, 12); + + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = new ValidationContext(instance) + }; + + await validatableTypeInfo.ValidateAsync(instance, context, CancellationToken.None); + + Assert.NotNull(context.ValidationErrors); + var classAttributeError = Assert.Single(context.ValidationErrors); + Assert.Equal("ObjectProperty", classAttributeError.Key); + Assert.Equal("Sum is too high", classAttributeError.Value.Single()); + } + }); + } +} diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.IValidatableObject.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.IValidatableObject.cs index 590195468298..ea4c7c48fb63 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.IValidatableObject.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.IValidatableObject.cs @@ -104,11 +104,10 @@ public class TestService await Verify(source, out var compilation); await VerifyEndpoint(compilation, "/validatable-object", async (endpoint, serviceProvider) => { - await ValidateMethodCalledIfPropertyValidationsFail(); - await ValidateForSubtypeInvokedFirst(); + await ValidateMethodNotCalledIfPropertyValidationsFail(); await ValidateForTopLevelInvoked(); - async Task ValidateMethodCalledIfPropertyValidationsFail() + async Task ValidateMethodNotCalledIfPropertyValidationsFail() { var httpContext = CreateHttpContextWithPayload(""" { @@ -136,46 +135,6 @@ async Task ValidateMethodCalledIfPropertyValidationsFail() { Assert.Equal("SubType.RequiredProperty", error.Key); Assert.Equal("The RequiredProperty field is required.", error.Value.Single()); - }, - error => - { - Assert.Equal("SubType.Value3", error.Key); - Assert.Equal("The field ValidatableSubType must be 'some-value'.", error.Value.Single()); - }, - error => - { - Assert.Equal("Value1", error.Key); - Assert.Equal("The field Value1 must be between 10 and 100.", error.Value.Single()); - }); - } - - async Task ValidateForSubtypeInvokedFirst() - { - var httpContext = CreateHttpContextWithPayload(""" - { - "Value1": 5, - "Value2": "test@test.com", - "SubType": { - "Value3": "foo", - "RequiredProperty": "some-value-2", - "StringWithLength": "element" - } - } - """, serviceProvider); - - await endpoint.RequestDelegate(httpContext); - - var problemDetails = await AssertBadRequest(httpContext); - Assert.Collection(problemDetails.Errors, - error => - { - Assert.Equal("SubType.Value3", error.Key); - Assert.Equal("The field ValidatableSubType must be 'some-value'.", error.Value.Single()); - }, - error => - { - Assert.Equal("Value1", error.Key); - Assert.Equal("The field Value1 must be between 10 and 100.", error.Value.Single()); }); } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.Polymorphism.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.Polymorphism.cs index 39dbec97a78c..206908d1a059 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.Polymorphism.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.Polymorphism.cs @@ -129,11 +129,6 @@ await VerifyEndpoint(compilation, "/validatable-polymorphism", async (endpoint, { Assert.Equal("Value3", error.Key); Assert.Equal("The Value3 field is not a valid e-mail address.", error.Value.Single()); - }, - error => - { - Assert.Equal("Value1", error.Key); - Assert.Equal("The field Value 1 must be between 10 and 100.", error.Value.Single()); }); httpContext = CreateHttpContextWithPayload(""" diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanDiscoverGeneratedValidatableTypeAttribute#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanDiscoverGeneratedValidatableTypeAttribute#ValidatableInfoResolver.g.verified.cs index 226fc69f7d3a..528d05df6fae 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanDiscoverGeneratedValidatableTypeAttribute#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanDiscoverGeneratedValidatableTypeAttribute#ValidatableInfoResolver.g.verified.cs @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => ValidationAttributeCache.GetPropertyValidationAttributes(ContainingType, Name); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -121,15 +130,17 @@ private sealed record CacheKey( [property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type ContainingType, string PropertyName); - private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _propertyCache = new(); + private static readonly global::System.Lazy> _lazyTypeCache = new (() => new ()); + private static global::System.Collections.Concurrent.ConcurrentDictionary TypeCache => _lazyTypeCache.Value; - public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetPropertyValidationAttributes( [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type containingType, string propertyName) { var key = new CacheKey(containingType, propertyName); - return _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); @@ -166,5 +177,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanUseBothFrameworkAndGeneratedValidatableTypeAttributes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanUseBothFrameworkAndGeneratedValidatableTypeAttributes#ValidatableInfoResolver.g.verified.cs index 20a3fe742bd2..7d5887992ec3 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanUseBothFrameworkAndGeneratedValidatableTypeAttributes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanUseBothFrameworkAndGeneratedValidatableTypeAttributes#ValidatableInfoResolver.g.verified.cs @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => ValidationAttributeCache.GetPropertyValidationAttributes(ContainingType, Name); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -142,15 +151,17 @@ private sealed record CacheKey( [property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type ContainingType, string PropertyName); - private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _propertyCache = new(); + private static readonly global::System.Lazy> _lazyTypeCache = new (() => new ()); + private static global::System.Collections.Concurrent.ConcurrentDictionary TypeCache => _lazyTypeCache.Value; - public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetPropertyValidationAttributes( [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type containingType, string propertyName) { var key = new CacheKey(containingType, propertyName); - return _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); @@ -187,5 +198,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateClassTypesWithAttribute#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateClassTypesWithAttribute#ValidatableInfoResolver.g.verified.cs index bba18fd6b92f..9043384c0644 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateClassTypesWithAttribute#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateClassTypesWithAttribute#ValidatableInfoResolver.g.verified.cs @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => ValidationAttributeCache.GetPropertyValidationAttributes(ContainingType, Name); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -193,15 +202,17 @@ private sealed record CacheKey( [property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type ContainingType, string PropertyName); - private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _propertyCache = new(); + private static readonly global::System.Lazy> _lazyTypeCache = new (() => new ()); + private static global::System.Collections.Concurrent.ConcurrentDictionary TypeCache => _lazyTypeCache.Value; - public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetPropertyValidationAttributes( [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type containingType, string propertyName) { var key = new CacheKey(containingType, propertyName); - return _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); @@ -238,5 +249,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs index 538c884403fe..dc941b30b736 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => ValidationAttributeCache.GetPropertyValidationAttributes(ContainingType, Name); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -199,15 +208,17 @@ private sealed record CacheKey( [property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type ContainingType, string PropertyName); - private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _propertyCache = new(); + private static readonly global::System.Lazy> _lazyTypeCache = new (() => new ()); + private static global::System.Collections.Concurrent.ConcurrentDictionary TypeCache => _lazyTypeCache.Value; - public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetPropertyValidationAttributes( [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type containingType, string propertyName) { var key = new CacheKey(containingType, propertyName); - return _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); @@ -244,5 +255,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypesWithJsonIgnore#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypesWithJsonIgnore#ValidatableInfoResolver.g.verified.cs index 496844084845..c6957d8a67ec 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypesWithJsonIgnore#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypesWithJsonIgnore#ValidatableInfoResolver.g.verified.cs @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => ValidationAttributeCache.GetPropertyValidationAttributes(ContainingType, Name); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -130,15 +139,17 @@ private sealed record CacheKey( [property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type ContainingType, string PropertyName); - private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _propertyCache = new(); + private static readonly global::System.Lazy> _lazyTypeCache = new (() => new ()); + private static global::System.Collections.Concurrent.ConcurrentDictionary TypeCache => _lazyTypeCache.Value; - public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetPropertyValidationAttributes( [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type containingType, string propertyName) { var key = new CacheKey(containingType, propertyName); - return _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); @@ -175,5 +186,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs index 34303dad9370..7c3d76702b7c 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => ValidationAttributeCache.GetPropertyValidationAttributes(ContainingType, Name); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -150,15 +159,17 @@ private sealed record CacheKey( [property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type ContainingType, string PropertyName); - private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _propertyCache = new(); + private static readonly global::System.Lazy> _lazyTypeCache = new (() => new ()); + private static global::System.Collections.Concurrent.ConcurrentDictionary TypeCache => _lazyTypeCache.Value; - public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetPropertyValidationAttributes( [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type containingType, string propertyName) { var key = new CacheKey(containingType, propertyName); - return _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); @@ -195,5 +206,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs index b77ed0ac109d..358791ffd09d 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => ValidationAttributeCache.GetPropertyValidationAttributes(ContainingType, Name); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -130,15 +139,17 @@ private sealed record CacheKey( [property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type ContainingType, string PropertyName); - private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _propertyCache = new(); + private static readonly global::System.Lazy> _lazyTypeCache = new (() => new ()); + private static global::System.Collections.Concurrent.ConcurrentDictionary TypeCache => _lazyTypeCache.Value; - public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetPropertyValidationAttributes( [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type containingType, string propertyName) { var key = new CacheKey(containingType, propertyName); - return _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); @@ -175,5 +186,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs index 94c9338a1ab0..81e8cc0ba5a2 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => ValidationAttributeCache.GetPropertyValidationAttributes(ContainingType, Name); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -130,15 +139,17 @@ private sealed record CacheKey( [property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type ContainingType, string PropertyName); - private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _propertyCache = new(); + private static readonly global::System.Lazy> _lazyTypeCache = new (() => new ()); + private static global::System.Collections.Concurrent.ConcurrentDictionary TypeCache => _lazyTypeCache.Value; - public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetPropertyValidationAttributes( [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type containingType, string propertyName) { var key = new CacheKey(containingType, propertyName); - return _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); @@ -175,5 +186,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs index 0d90e4a481aa..96f7a763268f 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => ValidationAttributeCache.GetPropertyValidationAttributes(ContainingType, Name); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -180,15 +189,17 @@ private sealed record CacheKey( [property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type ContainingType, string PropertyName); - private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _propertyCache = new(); + private static readonly global::System.Lazy> _lazyTypeCache = new (() => new ()); + private static global::System.Collections.Concurrent.ConcurrentDictionary TypeCache => _lazyTypeCache.Value; - public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetPropertyValidationAttributes( [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type containingType, string propertyName) { var key = new CacheKey(containingType, propertyName); - return _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); @@ -225,5 +236,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs index 786e16b5bca4..e308777967aa 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => ValidationAttributeCache.GetPropertyValidationAttributes(ContainingType, Name); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -226,15 +235,17 @@ private sealed record CacheKey( [property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type ContainingType, string PropertyName); - private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _propertyCache = new(); + private static readonly global::System.Lazy> _lazyTypeCache = new (() => new ()); + private static global::System.Collections.Concurrent.ConcurrentDictionary TypeCache => _lazyTypeCache.Value; - public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetPropertyValidationAttributes( [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type containingType, string propertyName) { var key = new CacheKey(containingType, propertyName); - return _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); @@ -271,5 +282,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypesWithAttribute#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypesWithAttribute#ValidatableInfoResolver.g.verified.cs index bba18fd6b92f..9043384c0644 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypesWithAttribute#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypesWithAttribute#ValidatableInfoResolver.g.verified.cs @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => ValidationAttributeCache.GetPropertyValidationAttributes(ContainingType, Name); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -193,15 +202,17 @@ private sealed record CacheKey( [property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type ContainingType, string PropertyName); - private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _propertyCache = new(); + private static readonly global::System.Lazy> _lazyTypeCache = new (() => new ()); + private static global::System.Collections.Concurrent.ConcurrentDictionary TypeCache => _lazyTypeCache.Value; - public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetPropertyValidationAttributes( [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type containingType, string propertyName) { var key = new CacheKey(containingType, propertyName); - return _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); @@ -238,5 +249,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs index b267e4669007..45888276cc09 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => ValidationAttributeCache.GetPropertyValidationAttributes(ContainingType, Name); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -121,15 +130,17 @@ private sealed record CacheKey( [property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type ContainingType, string PropertyName); - private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _propertyCache = new(); + private static readonly global::System.Lazy> _lazyTypeCache = new (() => new ()); + private static global::System.Collections.Concurrent.ConcurrentDictionary TypeCache => _lazyTypeCache.Value; - public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetPropertyValidationAttributes( [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type containingType, string propertyName) { var key = new CacheKey(containingType, propertyName); - return _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); @@ -166,5 +177,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs index 18c30835da75..4664a3bb4b43 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => ValidationAttributeCache.GetPropertyValidationAttributes(ContainingType, Name); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -163,15 +172,17 @@ private sealed record CacheKey( [property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type ContainingType, string PropertyName); - private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _propertyCache = new(); + private static readonly global::System.Lazy> _lazyTypeCache = new (() => new ()); + private static global::System.Collections.Concurrent.ConcurrentDictionary TypeCache => _lazyTypeCache.Value; - public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetPropertyValidationAttributes( [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type containingType, string propertyName) { var key = new CacheKey(containingType, propertyName); - return _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); @@ -208,5 +219,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateValidationAttributesOnClasses#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateValidationAttributesOnClasses#ValidatableInfoResolver.g.verified.cs new file mode 100644 index 000000000000..932d48d62b1f --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateValidationAttributesOnClasses#ValidatableInfoResolver.g.verified.cs @@ -0,0 +1,209 @@ +//HintName: ValidatableInfoResolver.g.cs +#nullable enable annotations +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable +#pragma warning disable ASP0029 + +namespace System.Runtime.CompilerServices +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : System.Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace Microsoft.Extensions.Validation.Generated +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.Extensions.Validation.ValidatablePropertyInfo + { + public GeneratedValidatablePropertyInfo( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + global::System.Type containingType, + global::System.Type propertyType, + string name, + string displayName) : base(containingType, propertyType, name, displayName) + { + ContainingType = containingType; + Name = name; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + internal global::System.Type ContainingType { get; } + internal string Name { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetPropertyValidationAttributes(ContainingType, Name); + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class GeneratedValidatableTypeInfo : global::Microsoft.Extensions.Validation.ValidatableTypeInfo + { + public GeneratedValidatableTypeInfo( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type, + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file class GeneratedValidatableInfoResolver : global::Microsoft.Extensions.Validation.IValidatableInfoResolver + { + public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) + { + validatableInfo = null; + if (type == typeof(global::NestedType)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::NestedType), + members: [] + ); + return true; + } + if (type == typeof(global::ComplexType)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::ComplexType), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexType), + propertyType: typeof(int), + name: "X", + displayName: "X" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexType), + propertyType: typeof(int), + name: "Y", + displayName: "Y" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexType), + propertyType: typeof(global::NestedType), + name: "ObjectProperty", + displayName: "ObjectProperty" + ), + ] + ); + return true; + } + + return false; + } + + // No-ops, rely on runtime code for ParameterInfo-based resolution + public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) + { + validatableInfo = null; + return false; + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file static class GeneratedServiceCollectionExtensions + { + [InterceptsLocation] + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidation(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::System.Action? configureOptions = null) + { + // Use non-extension method to avoid infinite recursion. + return global::Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(services, options => + { + options.Resolvers.Insert(0, new GeneratedValidatableInfoResolver()); + if (configureOptions is not null) + { + configureOptions(options); + } + }); + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file static class ValidationAttributeCache + { + private sealed record CacheKey( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + global::System.Type ContainingType, + string PropertyName); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _propertyCache = new(); + private static readonly global::System.Lazy> _lazyTypeCache = new (() => new ()); + private static global::System.Collections.Concurrent.ConcurrentDictionary TypeCache => _lazyTypeCache.Value; + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetPropertyValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + global::System.Type containingType, + string propertyName) + { + var key = new CacheKey(containingType, propertyName); + return _propertyCache.GetOrAdd(key, static k => + { + var results = new global::System.Collections.Generic.List(); + + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) + { + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); + + results.AddRange(propertyAttributes); + } + + // Check constructors for parameters that match the property name + // to handle record scenarios + foreach (var constructor in k.ContainingType.GetConstructors()) + { + // Look for parameter with matching name (case insensitive) + var parameter = global::System.Linq.Enumerable.FirstOrDefault( + constructor.GetParameters(), + p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase)); + + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); + + results.AddRange(paramAttributes); + + break; + } + } + + return results.ToArray(); + }); + } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } + } +} \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs index 9db7cb497042..d596a97173f9 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => ValidationAttributeCache.GetPropertyValidationAttributes(ContainingType, Name); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -115,15 +124,17 @@ private sealed record CacheKey( [property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type ContainingType, string PropertyName); - private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _propertyCache = new(); + private static readonly global::System.Lazy> _lazyTypeCache = new (() => new ()); + private static global::System.Collections.Concurrent.ConcurrentDictionary TypeCache => _lazyTypeCache.Value; - public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetPropertyValidationAttributes( [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type containingType, string propertyName) { var key = new CacheKey(containingType, propertyName); - return _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); @@ -160,5 +171,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnClassProperties#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnClassProperties#ValidatableInfoResolver.g.verified.cs index d8135a12e14c..ef96d04824e1 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnClassProperties#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnClassProperties#ValidatableInfoResolver.g.verified.cs @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => ValidationAttributeCache.GetPropertyValidationAttributes(ContainingType, Name); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -172,15 +181,17 @@ private sealed record CacheKey( [property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type ContainingType, string PropertyName); - private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _propertyCache = new(); + private static readonly global::System.Lazy> _lazyTypeCache = new (() => new ()); + private static global::System.Collections.Concurrent.ConcurrentDictionary TypeCache => _lazyTypeCache.Value; - public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetPropertyValidationAttributes( [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type containingType, string propertyName) { var key = new CacheKey(containingType, propertyName); - return _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); @@ -217,5 +228,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnEndpointParameters#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnEndpointParameters#ValidatableInfoResolver.g.verified.cs index 9db7cb497042..d596a97173f9 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnEndpointParameters#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnEndpointParameters#ValidatableInfoResolver.g.verified.cs @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => ValidationAttributeCache.GetPropertyValidationAttributes(ContainingType, Name); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -115,15 +124,17 @@ private sealed record CacheKey( [property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type ContainingType, string PropertyName); - private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _propertyCache = new(); + private static readonly global::System.Lazy> _lazyTypeCache = new (() => new ()); + private static global::System.Collections.Concurrent.ConcurrentDictionary TypeCache => _lazyTypeCache.Value; - public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetPropertyValidationAttributes( [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type containingType, string propertyName) { var key = new CacheKey(containingType, propertyName); - return _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); @@ -160,5 +171,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnRecordProperties#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnRecordProperties#ValidatableInfoResolver.g.verified.cs index 5fff86fac3f1..6012dcb9cac9 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnRecordProperties#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnRecordProperties#ValidatableInfoResolver.g.verified.cs @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => ValidationAttributeCache.GetPropertyValidationAttributes(ContainingType, Name); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -130,15 +139,17 @@ private sealed record CacheKey( [property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type ContainingType, string PropertyName); - private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _propertyCache = new(); + private static readonly global::System.Lazy> _lazyTypeCache = new (() => new ()); + private static global::System.Collections.Concurrent.ConcurrentDictionary TypeCache => _lazyTypeCache.Value; - public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetPropertyValidationAttributes( [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type containingType, string propertyName) { var key = new CacheKey(containingType, propertyName); - return _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); @@ -175,5 +186,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsClassesWithNonAccessibleTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsClassesWithNonAccessibleTypes#ValidatableInfoResolver.g.verified.cs index 0ce09882da8d..5fbf16937735 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsClassesWithNonAccessibleTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsClassesWithNonAccessibleTypes#ValidatableInfoResolver.g.verified.cs @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => ValidationAttributeCache.GetPropertyValidationAttributes(ContainingType, Name); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -115,15 +124,17 @@ private sealed record CacheKey( [property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type ContainingType, string PropertyName); - private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _propertyCache = new(); + private static readonly global::System.Lazy> _lazyTypeCache = new (() => new ()); + private static global::System.Collections.Concurrent.ConcurrentDictionary TypeCache => _lazyTypeCache.Value; - public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetPropertyValidationAttributes( [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] global::System.Type containingType, string propertyName) { var key = new CacheKey(containingType, propertyName); - return _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); @@ -160,5 +171,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.Tests/TestValidatableTypeInfo.cs b/src/Validation/test/Microsoft.Extensions.Validation.Tests/TestValidatableTypeInfo.cs new file mode 100644 index 000000000000..09c6a8c8d5cf --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.Tests/TestValidatableTypeInfo.cs @@ -0,0 +1,18 @@ +#pragma warning disable ASP0029 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.Extensions.Validation.Tests; + +internal class TestValidatableTypeInfo( + Type type, + ValidatablePropertyInfo[] members, + ValidationAttribute[]? attributes = default) : ValidatableTypeInfo(type, members) +{ + private readonly ValidationAttribute[] _attributes = attributes ?? []; + + protected override ValidationAttribute[] GetValidationAttributes() => _attributes; +} \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableInfoResolverTests.cs b/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableInfoResolverTests.cs index 0a745a62a209..5e19a079a7bb 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableInfoResolverTests.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableInfoResolverTests.cs @@ -214,10 +214,4 @@ public TestValidatableParameterInfo( protected override ValidationAttribute[] GetValidationAttributes() => _validationAttributes; } - - private class TestValidatableTypeInfo( - Type type, - ValidatablePropertyInfo[] members) : ValidatableTypeInfo(type, members) - { - } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableParameterInfoTests.cs b/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableParameterInfoTests.cs index 3ede11071d3d..0bf7530fecc5 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableParameterInfoTests.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableParameterInfoTests.cs @@ -348,12 +348,6 @@ public TestValidatablePropertyInfo( protected override ValidationAttribute[] GetValidationAttributes() => _validationAttributes; } - private class TestValidatableTypeInfo( - Type type, - ValidatablePropertyInfo[] members) : ValidatableTypeInfo(type, members) - { - } - private class TestValidationOptions : ValidationOptions { public TestValidationOptions(Dictionary typeInfoMappings) diff --git a/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableTypeInfoTests.cs b/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableTypeInfoTests.cs index 002ebb1582b0..dd761996442a 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableTypeInfoTests.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableTypeInfoTests.cs @@ -586,6 +586,68 @@ public async Task Validate_IValidatableObject_WithZeroAndMultipleMemberNames_Beh }); } + // The expected order of validation is: + // 1. Attributes on properties + // 2. Attributes on the type + // 3. IValidatableObject implementation + // If any of these steps report an error, the later steps are skipped. + [Fact] + public async Task Validate_IValidatableObject_WithPropertyErrors_ShortCircuitsProperly() + { + var testTypeInfo = new TestValidatableTypeInfo( + typeof(PropertyAndTypeLevelErrorObject), + [ + CreatePropertyInfo(typeof(PropertyAndTypeLevelErrorObject), typeof(int), "Value", "Value", + [new RangeAttribute(0, int.MaxValue) { ErrorMessage = "Property attribute error" }]) + ], + [ + new CustomValidationAttribute() + ]); + + // First case: + var testTypeInstance = new PropertyAndTypeLevelErrorObject { Value = 15 }; + + var context = new ValidateContext + { + ValidationOptions = new TestValidationOptions(new Dictionary + { + { typeof(PropertyAndTypeLevelErrorObject), testTypeInfo } + }), + ValidationContext = new ValidationContext(testTypeInstance) + }; + + await testTypeInfo.ValidateAsync(testTypeInstance, context, default); + + Assert.NotNull(context.ValidationErrors); + var interfaceError = Assert.Single(context.ValidationErrors); + Assert.Equal(string.Empty, interfaceError.Key); + Assert.Equal("IValidatableObject error", interfaceError.Value.Single()); + + // Second case: + testTypeInstance.Value = 5; + context.ValidationErrors = []; + context.ValidationContext = new ValidationContext(testTypeInstance); + + await testTypeInfo.ValidateAsync(testTypeInstance, context, default); + + Assert.NotNull(context.ValidationErrors); + var classAttributeError = Assert.Single(context.ValidationErrors); + Assert.Equal(string.Empty, classAttributeError.Key); + Assert.Equal("Class attribute error", classAttributeError.Value.Single()); + + // Third case: + testTypeInstance.Value = -5; + context.ValidationErrors = []; + context.ValidationContext = new ValidationContext(testTypeInstance); + + await testTypeInfo.ValidateAsync(testTypeInstance, context, default); + + Assert.NotNull(context.ValidationErrors); + var propertyAttributeError = Assert.Single(context.ValidationErrors); + Assert.Equal("Value", propertyAttributeError.Key); + Assert.Equal("Property attribute error", propertyAttributeError.Value.Single()); + } + // Returns no member names to validate https://github.com/dotnet/aspnetcore/issues/61739 private class GlobalErrorObject : IValidatableObject { @@ -618,6 +680,36 @@ public IEnumerable Validate(ValidationContext validationContex } } + [CustomValidation] + private class PropertyAndTypeLevelErrorObject : IValidatableObject + { + [Range(0, int.MaxValue, ErrorMessage = "Property attribute error")] + public int Value { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) + { + if (Value < 20) + { + yield return new ValidationResult($"IValidatableObject error"); + } + } + } + + private class CustomValidationAttribute : ValidationAttribute + { + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) + { + if (value is PropertyAndTypeLevelErrorObject instance) + { + if (instance.Value < 10) + { + return new ValidationResult($"Class attribute error"); + } + } + return ValidationResult.Success; + } + } + private ValidatablePropertyInfo CreatePropertyInfo( Type containingType, Type propertyType, @@ -781,16 +873,6 @@ public TestValidatablePropertyInfo( protected override ValidationAttribute[] GetValidationAttributes() => _validationAttributes; } - private class TestValidatableTypeInfo : ValidatableTypeInfo - { - public TestValidatableTypeInfo( - Type type, - ValidatablePropertyInfo[] members) - : base(type, members) - { - } - } - private class TestValidationOptions : ValidationOptions { public TestValidationOptions(Dictionary typeInfoMappings)