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!!