以 POST 實現會員登入 API

當前後端分離後,後端退守 API,使用 .NET Core 實現 Web API 為最常見的功能,本文將以實務上常使用的 會員登入 為範例,示範如何實作出 POST action 的 Web API。

Version


macOS High Sierra 10.13.4
.NET Core 2.1
Rider 2018.2
Paw 3.1.5

Login API (POST)


Login Success

post000

  1. 使用 POSThttp://localhost:5001/api/login
  2. 傳入
1
2
3
4
{
username: "Sam",
password: "1234"
}
  1. 回傳
1
2
3
4
{
success: "true",
username: "Sam"
}

若 login 成功,會回傳 successtrue,與 username 為 user 名稱。

Login Failed

post001

  1. 使用 POSThttp://localhost:5001/api/login
  2. 傳入
1
2
3
4
{
username: "Kevin",
password: "1234"
}
  1. 回傳
1
2
3
4
{
success: "false",
username: ""
}

若 login 失敗,會回傳 successfalse,與 username空白

LoginController.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
using System.Collections.Generic;
using System.Linq;
using Login.Models;
using Microsoft.AspNetCore.Mvc;

namespace Login.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class LoginController : ControllerBase
{
private readonly List<Member> _members
= new List<Member> {new Member {Username = "Sam", Password = "1234"}};

// POST api/login
[HttpPost]
public ActionResult<Response> LoginMember([FromBody] Member member)
{

return _members
.Where(IsMember)
.DefaultIfEmpty(DefaultMember())
.Select(Result)
.First();

bool IsMember(Member x)
=> x.Username == member.Username && x.Password == member.Password;


Member DefaultMember()
=> new Member {Username = string.Empty, Password = string.Empty};


Response Result(Member x)
=> new Response

{
Success = !string.IsNullOrEmpty(x.Username),
Username = x.Username
};
}
}
}

12 行

1
2
private readonly List<Member> _members
= new List<Member> {new Member {Username = "Sam", Password = "1234"}};

為簡化起見,就沒從 database 判斷 username 與 password,暫時將資料放在 List 內。

16 行

1
2
[HttpPost]
public ActionResult<Response> LoginMember([FromBody] Member member)
  • 使用 HttpPost attribute 描述 LoginMember 使用 POST action
  • 使用 FromBody attribute 描述 data 來自於 Body,並且自動轉型 Member 自訂型別
  • 回傳值 Response 自訂型別,被包在 .NET Core 規定的 ActionResult

25 行

1
2
3
4
5
return _members
.Where(IsMember)
.DefaultIfEmpty(DefaultMember())
.Select(Result)
.FirstOrDefault();

Where():判斷資料是否在 List

LINQ 的 Where() 相當於 FP 的 filter()

DefaultIfEmpty()Where() 一定可能找不到資料,因此會回傳 null,但 null 相當於癌細胞,只要一使用到 null,就必須到處判斷 null ,非常麻煩,而且只要有一個地方沒判斷到 null,程式就可能在 run-time 出錯。

比較好的方式是在 Where() 之後馬上配上 DefaultIfEmpty() 描述,只要 Where() 找不到資料,就不會回傳 null,而是回傳 default object。

DefaultIfEmpty() 就是 LINQ 支援 Null Object Pattern / Default Object Pattern 的一種實現

Select():我們要回傳的是 Response 自訂型別,而不是 Member 型別,勢必要做轉換,Select() 幫我們將 Member 自訂型別轉成 Result 自訂型別。

LINQ 的 Select() 相當於 FP 的 map()

First():別忘了 Select() 的轉換有個特色:原本是 CollectionSelect() 之後還是 Collection,但我們要的是 Response,因此使用 First() 只取一筆 Response

25 行

1
2
bool IsMember(Member x)
=> x.Username == member.Username && x.Password == member.Password;

IsMember() Local Function,提供 Where() 的 Predicate Function。

Q : Parameter 使用 x 符合 Clean Code 原則 ?

Local Function 與 Lambda 強調的就是 function 要短要小,且常常配合 Body Expression,若 parameter 名稱取太長,Lambda 就不美了。

由於 Lambda 來自於數學,一般會採用數學 y = f(x) 習慣,使用 xyz 即可。

x 會難閱讀嗎 ? 由於 Local Function 與 Lambda 很短,很容易看到看到其型別定義,所以沒有閱讀與維護的問題。

28 行

1
2
Member DefaultMember()
=> new Member {Username = string.Empty, Password = string.Empty};

DefaultMember() Local Function,主要提供 DefaultIfEmpty() 所需要的 default object。

31 行

1
2
3
4
5
6
Response Result(Member x)
=> new Response

{
Success = !string.IsNullOrEmpty(x.Username),
Username = x.Username
};

Result() Local Function,主要提供 Select() 所需要的 data 與型別轉換。

Conclusion


  • .NET Core 提供了 HttpPost attribute 與 FromBody attribute,讓我們簡單的將 Controller 的 Method 提供 Web API 的 POST 服務
  • 使用 LINQ 的 Where()DefaultIfEmpty() 讓我們避開 null 魔咒,不用再到處判斷 null,也就是所謂的 Null Object Pattern 或 Default Object Pattern 的實踐
  • Local Function 與 Lambda 的 parameter 可以打破 OOP 與 Imperative 的習慣,以 xyz 命名即可,因為 Lambda 功能都很單一,且 function 很短,不像 Imperative 會拉很長,因此才需要取有意義的 parameter 名稱

Sample Code


完整的範例可以在我的 GitHub 上找到