Todo => task zero

จริงๆ ตอนแรกเคยคิดว่าจะเขียนบทความเกี่ยวกับการสร้าง microservice ของระบบตัวหนึงขึ้นมาจนจบ แต่จนแล้วจนรอดก็ยังค้างอยู่ที่เดิม เพราะยังหาเวลาเขียนให้จบไม่ได้เลยจริงๆ จนตอนนี้เลยตัดสินใจลบบทความชุดนั้นทิ้งไป และอยากจะลองเขียนใหม่ในแบบที่เข้าใจง่ายๆ และทำให้เข้าใจว่าทำใมถึงต้องแตกโปรเจคออกมาเป็น N-Tiers และเอาไปทำ Docker ยังไง ซึ่งก็หวังว่าน่าจะพอมีประโยชน์สำหรับใครที่กำลังไม่รู้ว่าจะเริ่มต้นอย่างไรดีนะครับ

สำหรับ Overview ของบทความนี้ เราจะมาสร้าง ระบบ Todo กัน ซึ่งภายในโปรเจคเองจะประกอบไปด้วย Todo.API, Todo.UI และ Todo.Identity

Todo.API: โปรเจคตัวนี้จะเป็นส่วนของการสร้าง, แก้ไข และจัดการ Todo Task ต่างๆ
Todo.UI: โปรเจคตัวนี้จะเป็นส่วน UI เพื่อเป็นการใช้งาน

สำหรับภาษาโปรแกรมที่จะนำมาใช้งานก็จะเป็น C# .NET Core 3.1 (ถ้า .NET 5 ออกอาจจะกระโดดไปใช้งานตัวนี้แทนครับ)

และตัวฐานข้อมูลเราก็จะใช้งาน MSSQL ในการจัดเก็บข้อมูล

และเพื่อไม่ให้เป็นการเสียเวลา เรามาเริ่มต้นสร้างโปรเจคกันเลยดีกว่าครับ

Video Tutorial

โดยที่เราจะสร้างไฟล์ sln ขึ้นมาก่อน โดยตั้งชื่อว่า Todo เอาไวและโฟล์เดอร์ src สำหรับจัดเก็บไฟล์โค๊ดของเรา ดังภาพ

ไฟล์ Solution และ src โฟล์เดอร์

หลังจากนั้นผมจะสร้างโปรเจค API ขึ้นมาและตั้งชื่อว่า Todo.API สำหรับขั้นตอนการสร้างผมขอข้ามรายละเอียดนะครับ จะใช้ CLI หรือจะสร้างจาก VS ก็ได้ครับ และอย่าลืมเก็บเอาไว้ในโฟล์เดอร์ src ด้วยนะครับ

Todo.API โปรเจค
Create Project Type
Project architecture

จากนั้นก็จะทำการเพิ่ม Models เข้ามาในโปรเจค โดยให้เราสร้างคลาสชื่อ Todo ขึ้นมา โดยให้สร้างไว้ในโฟลเดอร์ Models ดังภาพ

Todo location

สำหรับ Properties ต่างๆ ภายในคลาส ก็จะมีดังนี้

