Create a Minimal API with ASP.NET Core and Entity Framework

What is Minimal API?

Minimal APIs are architected to create HTTP APIs with minimal dependencies. They are ideal for microservices and apps that want to include only the minimum files, features, and dependencies in ASP.NET Core.

Choose between controller-based APIs and minimal APIs

ASP.NET Core supports two approaches to creating APIs: a controller-based approach and minimal APIs. Controllers in an API project are classes that derive from ControllerBase. Minimal APIs define endpoints with logical handlers in lambdas or methods.

The design of minimal APIs hides the host class by default and focuses on configuration and extensibility via extension methods that take functions as lambda expressions. Controllers are classes that can take dependencies via constructor injection or property injection and generally follow object-oriented patterns. Minimal APIs support dependency injection through other approaches such as accessing the service provider.

Minimal APIs have many of the same capabilities as controller-based APIs. They support the configuration and customization needed to scale to multiple APIs, handle complex routes, apply authorization rules, and control the content of API responses.

There are a few capabilities available with controller-based APIs that are not yet supported or implemented by minimal APIs.

  • No built-in support for model binding (IModelBinderProvider, IModelBinder). Support can be added with a custom binding shim.
  • No built-in support for validation (IModelValidator).
  • No support for application parts or the application model. There's no way to apply or build your own conventions.
  • No built-in view rendering support. We recommend using Razor Pages for rendering views.
  • No support for JsonPatch
  • No support for OData

In .NET 6.0 and later versions, minimal APIs have been introduced to simplify the process of building lightweight HTTP APIs. Minimal APIs use a more concise syntax compared to traditional approaches. The MapGet, MapPut, MapDelete, and MapPost methods are part of this new approach and are used to define HTTP routes for handling specific HTTP methods.

  • MapGet is used to handle HTTP GET requests to the root ("/") and respond with a "Hello, World!" message.
  • MapPut is used to handle HTTP PUT requests to the "/update" route and respond with an "Update resource" message.
  • MapDelete is used to handle HTTP DELETE requests to the "/delete" route and respond with a "Delete resource" message.
  • MapPost is used to handle HTTP POST requests to the "/create" route and respond with a "Create resource" message.

These methods provide a clean and concise way to define routes for specific HTTP methods in minimal APIs.

Key differences between minimal APIs and controller-based APIs


Syntax and Structure

  • Controller-based API: In traditional controller-based APIs, you define controllers and actions. Controllers are classes that inherit from ControllerBase, and actions are methods within those controllers that handle specific HTTP requests.
  • Minimal API: With minimal APIs, the syntax is more concise. You can define routes and handle requests directly without the need for controllers.

Boilerplate Code

  • Controller-based API: Traditional controllers may require more boilerplate code, including the definition of controller classes, action methods, and additional attributes for routing.
  • Minimal API: Minimal APIs aim to reduce boilerplate code, making it quicker to define simple routes and handlers.

Flexibility

  • Controller-based API: Controllers provide a structured way to organize code, especially for larger and more complex APIs. They support features like dependency injection, action filters, and model binding.
  • Minimal API: Minimal APIs are more suitable for smaller and simpler scenarios. They are designed to be lightweight and may not provide the same level of structure and extensibility as controller-based APIs.

Convention vs. Configuration

  • Controller-based API: Controllers and actions are typically discovered through naming conventions, and routing is often configured in the startup class.
  • Minimal API: Routes are defined explicitly in the startup code, which can be seen as a form of configuration. There is less reliance on naming conventions for method discovery.

Ease of Use

  • Controller-based API: Traditional controllers may be more familiar to developers who have experience with ASP.NET Core MVC.
  • Minimal API: Minimal APIs are designed to be simpler and more approachable, especially for quick prototyping and smaller projects.

Let's create a WebAPI project with .Net Minimal API.

Step 1. Start creating a new ASP.NET core Web API project. At the Additional Information section do not select the Use Controller option to create a Web API project with Minimal API.

Additional information

Step 2. Once the project is created, Install the below packages for EntityFrameWorkCore.

  1. Microsoft.EntityFrameworkCore
  2. Microsoft.EntityFrameworkCore.Design
  3. Microsoft.EntityFrameworkCore.Tools
  4. Microsoft.EntityFrameworkCore.SqlServer

