Migrate minimal APIs to controller based APIs
In the previous part
of this article series we discussed a few ways of organizing minimal APIs. There
can be situations when you would want to migrate your minimal APIs to controller
based APIs. Minimal APIs are geared towards simplicity of development and use.
However, they come with their own set of limitations. Some of the differences
between minimal APIs and controller based APIs are listed in the official
documentation
here. If you stumble upon these limitations you may want to evolve your
minimal APIs to API controllers. You might also opt to do such a migration for
API organizational reasons; especially if your endpoints reach a very high
number. In this article we will move our minimal APIs to controller based APIs
and see how much work is involved.
Add API controllers
Minimal APIs exist simply as a bunch of endpoints. By default, there is no
specific organization to these endpoints. You simply add them to Program.cs one
after the other. When you decide to migrate to controller based APIs, the first
thing to decide is what all endpoints will go together in an API controller. In
our example, we have five endpoints that are related to Employee CRUD operations
and two that are related to JWT authentication. Clearly, we need at least two
API controllers to house these two groups.
So, first task is to add two API controllers namely EmployeesController and
SecurityController in our project. You can make use of empty API Controller
template available in the Add New Item dialog.
You can place them inside Controllers folder as shown below.
Decorate API controllers with [Route], [ApiController], and [Authorize]
attributes
The next thing to decide is the endpoint routes. Minimal APIs allow you to
specify the endpoint URLs simply as string values. When you create an API
controller you typically use attribute based routing via the [Route]
attribute. The [Route] attribute can be added to controller or individual
actions. Take a look at the API controllers we just added:
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class EmployeesController : ControllerBase
{
...
}
[Route("api/[controller]")]
[ApiController]
public class SecurityController : ControllerBase
{
...
}
As you can see, both the API controllers are decorated with [Route]
attribute. The [controller] token is used to substitute the name of the
underlying controller class. So, the API controllers can be accessed at /api/Employees
and /api/Security respectively. Both of the controllers also have [ApiController]
directive that helps us in model binding and model validation. You may read more
about the [ApiController] attribute
here. The
EmployeesController also has [Authorize] attribute. We added [Authorize] to all
the minimal API handlers. In API controllers you can simply mark the controller
with [Authorize] instead of marking each and every action individually.
Inject AppDbContext and UserManager in the constructor
Earlier we injected dependencies (such as AppDbContext and UserManager) in
each and every endpoint handler function. For example, consider the following
MapGet() call.
app.MapGet("/minimalapi/employees",
[Authorize](AppDbContext db) =>
{
return Results.Ok(db.Employees.ToList());
});
API controllers allow us to inject dependencies in the constructor. This
simplifies the actions because actions take only the model data (and ID wherever
required). The following code shows the constructors of EmployeesController and
SecurityController respectively.
private readonly AppDbContext db;
public EmployeesController(AppDbContext db)
{
this.db = db;
}
And
private readonly UserManager<IdentityUser> userMgr;
private readonly IConfiguration configuration;
public SecurityController(UserManager<IdentityUser>
userMgr, IConfiguration configuration)
{
this.userMgr = userMgr;
this.configuration = configuration;
}
As you can see, we inject AppDbContext in the EmployeesController
constructor. And UserManager and IConfiguration are injected into
SecurityController constructor. We inject IConfiguration in the
SecurityController because we need to read JWT configuration from
appsettings.json.
Convert "Map" calls to controller actions
Next, we will convert all the "Map" calls such as MapGet(), MapPost(), MapPut(),
and MapDelete() into actions. During this conversion return type and parameters
will need to be changed as per API controller requirements. Consider the
following MapGet() call:
app.MapGet("/minimalapi/employees",
[Authorize](AppDbContext db) =>
{
return Results.Ok(db.Employees.ToList());
});
The equivalent code in the EmployeesController is shown below:
[HttpGet]
public IActionResult GetAllEmployees()
{
return Ok(db.Employees.ToList());
}
Notice the following changes:
- The GET request mapping is done through the [HttpGet] attribute.
- The return type is changed from IResult to IActionResult to make the
action in line with typical API / MVC controllers.
- No need to inject AppDbContext in the action since we already did it via
constructor onjection.
- Instead of returning data using Results.Ok() we now use Ok() method
available in the ControllerBase (API controllers inherit from ControllerBase).
- The action doesn't have [Authorize] attribute because we added it to the
EmployeesController class.
The following code shows the completed EmployeesController with all the five
actions - GetAllEmployees(), GetEmployeeByID(), InsertEmployee(), UpdateEmployee(),
and DeleteEmployee().
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class EmployeesController : ControllerBase
{
private readonly AppDbContext db;
public EmployeesController(AppDbContext db)
{
this.db = db;
}
[HttpGet]
public IActionResult GetAllEmployees()
{
return Ok(db.Employees.ToList());
}
[HttpGet("{id}")]
public IActionResult GetEmployeeByID(int id)
{
return Ok(db.Employees.Find(id));
}
[HttpPost]
public IActionResult InsertEmployee(Employee emp)
{
db.Employees.Add(emp);
db.SaveChanges();
return CreatedAtAction
($"/minimalapi/employees/{emp.EmployeeID}", emp);
}
[HttpPut("{id}")]
public IActionResult UpdateEmployee
(int id, Employee emp)
{
db.Employees.Update(emp);
db.SaveChanges();
return NoContent();
}
[HttpDelete("{id}")]
public IActionResult DeleteEmployee(int id)
{
var emp = db.Employees.Find(id);
db.Remove(emp);
db.SaveChanges();
return NoContent();
}
}
You can migrate authentication related endpoints in a similar way. Since
these handlers are asynchronous the corresponding actions will return Task<IActionResult>
instead of IActionResult. The following code shows the completed
SecurityController class.
[Route("api/[controller]")]
[ApiController]
public class SecurityController : ControllerBase
{
private readonly UserManager<IdentityUser> userMgr;
private readonly IConfiguration configuration;
public SecurityController(UserManager<IdentityUser>
userMgr, IConfiguration configuration)
{
this.userMgr = userMgr;
this.configuration = configuration;
}
[Route("[action]")]
[HttpPost]
public async Task<IActionResult> GetToken(User user)
{
var identityUsr = await userMgr.FindByNameAsync
(user.UserName);
if (await userMgr.CheckPasswordAsync
(identityUsr, user.Password))
{
var issuer = configuration["Jwt:Issuer"];
var audience = configuration["Jwt:Audience"];
var securityKey = new SymmetricSecurityKey
(Encoding.UTF8.GetBytes(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 Ok(stringToken);
}
else
{
return Unauthorized();
}
}
[Route("[action]")]
[HttpPost]
public async Task<IActionResult> CreateUser(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 Ok();
}
else
{
return BadRequest();
}
}
}
Notice the code marked in bold letters. In the case of SecurityController we
used [Route] attribute on top of individual actions to create /api/GetToken and
/api/CreateUser routes. This is required because GetToken() and CreateUser()
both deal with POST verb. Using [action] token with the [Route] attribute will
substitute the name of the underlying action in the route (GetToken and
CreateUser respectively).
The remainder of SecurityController is quite straightforward and follows the
changes outlined earlier.
Call AddControllers() and MapControllers() methods in Program.cs
Since we have migrated all the minimal APIs to controller based APIs, you can
remove them or comment them out from Program.cs. In order to successfully
consume the controller based APIs you need to call AddControllers() and
MapControllers() methods in the Program.cs as shown below:
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.
GetConnectionString("AppDb");
builder.Services.AddDbContext<AppDbContext>
(o => o.UseSqlServer(connectionString));
builder.Services.AddDbContext<AppIdentityDbContext>
(o => o.UseSqlServer(connectionString));
builder.Services.AddControllers();
...
...
// other code goes here
...
...
app.UseAuthentication();
app.UseAuthorization();
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json",
"Swagger Demo Minimal API v1");
});
app.MapControllers();
app.Run();
...
...
// other code goes here
...
...
Build and run the application. If all goes well, you will see Swagger UI in
the browser as shown below:
As you can see, the two API controllers now list the respective actions.
Notice that the routes now contain /api instead of /minimalapi due to the
[Route] attribute. You can invoke the CRUD operation after generating a JWT as
described in the previous parts.
So far in this article series we used Swagger UI to invoke our minimal APIs.
In the next part we will build a simple JavaScript client using jQuery that
performs the login and CRUD operations.
That's it for now! Keep coding!!