namespace Todo.API.Models
{
[Serializable]
public class Todo
{
public Guid Id { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public string Description { get; set; }
public Status Status { get; set; }
public bool IsActive { get; set; }
public bool IsDelete { get; set; }
public string CreatedBy { get; set; }
public DateTime CreatedOn { get; set; }
public string UpdatedBy { get; set; }
public DateTime UpdatedOn { get; set; }
public string DeletedBy { get; set; }
public DateTime DeletedOn { get; set; }
}
}

และให้สร้าง enum ขึ้นมา โดยให้ตั้งชื่อว่า Status และเก็บเอาไว้ในโฟลเดอร์ Enums โดยที่ภายในจะมี enum ต่างๆ ดังนี้

namespace Todo.API.Enums
{
public enum Status : byte
{
Pending = 0,
Doing = 1,
Success = 2,
Cancel = 3
}
}

หลังจากที่เราสร้าง Model เพื่อใช้เป็น Model สำหรับการจัดเก็บข้อมูลในฐานข้อมูลแล้ว ขั้นตอนต่อไปเราจะทำการสร้าง DbContext เพื่อใช้สำหรับการติดต่อกับฐานข้อมูล โดยที่ขั้นตอนแรกเราจะต้องติดตั้ง EntityFrameworkCore กันก่อน โดยรายการที่จะติดตั้งมีดังนี้

install-package Microsoft.EntityFrameworkCore
install-package Microsoft.EntityFrameworkCore.SqlServer
install-package Microsoft.EntityFrameworkCore.Design
install-package Microsoft.EntityFrameworkCore.Tools

หลังจากนั้นก็จะทำการสร้างคลาสชื่อ ApplicationDbContext โดยจะเก็บไว้ในโฟลเดอร์ Connections โดยที่โค๊ดภายในจะมีดังนี้

using Microsoft.EntityFrameworkCore;

namespace Todo.API.Connections
{
public class ApplicationDbContext : DbContext
{
protected readonly IConfiguration Configuration;
protected readonly DbContextOptions<ApplicationDbContext> ContextOptions;

public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{
}

public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options, IConfiguration configuration) :
base(options)
{
ContextOptions = options;
Configuration = configuration;
}
}
}

จากโค๊ดเราจะสร้าง Constructor เอาไว้ 2 แบบ เพื่อใช้สำหรับการสร้าง dbOption ในภายหลังสำหรับการทำ UnitTest หรือเพื่อใช้สำหรับการ Inherit ไปใช้งานอีกที

และสำหรับขั้นตอนต่อไปเราจะทำการสร้าง Schema ของ Table ที่ใช้จัดเก็บข้อมูลภายในฐานข้อมูลกัน โดยเราจะทำการ override method ชื่อ OnModelCreating และทำการสร้างโดยใช้คำสั่ง ดังนี้

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Models.Todo>().ToTable("tb_todos");
modelBuilder.Entity<Models.Todo>().HasKey(k => k.Id);
modelBuilder.Entity<Models.Todo>().Property(p => p.Id).ValueGeneratedOnAdd();
modelBuilder.Entity<Models.Todo>().Property(p => p.StartDate).HasColumnType("datetime2")
.HasColumnName("start_date");
modelBuilder.Entity<Models.Todo>().Property(p => p.EndDate).HasColumnType("datetime2")
.HasColumnName("end_date");
modelBuilder.Entity<Models.Todo>().Property(p => p.Description).HasColumnName("descriptions");
modelBuilder.Entity<Models.Todo>().Property(p => p.Status).HasColumnName("status");
modelBuilder.Entity<Models.Todo>().Property(p => p.IsActive).HasColumnName("is_active");
modelBuilder.Entity<Models.Todo>().Property(p => p.IsDelete).HasColumnName("is_delete");
modelBuilder.Entity<Models.Todo>().Property(p => p.CreatedBy).HasColumnName("created_by");
modelBuilder.Entity<Models.Todo>().Property(p => p.CreatedOn).HasColumnType("datetime2")
.HasColumnName("created_on");
modelBuilder.Entity<Models.Todo>().Property(p => p.UpdatedBy).HasColumnName("updated_by").IsRequired(false);
modelBuilder.Entity<Models.Todo>().Property(p => p.UpdatedOn).HasColumnType("datetime2")
.HasColumnName("updated_on").IsRequired(false);
modelBuilder.Entity<Models.Todo>().Property(p => p.DeletedBy).HasColumnName("deleted_by").IsRequired(false);
modelBuilder.Entity<Models.Todo>().Property(p => p.DeletedOn).HasColumnType("datetime2")
.HasColumnName("deleted_on").IsRequired(false);
}

สำหรับภาพรวมของคลาด ApplicationDbContext มีดังนี้

ApplicationDbContext.cs

