Dynamically Extracting Endpoint Names and Attributes from a Custom C# MVC API
Recently, I was asked whether a specific custom API endpoint functionality existed for a client. Unfortunately, I realized that I had been a bit lax in documenting the API endpoints as I added new functionality. In fact, more than a bit lax as the endpoint documentation was practically non-existent (shhh, keep that a secret). Rather than manually going through my code to extract each endpoint name, action, method, and route details (a process that is both time-consuming and error-prone), I decided to automate the task. My goal was to encapsulate the process within the custom API, keeping everything self-contained while creating a streamlined, dynamic way to generate API endpoint details. Even before getting started, one of the first questions I considered was where to add all the necessary code within my existing Visual Studio project and which methodologies would be best suited to achieve this, which ultimately led to a significant amount of time researching the topic. In this write-up, I walk through the process I followed to automate the extraction of endpoint names, actions, methods, and route details from one of my custom C# MVC APIs using Reflection and Dependency Injection. Note: References to MyCustomAPI is a placeholder for the actual project name used. I replaced the real name for clarity and confidentiality. What is Reflection? It is a way for a program to "look at itself" while it's running. It is like a program looking in a mirror. It can see its own parts (like classes and methods) and use them, even if it did not know about them when it was written. What is Dependency Injection (DI)? It is like ordering parts for a machine. Instead of the machine finding its own parts, someone else gives it exactly what it needs to work. It is a way for managing how objects in a program get the things (dependencies/parts) they need to work. Overview of the Steps to Set This Up in My Existing Project Create a new service named "EndpointMetadataService.cs". Register the new service by adding code to Program.cs Create a new controller named "DocsController.cs". Create a new model named "EndpointMetadata.cs" to represent the endpoint metadata. Step-by-Step Implementation 1.Create a Service for Endpoint Extraction I am using a dedicated service for endpoint extraction to keep the logic separate from other parts of the application (Separation of Concerns). This approach isolates the endpoint extraction logic from controllers and startup configurations, making it easier to maintain, reuse, and integrate. A folder named "Services" did not exist in my project, so I created a new folder named "Services" in the root of the project (at the same level as "Controllers", "Models", and "Views"). I added a new blank class file to my project named "EndpointMetadataService.cs" in the new "Services" folder."EndpointMetadataService.cs" will be used to handle the reflection and endpoint metadata extraction. This is the code for the "EndpointMetadataService.cs" Service. using MyCustomAPI.Models; // add reference to the model namespace namespace MyCustomAPI.Services { public class EndpointMetadataService { // method to retrieve metadata about all endpoints in the application public List GetEndpoints() { // Get the assembly that contains the executing code (current project) var assembly = Assembly.GetExecutingAssembly(); // Find all classes in the assembly that are derived from ControllerBase (i.e., are controllers) // Exclude abstract classes as they cannot be instantiated var controllers = assembly.GetTypes() .Where(type => typeof(ControllerBase).IsAssignableFrom(type) && !type.IsAbstract); // Create metadata for each controller and its associated actions var endpoints = controllers.Select(controller => new EndpointMetadata { Controller = controller.Name.Replace("Controller", ""), // remove the controller suffix // Retrieve all public instance methods that are action methods Actions = controller.GetMethods(BindingFlags.Instance | BindingFlags.Public) // Filter methods that have HTTP method attributes (e.g., [HttpGet], [HttpPost]) .Where(method => method.GetCustomAttributes().Any()) .Select(method => new EndpointAction { Action = method.Name, // Name of the action method // HTTP methods associated with the action (e.g., GET, POST) Methods = method.GetCustomAttributes().Select(attr => string.Join(", ", attr.HttpMethods)), // Route templates defined for the action (e.g., "api/values/{id}") Routes = method.GetCustomAttributes().Select(attr => attr.Template ?? "[default]"), // Default to "[default]" if no route is specified Description = method.GetCustomAttribute()?.Description ?? "No description available", Parameters = method.GetParameters().Select(pa
Recently, I was asked whether a specific custom API endpoint functionality existed for a client. Unfortunately, I realized that I had been a bit lax in documenting the API endpoints as I added new functionality. In fact, more than a bit lax as the endpoint documentation was practically non-existent (shhh, keep that a secret).
Rather than manually going through my code to extract each endpoint name, action, method, and route details (a process that is both time-consuming and error-prone), I decided to automate the task. My goal was to encapsulate the process within the custom API, keeping everything self-contained while creating a streamlined, dynamic way to generate API endpoint details.
Even before getting started, one of the first questions I considered was where to add all the necessary code within my existing Visual Studio project and which methodologies would be best suited to achieve this, which ultimately led to a significant amount of time researching the topic.
In this write-up, I walk through the process I followed to automate the extraction of endpoint names, actions, methods, and route details from one of my custom C# MVC APIs using Reflection and Dependency Injection.
Note: References to MyCustomAPI is a placeholder for the actual project name used. I replaced the real name for clarity and confidentiality.
What is Reflection?
It is a way for a program to "look at itself" while it's running. It is like a program looking in a mirror. It can see its own parts (like classes and methods) and use them, even if it did not know about them when it was written.
What is Dependency Injection (DI)?
It is like ordering parts for a machine. Instead of the machine finding its own parts, someone else gives it exactly what it needs to work. It is a way for managing how objects in a program get the things (dependencies/parts) they need to work.
Overview of the Steps to Set This Up in My Existing Project
- Create a new service named "EndpointMetadataService.cs".
- Register the new service by adding code to Program.cs
- Create a new controller named "DocsController.cs".
- Create a new model named "EndpointMetadata.cs" to represent the endpoint metadata.
Step-by-Step Implementation
1.Create a Service for Endpoint Extraction
I am using a dedicated service for endpoint extraction to keep the logic separate from other parts of the application (Separation of Concerns). This approach isolates the endpoint extraction logic from controllers and startup configurations, making it easier to maintain, reuse, and integrate.
A folder named "Services" did not exist in my project, so I created a new folder named "Services" in the root of the project (at the same level as "Controllers", "Models", and "Views").
I added a new blank class file to my project named "EndpointMetadataService.cs" in the new "Services" folder."EndpointMetadataService.cs" will be used to handle the reflection and endpoint metadata extraction.
This is the code for the "EndpointMetadataService.cs" Service.
using MyCustomAPI.Models; // add reference to the model namespace
namespace MyCustomAPI.Services
{
public class EndpointMetadataService
{
// method to retrieve metadata about all endpoints in the application
public List GetEndpoints()
{
// Get the assembly that contains the executing code (current project)
var assembly = Assembly.GetExecutingAssembly();
// Find all classes in the assembly that are derived from ControllerBase (i.e., are controllers)
// Exclude abstract classes as they cannot be instantiated
var controllers = assembly.GetTypes()
.Where(type => typeof(ControllerBase).IsAssignableFrom(type) && !type.IsAbstract);
// Create metadata for each controller and its associated actions
var endpoints = controllers.Select(controller => new EndpointMetadata
{
Controller = controller.Name.Replace("Controller", ""), // remove the controller suffix
// Retrieve all public instance methods that are action methods
Actions = controller.GetMethods(BindingFlags.Instance | BindingFlags.Public)
// Filter methods that have HTTP method attributes (e.g., [HttpGet], [HttpPost])
.Where(method => method.GetCustomAttributes().Any())
.Select(method => new EndpointAction
{
Action = method.Name, // Name of the action method
// HTTP methods associated with the action (e.g., GET, POST)
Methods = method.GetCustomAttributes().Select(attr => string.Join(", ", attr.HttpMethods)),
// Route templates defined for the action (e.g., "api/values/{id}")
Routes = method.GetCustomAttributes().Select(attr => attr.Template ?? "[default]"), // Default to "[default]" if no route is specified
Description = method.GetCustomAttribute()?.Description ?? "No description available",
Parameters = method.GetParameters().Select(param => new ParameterMetadata
{
Name = param.Name!,
Type = param.ParameterType.Name,
IsRequired = !param.HasDefaultValue
}).ToList(), // Convert actions to a list
ResponseType = method.ReturnType.Name
}).ToList() // Convert controllers to a list
}).ToList();
// Return the constructed list of endpoint metadata
return endpoints;
}
}
}
2.Register the Service in Dependency Injection
I am using ASP.NET Core 6+, so I modified" Program.cs". .NET versions older than 6 would require you to make the changes in "Startup.cs".
I added this code to ensure that the "EndpointMetadataService" is available for use in all of the controllers and other parts of the API.
// Register the EndpointMetadataService service
builder.Services.AddSingleton
This should be done before building and running the application with "builder.Build()". I put the new code directly after this line of code.
var builder = WebApplication.CreateBuilder(args);
3.Create a new Controller
I created a new Controller named "DocsController.cs" with an API endpoint ([HttpGet("endpoints")])
for exposing the endpoint metadata. I created this new Controller in the "Controller" folder of my project with a new "endpoints" endpoint.
This is the code for the "DocsController.cs" Controller.
using MyCustomAPI.Services;
using Microsoft.AspNetCore.Mvc;
namespace MyCustomAPI.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class DocsController : ControllerBase
{
private readonly EndpointMetadataService _metadataService; // Dependency to handle endpoint metadata
// Constructor to inject the EndpointMetadataService dependency
public DocsController(EndpointMetadataService metadataService)
{
_metadataService = metadataService; // Assign the injected service to a private readonly field
}
// Handles GET requests to "api/docs/endpoints".
// Retrieves a list of endpoints metadata from the service and returns it in the HTTP response.
[HttpGet("endpoints")] // basically the addressable endpoint name for the URL
[Description("Retrieve each endpoint name, action, method, parameters, and route details")]
public IActionResult GetEndpoints()
{
// Fetches endpoint metadata
var endpoints = _metadataService.GetEndpoints();
// return an OK response with the endpoints as the response body
return Ok(endpoints);
}
}
}
4.Create a new model to represent the endpoint metadata
I created a new Model named "EndpointMetadata.cs" to represent the endpoint metadata. I created this new Model in the "Model" folder of my project.
The class definitions for the "EndpointMetadata.cs" Model.
namespace MyCustomAPI.Models
{
public class EndpointMetadata
{
public string? Controller { get; set; }
public IEnumerable? Actions { get; set; }
}
public class EndpointAction
{
public string? Action { get; set; }
public IEnumerable? Methods { get; set; }
public IEnumerable? Routes { get; set; }
public string? Description { get; set; }
public List? Parameters { get; set; }
public string? ResponseType { get; set; }
}
public class ParameterMetadata
{
public string Name { get; set; }
public string Type { get; set; }
public bool IsRequired { get; set; }
}
}
The "EndpointMetadata" class represents the metadata for each controller with a list of "EndpointAction" objects that detail the actions (methods) as well as HTTP methods and routes.
The "EndpointAction" class represents each action in the controller, including its name, HTTP methods, and routes.
Accessing the new Endpoint
To access the new "endpoints" endpoint in the DocsController.cs in development, I simply reference the following URL:
http://localhost/api/docs/endpoints
The endpoint will return the dynamically extracted metadata of my API endpoints, including their names, actions, HTTP methods, and routes, and display that information in a JSON format, either in my browser or Postman, depending on which method I use.
Conclusion
Adding a description tag after the endpoint verb declaration can be a helpful way to describe the endpoint's purpose. For instance:
[HttpPost]
[Description("Description of this API endpoint")]
[Route("{controller}/{action}/{variable1}/{variable2}")]
Documentation is a critical part of application design and development, and in this case, I admittedly missed the mark. While there are other methods to achieve the functionality outlined in this write-up, my research led me to conclude that leveraging Reflection and Dependency Injection offers the most straightforward and easy-to-implement solution.
Automating the extraction of API endpoint names and attribute information in my application not only saves time but also ensures the data remains up-to-date with minimal manual effort. By leveraging Reflection and integrating it into my C# MVC API, I can dynamically extract endpoint details, ensuring accuracy, minimizing human error, and providing a scalable solution for managing API endpoint information.