Six ways to organize minimal APIs in ASP.NET Core applications

In the previous part of this article series you learned to add minimal APIs to the Startup class. So far we have added minimal APIs to the Program.cs file. If you have only a handful of APIs this won't create any problem. But if you have a lots of minimal APIs to deal with, at some point you will want to organize them in a better way. Moreover, Program.cs is primarily intended to place your app startup and initialization code. It's not the best place to house too much of your application logic or functionality. There is no inbuilt way to organize minimal APIs but you can put your knowledge of .NET and C# to use, and organize them in some way. In this part of the series, we will discuss a few possible ways to accomplish this task.

1. Regions

The simplest way to organize your minimal APIs without moving your code here and there is to use regions. We have seven APIs out of which five are related to employee CRUD operations and two are related to JWT authentication. So, you may create two regions that separate these APIs. The following code shows how.

#region Employee CRUD Minimal APIs

app.MapGet("/minimalapi/employees", 
[Authorize](AppDbContext db) =>
{
    return Results.Ok(db.Employees.ToList());
});

// remaining code here

#endregion

#region JWT Authentication Minimal APIs

app.MapPost("/minimalapi/security/getToken", 
[AllowAnonymous]async (UserManager<IdentityUser> 
userMgr, User user) => ...

// remaining code here

#endregion

 Once created they can be collapsed in the Visual Studio as shown below: 

Organizing APIs using regions will help you to quickly jump to the required set of APIs but all the code is still housed inside Program.cs.

A word of caution. Many developers don't like using regions. If you are an experienced .NET developer you are probably aware of the reasons. Regions are mentioned here simply because that's one of the possibility.

2. Local functions

Although regions allow you to easily locate, expand and collapse pieces of code, the code is till "inline" with the other code. Another easy way to organize minimal APIs in to put them inside local functions. And then call those local functions from the main code. Local functions must be added before any type definitions in the Program.cs file. For example, the following code shows two local functions namely RegisterEmployeeAPIs() and RegisterAuthenticationAPIs().

app.Run();

void RegisterEmployeeAPIs()
{
}

void RegisterAuthenticationAPIs()
{
}

Notice that these functions are added just after the app.Run() call and before the entity and DbContext classes.

Once added you can move employee CRUD minimal APIs in the RegisterEmployeeAPIs() method as shown below:

void RegisterEmployeeAPIs()
{
    app.MapGet("/minimalapi/employees", 
[Authorize](AppDbContext db) =>
    {
        return Results.Ok(db.Employees.ToList());
    });

    // other CRUD operations here
}

And you can move JWT authentication related APIs to the RegisterAuthenticationAPIs() method.

void RegisterAuthenticationAPIs()
{
    app.MapPost("/minimalapi/security/getToken", 
[AllowAnonymous]async (UserManager<IdentityUser> 
userMgr, User user) =>
    {...}

    // other code here
}

Finally, you can call these local functions from the top-level statements as shown below:

...
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
...
...
RegisterEmployeeAPIs();
RegisterAuthenticationAPIs();
app.Run();

3. Local functions for handlers

In the previous technique we placed all the "Map" calls in separate functions. You can also move just the endpoint handler into a separate function and mention the function name in the "Map" calls. Consider the code below:

app.MapGet("/minimalapi/employees", GetAllEmployees);
app.MapGet("/minimalapi/employees/{id}", GetEmployeeByID);
app.MapPost("/minimalapi/employees", InsertEmployee);
app.MapPut("/minimalapi/employees/{id}", UpdateEmployee);
app.MapDelete("/minimalapi/employees/{id}", DeleteEmployee);
app.MapPost("/minimalapi/security/getToken", GetToken);
app.MapPost("/minimalapi/security/createUser", CreateUser);

As you can see, instead of writing the handler in the "Map" calls themselves, we have placed them in local functions such as GetAllEmployees() and GetToken(). This makes the "Map" calls compact and quick to locate.

These functions are created just like the local functions in the previous example. For your understanding GetAllEmployees() and GetToken() methods are shown below.

[Authorize]
IResult GetAllEmployees(AppDbContext db)
{
    return Results.Ok(db.Employees.ToList());
}

[AllowAnonymous]
async Task<IResult> GetToken(UserManager<IdentityUser> 
userMgr, User user)
{
  // method code goes here
}

These functions return IResult and Task<IResult> respectively. They are also decorated with attributes such as [Authorize] and [AllowAnonymous].

4. Static methods

If you feel that keeping the minimal APIs in Program.cs is causing too much of clutter, you may move  them out into a separate class. For example, you may create a static class called MinimalApiEndpoints and add two static methods to it - one for employee CRUD operations and other for JWT authentication. The following code shows the skeleton of such a class:

public static class MinimalApiEndpoints
{
    public static void RegisterEmployeeAPIs
(WebApplication app)
    {
    }

    public static void RegisterAuthenticationAPIs
(WebApplication app)
    {
    }
}

As you can see, both the methods take WebApplication as their parameter. Inside, you can call Map methods on the WebApplication as before. The following code shows a sample.

public static void RegisterEmployeeAPIs(WebApplication app)
{
    app.MapGet("/minimalapi/employees", 
[Authorize](AppDbContext db) =>
    {
        return Results.Ok(db.Employees.ToList());
    });

    // other calls here
}
public static void RegisterAuthenticationAPIs
(WebApplication app)
{
    app.MapPost("/minimalapi/security/getToken", 
[AllowAnonymous]async (UserManager<IdentityUser> 
userMgr, User user) =>
    {...}

   // other code here
}

Now, you can call RegisterEmployeeAPIs() and RegisterAuthenticationAPIs() methods from Program.cs like this:

...
MinimalApiEndpoints.RegisterEmployeeAPIs(app);
MinimalApiEndpoints.RegisterAuthenticationAPIs(app);

app.Run();
...

Just above app.Run() you place the calls to both the static methods.

If you want you can create static methods just for the handler functions.

5. Extension methods

If you want you can create these methods as the extension methods of WebApplication class. You can simplify calling them like this:

...
app.RegisterEmployeeAPIs();
app.RegisterAuthenticationAPIs();

app.Run();
...

To create RegisterEmployeeAPIs() and RegisterAuthenticationAPIs() methods as extension methods of WebApplication simply change their signature as shown below:

public static void RegisterEmployeeAPIs
(this WebApplication app)
{
  // API code here
}

public static void RegisterAuthenticationAPIs
(this WebApplication app)
{
  // API code here
}

As you can see we added this keyword to the WebApplication parameter. Once done Visual Studio will show them in the IntelliSense for the app object in Program.cs.

6. Separate class with constructor injection

If you observe the minimal APIs we created so far, you will find that at many places we are passing parameters that are injected by DI. For example, AppDbContext and UserManager objects are injected by DI in all the minimal APIs. This is required because each handler method needs to work with AppDbContext / UserManager. Can we avoid passing them in each and every API? Can we just inject them once and let the APIs use the injected instance (just like controller based APIs or MVC controllers)? With a bit of code you can accomplish this task. Let's see how.

Add a new class to the project named MinimalApiEndpointsWithDI. Then add members, a constructor, and two methods as shown in the class skeleton below:

public class MinimalApiEndpointsWithDI
{
    private readonly AppDbContext db;
    private readonly UserManager<IdentityUser> userMgr;

    public MinimalApiEndpointsWithDI(AppDbContext db, 
UserManager<IdentityUser> userMgr)
    {
        this.db = db;
        this.userMgr = userMgr;
    }

    public void RegisterEmployeeAPIs(WebApplication app)
    {
      // code here
    }

    public void RegisterAuthenticationAPIs(WebApplication app)
    {
      // code here
    }
}

As you can see, we have now declared AppDbContext and UserManager at the top of the class and we inject them in the constructor. The RegisterEmployeeAPIs() and RegisterAuthenticationAPIs() methods serve the same purpose as before but various Map methods won't accept AppDbContext and UserManager parameters because they are already injected in the constructor. The following code shows how the modified Map calls look like.

public void RegisterEmployeeAPIs(WebApplication app)
{
    app.MapGet("/minimalapi/employees", [Authorize]() =>
    {
        return Results.Ok(db.Employees.ToList());
    });

    app.MapGet("/minimalapi/employees/{id}", 
[Authorize](int id) =>
    {
        return Results.Ok(db.Employees.Find(id));
    });

    app.MapPost("/minimalapi/employees", 
[Authorize](Employee emp) =>
    {
        db.Employees.Add(emp);
        db.SaveChanges();
        return Results.Created($"/minimalapi/employees/
{emp.EmployeeID}", emp);
    });

    app.MapPut("/minimalapi/employees/{id}", 
[Authorize](int id, Employee emp) =>
    {
        db.Employees.Update(emp);
        db.SaveChanges();
        return Results.NoContent();
    });

    app.MapDelete("/minimalapi/employees/{id}", 
[Authorize](int id) =>
    {
        var emp = db.Employees.Find(id);
        db.Remove(emp);
        db.SaveChanges();
        return Results.NoContent();
    });
}

public void RegisterAuthenticationAPIs(WebApplication app)
{
    app.MapPost("/minimalapi/security/getToken", 
[AllowAnonymous]async (User user) =>
    {
        var identityUsr = await userMgr.
FindByNameAsync(user.UserName);

        if (await userMgr.CheckPasswordAsync
(identityUsr, user.Password))
        {
            var issuer = app.Configuration["Jwt:Issuer"];
            var audience = app.Configuration["Jwt:Audience"];
            var securityKey = new SymmetricSecurityKey
        (Encoding.UTF8.GetBytes(app.Configuration["Jwt:Key"]));
            var credentials = new SigningCredentials
(securityKey, SecurityAlgorithms.HmacSha256);

            var token = new JwtSecurityToken(issuer: issuer,
                audience: audience,
                signingCredentials: credentials);

            var tokenHandler = new JwtSecurityTokenHandler();
            var stringToken = tokenHandler.WriteToken(token);

            return Results.Ok(stringToken);
        }
        else
        {
            return Results.Unauthorized();
        }
    });


    app.MapPost("/minimalapi/security/createUser", 
[AllowAnonymous] async (User user) =>
    {
        var identityUser = new IdentityUser()
        {
            UserName = user.UserName,
            Email = user.UserName + "@example.com"
        };

        var result = await userMgr.CreateAsync
(identityUser, user.Password);

        if (result.Succeeded)
        {
            return Results.Ok();
        }
        else
        {
            return Results.BadRequest();
        }
    });
}

Notice the code shown in bold letters that indicates that AppDbContext and UserManager are no longer passed as method parameters.

Now that the MinimalApiEndpointsWithDI class is ready, we need to register it with DI container as follows:

...
builder.Services.AddDbContext<AppDbContext>
(o => o.UseSqlServer(connectionString));
builder.Services.AddDbContext<AppIdentityDbContext>
(o => o.UseSqlServer(connectionString));
builder.Services.AddScoped<MinimalApiEndpointsWithDI>();
...

As you can see, we used AddScoped() method to register MinimalApiEndpointsWithDI with DI container.

Next, we will get an instance of MinimalApiEndpointsWithDI from DI so that we can call its RegisterEmployeeAPIs() and RegisterAuthenticationAPIs() methods. Take a look at the following code that does this. Place this code just above app.Run() call.

...
using (var scope = app.Services.CreateScope())
{
    var service = scope.ServiceProvider.
GetService<MinimalApiEndpointsWithDI>();
    service.RegisterEmployeeAPIs(app);
    service.RegisterAuthenticationAPIs(app);
}
app.Run();
...

Notice the code marked in bold letters. We use CreateScope() method followed by GetService() method to get an instance of MinimalApiEndpointsWithDI. We then call RegisterEmployeeAPIs() and RegisterAuthenticationAPIs() methods by passing WebApplication object to them.

We could have skipped registering MinimalApiEndpointsWithDI with DI container but then we would have required to pass AppDbContext and UserManager parameters by retrieving them from DI. Registering MinimalApiEndpointsWithDI with DI saves us from that job.

As you can see, the first four approaches are quite straightforward and require no changes to the Map methods. The fifth approach is a bit complicated one and might prove to be "too much" in many situations. But it's possible just in case you need to do that. You have to decide whether you can justify the complexity involved, otherwise sticking to any of the previous approaches would do the job.

Minimal APIs were introduced to make API development under ASP.NET Core simple and quick (especially for developers new to ASP.NET Core). However, there could be situations where you need to evolve from minimal APIs to controller based APIs. In the next part of this article series we will see how much work is involved in such cases by moving our code to controller based APIs.

That's it for now! Keep coding!!


Bipin Joshi is an independent software consultant and trainer by profession specializing in Microsoft web development technologies. Having embraced the Yoga way of life he is also a yoga mentor, meditation teacher, and spiritual guide to his students. He is a prolific author and writes regularly about software development and yoga on his websites. He is programming, meditating, writing, and teaching for over 27 years. To know more about his private online courses on ASP.NET and meditation go here and here.

Posted On : 05 January 2022


Tags : ASP.NET ASP.NET Core MVC .NET Framework C# Visual Studio