Updated on
Hiding an endpoint means keeping it out of the OpenAPI document our application generates, so it never appears in Swagger UI. The endpoint itself keeps working: every technique below except [NonAction] leaves the route reachable over HTTP.
Which one we reach for depends on how much we want to hide and from what. Attributes like [ApiExplorerSettings(IgnoreApi = true)] act on the framework’s API Explorer and hide an action from every generator at once. DocInclusionPredicate() and document filters act inside Swashbuckle, and the OpenAPI generator that ships with .NET 9 and .NET 10 has its own equivalents for both.
Why Would We Hide an Endpoint in Swagger?
Hiding an endpoint means keeping its operation out of the OpenAPI document our application generates. Swagger UI only draws what that document contains, so an operation missing from it is missing from the page, and there is no “Try it out” button to press.
Nothing about the route itself changes. The endpoint still answers requests, still runs its authorization, and still sits in the routing table. The single exception is [NonAction], which tells MVC the method is not an action at all, so the route disappears along with the documentation and the URL starts returning 404.
That difference is the whole safety story, and it is worth being blunt about. Hiding an operation is a documentation decision, not a security control. Anyone who knows the URL can still call a hidden endpoint, so a sensitive one needs real authorization first, with hiding as a second layer at most.
Swagger is the toolchain built around that document, and configuring Swagger UI in an ASP.NET Core Web API is where the setup this article assumes comes from.
There can be a variety of reasons why we wouldn’t want to expose an endpoint in our documentation:
- Sensitive operations
- Deprecated endpoints
- Internal endpoints
- Not fully implemented endpoints
How Do We Hide an Endpoint With Swashbuckle?
We look at five ways to hide an endpoint, and they fall into two groups that behave differently.
Three of them work through the API Explorer, which is the framework’s own inventory of our endpoints. [NonAction], [ApiExplorerSettings(IgnoreApi = true)], and an IActionModelConvention that sets ApiExplorer.IsVisible to false all remove the action before any document generator gets to see it.
The other two work inside Swashbuckle itself. DocInclusionPredicate() decides, endpoint by endpoint, whether an action belongs in a named document, and an IDocumentFilter edits the finished document and removes paths by name.
The practical difference is reach. The API Explorer group hides an endpoint from every generator we have installed and from every document at once. The Swashbuckle group hides it only from Swashbuckle, and only from the documents we configure, which is exactly what we want when one document should show an endpoint and another should not.
To start it off, we will create a new ASP.NET Core Web API project. We can do it using the ASP.NET Core Web API template in Visual Studio, or using .NET CLI.
Since .NET 9, that template no longer includes Swashbuckle. It installs Microsoft.AspNetCore.OpenApi and calls AddOpenApi() and MapOpenApi(), which write the OpenAPI document but serve no user interface. So we create the project with controllers and add Swashbuckle ourselves:
dotnet new webapi --use-controllers dotnet add package Swashbuckle.AspNetCore
Then three lines in Program.cs register the generator and the UI:
builder.Services.AddSwaggerGen(); app.UseSwagger(); app.UseSwaggerUI();
To show different ways of hiding an endpoint, let’s extend the default WeatherForecast controller with a few methods of our own:
[HttpGet("GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
return GetWeatherForecastData();
}
[HttpGet("GetMethodOne")]
public IEnumerable<WeatherForecast> GetMethodOne()
{
return GetWeatherForecastData();
}
[HttpGet("GetMethodTwo")]
public IEnumerable<WeatherForecast> GetMethodTwo()
{
return GetWeatherForecastData();
}
[HttpGet("GetMethodThree")]
public IEnumerable<WeatherForecast> GetMethodThree()
{
return GetWeatherForecastData();
}
[HttpGet("GetMethodFour")]
public IEnumerable<WeatherForecast> GetMethodFour()
{
return GetWeatherForecastData();
}
private IEnumerable<WeatherForecast> GetWeatherForecastData()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
We expand the existing WeatherForecast controller with four more methods that we can use to experiment with hiding endpoints. We have kept the names of the methods and the endpoint URIs straightforward for simplicity, and in real-life scenarios we should follow the REST URI formatting.
Note that we give each method a route segment rather than the template’s route name, so every endpoint gets its own path.
Now, if we run our application and open http://localhost:<port>/swagger/index.html, we can see how all the routes are documented in Swagger:

Since now we have everything ready, let’s try to hide each one of these methods differently.
Using DocInclusionPredicate
The first way we can go about hiding an endpoint in Swagger is to use DocInclusionPredicate().
DocInclusionPredicate() is a delegate that is invoked against every ApiDescription that’s surfaced by our application. It takes two parameters, docName and apiDesc. docName refers to the name of the Swagger document, which can be useful to distinguish between multiple Swagger documents.
Meanwhile, apiDesc parameter represents the API description, which contains information about the processed endpoint.
We can set up DocInclusionPredicate() when configuring Swagger, and customize it to filter the endpoints and operations included in the generated Swagger documentation.
So let’s say we want to hide our first endpoint named GetWeatherForecast:
builder.Services.AddSwaggerGen(c =>
{
c.DocInclusionPredicate((docName, apiDesc) =>
{
var routeTemplate = apiDesc.RelativePath;
if (routeTemplate == "WeatherForecast/GetWeatherForecast")
return false;
return true;
});
});
First, we extract the relative path of the API endpoint that is being evaluated in the routeTemplate variable. After that, we check if the endpoint routeTemplate matches "WeatherForecast/GetWeatherForecast".
If it matches, we return false, which means that the API endpoint will be excluded from the Swagger documentation. In any other case, we return true, meaning that the endpoint will be included in the Swagger documentation.
Using NonAction Attribute
One more thing we can do when we want to hide an endpoint from Swagger is to use NonAction attribute on the endpoint method we want to hide:
[HttpGet("GetMethodOne")]
[NonAction]
public IEnumerable<WeatherForecast> GetMethodOne()
{
return GetWeatherForecastData();
}
By using NonAction attribute we indicate that the controller method is not an action method. Since Swagger relies on the API actions to generate the documentation, it will ignore any endpoint we mark with this attribute during the scanning process, and therefore the endpoint will not appear in the Swagger documentation. It also hides the public availability of the action.
We can apply this attribute only to controller endpoints, and not to the whole controller itself.
Using ApiExplorerSettings Attribute With the IgnoreApi Parameter
Another way to hide an endpoint from Swagger is to use the ApiExplorerSettings attribute with the IgnoreApi parameter set to true:
[HttpGet("GetMethodTwo")]
[ApiExplorerSettings(IgnoreApi = true)]
public IEnumerable<WeatherForecast> GetMethodTwo()
{
return GetWeatherForecastData();
}
When using the [ApiExplorerSettings(IgnoreApi = true)] attribute, we instruct the API Explorer, which supplies the endpoints for the Swagger documentation, to exclude a specific endpoint from the generated documentation.
Contrary to the NonAction attribute, we can use this attribute on the controller level, and hide all endpoints inside it.
Using IActionModelConvention
What can also come in handy when trying to hide an endpoint in Swagger is IActionModelConvention.
In general, IActionModelConvention is an interface that we can use to apply conventions to individual action methods. To use it, we need to implement the interface and the Apply() method that takes an ActionModel object as a parameter and applies desired conventions to it:
public class HideControllerConvention : IActionModelConvention
{
public void Apply(ActionModel action)
{
if (action.ActionName == "GetMethodThree")
{
action.ApiExplorer.IsVisible = false;
}
}
}
In our case, we want to hide the GetMethodThree endpoint. First, we check the action ActionName property to find the wanted action name, and after we have it, we set its visibility to false.
Now, when we set up the desired convention, we also need to register it in our Program.cs when adding controllers to make everything work as expected:
builder.Services.AddControllers(s => s.Conventions.Add(new HideControllerConvention() ));
Using IDocumentFilter
The last route we will take in hiding the Swagger endpoint is to use IDocumentFilter.
The IDocumentFilter interface is part of Swashbuckle.AspNetCore.SwaggerGen namespace and it allows us to modify the generated Swagger document before it’s presented. In our case, we create SwaggerDocumentFilter class which implements the IDocumentFilter interface:
public class SwaggerDocumentFilter : IDocumentFilter
{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
swaggerDoc.Paths.Remove("/WeatherForecast/GetMethodFour");
}
}
Inside the Apply() method, we have access to the OpenApiDocument and the DocumentFilterContext. The OpenApiDocument represents the generated Swagger document, and DocumentFilterContext provides us with additional context during the document generation process.
By calling swaggerDoc.Paths.Remove("/WeatherForecast/GetMethodFour"), we remove the entry for this specific endpoint from the Paths property of the Swagger document, In this way, we are making sure that the method will be hidden from the generated Swagger documentation.
Removing a path is only one of the jobs a filter does, and a sibling hook, IOperationFilter, is how we go about adding a header parameter to every endpoint.
Lastly, we need to add the document filter when configuring Swagger in our Program.cs:
builder.Services.AddSwaggerGen(c =>
{
c.DocumentFilter<SwaggerDocumentFilter>();
c.DocInclusionPredicate((docName, apiDesc) =>
{
var routeTemplate = apiDesc.RelativePath;
if (routeTemplate == "WeatherForecast/GetWeatherForecast")
return false;
return true;
});
});
Now, if we rerun our application, we can see that the generated spec file does not present any of the endpoints:

