Friday, November 19, 2021

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. 

No comments:

Post a Comment

No String Argument Constructor/Factory Method to Deserialize From String Value

  In this short article, we will cover in-depth the   JsonMappingException: no String-argument constructor/factory method to deserialize fro...