ASP.NET Core Minimal API 實戰 DevOps 部署 IIS Server

2024-07-02

實作將 ASP.NET Core Minimal API 部署到 IIS Server 的 DevOps 流程與設定細節。

logo

說明

Coding

建立專案安裝所需要的 Nuget Packges

<ItemGroup>
  <PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.6" />
  <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.6" />
  <PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup>

加入 .editorconfig 檔案,設定 namespace declarations 為 file scoped。

csharp_style_namespace_declarations = file_scoped:silent

建立 Todo.cs 作為 Model

public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

建立 TodoDb.cs 作為 DbContext,進行與資料庫的溝通,這裡使用 InMemory Database,簡化範例。

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

    public DbSet<Todo> Todos => Set<Todo>();
}

接著建立 TodoItemDTO.cs 作為 Data Transfer Object,用來將 Entity 轉換為 DTO。

public class TodoItemDTO
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }

    public TodoItemDTO() { }
    public TodoItemDTO(Todo todoItem) =>
    (Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}

主要的執行設定在 Program.cs ,設定 Minimal API 與 Swagger。

不同於預設 Minimal API 使用 MapGet 以及 MapPost 等方法,這裡使用 MapTodoEndpoints 來將 Todo 相關的 API 路由設定獨立出來。

using Microsoft.EntityFrameworkCore;
using MinimalAPI;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

// Register Swagger services
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Enable middleware about Swagger
app.UseSwagger();
app.UseSwaggerUI();

// Register Todo endpoints
app.MapTodoEndpoints();

app.Run();

TodoEndpoints.cs 中設定 Todo 相關的 API Routing,一共使用以下的技巧:

  • MapGroup 整合相同路由前綴的 API
  • 使用 static method 來定義 API 的實作
  • 使用 TypedResults 來回傳不同的 HTTP Status Code
  • 使用 TodoItemDTO 來將 Entity 轉換為 DTO,避免直接回傳 Entity
using Microsoft.EntityFrameworkCore;

namespace MinimalAPI;

public static class TodoEndpointsExtensions
{
    public static void MapTodoEndpoints(this WebApplication app)
    {
        RouteGroupBuilder todoItems = app.MapGroup("/todoitems");

        todoItems.MapGet("/", GetAllTodos);
        todoItems.MapGet("/complete", GetCompleteTodos);
        todoItems.MapGet("/{id}", GetTodo);
        todoItems.MapPost("/", CreateTodo);
        todoItems.MapPut("/{id}", UpdateTodo);
        todoItems.MapDelete("/{id}", DeleteTodo);
    }

    static async Task<IResult> GetAllTodos(TodoDb db)
    {
        return TypedResults.Ok(await db.Todos.Select(x => new TodoItemDTO(x)).ToArrayAsync());
    }

    static async Task<IResult> GetCompleteTodos(TodoDb db)
    {
        return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).Select(x => new TodoItemDTO(x)).ToListAsync());
    }

    static async Task<IResult> GetTodo(int id, TodoDb db)
    {
        return await db.Todos.FindAsync(id)
            is Todo todo
                ? TypedResults.Ok(new TodoItemDTO(todo))
                : TypedResults.NotFound();
    }

    static async Task<IResult> CreateTodo(TodoItemDTO todoItemDTO, TodoDb db)
    {
        var todoItem = new Todo
        {
            IsComplete = todoItemDTO.IsComplete,
            Name = todoItemDTO.Name
        };

        db.Todos.Add(todoItem);
        await db.SaveChangesAsync();

        todoItemDTO = new TodoItemDTO(todoItem);

        return TypedResults.Created($"/todoitems/{todoItem.Id}", todoItemDTO);
    }

    static async Task<IResult> UpdateTodo(int id, TodoItemDTO todoItemDTO, TodoDb db)
    {
        var todo = await db.Todos.FindAsync(id);

        if (todo is null) return TypedResults.NotFound();

        todo.Name = todoItemDTO.Name;
        todo.IsComplete = todoItemDTO.IsComplete;

        await db.SaveChangesAsync();

        return TypedResults.NoContent();
    }

    static async Task<IResult> DeleteTodo(int id, TodoDb db)
    {
        if (await db.Todos.FindAsync(id) is Todo todo)
        {
            db.Todos.Remove(todo);
            await db.SaveChangesAsync();
            return TypedResults.NoContent();
        }

        return TypedResults.NotFound();
    }
}

Deploy

接著我們要透過 Azure DevOps 來進行部署,將專案推上 Azure DevOps 後,首先要設定 Pipeline 與 .NET Framework 的設定相同,只需要注意 Build Machine 要選擇支援 .NET 8 的版本。

在 Deploy Pipeline 中,設定也是相同,只要注意應用程式集區的 .NET 版本要選擇為 No Managed Code

最重要的是部署的伺服器,需要預先安裝 .NET 8 的 Hosting Bundle,這樣才能正確執行 ASP.NET Core Minimal API (注意下載的時候不要選 x32 或 x64 版本,要選 Hosting Bundle 版本)。

參考資料

https://learn.microsoft.com/en-us/aspnet/core/tutorials/min-web-api?view=aspnetcore-8.0&tabs=visual-studio