How Do We Hide an Endpoint With the Built-In OpenAPI Generator?
Microsoft.AspNetCore.OpenApi is the generator the Web API template installs today, and every technique above has a counterpart in it. It is already installed on a new project, which is why the setup section had to add Swashbuckle by hand.
The three API Explorer techniques need no change at all. [NonAction], [ApiExplorerSettings(IgnoreApi = true)], and an IActionModelConvention hide an action here exactly as they do under Swashbuckle, because none of them touches the generator.
The two Swashbuckle-specific ones have direct replacements. DocInclusionPredicate() becomes the ShouldInclude delegate on OpenApiOptions, and IDocumentFilter becomes IOpenApiDocumentTransformer, whose TransformAsync() method receives the same OpenApiDocument and removes paths the same way.
One trap comes with ShouldInclude, and it is easy to walk into. Assigning it replaces the default predicate rather than adding to it, and the default is what keeps grouped endpoints in their own documents. Overwrite it without repeating that check and every grouped endpoint shows up in every document.
The wiring goes in Program.cs:
builder.Services.AddOpenApi(options =>
{
options.ShouldInclude = apiDesc => apiDesc.RelativePath != "WeatherForecast/GetWeatherForecast";
options.AddDocumentTransformer<HideEndpointDocumentTransformer>();
});
And the transformer replaces the document filter:
public class HideEndpointDocumentTransformer : IOpenApiDocumentTransformer
{
public Task TransformAsync(OpenApiDocument document,
OpenApiDocumentTransformerContext context,
CancellationToken cancellationToken)
{
document.Paths.Remove("/WeatherForecast/GetMethodFour");
return Task.CompletedTask;
}
}
Side by side, that gives us one lookup for every technique on both generators, and for whether the endpoint still answers after we hide it:
| To hide | With Swashbuckle | With the built-in generator | Still reachable over HTTP? |
|---|---|---|---|
| One action | [ApiExplorerSettings(IgnoreApi = true)] | The same attribute | Yes |
| One action, and switch it off | [NonAction] | The same attribute | No, the route returns 404 |
| Every action on a controller | [ApiExplorerSettings(IgnoreApi = true)] on the class | The same attribute | Yes |
| A minimal API endpoint | .ExcludeFromDescription() | .ExcludeFromDescription() | Yes |
| Actions matching a rule we write | DocInclusionPredicate((docName, apiDesc) => …) | options.ShouldInclude = apiDesc => … | Yes |
| Actions matching a rule, before the generator runs | IActionModelConvention setting ApiExplorer.IsVisible to false | The same convention | Yes |
| A path already in the finished document | IDocumentFilter calling Paths.Remove(path) | IOpenApiDocumentTransformer calling Paths.Remove(path) | Yes |
Minimal API endpoints have their own way in: app.MapGet("/internal", …).ExcludeFromDescription() keeps an endpoint out of the document under both generators. There is an [ExcludeFromDescription] attribute too, but it is minimal API metadata: putting it on an MVC controller action does nothing, and the action stays in the document.
If the real requirement is “internal and public documentation from one API” rather than “hide this forever”, AddOpenApi("internal") alongside AddOpenApi("v1") gives us two documents, and [ApiExplorerSettings(GroupName = "internal")] on a controller decides which one an endpoint lands in. A document transformer registered on one document does not run on the other.
The built-in generator writes the document and nothing else. app.MapOpenApi() serves it at /openapi/v1.json and there is no page to look at, so seeing the result means either reading the JSON or adding a UI package such as Swagger UI or Scalar. Microsoft’s own guide to generating OpenAPI documents spells out the same split between generating the document and displaying it.
Keeping a whole operation out of the document is one half of this job, and the other half is how to ignore model properties with Swagger, which hides a field inside the schema rather than the operation itself.
When Should We Use Each Method?
Start with the attribute, because it is the smallest thing that does the job. [ApiExplorerSettings(IgnoreApi = true)] hides one action, or every action on a controller when we put it on the class, and it needs no configuration anywhere else in the application.
Reach for [NonAction] only when we want the endpoint gone rather than hidden. It is the one option here that also removes the route, so the URL answers 404 instead of returning data.
Use a convention when the rule is about the actions themselves: hide everything in a namespace, everything carrying a marker attribute, everything an internal team owns. It runs once at startup, and no generator ever sees those actions.
Use ShouldInclude or DocInclusionPredicate() when the same endpoint belongs in one document and not another, and a document transformer or filter when the rule is about the finished document rather than about the endpoint.
For simple cases where we need to hide individual endpoints, using the IgnoreApi parameter of the ApiExplorerSettings attribute or the NonAction attribute can be sufficient. Their usage is the most straightforward one.
It’s worth noting that, the NonAction attribute hides the endpoint from the documentation and it completely disables it and we can’t reach it with HTTP calls, unlike the other techniques where the endpoint is hidden from the documentation but will stay reachable by HTTP calls targeting it.
For situations requiring more advanced filtering or transformations, the IActionModelConvention can be an excellent choice. We can hide an endpoint in Swagger based on any custom filter on all the properties available in the ActionModel class.
They run at different moments. DocInclusionPredicate() is asked about one endpoint at a time, before the document exists, and answers yes or no. An IDocumentFilter runs once at the end, with the finished document, every included ApiDescription and the document name in hand, and edits what is already there, which is why it can remove a path the predicate already let through. One benefit of the DocInclusionPredicate() is that it is configured directly when setting up a Swagger, and it doesn’t require any additional interface implementations.
Conclusion
In this article, we looked at different ways to hide API endpoints in Swagger documentation. We explored using DocInclusionPredicate() for direct configuration, as well as simpler options like the IgnoreApi parameter and the NonAction attribute. Lastly, we covered more advanced customization using IDocumentFilter and IActionModelConvention interfaces that give us a range of choices to hide endpoints. All of them, or their built-in counterparts, still apply on .NET 10, whether we generate the document with Swashbuckle or with the Microsoft.AspNetCore.OpenApi package the Web API template installs today.
Tested with .NET 10.0.10, Swashbuckle.AspNetCore 10.2.3 and Microsoft.AspNetCore.OpenApi 10.0.12.

Just using the private keyword for the action method also works. Is this recommended?
Hi Femi. Now, when you say it, yes, you can use it, but I am not sure whether it is recommended or not. On the other hand, I will share my opinion on this one.
First, you must note that all the methods from this article, except using the [NonAction] attribute will not hide the action from public use. This is not the goal here, but just to hide the endpoint from Swagger.
Now, you can say: “Yeah, but using the NonAction attribute will hide the action from public use, why can’t I just use the private keyword”.
Well, there, I have some conceptual issues. For me, if you want to mark something as private, this means that you want to keep it private as soon as you create it, and having said that, I can’t imagine why would someone create an action that isn’t publicly available. Yes, at some point, we can decide that we don’t want to use that action anymore, but let’s leave it there if we need it later on. In that case, I would again use the attribute over the private keyword, as this makes a crucial conceptual difference – We made this action publically available, and we will maybe enable it later on, just for now, we don’t want to do that. This is not something you state by changing the description of your action to private. Again, these are just my thoughts.