RestApiVisibility 1.0.1

dotnet add package RestApiVisibility --version 1.0.1
NuGet\Install-Package RestApiVisibility -Version 1.0.1
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="RestApiVisibility" Version="1.0.1" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add RestApiVisibility --version 1.0.1
#r "nuget: RestApiVisibility, 1.0.1"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install RestApiVisibility as a Cake Addin
#addin nuget:?package=RestApiVisibility&version=1.0.1

// Install RestApiVisibility as a Cake Tool
#tool nuget:?package=RestApiVisibility&version=1.0.1

REST API Visibility Control

Tools such a Swagger are widely used to document REST APIs. REST endpoints are grouped by controller and may have a name (OperationId). The UI typically shows all REST endpoints, which is not always desirable.

  • Hide endpoints that cannot be controlled from the UI.
  • Show or hide endpoints based on application context, such as audits or experience/integration levels.
  • Reduce endpoints in development for startup optimization and UI reduction.

The following solution approach shows how to show or hide REST endpoints based on configuration.

👉 Hiding an endpoint does not affect its availability; REST clients can still use it without restriction.

Filter Configuration

Endpoint visibility is defined in the appsettings.json application configuration file, where visible and/or invisible filters are set. The following options are available:

  • Filter on visible endpoints
  • Filter on invisible endpoints
  • Combined filter of visible endpoints with a subset of invisible endpoints.

The endpoint filter is an expression in ControllerMask[.OperationMask] format and supports ? and * masks.

Examples of filter expressions:

  • WeatherForecast - all endpoints of the WeatherForecast controller
  • *Audit - all endpoints of the controller whose name ends with Audit
  • WeatherForecast.Get* - all endpoints of the WeatherForecast controller whose operation name begins with Get.
  • *.Get* - all endpoints whose operation name begins with Get.

This results in the following usage matrix:

Mode Visible Hidden Example
Include ✔️ "VisibleItems": ["User.*", "WeatherForecast.Get*"]
Exclude ✔️ "HiddenItems": ["User.*", "WeatherForecast.DeleteWeatherForecast"]
Mixed ✔️ ✔️ "VisibleItems": ["*.Get*"],<br />"HiddenItems": ["User.Get*"]

The filters are defined in the ApiConfiguration section of the configuration file. Example Include filter:

"ApiConfiguration": {
  "VisibleItems": [
    "User.*",
    "WeatherForecast.Get*"
  ]
}

Example Exclude filter:

"ApiConfiguration": {
  "HiddenItems": [
    "User.*",
    "WeatherForecast.DeleteWeatherForecast"
  ]
}

Example of an Include filter combined with an Exclude filter:

"ApiConfiguration": {
  "VisibleItems": [
    "*.Get*"
  ],
  "HiddenItems": [
    "User.Get*"
  ]
}

👉 In development mode, it is recommended that you outsource endpoint configuration to User Secrets.

Filter Convention

ASP.NET provides the ability to define the visibility of endpoints using the ActionModelConvention. The ApiVisibilityConvention implementation controls the visibility of the endpoint based on visible and invisible elements:

internal sealed class ApiVisibilityConvention : IActionModelConvention
{
    private List<string> VisibleItems { get; }
    private List<string> HiddenItems { get; }

    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="visibleItems">List of visible items name masks (wildcards: *?)</param>
    /// <param name="hiddenItems">List of hidden items name masks (wildcards: *?)</param>
    internal ApiVisibilityConvention(IEnumerable<string>? visibleItems = null,
        IEnumerable<string>? hiddenItems = null)
    {
        VisibleItems = visibleItems != null ? new(visibleItems) : new();
        HiddenItems = hiddenItems != null ? new(hiddenItems) : new();
    }

    public void Apply(ActionModel action)
    {
        // visible
        if (VisibleItems.Count > 0)
        {
            action.ApiExplorer.IsVisible = VisibleItems.Any(
                x => MatchItem(action.Controller.ControllerName, GetOperationId(action), x));
        }

        // hidden
        if (HiddenItems.Count > 0)
        {
            if (VisibleItems.Count > 0)
            {
                // exclude from visible
                if (action.ApiExplorer.IsVisible == true)
                {
                    action.ApiExplorer.IsVisible = !HiddenItems.Any(
                        x => MatchItem(action.Controller.ControllerName, GetOperationId(action), x));
                }
            }
            else
            {
                action.ApiExplorer.IsVisible = !HiddenItems.Any(
                    x => MatchItem(action.Controller.ControllerName, GetOperationId(action), x));
            }
        }
    }

