Skip to content

Allow endpoint filter factories to manipulate endpoint metadata #41722

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
DamianEdwards opened this issue May 17, 2022 · 10 comments · Fixed by #42195
Closed

Allow endpoint filter factories to manipulate endpoint metadata #41722

DamianEdwards opened this issue May 17, 2022 · 10 comments · Fixed by #42195
Assignees
Labels
api-approved API was approved in API review, it can be implemented area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc Docs This issue tracks updating documentation feature-minimal-actions Controller-like actions for endpoint routing old-area-web-frameworks-do-not-use *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels
Milestone

Comments

@DamianEdwards
Copy link
Member

DamianEdwards commented May 17, 2022

When adding endpoint filters via a filter factory delegate, it would sometimes be useful to be able to manipulate the endpoint's metadata too, in the case where the filter will impact the effective endpoint operation.

For example, below shows a rudimentary XML formatting filter that adds the ability for an endpoint to format its response as XML if the client supports it, instead of the usual JSON. This filter should be able to update the endpoint metadata to indicate that it can now return XML as well, so that it's described as such in ApiExplorer and OpenAPI/Swagger.

app.MapGet("/todos", async Task<Ok<Todo[]>> (TodoDb db) => TypedResults.Ok(await db.Todos.ToArrayAsync())
    .AddFilter((context, next) =>
    {
        var returnType = context.MethodInfo.ReturnType;

        if (returnType.IsAssignableTo(typeof(Ok<Todo[]>)))
        {
            // Would be nice to be able to manipulate endpoint metadata here, e.g.
            context.EndpointMetadata.Add(new ProducesResponseTypeAttribute(typeof(Todo), 200, "text/xml"));

            // Return an "XML formatter" filter
            return async c =>
            {
                var result = await next(c);
                
                if (!c.HttpContext.Request.Headers.Accepts.Contains("text/xml")
                {
                    // Client doesn't support XML, just return original result
                    return result;
                }
                
                return result switch
                {
                    Ok<Todo[]> ok => CustomResults.OkXml(ok.Value),
                    _ => throw new InvalidOperationException("Result type cannot be formatted as XML.")
                };
            }
        }

        return next;
    });

The suggested API diff:

namespace Microsoft.AspNetCore.Http;

public sealed class RouteHandlerContext
{
-    public RouteHandlerContext(MethodInfo methodInfo, EndpointMetadataCollection endpointMetadata, IServiceProvider applicationServices);
+    public RouteHandlerContext(MethodInfo methodInfo, IList<object> endpointMetadata, IServiceProvider applicationServices);

-    public EndpointMetadataCollection EndpointMetadata { get; }
+    public IList<object> EndpointMetadata { get; }
}

Ordering considerations

Imagine the above example is extended so that other methods on the IEndpointConventionBuilder are called that also manipulate endpoint metadata, e.g.:

app.MapGet("/todos", async Task<Ok<Todo[]>> (TodoDb db) => TypedResults.Ok(await db.Todos.ToArrayAsync())
    .WithName("GetTodos")
    .WithTags("FirstTag")
    .AddFilter((context, next) =>
    {
        // Code from above...
    }
    .WithTags("SecondTag");

What is the order of operations WRT to the buildup of the endpoint metadata collection?
Will the filter factory delegate see:

  1. the metadata for just the methods declared before it (e.g. EndpointNameAttribute("GetTodos"), TagsAttribute("FirstTag")), and not that for the methods declared after it; or
  2. all the endpoint metadata added by such methods; or
  3. none of the endpoint metadata added by such methods

Ideally I think, it would see the endpoint metadata added by any methods declared before it in the endpoint builder (point 1), along with any metadata added by RequestDelegateFactory it self for the endpoint delegate.

@DamianEdwards DamianEdwards added feature-minimal-actions Controller-like actions for endpoint routing old-area-web-frameworks-do-not-use *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels labels May 17, 2022
@halter73 halter73 added this to the .NET 7 Planning milestone May 17, 2022
@ghost
Copy link

ghost commented May 17, 2022

Thanks for contacting us.

We're moving this issue to the .NET 7 Planning milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s).
If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues.
To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

@davidfowl
Copy link
Member

How useful is this in general? Are there analogues to what this is in MVC (application model)? I'd like to see more use cases before we do this feature.

@DamianEdwards
Copy link
Member Author

When we talked about it today it turned out this was a simple oversight, i.e. when the filter factories were implemented it was intended that they could manipulate endpoint metadata, but it just wasn't implemented correctly.

WRT how useful this is in general, this enables a filter to now intercept/modify any metadata-driven aspects of endpoints, so any system that relies on metadata can now be enabled, disabled, modified, etc. by filters (CORS, authz, OpenAPI, etc.).

@davidfowl
Copy link
Member

Sure I know what it can do I just want to see more examples of how it would be used.

@davidfowl
Copy link
Member

Is the idea that this customers would use this to mutate the metdata while just leave the filter as purely passthrough?

@DamianEdwards
Copy link
Member Author

That wasn't the intent I had in mind, but indeed that could be a scenario it enables, which is a scenario we've discussed a few times in the past (central manipulation of endpoint metadata). This feels to me like a building block level capability, esp. given the fact that filters, by design, can effectively manipulate the original spec/behavior of a route handler.

@DamianEdwards
Copy link
Member Author

Thinking about this more, It occurred to me that one can easily create an extension method to encapsulate the adding of a filter factory and an endpoint convention builder delegate that mutates endpoint metadata, resulting in a single method-call experience that achieves the desired outcome, which somewhat reduces the importance of filter factories being able to manipulate endpoint metadata directly:

app.MapGet("/todos", async Task<Ok<Todo[]>> (TodoDb db) => TypedResults.Ok(await db.Todos.ToArrayAsync())
    .WithXmlFormatter();

public static class XmlFormatterExtensions
{
    public static RouteHandlerBuilder WithXmlFormatter(this RouteHandlerBuilder builder) =>
        builder.AddFilter((context, next) =>
        {
            var returnType = context.MethodInfo.ReturnType;
    
            if (returnType.IsAssignableTo(typeof(Ok<Todo[]>)))
            {   
                // Return an "XML formatter" filter
                return async c =>
                {
                    var result = await next(c);
                    
                    if (!c.HttpContext.Request.Headers.Accepts.Contains("text/xml")
                    {
                        // Client doesn't support XML, just return original result
                        return result;
                    }
                    
                    return result switch
                    {
                        Ok<Todo[]> ok => CustomResults.OkXml(ok.Value),
                        _ => throw new InvalidOperationException("Result type cannot be formatted as XML.")
                    };
                }
            }
        }
        .WithMetadata(new ProducesResponseTypeAttribute(typeof(Todo), 200, "text/xml"));
}

@DamianEdwards
Copy link
Member Author

In practice this is still kinda awkward as the filter factory delegate itself can't see the metadata, both the callbacks (endpoint builder and filter itself) need to look at metadata and no-op (in this case) independently. Would be much more convenient to just do this in the filter factory delegate itself.

@halter73 halter73 self-assigned this Jun 15, 2022
@halter73 halter73 added the api-ready-for-review API is ready for formal API review - https://github.com/dotnet/apireviews label Jun 15, 2022
@ghost
Copy link

ghost commented Jun 15, 2022

Thank you for submitting this for API review. This will be reviewed by @dotnet/aspnet-api-review at the next meeting of the ASP.NET Core API Review group. Please ensure you take a look at the API review process documentation and ensure that:

  • The PR contains changes to the reference-assembly that describe the API change. Or, you have included a snippet of reference-assembly-style code that illustrates the API change.
  • The PR describes the impact to users, both positive (useful new APIs) and negative (breaking changes).
  • Someone is assigned to "champion" this change in the meeting, and they understand the impact and design of the change.

@halter73 halter73 added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for formal API review - https://github.com/dotnet/apireviews labels Jun 17, 2022
@halter73
Copy link
Member

API review notes from @captainsafia :

  • [this proposal] makes sense given the other changes made around EndpointMetadata in the route groups PR.

@ghost ghost locked as resolved and limited conversation to collaborators Jul 21, 2022
@rafikiassumani-msft rafikiassumani-msft added the Docs This issue tracks updating documentation label Aug 4, 2022
@amcasey amcasey added the area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc label Jun 2, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc Docs This issue tracks updating documentation feature-minimal-actions Controller-like actions for endpoint routing old-area-web-frameworks-do-not-use *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants