Minimum APIs and why not controllers in ASP.NET Core

Cezary Walenciuk

Minimum APIs
and why not controllers in
ASP.NET Core

@walenciukC

Speaker
blog
CW 2021

Console Template for .NET 2-5

                    
                        using System;

                        namespace ConsoleApplication285
                        {
                            class Program
                            {
                                static void Main(string[] args)
                                {
                                    Console.WriteLine("Hello, World!");
                                }
                            }
                        }
                    
                

Console Template for .NET 6

                    
                        // See https://aka.ms/new-console-template for more information
                        Console.WriteLine("Hello, World!");
                    
                
blog
CW 2021

Program.cs in ASP.NET Empty template for .NET 6

                    
                        var builder = WebApplication.CreateBuilder(args);
                        var app = builder.Build();
                        
                        if (app.Environment.IsDevelopment())
                        {
                            app.UseDeveloperExceptionPage();
                        }
                        
                        app.MapGet("/", () => "Hello World!");
                        
                        app.Run();
                    
                

Minimal Application Example

                    
















                        










                        










                        var builder = WebApplication.CreateBuilder(args);

                        builder.Services.AddEndpointsApiExplorer();
                        builder.Services.AddSwaggerGen();
                        builder.Services.AddSingleton<IGameRepository, FakeGameRepository>();
                        
                        
                        var app = builder.Build();
                        
                        if (app.Environment.IsDevelopment())
                        {
                            app.UseSwagger();
                            app.UseSwaggerUI();
                        }
                        
                        app.UseHttpsRedirection();
                        
                        
                        app.MapPost("games", async (Game game, IGameRepository repo)
                            =>
                            {
                                await repo.CreateAsync(game);
                                return Results.Created($"games/{game.Id}", game);
                            }
                        );
                        
                        
                        app.MapGet("games/{id}", async (int id, IGameRepository repo)
                            =>
                        {
                            var game = await repo.GetAsync(id);
                        
                            if (game is null)
                            {
                                return Results.NotFound();
                        
                            }
                        
                            return Results.Ok(game);
                        }
                        );
                        
                        app.MapGet("games", async (string? likename, IGameRepository repo)
                            =>
                            {
                                if (likename is null)
                                {
                                    var allgames = await repo.GetAll();
                                    return Results.Ok(allgames);
                                }
                        
                                var matchedGames = await repo.GetGameByLikeName(likename);
                                return Results.Ok(matchedGames);
                            }
                        );
                            
                        app.MapPut("games", async (Game game, IGameRepository repo)
                            =>
                            {
                                await repo.UpdateAsync(game);
                                return Results.Ok(game);
                            }
                        );
                        

                        app.Run();

                    
                
blog
CW 2021
blog
CW 2021
blog
CW 2021

Program.cs in ASP.NET Web template for .NET 6

                    
                        var builder = WebApplication.CreateBuilder(args);

                        // Add services to the container.
                        builder.Services.AddControllersWithViews();
                        
                        var app = builder.Build();
                        
                        // Configure the HTTP request pipeline.
                        if (app.Environment.IsDevelopment())
                        {
                            app.UseDeveloperExceptionPage();
                        }
                        else
                        {
                            app.UseExceptionHandler("/Home/Error");
                            // The default HSTS value is 30 days.
                            app.UseHsts();
                        }
                        
                        app.UseHttpsRedirection();
                        app.UseStaticFiles();
                        
                        app.UseRouting();
                        
                        app.UseAuthorization();
                        
                        app.MapControllerRoute(
                            name: "default",
                            pattern: "{controller=Home}/{action=Index}/{id?}");
                        
                        app.Run();
                    
                
This is nothing new for other languages

NodeJs Hello World Example

                    
                        // Import the top-level function of express
                        const express = require('express');
                        
                        // Creates an Express application using the top-level function
                        const app = express();
                        
                        // Define port number as 3000
                        const port = 3000;
                        
                        // Routes HTTP GET requests to the specified path 
                        // "/" with the specified callback function
                        app.get('/', function(request, response) {
                          response.send('Hello, World!');
                        });
                        
                        // Make the app listen on port 3000
                        app.listen(port, function() {
                          console.log('Server listening on http://localhost:' + port);
                        });
                    
                

Go REST API with gin

                    
















                        











                        










                        package main

                        import (
                            "net/http"
                        
                            "github.com/gin-gonic/gin"
                        )

                        func main() {
                            router := gin.Default()
                            router.GET("/albums", getAlbums)
                            router.GET("/albums/:id", getAlbumByID)
                            router.POST("/albums", postAlbums)
                        
                            router.Run("localhost:8080")
                        }

                        // album represents data about a record album.
                        type album struct {
                            ID     string  `json:"id"`
                            Title  string  `json:"title"`
                            Artist string  `json:"artist"`
                            Price  float64 `json:"price"`
                        }
                        
                        // albums slice to seed record album data.
                        var albums = []album{
                            {ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99},
                            {ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99},
                            {ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99},
                        }
                        
                        // getAlbums responds with the list of all albums as JSON.
                        func getAlbums(c *gin.Context) {
                            c.IndentedJSON(http.StatusOK, albums)
                        }
                        
                        // postAlbums adds an album from JSON received in the request body.
                        func postAlbums(c *gin.Context) {
                            var newAlbum album
                        
                            // Call BindJSON to bind the received JSON to
                            // newAlbum.
                            if err := c.BindJSON(&newAlbum); err != nil {
                                return
                            }
                        
                            // Add the new album to the slice.
                            albums = append(albums, newAlbum)
                            c.IndentedJSON(http.StatusCreated, newAlbum)
                        }
                        
                        // getAlbumByID locates the album whose ID value matches the id
                        // parameter sent by the client, then returns that album as a response.
                        func getAlbumByID(c *gin.Context) {
                            id := c.Param("id")
                        
                            // Loop through the list of albums, looking for
                            // an album whose ID value matches the parameter.
                            for _, a := range albums {
                                if a.ID == id {
                                    c.IndentedJSON(http.StatusOK, a)
                                    return
                                }
                            }
                            c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
                        }
                    
                

REST API : Python FastApi

                    
                        # main.py

                        from fastapi import FastAPI
                        
                        app = FastAPI()
                        
                        @app.get("/users/me")
                        async def read_user_me():
                            return {"user_id": "the current user"}
                        
                        @app.get("/users/{user_id}")
                        async def read_user(user_id: str):
                            return {"user_id": user_id}
                    
                
blog
CW 2021

NancyFX is a lightweight, low-ceremony, framework for building HTTP based services on .NET Framework/Core

                    
                        public class Module : NancyModule
                        {
                            public Module()
                            {
                                Get("/greet/{name}", x => {
                                    return string.Concat("Hello ", x.name);
                                });
                            }
                        }
                    
                
You mean people didn't like controllers in the past in .NET
😲
How is this possible in C#?
👓
blog
CW 2021
blog
CW 2021
C# 9
Top Level Statements

C# 9.0 : Console Top Level Calls

                    
                        using System;

                        if (args.Length == 0)
                        {
                            System.Console.WriteLine("Please enter a numeric argument.");
                        }
                    
                
blog
Copyright © Cezary Walenciuk
blog
Copyright © Cezary Walenciuk
blog
Copyright © Cezary Walenciuk
blog
Copyright © Cezary Walenciuk
C# 10
File-Scoped Namespaces

File-Scoped Namespaces

                    
                        namespace ViewerApp.Models
                        {
                            public class Viewer
                            {
                                public string? FirstName { get; set; }
                                public string? LastName { get; set; }
                                public string? StreetAddress { get; set; }
                            }
                        }
                    
                

File-Scoped Namespaces 2

                    
                        namespace ViewerApp.Models;

                        public class Viewer2
                        {
                            public string? FirstName { get; set; }
                            public string? LastName { get; set; }
                            public string? StreetAddress { get; set; }
                        }
                    
                