Step 3. Add DB connection string into appsettings.json.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "PersonDB": "Data Source=****\\sqlexpress;Initial Catalog=Persons;trusted_connection=true;TrustServerCertificate=True;"
  }
}

Step 4. There will be a default weatherforecast API got created. Let's remove some of the default APIs that were created and start adding the APIs differently. Modify Program.cs file as below.

using Microsoft.EntityFrameworkCore;
using MinimalAPI.API_Setup;
using MinimalAPI.DBContexts;
using MinimalAPI.Repository;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddWebApi(typeof(Program));

var objBuilder = new ConfigurationBuilder()
          .SetBasePath(Directory.GetCurrentDirectory())
          .AddJsonFile("appSettings.json", optional: true, reloadOnChange: true);
IConfiguration conManager = objBuilder.Build();
var conn = conManager.GetConnectionString("PersonDB");


builder.Services.AddDbContext<PersonContext>(options =>
{
    options.UseSqlServer(conn);
});
builder.Services.AddTransient<IPersonRepository, PersonRepository>();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

await app.RegisterWebApisAsync();
await app.RunAsync();

Step 5. Create a folder called API_setup and add the below files to register the APIs dynamically during run time.

APIs Setup

IWebApi.cs

public interface IWebApi
{
    void Register(WebApplication app);
}

public interface IWebApiAsync
{
    Task RegisterAsync(WebApplication app);
}

WebApiSetup.cs

The reference for methods AddWebApi and RegisterWebApisAsync are available in the program.cs.

public static class WebApiSetup
{
    public static void AddWebApi(this IServiceCollection services, Type markerType)
    {
        services.RegisterImplementationsOf<IWebApi>(markerType);
        services.RegisterImplementationsOf<IWebApiAsync>(markerType);
    }

    public static async Task RegisterWebApisAsync(this WebApplication app)
    {

        using (var scope = app.Services.CreateScope())
        {
            var scopedProvider = scope.ServiceProvider;

            var webApis = scopedProvider.GetServices<IWebApi>();
            foreach (var webApi in webApis)
            {
                webApi.Register(app);
            }

            var asyncWebApis = scopedProvider.GetServices<IWebApiAsync>();
            await Task.WhenAll(asyncWebApis.Select(x => x.RegisterAsync(app)));
        }
    }
}

ServiceCollectionExtensions.cs

public static class ServiceCollectionExtensions
{
    public static void RegisterImplementationsOf<T>(this IServiceCollection services, Type markerType, ServiceLifetime lifetime = ServiceLifetime.Transient) =>
        services.RegisterImplementationsOf(markerType, typeof(T), lifetime);

    public static void RegisterImplementationsOf(this IServiceCollection services, Type markerType, Type interfaceType, ServiceLifetime lifetime = ServiceLifetime.Transient) =>
        markerType.Assembly.GetTypes()
            .Where(x => x.DoesImplementInterfaceType(interfaceType))
            .ForEach(x => services.Add(new ServiceDescriptor(x.GetInterfaces()
                .First(y => y.IsGenericType ? y.GetGenericTypeDefinition() == interfaceType : y == interfaceType), x, lifetime)));

    public static bool DoesImplementInterfaceType(this Type type, Type interfaceType) =>
        !type.IsAbstract &&
        type.IsClass &&
        type.GetInterfaces().Any(y => y.IsGenericType ? y.GetGenericTypeDefinition() == interfaceType : y == interfaceType);
}

Step 6. Create a folder Model and add details of the Person object.

Model

Person.cs

public class Person
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string SSN { get; set; }
    public DateTime InsertDte_sys { get; set; }
    public DateOnly DOB_sys { get; set; }
    public TimeOnly WorkStartsAt_sys { get; set; }
    public DateTimeOffset CurrentTime_sys { get; set; }
}

Step 7. Let's create a folder called DBContexts and add PersonContext with some initial data.

DB Context

PersonContext.cs

PersonContext reference is available in the Program.cs file.

public class PersonContext : DbContext
{
    public PersonContext(DbContextOptions<PersonContext> options) : base(options)
    {
    }

