延續上一篇 寫的內容,將 MongoDB 的功能加進來,順便練習 F#
Work with MongoDB
.net core 專案不管是 C# 或是 F# 要使用 MongoDB 都需要安裝一個 package MongoDB.Driver
,安裝完成後就可以連接 MongoDB 了,這邊就假設各位的電腦已經有一個正常運行的 MongoDB,如果沒有,網路上有很多安裝教學
接下來的檔案都是在 Todo.fs
內做異動
-
調整
Todo
type 定義1
2
3
4
5type Todo = {
id: BsonObjectId // mongodb 自己的 key 欄位, 需要 open MongoDB.Bson
uid: string // 上一篇使用 int, 這裡改用 Guid 當作 uid
title: string
isDone: bool } -
增加 property 和 method 到
ITodoService
type1
2
3
4
5type ITodoService =
abstract mongo: MongoClient
abstract db: IMongoDatabase
abstract GetTodos: unit -> Todo seq
abstract SaveTodo: Todo -> bool -
將缺少的部分實做補齊
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
33let TodoService =
{ new ITodoService with
member __.mongo = MongoClient("your mongodb connection string")
member __.db = __.mongo.GetDatabase "todos" // 可以換成自己建立的 database 名稱
member __.GetTodos(): seq<Todo> =
__.db
.GetCollection<Todo>("todos")
.Find(Builders.Filter.Empty)
.ToEnumerable() |> Seq.cast
member __.SaveTodo todo =
let collection = __.db.GetCollection<Todo>("todos")
let todos =
collection
.Find(fun x-> x.uid = todo.uid)
.ToEnumerable()
match Seq.isEmpty todos with
| true ->
collection.InsertOne { todo with
id = BsonObjectId(ObjectId.GenerateNewId())
uid = Guid.NewGuid().ToString() } |> ignore
true
| false ->
let filter = Builders<Todo>.Filter.Eq((fun x -> x.uid), todo.uid)
let update =
Builders<Todo>.Update
.Set((fun x -> x.title), todo.title)
.Set((fun x -> x.isDone), todo.isDone)
collection.UpdateOne(filter, update) |> ignore
true
}__.GetTodos()
這一段我稍微卡到的地方在於IEnumerable
轉型到Seq
的地方,查了一下文件,發現IEnumerable
和Seq
的本質上是一樣的,所以只要使用Seq.cast
的方法轉一下即可- MongoDB Collection 的查詢是使用
Builders.filter
的方法建立,這裡因為是要全撈,所以就單純使用Builders.Filter.Empty
__.SaveTodo
的部分,我將建立與更新寫在一起,判斷方式是先用傳進來的todo.uid
去尋找是否有存在的記錄,透過 pattern match 的寫法來區分新增與更新- line 24: 建立
filter
與update
的定義,colletion.UpdateOne
方法需要傳進這兩個定義
-
修改
addTodo
的方法1
2
3let private addTodo =
Func<ITodoService, Todo, bool>
(fun (todos: ITodoService) (todo) -> todos.SaveTodo todo)有好一段時間沒有寫 C#,熊熊忘記
Func
的使用方式,還好後來有想起來,而這邊跟寫 C# 的差異在於 line 3 的部分,如果是 C# 會這樣子寫(todos: ITodoService , todo) => {...}
,但在 F# 的世界裡,因為Func
的 signature 是Func(a -> b -> c)
,所以才會有 line 3 的寫法出現了,算是 FP 的特性之一。BTW,如果想要將 FP 學好,要學會看懂 signature
這樣調整完後,重新執行測試 API ,沒有意外就可以看到資料有儲存到 MongoDB 內了
注入 IConfiguration
經過一晚的思考,還是將如何使用 Configuration 的作法補上來,順便熟悉 Minimal API
的 DI 機制,接下來會分兩塊來看
-
map func 直接注入 dependency
1
2
3
4
5
6
7
8
9
10
11
12
13
14
module Home
open System
open Microsoft.AspNetCore.Builder
open Microsoft.Extensions.Configuration
let showVersion =
let getCurrentVersion (config:IConfiguration) = config["version"]
Func<IConfiguration, string>(getCurrentVersion)
let registerRoutes (app: WebApplication) =
app.MapGet("/", showVersion) |> ignore
app- line 10: 的
Func
寫法如同上面的,可以用這樣子的方式取得註冊在 services 內的服務 (framework 幫我們完成)
- line 10: 的
-
註冊 service 層的 DI
1
2
3
4
5let TodoService (config: IConfiguration) =
{ new ITodoService with
member __.mongo = MongoClient(config["mongodb"])
member __.db = __.mongo.GetDatabase "todos"
....}- line 1: 在宣告時就表明要給
IConfiuration
1
2
3builder
.services
.AddSingleton<Todo.ITodoService>(fun _ -> Todo.TodoService builder.Configuration) |> ignore- 所以在註冊的時候就餵給他 Configuration 就好
- line 1: 在宣告時就表明要給
這樣子就完成了,這部分的寫法我一開始有點鬼打牆,主要是因為我卡在 C# 版本的觀念,service 的 DI 要從 constructure
注入,後來想通其實他就是 function,就直接傳進去就好了。
換另外一個層面來看,當你發現一個 function 要傳入的東西太多時,就要重新思考這段程式碼是否有問題,是不是負責太多事情了。