Monday, June 7, 2021

.NET 6 Preview 4: Introducing Minimal APIs in ASP.NET Core

In this post, let's see one of the nicest features coming in with ASP.NET Core in .NET 6. This feature is called Minimal APIs, the basic idea is being able to create a REST API with minimal code. 

Right now, if you create an ASP.NET Core Web API project using Visual Studio or dotnet CLI, you will see something like this, a project with multiple files.
Not Minimal API
Basically, you have a Program.cs, Startup.cs and Controller class. The motivation    behind the ASP.NET team for introducing Minimal API is, we don't have to do such ceremony to write smaller APIs or a small microservice.

If you have installed .NET 6 Preview 4 and do dotnet new web, you should see something new.

dotnet new web -n MinimalApi
Minimal API
There is only going to be just one .cs file which is Program.cs.

Program.cs

using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Hosting;

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();

This is just a simple API, which will return just "Hello World". There is no Main method, it's using Top-level statements (a C# 9.0 feature). 

Here you can basically do all the things that you used to do like set up dependencies and configure the HTTP Request Pipeline (what we usually do in ConfigureServices and Configure methods in Startup.cs respectively).

To give an example, something like this.

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

// ConfigureServices
builder.Services.AddDbContext<EmployeeContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

await using WebApplication app = builder.Build();

// Configure
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
} // Setup routes and run the app

Here, I have set up a EF DbContext within the container and in the HTTP Request Pipeline, have set up a DeveloperExceptionPage if the environment is Development (again we used to do this in ConfigureServices and Configure methods in Startup.cs respectively).

Consider the below EmployeeContext.

namespace MinimalApi
{
    public class EmployeeContext : DbContext
    {
        public EmployeeContext(DbContextOptions options) : base(options)
        {
        }

        public DbSet<Employee> Employees { getset; }
    }

    public class Employee
    {
        public int Id { getset; }

        public string Name { getset; }
    }
}

I can basically create a CRUD API for Employees here easily, something like below.

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

// ConfigureServices
builder.Services.AddDbContext<EmployeeContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

await using WebApplication app = builder.Build();

// Configure
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.MapGet("/employees"async ([FromServices] EmployeeContext dbContext) =>
{
    return await dbContext.Employees.ToListAsync();
});

app.MapGet("/employees/{id}"async ([FromServices] EmployeeContext dbContextint id) =>
{
    Employee employee = await dbContext.Employees.FindAsync(id);
    if (employee is null)
    {
        return NotFound();
    }

    return Ok(employee);
});

app.MapPost("/employees"async ([FromServices] EmployeeContext dbContext, Employee employee) =>
{
    await dbContext.Employees.AddAsync(employee);
    await dbContext.SaveChangesAsync();

    return Ok(employee);
});

app.MapPut("/employees/{id}"async ([FromServices] EmployeeContext dbContextint id, Employee employee) =>
{
    if (id != employee.Id)
    {
        return BadRequest();
    }

    if (!await dbContext.Employees.AnyAsync(x => x.Id == id))
    {
        return NotFound();
    }

    dbContext.Entry(employee).State = EntityState.Modified;
    await dbContext.SaveChangesAsync();

    return NoContent();
});

app.MapDelete("/employees/{id}"async ([FromServices] EmployeeContext dbContextint id) =>
{
    Employee employee = await dbContext.Employees.FindAsync(id);
    if (employee is null)
    {
        return NotFound();
    }

    dbContext.Employees.Remove(employee);
    await dbContext.SaveChangesAsync();

    return NoContent();
});

await app.RunAsync();

There is an interesting thing here. That is from the endpoints, all my return types are custom types that implement IResult, a new type that is coming with .NET 6.


My Return types are mapped manually here, through this class ResultMapper class.

public static class ResultMapper
{
    public static IResult BadRequest() => new StatusCodeResult(StatusCodes.Status400BadRequest);

    public static IResult NotFound() => new StatusCodeResult(StatusCodes.Status404NotFound);

    public static IResult NoContent() => new StatusCodeResult(StatusCodes.Status204NoContent);

    public static OkResult<T> Ok<T>(T value) => new(value);

    public class OkResult<T> : IResult
    {
        private readonly T _value;

        public OkResult(T value)
        {
            _value = value;
        }

        public Task ExecuteAsync(HttpContext httpContext)
        {
            return httpContext.Response.WriteAsJsonAsync(_value);
        }
    }
}

Hopefully, we will not have to do this in the next releases.

So as developers or newbies, we can start from the Minimal APIs and we can grow as we go rather than having a somewhat complex structure from the first day. Microsoft is going to provide tooling (I believe through Visual Studio and VS Code), so we can refactor the code into the current structure (separate Controller etc) as the code grows. An important thing is, this is not going to replace the existing code structure, it's just we can start with as little code as possible and then grow as we go!

You can find the sample code here,
   https://github.com/jaliyaudagedara/minimal-api

In this sample code, you will see I am using global usings (a C# 10 feature), you can read more about that here: C# 10.0: Introducing Global Usings

Hope this helps.

Happy Coding.

Regards,
Jaliya

No comments:

Post a Comment