If you’re building apps with .NET Minimal APIs, you’ll likely want to implement OpenAPI along with the OpenAPI’s UI interface. The UI interface allows API developers to give consuming clients the ability to learn more about the surface API related to requests and responses. With ASP.NET Core MVC, you commonly do this by using attributes on MVC actions and controllers. However, as you may already be aware, there are no controllers and actions in a Minimal API app, only endpoints.

In this short post, we’ll go through the steps required to add more detailed information to your OpenAPI Schema files, resulting in more visible details on your OpenAPI UI.

Adding Swashbuckle Dependencies to An ASP.NET Core app

Starting with a new Empty Minimal API Project, you’ll want to add our third-party dependencies, the most important of them being Swashbuckle.AspNetCore.

dotnet add package Swashbuckle.AspNetCore 

The dotnet add package command should update your .csproj file with the following item group.

<ItemGroup>
  <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>

Since we’re already in the .csproj file of your Minimal API project, we’ll also want to take an additional step.

Generating XML Documentation Files

XML documentation is a feature of .NET that allows you to document your code inline with detailed information and later pull that information out into reusable XML files. For example, if you’ve ever used a /// in your codebase, then you’ve used this feature. For this case, you’ll be using XML documentation to document our OpenAPI schema and detail information about our endpoints.

In the first PropertyGroup in your Minimal API project, you’ll want to add the GenerateDocumentationFile tag with a true value. The following XML is what your complete .csproj should look like so far.

<Project Sdk="Microsoft.NET.Sdk.Web">

    <PropertyGroup>
        <TargetFramework>net7.0</TargetFramework>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
        <GenerateDocumentationFile>true</GenerateDocumentationFile>
    </PropertyGroup>

    <ItemGroup>
      <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
    </ItemGroup>

</Project>

Documenting Requests, Responses, and Methods

If you’ve followed along, the next step is a breeze. You’ll want to start documenting all the models associated with your API. That includes the requests, responses, and even the endpoints themselves. However, there is a catch; you’ll need to move all methods into a containing class and declare them as static methods.

Let’s take a look at some documented code.

class Request
{
    /// <summary>
    /// The name of someone you want to echo back
    /// </summary>
    /// <example>"David Fowler"</example>
    public string Name { get; set; } = "";
}

class Response
{
    /// <summary>
    /// The message with the form of "Hello, [name]!"
    /// </summary>
    public string Message { get; set; } = "";
}

class Methods
{
    /// <summary>
    /// Says Hello with the provided username
    /// </summary>
    /// <remarks>Cool Beans!</remarks>
    /// <param name="request">The request containing a name</param>
    /// <response code="200">The response with message</response>
    public static Response SayHello(Request request) =>
        new() { Message = $"Hello, {request.Name}!" };

    /// <summary>
    /// Says howdy to the name
    /// </summary>
    /// <remarks>Awesomeness!</remarks>
    /// <param name="name" example="Khalid">name</param>
    /// <response code="200">Howdy</response>
    public static Response Howdy(string? name) => 
        new() { Message = $"Howdy {name ?? "partner"}!" };
}

In the previous code block, you’ll notice request and response models and two endpoints that utilize them.

Now, let’s implement our API.

Implementing The Documented Minimal API

If you’re familiar with Minimal API apps, this won’t be surprising, but it should help put all we’ve done into perspective. Our first step is to register our services in our app.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new() { Title = "My API", Version = "v1" });
    var filePath = Path.Combine(AppContext.BaseDirectory, "Swaggerly.xml");
    c.IncludeXmlComments(filePath);
});

The Swaggerly.xml file is the name of our assembly. Be sure to look in the bin directory to find the exact name of your XML file. It should be the same as your assembly name.

The next step is to register your endpoints with the ASP.NET Core pipeline.

var app = builder.Build();

app.MapSwagger();
app.MapGet("/", () => Results.LocalRedirect("/swagger"));
app.MapGet("/howdy", Methods.Howdy);
app.MapPost("/hi", Methods.SayHello);
app.UseSwaggerUI();
app.Run();

You’ll notice the use of the Methods type, which contains our static method implementations for each endpoint. Again, it would be best to implement endpoints this way so that you may use XML Documentation, which is only allowed on classes, methods, and properties.

Let’s look at a complete implementation of the Minimal API app.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new() { Title = "My API", Version = "v1" });
    var filePath = Path.Combine(AppContext.BaseDirectory, "Swaggerly.xml");
    c.IncludeXmlComments(filePath);
});

var app = builder.Build();

app.MapSwagger();
app.MapGet("/", () => Results.LocalRedirect("/swagger"));
app.MapGet("/howdy", Methods.Howdy);
app.MapPost("/hi", Methods.SayHello);
app.UseSwaggerUI();
app.Run();

class Request
{
    /// <summary>
    /// The name of someone you want to echo back
    /// </summary>
    /// <example>"David Fowler"</example>
    public string Name { get; set; } = "";
}

class Response
{
    /// <summary>
    /// The message with the form of "Hello, [name]!"
    /// </summary>
    public string Message { get; set; } = "";
}

class Methods
{
    /// <summary>
    /// Says Hello with the provided username
    /// </summary>
    /// <remarks>Cool Beans!</remarks>
    /// <param name="request">The request containing a name</param>
    /// <response code="200">The response with message</response>
    public static Response SayHello(Request request) =>
        new() { Message = $"Hello, {request.Name}!" };

    /// <summary>
    /// Says howdy to the name
    /// </summary>
    /// <remarks>Awesomeness!</remarks>
    /// <param name="name" example="Khalid">name</param>
    /// <response code="200">Howdy</response>
    public static Response Howdy(string? name) => 
        new() { Message = $"Howdy {name ?? "partner"}!" };
}

Running your app, you’ll see the OpenAPI interface is showing all your documentation. Awesome!

Conclusion

You can undoubtedly mix this approach with other helper methods that ship with Minimal APIs, but I think this approach is more suitable for API development for a few reasons:

  1. Documentation is built at compile time
  2. Code is less noisy, and documentation is collapsable
  3. Documentation is reusable outside of OpenAPI

Those three things make this approach better, in my opinion.

If you’re concerned about strings and forgetting values in your documentation, don’t be. I find most modern IDEs, especially tools like JetBrains Rider, will give you warnings about missing values and parameters.

Documenting your APIs using this approach is excellent, but it took a bit of discovery to find out how to put it all together. I hope you enjoyed this post and will share it with friends and colleagues. If you have questions, follow me on Twitter @buhakmeh and ask away. Cheers!