Minimal API Discovery Tool
Tags: csharp, rest, aspnetcore, dotnetcore
Published: 2023-02-26 23:26:00

---
title: Minimal API Discovery Tool
published: true
date: 2023-02-22 00:00:00 UTC
tags: 
  - csharp
  - rest
  - aspnetcore
  - dotnetcore
canonical_url: https://wildermuth.com/2023/02/22/minimal-api-discovery/
cover_image: "https://wilderminds.blob.core.windows.net/img/2023/02/22/cover.jpg"
---

I’ve been posting and making videos about ideas I’ve had for discovering Minimal APIs instead of mapping them all in `Program.cs` for a while. I’ve finally codified it into an experimental nuget package. Let’s talk about how it works.

I also made a Coding Short video that covers this same topic, if you’d rather watch than read:

<iframe src="https://www.youtube.com/embed/i0sSXHQvRhU" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe>

## How It Works

The package can be installed via the dotnet tool:

```
dotnet add package WilderMinds.MinimalApiDiscovery

```

Once it is installed, you can use an interface called `IApi` to implement classes that can register Minimal APIs. The `IApi` interface looks like this:

```
/// <summary>
/// An interface for Identifying and registering APIs
/// </summary>
public interface IApi
{
  /// <summary>
  /// This is automatically called by the library to add your APIs
  /// </summary>
  /// <param name="app">The WebApplication object to register the API </param>
  void Register(WebApplication app);
}

```

Essentially, you can implement classes that get passed the `WebApplication` object to map your API calls:

```
public class StateApi : IApi
{
  public void Register(WebApplication app)
  {
    app.MapGet("/api/states", (StateCollection states) =>
    {
      return states;
    });
  }
}

```

This would allow you to register a number of related API calls. I think one class per API is too restrictive. When used in .NET 7 and later, you could make a class per `group`:

```
  public void Register(WebApplication app)
  {
    var group = app.MapGroup("/api/films");

    group.MapGet("", async (BechdelRepository repo) =>
    {
      return Results.Ok(await repo.GetAll());
    })
      .Produces(200);

    group.MapGet("{id:regex(tt[0-9]*)}", 
      async (BechdelRepository repo, string id) =>
    {
      Console.WriteLine(id);
      var film = await repo.GetOne(id);
      if (film is null) return Results.NotFound("Couldn't find Film");
      return Results.Ok(film);
    })
      .Produces(200);

    group.MapGet("{year:int}", (BechdelRepository repo, 
        int year, 
        bool? passed = false) =>
        {
          var results = await repo.GetByYear(year, passed);
          if (results.Count() == 0)
          {
            return Results.NoContent();
          }

          return Results.Ok(results);
        })
      .Produces(200);

    group.MapPost("", (Film model) =>
    {
      return Results.Created($"/api/films/{model.IMDBId}", model);
    })
      .Produces(201);

  }

```

Because of lambdas missing some features (e.g. default values), you can always move the lambdas to just static methods:

```
public void Register(WebApplication app)
{
  var grp = app.MapGroup("/api/customers");
  grp.MapGet("", GetCustomers);
  grp.MapGet("", GetCustomer);
  grp.MapPost("{id:int}", SaveCustomer);
  grp.MapPut("{id:int}", UpdateCustomer);
  grp.MapDelete("{id:int}", DeleteCustomer);
}

static async Task<IResult> GetCustomers(CustomerRepository repo)
{
  return Results.Ok(await repo.GetCustomers());
}

//...

```

The reason for the suggestion of using static methods (instance methods would work too) is that you do not want these methods to rely on state. You might think that constructor service injection would be a good idea:

```
public class CustomerApi : IApi
{
  private CustomerRepository _repo;

  // MinimalApiDiscovery will log a warning because
  // the repo will become a singleton and lifetime 
  // will be tied to the implementation methods.
  // Better to use method injection in this case.
  public CustomerApi(CustomerRepository repo)
  {
    _repo = repo;
  }

// ...

```

This doesn’t work well as the call to `Register` happens once at startup and since this class is sharing that state, the injected service becomes a singleton for the lifetime of the server. The library will log a **warning** if you do this to help you avoid it. Because of that I suggest that you use static methods instead to prevent this from accidently happening.

> NOTE: I considered using static interfaces, but that requires that the instance is still a non-static class. It would also limit this library to use in .NET 7/C# 11 - which I didn’t want to do. It works in .NET 6 and above.

When you’ve created these classes, you can simple make two calls in startup to register all `IApi` classes:

```
using UsingMinimalApiDiscovery.Data;
using WilderMinds.MinimalApiDiscovery;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddTransient<CustomerRepository>();
builder.Services.AddTransient<StateCollection>();

// Add all IApi classes to the Service Collection
builder.Services.AddApis();

var app = builder.Build();

// Call Register on all IApi classes
app.MapApis();

app.Run();

```

The idea here is to use reflection to find all `IApi` classes and add them to the service collection. Then the call to `MapApis()` will get all `IApi` from the service collection and call Register.

## How it works

The call to `AddApis` simply uses reflection to find all classes that implement `IApi` and add them to the service collection:

```
  var apis = assembly.GetTypes()
    .Where(t => t.IsAssignableTo(typeof(IApi)) && 
                t.IsClass && 
                !t.IsAbstract)
    .ToArray();

  // Add them all to the Service Collection
  foreach (var api in apis)
  {
    // ...
    coll.Add(new ServiceDescriptor(typeof(IApi), api, lifetime));
  }

```

Once they’re all registered, the call to `MapApis` is pretty simple:

```
var apis = app.Services.GetServices<IApi>();

foreach (var api in apis)
{
  if (api is null) throw new InvalidProgramException("Apis not found");

  api.Register(app);
}

```

## Futures

While I’m happy with this use of Reflection since it is only a ‘startup’ time cost, I have it on my list to look at using a Source Generator instead.

> If you have experience with Source Generators and want to give it a shot, feel free to do a pull request at [https://github.com/wilder-minds/minimalapidiscovery](https://github.com/wilder-minds/minimalapidiscovery).

I’m also considering removing the AddApis and just have the `MapApis` call just reflect to find all the IApis and call register since we don’t actually need them in the Service Collection.

You can see the complete source and example here:

> [https://github.com/wilder-minds/minimalapidiscovery](https://github.com/wilder-minds/minimalapidiscovery)

[![Creative Commons License](http://i.creativecommons.org/l/by-nc-nd/3.0/88x31.png)](http://creativecommons.org/licenses/by-nc-nd/3.0/)

 This work by [Shawn Wildermuth](http://wildermuth.com) is licensed under a [Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported License](http://creativecommons.org/licenses/by-nc-nd/3.0/).  
 Based on a work at [wildermuth.com](http://wildermuth.com).

* * *

If you liked this article, see Shawn's courses on [Pluralsight](http://shawnw.me/pscourses).