Search code examples
c#asp.net-coreasp.net-core-middlewareduende

Is there a way to add metadata to all GET endpoints in ASP.NET Core?


In the following statement, the AsBffApiEndpoint() adds an attribute to all endpoints. Then there is a middleware specifically looking for that attribute and if present will check for an antiforgery header to be present.

endpoints.MapControllers().RequireAuthorization().AsBffApiEndpoint();

I need to be able to bypass that check on all GET endpoints. Most importantly, this is third party library, hence I have no control over the implementation.

I have try many things without success. Last attempt was to add a middleware custom middleware app.Use(...) and if the attribute was present, then remove it. However that's not possible since the list of metadata is readonly. Then, my last hope is to find a way to add same attribute -to all GET- with a flag false which ignores the check. In other words, all AsBffApiEndpoint() does is decorate an endpoint with [BffApi] attribute. This attribute ignores antiforery headers if use like this [BffApi(false)]. I know the solution is hacky because I will end up with something like this.

[BffApi]
[BffApi(false)]
//endpoint definition here

The good news is they get the endpoint metadata ordered endpoint.Metadata.GetOrderedMetadata<BffApiAttribute>(). Meaning as long as [BffApi(false)] takes priority in the list I should be good.


Solution

  • I found the solution and didn't involve adding a second attribute but to extend the builder rather. This way I could modify the endpoints metadata, which at that point are mutable.

    public static TBuilder AsBffApiEndpointBypassAntiforgeryOnGET<TBuilder>(this TBuilder builder, string routePrefix) where TBuilder : IEndpointConventionBuilder
    {
        builder.Add(endpointBuilder =>
        {
            var getAttribute = endpointBuilder.Metadata.FirstOrDefault(m => m.GetType() == typeof(HttpGetAttribute)) as HttpGetAttribute;
            var routeAttribute = endpointBuilder.Metadata.FirstOrDefault(m => m.GetType() == typeof(RouteAttribute)) as RouteAttribute;
            if (getAttribute != null && routeAttribute != null && routeAttribute.Template.StartsWith(routePrefix))
            {
                endpointBuilder.Metadata.Add(new BffApiAttribute(false));
            }
            else
            {
                endpointBuilder.Metadata.Add(new BffApiAttribute(true));
            }
        });
    
        return builder;
    }
    

    Then in Startup.cs

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapBffManagementEndpoints();
            endpoints.MapControllers().RequireAuthorization().AsBffApiEndpointBypassAntiforgeryOnGET("api/Foo");
        });
    

    A more generic implementation would be the following.

    public static IEndpointConventionBuilder AddConditionalMetadata(this IEndpointConventionBuilder builder, Func<EndpointBuilder, bool> evalEndpoint, Action<EndpointBuilder> onEvalTrue, Action<EndpointBuilder> onEvalFalse)
    {
        builder.Add(endpointBuilder =>
        {
            if (evalEndpoint.Invoke(endpointBuilder))
            {
                onEvalTrue.Invoke(endpointBuilder);
            }
            else
            {
                onEvalFalse.Invoke(endpointBuilder);
            }
        });
    
        return builder;
    }
    

    This way you expose the function to evaluate and the actions to perform. Then, your Startup.cs will change to this.

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapBffManagementEndpoints();
        endpoints.MapControllers().RequireAuthorization().AddConditionalMetadata(
            evalEndpoint: (endpointBuilder) => 
            {
                var routeAttribute = endpointBuilder.Metadata.FirstOrDefault(m => m.GetType() == typeof(RouteAttribute)) as RouteAttribute;
                var getAttribute = endpointBuilder.Metadata.FirstOrDefault(m => m.GetType() == typeof(HttpGetAttribute)) as HttpGetAttribute;
                return getAttribute != null && routeAttribute != null && routeAttribute.Template.StartsWith("api/Foo");
            }, 
            onEvalTrue: (endpointBuilder) => { endpointBuilder.Metadata.Add(new BffApiAttribute(false)); },
            onEvalFalse: (endpointBuilder) => { endpointBuilder.Metadata.Add(new BffApiAttribute()); });
    });
    

    I hope this can help someone else looking for the same thing.