    private static string? GetOperationId(ActionModel action) =>
        (action.Attributes.FirstOrDefault(x => x is HttpMethodAttribute) as HttpMethodAttribute)?.Name;

    private static bool MatchItem(string controllerName, string? operationId, string mask)
    {
        var controllerMask = mask;
        string? actionMask = null;

        var actionIndex = mask.IndexOf('.');
        if (actionIndex > 0)
        {
            controllerMask = mask.Substring(0, actionIndex);
            actionMask = mask.Substring(actionIndex + 1);
        }

        // controller mask only
        if (actionMask == null || string.IsNullOrWhiteSpace(operationId))
        {
            return MatchExpression(controllerName, controllerMask);
        }

        // controller and action mask
        return MatchExpression(controllerName, controllerMask) &&
               MatchExpression(operationId, actionMask);
    }

    private static bool MatchExpression(string text, string expression)
    {
        // no mask: simple string compare
        if (!expression.Contains('?') && !expression.Contains('*'))
        {
            return string.Equals(text, expression, StringComparison.InvariantCultureIgnoreCase);
        }

        // regex
        var regex = new Regex(expression.Replace(".", "[.]").Replace("*", ".*").Replace('?', '.'));
        return regex.IsMatch(text);
    }
}

Applying Endpoint Filters

The filters are read from the ApiConfiguration configuration at program startup and the ApiVisibilityConvention is registered when controllers are added:

1 public class Program
2 {
3    public static void Main(string[] args)
4    {
5        var builder = WebApplication.CreateBuilder(args);
6
7        // configuration
8        IConfiguration Configuration = new ConfigurationBuilder()
9            .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
10            .AddEnvironmentVariables()
11            .AddCommandLine(args)
12            .Build();
13        var apiConfiguration = Configuration.GetSection(nameof(ApiConfiguration)).Get<ApiConfiguration>();
14
15        // Add services to the container.
16        builder.Services.AddControllers(setupAction =>
17        {
18            if (apiConfiguration != null)
19            {
20                setupAction.Conventions.Add(new ApiVisibilityConvention(
21                    apiConfiguration.VisibleItems,
22                    apiConfiguration.HiddenItems));
23            }
24        });
25
26        // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
27        builder.Services.AddEndpointsApiExplorer();
28        builder.Services.AddSwaggerGen();
29
30        var app = builder.Build();
31
32        // Configure the HTTP request pipeline.
33        if (app.Environment.IsDevelopment())
34        {
35            app.UseSwagger();
36            // show operation id
37            app.UseSwaggerUI(setupAction =>
38            {
39                setupAction.DisplayOperationId();
40            });
41        }
42
43        app.UseHttpsRedirection();
44
45        app.UseAuthorization();
46
47
48        app.MapControllers();
49
50        app.Run();
51    }
52 }
  • 8-13 - load the filter configuration
  • 16-24 - apply the visibility convention
  • 39 - display the operation id (optional)

If no endpoint filter is active, all available endpoints are displayed in the Web UI: <p align="center"> <img src="docs/AllEndpoints.png" width="500" alt="All Endpoints" /> </p>

Endpoints with the Include filter "VisibleItems": ["User.*", "WeatherForecast.Get*"]: <p align="center"> <img src="docs/FilterIncludeEndpoints.png" width="500" alt="Include Endpoints" /> </p>

Endpoints with the Exclude filter "HiddenItems": ["User.*", "WeatherForecast.DeleteWeatherForecast"]: <p align="center"> <img src="docs/FilterExcludeEndpoints.png" width="500" alt="Exclude Endpoints" /> </p>

Endpoints with the Exclude and Include filters "VisibleItems": ["*.Get*"], and "HiddenItems": ["User.Get*"]: <p align="center"> <img src="docs/FilterMixedEndpoints.png" width="500" alt="Exclude and Include Endpoints" /> </p>

Product Compatible and additional computed target framework versions.
.NET net7.0 is compatible.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
1.0.1 149 8/20/2023