Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// 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;

/// <summary>
/// Defines a contract used to specify metadata for skipping the StatusCodePage
/// middleware in <see cref="Endpoint.Metadata"/>.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we drop the in Endpoint.Metadata? I don't know if it helps clarify things?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the in Endpoint.Metadata. What's wrong with it? It could point the curious to API docs for endpoint routing.

/// </summary>
public interface ISkipStatusCodePagesMetadata
{
}
1 change: 1 addition & 0 deletions src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#nullable enable
*REMOVED*abstract Microsoft.AspNetCore.Http.HttpResponse.ContentType.get -> string!
abstract Microsoft.AspNetCore.Http.HttpResponse.ContentType.get -> string?
Microsoft.AspNetCore.Http.Metadata.ISkipStatusCodePagesMetadata
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.Diagnostics;
Expand Down Expand Up @@ -41,6 +42,13 @@ public async Task Invoke(HttpContext context)
{
var statusCodeFeature = new StatusCodePagesFeature();
context.Features.Set<IStatusCodePagesFeature>(statusCodeFeature);
var endpoint = context.GetEndpoint();
Copy link
Copy Markdown
Member

@Kahbazi Kahbazi Nov 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having context.GetEndpoint(); before _next(context) implies that UseStatusCodePages should be after UseRouting which might not be the case everywhere, including the sample in the docs.

This could be after _next(context) and also only be called if statusCodeFeature.Enabled is true.

Copy link
Copy Markdown
Member

@Tratcher Tratcher Nov 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a fair consideration, and we would want to make sure the doc was accurate.

Do we have examples of anything else that's endpoint aware but placed before UseRouting?

This ordering was recommended so that the attribute behavior could be applied early and then overridden by other middleware/code while processing the request. That's consistent with how the filter works today. If placed after _next, you wouldn't be able to tell if IStatusCodePagesFeature had the default value or if it was set by something in the app.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Tratcher Sorry, I'm confused a little bit! Are you saying that going forward StatusCodePagesMiddleware should always be after RoutingMiddleware?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StatusCodePagesMiddleware can stay where it is and continue to work for existing apps, but if you want it to be route aware and use the SkipStatusCodePages attribute then it will need to be placed after routing.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see! The implementation has been changed after I add the original comment. It's all clear now.

I assume if StatusCodePagesMiddleware with re-execute would be used after routing it will automatically add the routing, right?

if (app.Properties.TryGetValue(globalRouteBuilderKey, out var routeBuilder) && routeBuilder is not null)
{
return app.Use(next =>
{
RequestDelegate? newNext = null;
// start a new middleware pipeline
var builder = app.New();
// use the old routing pipeline if it exists so we preserve all the routes and matching logic
// ((IApplicationBuilder)WebApplication).New() does not copy globalRouteBuilderKey automatically like it does for all other properties.
builder.Properties[globalRouteBuilderKey] = routeBuilder;
builder.UseRouting();

What if StatusCodePagesFeature set the Enable property lazily? In this case it doesn't matter where StatusCodePagesMiddleware is as long as the feature is accessed after routing, right?

public class StatusCodePagesFeature : IStatusCodePagesFeature
{
    private readonly HttpContext httpContext;
    public StatusCodePagesFeature(HttpContext httpContext)
    {
        _httpContext = httpContext;
    }

    private bool? _enabled;
    public bool Enabled
    {
        get 
        {
            if (_enabled == null)
            {
                var endpoint = _httpContext.GetEndpoint();
                var skipStatusCodePageMetadata = endpoint?.Metadata.GetMetadata<ISkipStatusCodePagesMetadata>();
                if (skipStatusCodePageMetadata is not null)
                {
                    _enabled = false;
                }
                else
                {
                    _enabled = true;
                }
            }
            
            return _enabled;
        }
        set
        {
            _enabled = value;
        }
    }
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StatusCodePagesFeature is a public class, so if we were to change it, we'd have to make it continue to work as it does today when called with the empty constructor. I think it might be worthwhile to just use a new internal IStatusCodePagesFeature that lazily reads the endpoint.

The only thing I don't like is locking in the wrong value for Enabled if the property is read before UseRouting(). You could definitely argue it's no worse than today where it's locked in immediately when the feature is added, but that feels a little less surprising. I wonder if it'd be better to avoid negative caching unless the property is explicitly set, but maybe that's even more surprising.

var skipStatusCodePageMetadata = endpoint?.Metadata.GetMetadata<ISkipStatusCodePagesMetadata>();

if (skipStatusCodePageMetadata is not null)
{
statusCodeFeature.Enabled = false;
}

await _next(context);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<Reference Include="Microsoft.Extensions.DiagnosticAdapter" />
<Reference Include="Microsoft.Extensions.FileProviders.Embedded" />
<Reference Include="Microsoft.AspNetCore" />
<Reference Include="Microsoft.AspNetCore.Mvc.ViewFeatures" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
Expand Down Expand Up @@ -283,4 +284,38 @@ public async Task Reexecute_WorksAfterUseRoutingWithGlobalRouteBuilder()
var content = await response.Content.ReadAsStringAsync();
Assert.Equal("errorPage", content);
}

[Fact]
public async Task SkipStatusCodePages_SupportsEndpoints()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a test that verifies we let people change the feature in the body of the endpoint?

endpoints.MapGet("/", [SkipStatusCodePages] (c) =>
{
         c.GetFeature<IStatusCodePagesFeature>().Enabled = false;
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Except verify that you can set it to true! Although it really would be weird to reenable it after adding the attribute.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems fair! I missed seeing this before the auto-merge kicked it but I'll address in a follow-up.

{
var builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
await using var app = builder.Build();

app.UseRouting();

app.UseStatusCodePages();

app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", [SkipStatusCodePages] (c) =>
{
c.Response.StatusCode = 404;
return Task.CompletedTask;
});
});

app.Run((context) =>
{
throw new InvalidOperationException("Invalid input provided.");
});

await app.StartAsync();

using var server = app.GetTestServer();
var client = server.CreateClient();
var response = await client.GetAsync("/");
var content = await response.Content.ReadAsStringAsync();
Assert.Empty(content);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http.Metadata;
Comment thread
captainsafia marked this conversation as resolved.
using Microsoft.AspNetCore.Mvc.Filters;

namespace Microsoft.AspNetCore.Mvc;
Expand All @@ -11,7 +12,7 @@ namespace Microsoft.AspNetCore.Mvc;
/// A filter that prevents execution of the StatusCodePages middleware.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class SkipStatusCodePagesAttribute : Attribute, IResourceFilter
public class SkipStatusCodePagesAttribute : Attribute, IResourceFilter, ISkipStatusCodePagesMetadata
{
/// <inheritdoc />
public void OnResourceExecuted(ResourceExecutedContext context)
Expand Down