Minimal APIs in ASP.NET Core 6
ASP.NET Core 6 has introduced minimal APIs that provide a new way to
configure application startup and routing a request to a function. In my
previous article I
covered the new way of configuring application startup. In this article we will
see how the new routing APIs can be used to map HTTP verbs to a function.
Prior to ASP.NET Core 6 you wrote actions inside a Web API controller or MVC
controller. You then mapped HTTP verbs with the actions using attributes such as
[HttpGet] and [HttpPost]. And you also setup routes using [Route] or
UseEndpoints(). For example, consider the following code fragment from a Web
API.
[Route("api/[controller]")]
[ApiController]
public class EmployeesController : ControllerBase
{
...
...
[HttpGet]
public IActionResult Get()
{
List<Employee> data = db.Employees.ToList();
return Ok(data);
}
[HttpPost]
public IActionResult Post([FromBody]Employee emp)
{
db.Employees.Add(emp);
db.SaveChanges();
return CreatedAtAction("Get",
new { id = emp.EmployeeID }, emp);
}
}
That means you first created a separate class called EmployeesController
decorated with [Route] and [ApiController] attributes. You then added the
required actions to the controller. Finally, you used attributes such as [HttpGet]
and [HttpPost] to map an HTTP verb to an action. You will follow a similar steps
while creating an MVC controller.
If you are building a full-fledged Web API then all these efforts are
justified. However, consider a situation where you want to quickly create a
simple function and map it to an HTTP verb. Creating a separate class, actions,
and attribute decoration may be unnecessary in such cases. The newly introduced
routing APIs solve this problem.
Using the new routing APIs you can map a request to a function without
creating a separate controller class. You make a request to an endpoint with
desired HTTP verb and the mapped function is executed. So, there are three
things involved - an HTTP verb, a route, and the function to be invoked.
Let's write some code to make this understanding clear.
Begin by creating a new ASP.NET Core project in Visual Studio based on the
Empty project template.
You will notice that the Empty project template doesn't contain the Startup
class because Program.cs uses the
new hosting APIs.
Then open Program.cs to reveal this code:
var builder = WebApplication.CreateBuilder(args);
await using var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.MapGet("/", (Func<string>)(() => "Hello World!"));
await app.RunAsync();
Notice the code marked in bold letters. It uses the MapGet() method to map a
GET request to made to the route specified in the first parameter to the
function specified in the second parameter. Here, the route is / and the
function simply writes Hello World! to the response. A sample run of this app
will result in the following output.
A careful look at MapGet() will tell you that it's an extension method to
IEndpointRouteBuilder defined in
Microsoft.AspNetCore.Builder.MinmalActionEndpointRouteBuilderExtensions class
and returns MinimalActionEndpointConventionBuilder. In addition to MapGet() you
also have MapPost(), MapPut(), and MapDelete() to deal with the respective HTTP
verbs.
Now that you know a bit about the new routing APIs, let's build CRUD
operations using these new methods.
Open appsettings.json file and store the connection string for Northwind
database as shown below:
{
"ConnectionStrings": {
"AppDb": "data source=.;initial catalog=Northwind;
integrated security=true"
}
}
Then add an Employee class like this:
public class Employee
{
public int EmployeeID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
Now add Microsoft.EntityFrameworkCore.SqlServer NuGet package to the project
and write a custom DbContext class as follows:
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions
<AppDbContext> options) : base(options)
{
}
public DbSet<Employee> Employees { get; set; }
}
We created the AppDbContext class with Employees DbSet. We will inject it
into all the functions we write to perform the CRUD operations. So, register it
with DI container.
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.
GetConnectionString("AppDb");
builder.Services.AddDbContext<AppDbContext>
(o => o.UseSqlServer(connectionString));
...
...
Now, just below the existing MapGet() call write the following code.
app.MapGet("/api/employees", ([FromServices] AppDbContext db) =>
{
return db.Employees.ToList();
});
Here, we specified the route to be /api/employees. The function that follows
takes AppDbContext parameter. This parameter is injected by DI as indicated
using [FromServices] attribute. Note that using [FromServices] in this manner
(lambda attributes) requires that you enable C# preview features for the
project. You can enable them by adding this into the .csproj file:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>preview</LangVersion>
</PropertyGroup>
</Project>
The function then returns a List of all the Employee entities to the caller.
It should be noted that using [FromServices] is optional. If you don't
specify it the framework will try to automatically resolve the parameters (the
same happens with [FromBody] as mentioned later in this article).
Let's invoke this function in a browser. Run the application by pressing F5
and then navigate to /api/employees. If all goes well, you should get output
similar to this:
As you can see, it correctly returned JSON formatted list of employees to the
browser.
Let's add another MapGet() that accepts employee ID route parameter and
returns a single Employee based on it.
app.MapGet("/api/employees/{id}",
([FromServices] AppDbContext db, int id) =>
{
return db.Employees.Find(id);
});
Here, we passed id route parameter using {id} syntax. The function also has
matching id integer parameter. Inside, you pick an Employee matching that ID and
return to the browser. Below is the sample run of this method when you enter /api/employees/1
in the address bar.
Let's complete the CRUD operations by adding insert, update, and delete
functions.
app.MapPost("/api/employees", (
[FromServices] AppDbContext db, Employee emp) =>
{
db.Employees.Add(emp);
db.SaveChanges();
return new OkResult();
});
app.MapPut("/api/employees/{id}", (
[FromServices] AppDbContext db, int id, Employee emp) =>
{
db.Employees.Update(emp);
db.SaveChanges();
return new NoContentResult();
});
app.MapDelete("/api/employees/{id}", (
[FromServices] AppDbContext db, int id) =>
{
var emp = db.Employees.Find(id);
db.Remove(emp);
db.SaveChanges();
return new NoContentResult();
});
Here, we used MapPost() to make a POST request that inserts a new employee in
the database. Similarly, MapPut() and MapDelete() are used to update and delete
an employee respectively. Although not added in the above code, you could have
explicitly marked the emp parameter with [FromBody] attribute indicating that it
is coming from request body.
Also notice that the functions return objects such as OkResult and
NoContentResult. In controllers (Web API and MVC) you have readymade methods
such as CreatedAtAction() and CreatedAtRoute(); you can't use them here. You may
see David Fowler's
samples available
here for more details and alternate implementation.
Now that our CRUD operations are ready, let's test them using Postman.
First, run the application by pressing F5 and then open Postman to send a
POST request as follows.
As you can see, we specified request type to be POST and endpoint to be /api/employees.
We also specified request body to be a new employee in JSON format. EmployeeID
being identity column is not specified in the JSON data. You can also see the
response status code 200 OK indicating that the POST operation was successful.
To send a PUT request you need to send a modified employee to the application
as shown below:
Here, we send a PUT request to /api/employees/4179 where 4179 is the
EmployeeID to be modified. The request body also carries the modified employee
JSON data. The operation returned 204 No Content as the response.
Finally, make a DELETE request to delete an employee.
This time you don't need to send any JSON data since EmployeeID specified in
the URL is sufficient to delete the employee. This request was also successful
as indicated by the status code 204 No Content.
In this example we created a new application based on the Empty project
template. You used new hosting APIs for configuring app startup and you also
used the new routing APIs for wiring the functions. What if you have an existing
application that is being migrated to ASP.NET Core 6? In such a case you will
typically already have a Startup class with ConfigureServices() and Configure()
methods.
Luckily, you can use the new routing APIs inside a Startup class also. Here
is how you can accomplish this task:
public class Startup
{
private readonly IConfiguration config;
public Startup(IConfiguration config)
{
this.config = config;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(this.config.GetConnectionString("AppDb")));
}
public void Configure(IApplicationBuilder app,
IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseEndpoints(endpoints => {
endpoints.MapGet("/api/employees",
([FromServices] AppDbContext db) =>
{
return db.Employees.ToList();
});
endpoints.MapGet("/api/employees/{id}",
([FromServices] AppDbContext db, int id) =>
{
return db.Employees.Find(id);
});
endpoints.MapPost("/api/employees",
([FromServices] AppDbContext db, Employee emp) =>
{
db.Employees.Add(emp);
db.SaveChanges();
return new OkResult();
});
endpoints.MapPut("/api/employees/{id}",
([FromServices] AppDbContext db, int id, Employee emp) =>
{
db.Employees.Update(emp);
db.SaveChanges();
return new NoContentResult();
});
endpoints.MapDelete("/api/employees/{id}",
([FromServices] AppDbContext db, int id) =>
{
var emp = db.Employees.Find(id);
db.Remove(emp);
db.SaveChanges();
return new NoContentResult();
});
endpoints.MapDefaultControllerRoute();
});
}
}
As you will notice, the same methods such as MapGet(), MapPost(), MapPut(),
and MapDelete() are available inside the UseEndpoints() call also.
To test these calls make sure to make this
change in app startup:
var builder = WebApplication.CreateBuilder(args);
builder.Host.ConfigureWebHostDefaults(options=> {
options.UseStartup<Startup>();
});
await using var app = builder.Build();
await app.RunAsync();
The result should be identical to the pervious example.
That's it for now! Keep coding!!