ต่อจากนั้นเราก็จะไปทำการ Register ตัว DbContext ในส่วนของ ConfigureServices ที่อยู่ในไฟล์ Start.cs ดังนี้

public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
{
options
.ConfigureWarnings(warnings =>
warnings.Throw(RelationalEventId.QueryPossibleExceptionWithAggregateOperatorWarning))
.UseSqlServer(Configuration.GetValue<string>("DefaultConnection"), sqlOptions =>
{
sqlOptions.EnableRetryOnFailure(5, TimeSpan.FromSeconds(30), null!);
sqlOptions.MigrationsAssembly(
typeof(ApplicationDbContext).GetTypeInfo().Assembly.GetName().Name);
});
});
services.AddControllers();
}

หลังจากนั้นเราก็จะทำการเพิ่ม Connection String ในไฟล์ appsetting.json เพื่อใช้สำหรับติดต่อกับฐานข้อมูล ดังนี้

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"DefaultConnection": "Server=localhost;Database=TodoDatabase;User Id=<user>;Password=<password>;"
}

จากนั้นให้ใช้คำสั่งสำหรับให้ EF ทำการ Generate SQL Schema ซึ่งจะสามารถใช้ได้ทั้ง dotnet หรือ nuget console ใน VS ดังนี้

Dotnet CLI: dotnet ef migrations add InitialDatabase

Nuget CLI: add-migration InitialDatabase

สำหรับ dotnet core ต้องติดตั้ง ef tool ก่อน โดยใช้คำสั่ง

dotnet tool install — global dotnet-ef

หลังจากนั้น เราจะได้คลาสชื่อ InitialDatabase โดยจะเก็บเอาไว้ในโฟลเดอร์ migrations ซึ่งภายในจะมีคำสั่งดั้งภาพ

InitailaDatabase

จากนั้น เราก็จะมาเริ่มสร้าง Schema บน Database กันโดยใช้คำสั่ง

Nuget CLI: Update-database

Dotnet CLI: dotnet ef database update

หลังจากนั้นเมื่อเราเปิด SSMS (SQL Server Management) ขึ้นมาแล้วก็จะพบว่าได้มีการสร้าง Database ชื่อ TodoDatabase ขึ้นมาและมีการสร้าง Table ชื่อ Todo ขึ้นมา ดังภาพ

Todo database

ต่อไป เราจะมาสร้าง Controller เพื่อให้สามารถเรียกใช้งานกัน โดยจะสร้างไฟล์ชื่อ TodoController.cs เอาไว้ในโฟลเดอร์ Controllers ดังภาพ

TodoController

จากนั้นเราก็จะมาทำการสร้างโค๊คสำหรับการทำ CRUD กัน ดังนี้

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Todo.API.Connections;