    public DbSet<Person> Persons { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Person>().HasData(
            new Person
            {
                Id = 1,
                FirstName = "A_alice",
                LastName = "SuperSecret_A",
                SSN = "000000001",
                InsertDte_sys = DateTime.Now,
                DOB_sys = DateOnly.FromDateTime(DateTime.Today),
                WorkStartsAt_sys = TimeOnly.FromDateTime(DateTime.Now),
                CurrentTime_sys = DateTime.Now,
            },
            new Person
            {
                Id = 2,
                FirstName = "B_alice",
                LastName = "SuperSecret_B",
                SSN = "000000002",
                InsertDte_sys = DateTime.Now,
                DOB_sys = DateOnly.FromDateTime(DateTime.Today),
                WorkStartsAt_sys = TimeOnly.FromDateTime(DateTime.Now),
                CurrentTime_sys = DateTime.Now,
            },
            new Person
            {
                Id = 3,
                FirstName = "C_alice",
                LastName = "SuperSecret_C",
                SSN = "000000003",
                InsertDte_sys = DateTime.Now,
                DOB_sys = DateOnly.FromDateTime(DateTime.Today),
                WorkStartsAt_sys = TimeOnly.FromDateTime(DateTime.Now),
                CurrentTime_sys = DateTime.Now,
            }
        );

        modelBuilder.Entity<Person>()
            .Property(p => p.InsertDte_sys)
            .HasColumnType("datetime2");

        var dateOnlyConverter = new ValueConverter<DateOnly, DateTime>(
            x => x.ToDateTime(TimeOnly.MinValue),
            d => DateOnly.FromDateTime(d)
        );

        modelBuilder.Entity<Person>()
            .Property(p => p.DOB_sys)
            .HasConversion(dateOnlyConverter)
            .HasColumnType("date");

        var timeOnlyConverter = new ValueConverter<TimeOnly, TimeSpan>(
            x => x.ToTimeSpan(),
            d => TimeOnly.FromTimeSpan(d)
        );

        modelBuilder.Entity<Person>()
            .Property(p => p.WorkStartsAt_sys)
            .HasConversion(timeOnlyConverter)
            .HasColumnType("time");

        modelBuilder.Entity<Person>()
            .Property(p => p.CurrentTime_sys)
            .HasColumnType("datetimeoffset");
    }
}

Step 8. Create a folder called Repository and add the below files.

Repository

IPersonRepository.cs

 public interface IPersonRepository
 {

     IEnumerable<Person> GetPersons();

     Person GetPersonByID(int personID);

     void InsertPerson(Person person);

     void UpdatePerson(Person person);

     void Save();
 }

PersonRepository.cs

public class PersonRepository : IPersonRepository
{
    private readonly PersonContext _dbContext;

    public PersonRepository(PersonContext dbContext)
    {
        _dbContext = dbContext;
    }

    public void Save()
    {
        _dbContext.SaveChanges();
    }

    public IEnumerable<Person> GetPersons()
    {
        return _dbContext.Persons.ToList();
    }

    public Person GetPersonByID(int personId)
    {
        return _dbContext.Persons.Find(personId);
    }

    public void InsertPerson(Person person)
    {
        _dbContext.Add(person);
        Save();
    }


    public void UpdatePerson(Person person)
    {
        _dbContext.Entry(person).State = EntityState.Modified;
        Save();
    }
}

Step 9. Create a folder called API and add the below files to perform the GET/PUT/POST operation.

APIs

GetPersonByID.cs

public class GetPersonByID : IWebApi
{
    public void Register(WebApplication app)
    {
        app.MapGet("/GetPersonByID", (int id, [FromServices] IPersonRepository personRepository) =>
        {            
            var person = personRepository.GetPersonByID(id);
            return Results.Ok(person);
        })
    .WithMetadata(new EndpointNameMetadata("GetPersonByID"));
    }
}

GetPersons.cs

public class GetPersons : IWebApi
{
    public void Register(WebApplication app)
    {
        app.MapGet("/GetPersons", ([FromServices] IPersonRepository personRepository) =>
        {
            var persons = personRepository.GetPersons();
            return Results.Ok(persons);
        })
    .WithMetadata(new EndpointNameMetadata("GetPersons"));
    }
}