CW
Copyright © 2021 Cezary Walenciuk
CW
Copyright © 2021 Cezary Walenciuk
CW
Copyright © 2021 Cezary Walenciuk
CW
Copyright © 2021 Cezary Walenciuk
CW
Copyright © 2021 Cezary Walenciuk
C# 10
Implicit global usings

.NET 5 without implicit usings

                    
                        using System;
                        using System.Collections.Generic;
                        using System.Threading.Tasks;
                        
                        Console.WriteLine("Daj pozytwna ocene");
                        await Task.Delay(2000);
                        
                        List<string> list = new List<string>();
                        list.Add("Tej prelekcj");
                    
                

Turning on Implicit usings

                    
                        <Project Sdk="Microsoft.NET.Sdk">

                            <PropertyGroup>
                            <OutputType>Exe</OutputType>
                            <TargetFramework>net6.0</TargetFramework>
                            <ImplicitUsings>enable</ImplicitUsings>
                            <Nullable>enable</Nullable>
                            </PropertyGroup>
                        
                        </Project>
                    
                

.NET 6 with implicit usings

                    
                        //using System;
                        //using System.Collections.Generic;
                        //using System.Threading.Tasks;
                        
                        Console.WriteLine("Daj pozytwna ocene");
                        await Task.Delay(2000);
                        
                        List<string> list = new List<string>();
                        list.Add("Tej prelekcj");
                    
                
CW
Copyright © 2021 Cezary Walenciuk

ConsoleAppNET6CSharp10.GlobalUsings.g.cs

                    
                        // <auto-generated/>
                        global using global::System;
                        global using global::System.Collections.Generic;
                        global using global::System.IO;
                        global using global::System.Linq;
                        global using global::System.Net.Http;
                        global using global::System.Threading;
                        global using global::System.Threading.Tasks;
                    
                

For web application

                    
                        // <autogenerated />
                        global using global::System;
                        global using global::System.Collections.Generic;
                        global using global::System.IO;
                        global using global::System.Linq;
                        global using global::System.Net.Http;
                        global using global::System.Threading;
                        global using global::System.Threading.Tasks;
                        global using global::System.Net.Http.Json;
                        global using global::Microsoft.AspNetCore.Builder;
                        global using global::Microsoft.AspNetCore.Hosting;
                        global using global::Microsoft.AspNetCore.Http;
                        global using global::Microsoft.AspNetCore.Routing;
                        global using global::Microsoft.Extensions.Configuration;
                        global using global::Microsoft.Extensions.DependencyInjection;
                        global using global::Microsoft.Extensions.Hosting;
                        global using global::Microsoft.Extensions.Logging;
                    
                

Adding namespaces in project

                    


                        <Project Sdk="Microsoft.NET.Sdk">
                            <PropertyGroup>
                            <OutputType>Exe</OutputType>
                            <TargetFramework>net6.0</TargetFramework>
                            <ImplicitUsings>enable</ImplicitUsings>
                            <Nullable>enable</Nullable>
                            </PropertyGroup>
                            <ItemGroup>
                            <Using Include="System.Text.Json" />
                            <Using Include="System.Text.Json.Serialization" />
                            <Using Remove="System.IO" />
                            <Using Include="System.Console" Static="True" />
                            <Using Include="System.DateTime" Alias="DT" />
                            </ItemGroup>
                        </Project>
                      
                    
                
CW
Copyright © 2021 Cezary Walenciuk

Adding namespaces in code

                    
                        global using ViewerApp.Models;
                        global using static System.Console;
                        global using DT = System.DateTime;
                    
                
CW
Copyright © 2021 Cezary Walenciuk
C# 10
Inferred lambda types

C# 9.0

                    
                        // C# 9
                        Func getUserInput = Console.ReadLine;
                        Action tellUser = (string s) => Console.WriteLine(s);
                        Func waitForEnter = Console.ReadLine;
                         
                        tellUser("Please enter name");
                        var name = getUserInput();
                        tellUser($"Your name is {name}");
                        waitForEnter();
                    
                