namespace Todo.API.Controllers
{
[ApiController]
[Produces("application/json")]
[Route("api/[controller]/[action]")]
public class TodoController : ControllerBase
{
private readonly ApplicationDbContext _context;

public TodoController(ApplicationDbContext context)
{
_context = context;
}

[HttpGet]
public async Task<IActionResult> GetAllAsync()
{
var result = await _context.Set<Models.Todo>().Where(s => !s.IsDelete).ToListAsync();
return Ok(result);
}

[HttpGet("{id}")]
public async Task<IActionResult> GetByIdAsync(Guid id)
{
var result = await _context.Set<Models.Todo>().FirstOrDefaultAsync(s => s.Id == id);

if (result != null)
{
return Ok(result);
}

return NotFound();
}

[HttpPost]
public async Task<IActionResult> CreateAsync(Models.Todo request)
{
request.CreatedOn = DateTime.UtcNow;
request.UpdatedBy = request.CreatedBy;
request.UpdatedOn = DateTime.UtcNow;

await _context.Set<Models.Todo>().AddAsync(request);
if (await _context.SaveChangesAsync() > 0)
{
return Ok();
}

return BadRequest();
}

[HttpPut("{id}")]
public async Task<IActionResult> UpdateAsync(Guid id, Models.Todo request)
{
var exist = await _context.Set<Models.Todo>().FirstOrDefaultAsync(s => s.Id == id);
if (exist == null) return NotFound();

exist.StartDate = request.StartDate;
exist.EndDate = request.EndDate;
exist.Description = request.Description;
exist.Status = exist.Status;
exist.UpdatedBy = exist.UpdatedBy;
exist.UpdatedOn = DateTime.UtcNow;
exist.IsActive = request.IsActive;

_context.Set<Models.Todo>().Update(exist);

if (await _context.SaveChangesAsync() > 0)
{
return Ok();
}

return BadRequest();
}

[HttpDelete("{id}")]
public async Task<IActionResult> DeleteAsync(Guid id, string deletedBy)
{
var exist = await _context.Set<Models.Todo>().FirstOrDefaultAsync(s => s.Id == id);
if (exist == null) return NotFound();

exist.IsDelete = true;
exist.DeletedBy = deletedBy;
exist.DeletedOn = DateTime.UtcNow;

_context.Set<Models.Todo>().Update(exist);

if (await _context.SaveChangesAsync() > 0)
{
return Ok();
}

return NotFound();
}
}
}

หลังจากนั้นเราก็จะมาทำการทดสอบ API ของเราด้วยการสั่งรัน

VS: กด F5

CLI: dotnet run

จากนั้นให้เปิด Postman ขึ้นมาและใส่ Url ของ api เราเข้าไป เช่น

http://localhost:2446/api/todo/getall

Get all record

ทดลองเพิ่ม Record ใหม่ โดยเปลี่ยน type ของ Request เป็น Post โดยใส่ Properties ต่างๆ ดังนี้

URL: http://localhost:2446/api/todo/create
Content-Type: application/json
Body type: JSON
Body Data:
{
“StartDate”:”2020–10–30",
“EndDate”:”2020–10–31",
“Description”:”todo task zero”,
“Status”:0,
“IsActive”:true,
“IsDelete”:false,
“created_by”:”ai1love6"
}

Create new record

เมื่อเราลองเรียก GetAll อีกรอบ คราวนี้เราจะได้ Record ที่เราพึ่งเพิ่มเข้าไปกลับมา ดังภาพ

Get all record

ลองเรียกดูรายละเอียดผ่าน method GetById โดยใช้ Id จาก result ที่ผ่านมา

Url: http://localhost:2446/api/todo/getbyid/{id}

Record detail

หลังจากนั้นเราจะทดสอบการแก้ไขข้อมูล โดยใช้ Id ของ record ที่เราพึ่งใช้กัน ดังนี้

Url: http://localhost:2446/api/todo/update/{id}
Content-Type: application/json
Body Type: json
Body Data:
{
“StartDate”:”2020–10–30",
“EndDate”:”2020–10–31",
“Description”:”todo task zero updated”,
“Status”:0,
“IsActive”:true,
“IsDelete”:false,
“updated_by”:”Tester”
}

Update exist record

หลังจากนั้น เราก็มาทำการ delete record กัน โดยการ delete นั้น เราจะทำการ flag record เอาไว้ ซึ่งจะไม่ได้เป็นการลบข้อมูลแบบฐาวร เนื่องจากเราต้องการที่จะเก็บข้อมูลเอาไว้เพื่อใช้งานในอนาคต

Url: http://localhost:2446/api/todo/delete/{id}?deletedBy=ai1love6

Delete exist record

สำหรับบทความนี้ก็ขอจบลงเพียงเท่านี้ครับ สำหรับบทความหน้า เราจะมาทำ swagger เพิ่ม เพื่อให้สามารถเรียกใช้งาน API เราได้แบบง่ายๆ กันครับ

หากผิดพลาดประการใดต้องขออภัยมา ณ ที่นี้ด้วยนะครับ

Source Code (todo-task-zero):

https://github.com/VatthanachaiW/todo-project

--

--