InsertPerson.cs

public class InsertPerson : IWebApi
{
    public void Register(WebApplication app)
    {
        app.MapPost("/InsertPerson",  ([FromBody]Person person, 
                                        [FromServices] IPersonRepository personRepository, 
                                        [FromServices] LinkGenerator linkGenerator) =>
        {
           

            using (var scope = new TransactionScope())
            {
                person.InsertDte_sys = DateTime.Now;
                person.DOB_sys = DateOnly.FromDateTime(DateTime.Today);
                person.WorkStartsAt_sys = TimeOnly.FromDateTime(DateTime.Now);
                person.CurrentTime_sys = DateTime.Now;

                personRepository.InsertPerson(person);
                scope.Complete();
                return Results.Created($"/GetPersonByID/{person.Id}", person);                    
            }
        })
    .WithMetadata(new EndpointNameMetadata("InsertPerson"));
    }
}

UpdatePerson.cs

public class UpdatePerson : IWebApi
{       
    public void Register(WebApplication app)
    {
        app.MapPut("/UpdatePerson", ([FromBody]Person person, [FromServices] IPersonRepository personRepository) =>
        {
            if (person != null)
            {
                using (var scope = new TransactionScope())
                {
                    person.InsertDte_sys = DateTime.Now;
                    person.DOB_sys = DateOnly.FromDateTime(DateTime.Today);
                    person.WorkStartsAt_sys = TimeOnly.FromDateTime(DateTime.Now);
                    person.CurrentTime_sys = DateTime.Now;

                    personRepository.UpdatePerson(person);
                    scope.Complete();
                    return Results.Ok();
                }
            }
            return Results.NoContent();
        })
    .WithMetadata(new EndpointNameMetadata("UpdatePerson"));
    }
}

The above APIs are inherited from the IWebApi interface and implement the Register method which helps in registering the APIs dynamically during runtime.

Step 10. Now add and update migration to create a Person table in the database and insert initial values.

Open the Tools menu (available in the Visual Studio toolbar)

Select NuGet package manager

Then Select Package Manager Console.

Package Manager Console Description
add-migration [name] Create a new migration with the specific migration name.
remove-migration Remove the latest migration.
update-database Update the database to the latest migration.
update-database [name] Update the database to a specific migration name point.
get-migrations Lists all available migrations.
script-migration Generates an SQL script for all migrations.
drop-database Drop the database.

Run the add-migration [name] command to generate a DB migration file.

Package manager console

The above command will create a Migrations folder and add a migration script with the mentioned name and Snapshot.

Migrations

Then, run the update-database command to reflect the migration change on our database side.

Update database

Code

Step 11. Once the update database runs successfully, you will verify your Database in the SQL Server like this.

Databases

Run the select query to verify the seed values have been inserted.

SQL Query

Step 12. Build and run the application. If everything is correct, then the below swagger page gets loaded with the added APIs.

Swagger

Step 13. Now let's try to get all records using GetPersons API.

GET

GET Response

Step 14. Now let's try to Get a record using GetPersonByID API.

GetPersonById API

GetPersonById API

Step 15. Now let's try to Insert values using InsertPerson API.

POST

POST Response

Step 16. Now let's try to Update values using UpdatePerson API.

PUT

PUT Response

Step 17. All the above activities can be verified at DB.

Verified at Database

Summary

The introduction of minimal APIs in .NET 6 and later versions provides developers with a more concise and lightweight way to build HTTP APIs compared to traditional controller-based APIs in ASP.NET Core. Minimal APIs are a simplified approach for building fast HTTP APIs with ASP.NET Core. You can build fully functioning REST endpoints with minimal code and configuration. Skip traditional scaffolding and avoid unnecessary controllers by fluently declaring API routes and actions. Minimal APIs support the configuration and customization needed to scale to multiple APIs, handle complex routes, apply authorization rules, and control the content of API responses.

It's important to note that the choice between minimal APIs and controller-based APIs depends on the specific requirements of your project. Minimal APIs are well-suited for lightweight scenarios, prototyping, and smaller projects, while controller-based APIs may be a better fit for larger and more complex applications where structure and extensibility are crucial.