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


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 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 ASP.NET online courses go here. More details about his Kriya and Meditation online course are available here.

Posted On : 19 January 2022


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