C# 10 Inferred lambda types

                    
                        // C# 10
                        var getUserInput = Console.ReadLine;
                        var tellUser = (string s) => Console.WriteLine(s);
                        var waitForEnter = Console.ReadLine;
                        
                        tellUser("Please enter name");
                        var name = getUserInput();
                        tellUser($"Your name is {name}");
                        waitForEnter();
                    
                

Explicit return types in lambdas

                    
                        var a1 = string () => string.Empty;

                        var a2 = int () => int.MaxValue;

                        var a3 = static void () => { };

                        var a4 = string? () => null;
                        
                        public static void Example<T>()
                        {
                            var a5 = T () => default;
                        }
                    
                
C# 10
Attributes on lambdas

MyAttribute on lambdas

                    
                        Func<int> f1 = [MyAttribute] () => { return 0; };
                        var f2 = [return: MyAttribute] () => { return 0; };
                        Func<int,int> f3 = ([MyAttribute] x)=> { return x; };
                        
                        class MyAttribute : Attribute
                        {
                        
                        }
                    
                
But why?
  1. Why Minimal applications in .NET
  1. Why Minimal applications in .NET
But seriously why?
  1. What minimal APIs are not?      
Minimal APIs maybe would perform much better than controller-based APIs ?
🤔
Yes, kinda
😅
  1. What is not supported
  1. What is not supported
What is wrong with Controllers?
😨
How many Controllers you created that return a View
🤔
What is wrong with Controllers for APIs?
😨
  1. What is wrong with Controllers for APIs?
You can create BIG FAT Controllers
  1. What is wrong with Controllers for APIs?
What about DDD, Clean Architecture, CQRS
You know buzzwords
blog
CW 2021
blog
CW 2021
  1. In this context the most important thing in controller is?
  1. Other important things in controller are?
blog
CW 2021
blog
CW 2021
blog
CW 2021
Code Example of Clean Architecture
Return to Minimal Application

Minimal Application Example

                    
















                        





                        






                        var builder = WebApplication.CreateBuilder(args);
                        builder.Services.AddSingleton<IEmployeeService, EmployeeService>();
                        builder.Services.AddSingleton<IHelloWorldService, HelloWorldService>();
                        
                        var app = builder.Build();
                        
                        app.MapGet("/", () => "Daj Like");
                        
                        app.MapGet("/quote/", async () =>
                            await new HttpClient().GetStringAsync
                                ("https://ron-swanson-quotes.herokuapp.com/v2/quotes"));
                        
                        app.MapPost("/employee",
                            (Employee e, IEmployeeService ser) =>
                            {
                                return ser.ShowEmployee(e);
                            });
                        
                        app.MapGet("/hello",
                            (HttpContext context, IHelloWorldService service)
                            =>
                            {
                                return service.SayHello
                                            (context.Request.Query["name"].ToString());
                            });
                        
                        app.Run();
                        
                        
                        public class HelloWorldService : IHelloWorldService
                        {
                            public string SayHello(string user)
                            {
                                return $"Hello {user}";
                            }
                        }
                        
                        public interface IHelloWorldService
                        {
                            public string SayHello(string user);
                        }
                        
                        public record Employee(string FirstName, [Required] string LastName);
                        
                        public interface IEmployeeService
                        {
                            public string ShowEmployee(Employee employee);
                        }
                        
                        public class EmployeeService : IEmployeeService
                        {
                            public string ShowEmployee(Employee e)
                            {
                                return "We have a new employee:" +
                                    $" {e.FirstName}  {e.LastName}";
                            }
                        }
                    
                
Show me example of CQRS with minimal application
You: OK...
You: OK, so you created one big file instead of controllers
You: This is lame
😟
Nah...Man
You don't see everything
😎
https://jaysbrickblog.com/news/comparing-10305-lion-knights-castle-to-other-historic-lego-castles/
CW 2021
You are forced to always make a castle
CW 2021
But maybe you want more control
without IControllerFactory or other hacks
Show me a system for minimal application
Let's end this with this slide
CW 2021