Skip to content
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

Add a result filter for razor pages #34

Closed
haacked opened this issue Dec 11, 2019 · 10 comments
Closed

Add a result filter for razor pages #34

haacked opened this issue Dec 11, 2019 · 10 comments

Comments

@haacked
Copy link

haacked commented Dec 11, 2019

Right now, there's no nice declarative way (that I could find) to add a feature flag to a razor page.

[FeatureGate(FeatureFlags.FeatureA)]
public class IndexModel : PageModel {...}

Doesn't work because FeatureGateAttribute only overrides OnActionExecutionAsync. I haven't tested this myself, but I believe if you also implement OnResultExecutingAsync you'll cover Razor pages. At least according to the docs Filter methods for Razor Pages in ASP.NET Core

@haacked
Copy link
Author

haacked commented Dec 11, 2019

Just as a follow-up, I hacked up a proof of concept to show this would work.

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class FeatureToggleAttribute : FeatureGateAttribute
{
    public FeatureToggleAttribute(params string[] features)
        : this(RequirementType.All, features)
    {
    }

    public FeatureToggleAttribute(RequirementType requirementType, params string[] features)
        : base(requirementType, features)
    {
    }

    public FeatureToggleAttribute(RequirementType requirementType, params object[] features)
        : base(requirementType, features)
    {
    }
    
    public FeatureToggleAttribute(params object[] features)
        : this(RequirementType.All, features)
    {
    }

    public override async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
    {
        var fm = context.HttpContext.RequestServices.GetRequiredService<IFeatureManagerSnapshot>();
        bool flag;
        if (RequirementType == RequirementType.All)
            flag = await Features.All<string>((Func<string, Task<bool>>) (async feature =>
                await fm.IsEnabledAsync(feature).ConfigureAwait(false)));
        else
            flag = await Features.Any<string>((Func<string, Task<bool>>) (async feature =>
                await fm.IsEnabledAsync(feature).ConfigureAwait(false)));
        if (flag)
        {
            await next().ConfigureAwait(false);
        }
        else
        {
            context.HttpContext.Response.StatusCode = 404;
            await context.HttpContext.Response.CompleteAsync();
        }
    }
}

This code makes use of internal classes within this repo that I didn't copy. But it shows that the basic idea would work.

@ArieJones
Copy link

+1 for this as this is exactly what we were looking at as a solution in one of our apps ..

@jimmyca15
Copy link
Member

I spent some time looking into this because I wanted to provide some out of the box support for Razor Pages in the same way that we provide it for MVC controllers. There is an unfortunate difference between supporting controllers via OnActionExecutionAsync and support Razor Pages via OnResultExecutionAsync. OnResultExecutionAsync is invoked after a page handler has already executed. You are correct that this is the only way to declaratively add a handler to a Page Handler. If we wanted to support Razor Pages with just an attribute, we would have to take that route, but the intent of the FeatureGate attribute is to prevent the action/handler from running if the feature is not enabled. Therefore, it looks like support for this may not be a good option with what is available directly out of ASP.NET Core.

I will look a bit more to see if it makes sense to provide a global IPageFilter that can check if a page is decorated with a FeatureGateAttribute and execute it before the handler similar to how it works in an MVC scenario. This may require expensive reflection though and additional boiler plate code on startup to add the global filter.

@haacked
Copy link
Author

haacked commented Jun 26, 2020

Good point on the result execution filter being too late.

Could FeatureGateAttribute itself implement IPageFilter and IAsyncPageFilter?

@mdarefull
Copy link

@jimmyca15 IMO, this would really increase the value of this library and shouldn't be difficult to implement.

We already implemented a a very simple IAsyncPageFilter with similar implementation provided by @haacked and it's nicely working:

    [AttributeUsage(AttributeTargets.Class)]
    public class RazorPageFeatureGate : Attribute, IAsyncPageFilter
    {
        private string Feature { get; }
        public RazorPageFeatureGate(string feature)
            => Feature = feature;

        public virtual async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next)
        {
            var fm = context.HttpContext.RequestServices.GetRequiredService<IFeatureManagerSnapshot>();
            var isEnabled = await fm.IsEnabledAsync(Feature).ConfigureAwait(false);
            if (isEnabled)
            {
                await next.Invoke().ConfigureAwait(false);
            }
            else
            {
                context.HttpContext.Response.StatusCode = 404;
                await context.HttpContext.Response.CompleteAsync().ConfigureAwait(false);
            }
        }

        public virtual Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context) => Task.CompletedTask;
    }

@imsaha
Copy link

imsaha commented Dec 22, 2020

 services
               .AddRazorPages(options =>
               {
                   ...
                  options.Conventions.AddAreaFolderApplicationModelConvention("Admin", "/Customers", cOption =>
                   {
                       cOption.Filters.Add(new RazorPageFeatureGate(ApplicationFeatureFlags.ManageCustomer));
                   });
               })

Apply feature gate filter to area folder

@MarcWils
Copy link

MarcWils commented Mar 3, 2021

Calling Complete on the response also disables custom error pages.

context.HttpContext.Response.StatusCode = 404;
await context.HttpContext.Response.CompleteAsync().ConfigureAwait(false);

Wouldn't it be better to avoid page execution by setting the PageHandlerExecutingContext.Result?

context.Result = new NotFoundResult();

@CarlVerret
Copy link

CarlVerret commented Sep 2, 2021

We already implemented a a very simple IAsyncPageFilter with similar implementation provided by @haacked and it's nicely working:

I suggest you make a PR from this. Worked like a charm ! Thanks for sharing

@jimmyca15
Copy link
Member

Since page filters and action filters are executed in isolation we can add this new functionality in the existing FeatureGateAttribute. I have a sent out a PR #166.

@jimmyca15
Copy link
Member

This has been released in Microsoft.FeatureManagement.AspNetCore 2.5.0

Documentation update is in the works.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants