2024年10月27日 星期日

FHIR SDK(DotNet)- FHIR Type Framework - Choice Type

Choice Type是個美麗又邪惡的小女孩,她的彈性是種讓你無法承受的愛。想在密密麻麻的文件中尋得他的蹤跡,是件苦差事。當你揭開她的神秘面紗,你一定會深深的愛上他。

上一篇已經提到Choice Type 是該面對他的時候。

想要深入瞭解他,得從Extension這個資料型態的文件去瞭解他。


這個value[x] ,尤其是Type欄位的*,她帶著什麼樣的神秘事件呢?

他的意思是說,Extension有欄位叫value,他可以任意一種資料型態。能有多任意呢?以下的都可以。


有注意到了嗎?在JSON中,他的Tag Name(key) 是(欄位名稱+資料型態名稱)而資料型態名稱還得首字大寫。
其他資料型態也有此Choice Type的有Timing的repeat(這是個詭異結構)與Annotation。



謝天謝地,這兩個的Choice Type有限定資料型態。
Choice Type大部份都是出現在Resource中,還好解法皆相同。
==========

程式解法很單純,提供一個ChoiceType當父類別,來處理一切雜事。提供各種情境的建構元。


接著就是複寫來自DataType的函數與實做介面。

這時候,74: IsChoiceType()就得設成true了。

其他最重要的就是88:與89:這兩行。

89:宣告一個抽象函數,讓子類別來告訴父類別,目前這個Choice Type限定哪些資料型態。

88:然後有了GetSupportDataType()這個函數讓建構元來使用,確保抓到屬於這個Choice Type該有的Tag。

這就是子類別的樣子,其實差異只在於SupportType而已。



FHIR SDK(DotNet)- FHIR Type Framework - Complex Type - 3

也許應該要感謝DotNet平台提供了Reflection機制,讓程式結構可以做到簡化,但要能發揮他的功效,需要抽象概念轉換,吃了不少苦頭。

=====SetupPropertyValue=====




54-55:首先要先取得這個Complex有哪些欄位。還記得這個T是在宣告類別時,從子類別定義來的。

56-143:就是分別對這個欄位進行資料轉換作業。因為欄位的性質不同,處理的方式也就要不同。

58-60:先把來源資料與欄位關係確立好。

63:順便把可能是Primitie Element那塊的json資料Key準備好。

64-103:如果這個欄位定義為[0..*]時;在類別宣告上,當為多時都用List<T>。

66:得先知道這個多的欄位他的資訊型態是什麼?

68-84:如果是Choice Type,那就走Choice Type流程。

85-102:如果是Complex或Primitive的話,就走這邊。奇怪,這邊怎們不分開處理?因為是多,逐一執行時,還是會回到單一一筆的流程,那邊就會分了。

106-141:如果欄位宣告是[0..1]時。就依據資料型態個別處理吧。


=====SetupJsonObject=====


就根據欄位產生JsonObject,其中SetupPropertyValue函數也是用到Reflection,這邊就不贅述了。

FHIR SDK(DotNet)- FHIR Type Framework - Complex Type - 2

雖然Complex Type有很多,但他們的行為模式都是一樣,設計一個父類別來處理。等等,不是有最討厭的Element嗎?這邊可以放心,因為Complex對應回JSON就是一個物件結構,id與extension就如同其他欄位一樣的自然存在。


宣告ComplexType<T>,用到泛型,T就是哪些有宣告欄位的Complex Type們。建構元也很簡單,就是把傳進來的值進行解析,然後去更新所有的欄位內容值。在此是呼叫SetupPropertyValue()這個函數。(有點複雜,容後說明)


接著複寫來自DataType的函數。應該可以理解,目前IsCompelx()就得設成true了。另外,取得JsonString的部份,用了C# Extension Method機制,將JsonNode,多了產生JSON String 的方法。(為何多此一舉?唉~那就要問微軟了,他的DotNet的問題)


再來實做來自IComplexType的介面要求,而且是非常重要的函數服務GetJsonObject()。因為SDK其實是用Key-Value來存放JSON內容。Value的部份實際上是用JsonNode。身為Complex Type他一定是在Value這端,所以,這個函數很重要。把Key-Value轉成JsonNode出來。

因為Complex的欄位,也有可能是JsonValue、JsonObject或JsonArray,還好他們有共同父類別是JsonNode。只是要注意,這套程式庫,會咬死Json Path,不能輕易把一段JSON掛到某一個Element上,所以要用DeepClone()這個方法,來取得乾淨的JsonNode結構。

另外,還有一種情境,這個Complex是用new() {}然後設值的方式建構的,也就是說沒有經過建構元,那就不會建立啟始的_Properties內容,此時,就得透過SetupJsonObject()方式,來取得各欄位內容值,再轉換成JsonNode。

==========

SetupPropertyValue()是支援建構元,將JsonNode資料轉換成Complex 物件。

SetupJsonObject()是將Complex物件的內容轉換成JsonNode。

這兩個東西是整個SDK第二困難與複雜的點。就留待下篇來說明。

FHIR SDK(DotNet)- FHIR Type Framework - Complex Type - 1

 Complex Type這個字眼在R3之後,就不存在了,但我還是習慣用這個字眼。他代表著參考型態,是一堆欄位組合類別。現在是分成了General-Purpose DataTypes、Metadata Types與Special Purpose Datatypes。


目前SDK規劃了22個,未來應該還會增加,畢竟這塊FHIR也沒有穩定下來。

先舉個最簡單的範例Coding,下圖是其結構。


他繼承了Element,所以會有id與extension。(實際上他應該是繼承DataType,但他是空的,而FHIR文件都習慣寫繼承於Element)

他有五個欄位,所幸,所有欄位的資料型態都是Primitive。

其程式結構如下圖。


7: 他繼承了ComplexType<T>,其實所有Complex Type除了欄位結構不同外,所有行為模式都是一樣的。

9-13: 宣告Coding所用的欄位,其資料型態都是Primitive。

14-22: 公開屬性可以取值與設值。要注意的是20,他定義了當有屬性內容值變更時,要去呼叫OnPropertyChanged函數,目的是要去更新放在Base類別的_Properties屬性的內容。

23-51: 其他欄位的處理方式跟前者一樣。

60-62: 是建構元,原則上都是丟給父類別來處理。

看過Coding,應該會想到那CodeableConcept是長什麼樣子。其實,架構對了,都是同樣的處理手法。



FHIR SDK(DotNet)- FHIR Type Framework - Primitive Type - 2

 雖然Primitive Type號稱有20個,除了對應回程式語言的資料型態不同與Regex不同外,其實他的行為模式是高度一致的。所以,設計一個PrimitiveType<T1, T2>類別來處理所有雜事。


類別定義,用到了泛型。T1是指FHIR的資料型態,當然這些傢伙都是繼承了DataType。T2是指程式語言的資料型態。

這邊宣告了三個屬性。第一個屬性是此Primitive Type的字串型態的值。所有的值都可以被轉換成string。放這個,可以任意轉換成其他資料型態(如果可以轉的話)。

第二個是這個Primitive Type所擁有的Element內容。預設值為null。

第三個是這個Primitive Type所對應程式語言資料型態的值。


這是建構元的部份,其實就是接收外部宣告值來建立此物件的初始值。20個Primitive Type實際上都是交付到這兒來實做。其中protected PrimitiveType(JsonNode? value, string? elementName)這個非常重要。他是用來判斷接受到的JSON內容是單純的value嗎?那簡單就是落實在Primitive原始範疇內。如果不是,給的是物件,那就得交付 InitElementObject((JsonObject)value, elementName)這個私有函數來處理了。(最後說明)

也許最後幾行有點奇怪,不是說_elementValue是放Element嗎?怎麼冒出Id與Extension。別忘了這個類別繼承了DataType,而DataType繼承了Element,所以自然有這兩個欄位。


這兒是複寫遺留自DataType的函數。

因為從這兒開始都是Primitive,所以IsPrimitive()當然要設成true,其他兩個是false。

再來就是ToJsonString,依據JSON的要求,只有String與DateTime資料型態其值需要加引號,其他不用。特別提醒一下,這邊只需要注意Value的部份,至於Key是什麼,就是得看Complex Type或Resource的Element的名稱。


接著就是要實做Interface的部份,列出函數清單提供參考。

最後來說明一下 InitElementObject((JsonObject value, string elementName)這個私有函數,是用來處理Primitive Type卻收到JsonObject內容,那就表示我們拿到了Element。


124:我們要找的element名稱其實是前面有"_"者。

125-128:先把正常的Value先取走。

129-149:取Element的部份。注意,這段程式不會是一開始就能寫的。因為extension欄位的資料型態是Extension,他是屬於Complex Type,都還沒動到那邊。

FHIR SDK(DotNet)- FHIR Type Framework - Primitive Type - 1

Primitive Type是一切神奇的開始, 是FHIR與程式語言交握之處,是一切基石。設計的核心在於能夠適應FHIR後續建構需求之發展,更需要能夠落實於程式語言之實做。

就其FHIR結構而言,是如此的單薄無力,但他卻承擔著20個Primitive Type的實做意義。


首先我們遭遇到的問題是FHIR資料型態的名稱就是跟程式語言的資料型態名稱相衝突。為了讓實做保持一致性,所以,上述20個資料型態的名稱全部加上了Fhir。(實際上是21個,因為Xhtml也屬於)


雖然有21個,其實就是分成字串類與數值類兩種。而真正的分法其實是JSON的Value部份要不要加引號的問題。也就是說,要從JSON角度出發,想想這21個資料型態應對應回程式語言什麼樣的資料型態。

另外,還要思考Null值的Value要怎麼辦。所以,另外設計的一系列Inteface來要求此21個資料型態是怎麼對應回程式語言的資料型態。


舉個FhirString這個Primitive Type為例,他需要實做IStringValue,而實際上他的Value值是對應回程式語言的string。

FhirString也繼承了PrimitiveType<T1, T2>,這個應用到泛型,也就是把這21個資料型態共通性的實做放到了父類別,並且告知FHIR資料型態與對應的程式型態。這個父類別就留待下一篇說明。

另外再舉一個FhirInteger為例,他需要實做IInteger32Value,他是對應為程式語言的int資料型態。一樣繼承PrimitiveType<FhirInteger, int>。


因為C#的string有支援null而int沒有,所以,這邊有多一個HasValue屬性。這是來自IInteger32Value的介面要求。

建構式的部份除了接收JsonNode的之外,都一定會接收string。再來就是接收對應於程式語言的資料型態。

至於討人厭的Element,為避免破壞Primitive的單純性,將其拉出單獨處理之。

再者是驗證,這是依據FHIR官方文件所提供的Regex。

另外要提供的事GetJsonValue()函數,注意Primitive不要理會Element的話,在JSON的角度,他就是Key-value,而這個value的就是一個純數。那有Element怎麼辦。根據官方文件,這個key的名稱,會多一個"_",這也是我設計決策上,要把他獨立的原因。


其實這21個Primitive Type,沒有什麼,真正的神奇魔法是在PrimitiveType<T1, T2>這個類別。




2024年10月26日 星期六

FHIR SDK(DotNet)- FHIR Type Framework - DataType

雖然DataType看起來是個不起眼的類別,但他卻扮演著摩西分海的重要角色。因為,在他之下,會有Primitive、Complex與Choice三種類型,而這三種類型處理方式皆不同。

DataType是抽象類別,繼承Element,就有了id與extension這兩個屬性。也就是說繼承於DataType的也有。講白了Primitive也有,這就破壞了Primitive的定義。講到Primitive時再深入說明。

另外,應該有注意到,在Type Framework圖中,並沒有看到Complex與Choice呀,是的這兩個沒有,再看這張圖,除了繼承於PrimitiveType的都算是Complex Type。
在這邊就必須要有個設計決策。其他的類別跟Primitive Type都是繼承了DataType,但其他類別的性質真的跟Primitive不同,若把其他類別的實做放在DataType,就會影響到Primitive Type。為了避免這樣子的情形,就得為其他的類別創立一個父類別,稱之為Complex Type 繼承於DataType。其他類別共通性實做,就放在Complex Type上,避開影響Primitive Type。
另外新增一個Choice類別,他也不在這個FHIR Type Framework上,因為他可能組合了Primitive與Complex,所以,為了實做也要多一個Choice。
程式結構如下:


另外,為了區隔DataType的Element實做差異性,這邊的建構元,並沒有繼承於父類別。而這邊的建構元只是讓各類別格式相同,其實,真正建構元都分散在Primitive、Complex與Choice。
  




FHIR SDK(DotNet)- FHIR Type Framework - Element

Element類別最讓人討厭,為了他程式不斷地修改,往往為了Primitive好不容易搞定,然後碰到Complex又毀了。兩邊就是不斷地相互毀滅。


從Element的結構圖可知,他就是一個Complex Type結構,而且那個extension的資料型態是Extension,明明Frameworke都還沒搞定,就要使用到未來的東西。(而且這個Extension還繼承了Element)

目前還不是對這個類別深入說明的時候,先貼個程式大概內容,先理解他的複雜性。因為你得先知道這個Elment是跟誰而來,跟誰還分是Object(1),還是Array(多)。


因為他就是Complex Type,所以會有屬性異動的問題,就得去實做INotifyPropertyChanged,這部份是DotNet Framework的問題。其他語言平台是否有相同問題不得而知。

FHIR SDK(DotNet)- FHIR Type Framework - Base

上一篇已經提到這個東西,搞定這個應該就成功一半。只是很遺憾的告訴各位,真的要完成,要等Complex Type搞定之後,才能夠真正完成架構。因為那個該死的Element。

再把這張仔細端詳,Root Class叫做Base。跟所有物件導向程式語言一樣,都會有一個Root Class,在C#就是Object。先撇開子類別Resource這一支,那是另外一個故事。
Base很單純就是一個抽象類別。哈~別被騙了,他是整體架構的靈魂,什麼方法或屬性是所有繼承之下的類別會共用呢?別忘了,兩大分支Primitive Type與Complex Type。Complex Type的屬性的資料型態有可能是另一個Complex Type,也有可能就是Primitive Type。而Primitive Type原則上應該是沒有欄位,他只會有個最終Value,但是,他的父類別中,有個Element,也就讓他有了欄位。更糟糕的是,其中一個欄位的屬性,居然是Extension,也就是說,Primitive Type其實有Complex Type屬性。這就是設計的關鍵所在。



什麼是繼承者共同擁有的呢?回答這個問題之前,應該先說FHIR就是衍生自HL7 v3,他就是原生的XML,然後定義了JSON是怎麼一回事,RDF是怎麼一回事。

就採JSON概念吧,其實說穿了就是Key-Value結構。Key的部份,很容易理解,就是Element Name(Tag Name),只要注意這個Key是不可重複。啊~不可重複?FHIR不是一堆重複嗎?觀念糾正,Element Name沒有重複(這句話到這邊還算正確,之後就不是了,之後要說FHIR Path不重複),而是Value的部份重複。

這個Value非常有趣,他可能是單純Value,也有可能是物件,也有可能是陣列。應該要用什麼來表達呢?其實這塊DotNet Framework已經幫我們解決了。有興趣深入研究者就參考這兒吧。https://learn.microsoft.com/zh-tw/dotnet/api/system.text.json.nodes?view=net-8.0

以下是Base Class的程式內容


其中_Properties就是用來存放任何FHIR中的類別,而_ElementProperties是為了那萬惡的Eelement,讓整個Base的通用性高一點。其他的函數很容易理解。

注意,GetPropertyValues為什麼是給_ElementProperties呢?因為他的繼承者就是Element,只有當Element的而非Null時,這個_ElementProperties才有意義。
Element的繼承者是DataType從這之下又是另一個故事。






FHIR SDK (DotNet)- 思考

 什麼是SDK(Software Development kit, 軟體開發套件),他與API的差異在需用於特定的軟體套件、軟體框架、硬體平台與作業系統。所以,這次開發的SDK僅能在DotNet平台下使用。

目前還沒打算開源,先以介紹SDK開發為主,等因緣具足時再來思考這個問題。

===============

FHIR SDK的目的就是協助軟體開發者,能透過一組元件輕鬆駕馭FHIR Resource。R5的Resource有157個,照趨勢發展未來可能還會更多。而SDK就是提供這些Resource的結構,能夠從XML/JSON的字串類型的資料,與物件相互對應。畢竟程式開發中,以物件形式來操作是比較容易的事情。

雖然有157個,其結構是類似的。所以這塊其實有寫了另一個專案,利用T4技術弄個程式產生器,利用程式來產生程式。也就是說未來有了R6,只要結構沒有重大變化,這個SDK可以馬上支援最新版本的FHIR。至於這部份的技術,就看這系列文章的發展,是否有機會也來介紹。

回來原點,雖然Resource有157個,其實他們有遵循了FHIR Type Framework。而SDK的重要核心技術,就是如何在你選定的程式語言下來實踐這個Framweork。


圖中看這個架構,好像一切理所當然,實際套映在程式語言時,有許多關鍵技術需要被克服。例如他的相互錯換宣告模式,下層繼承上層,而上層卻使用用到下層、FHIR資料型態與程式語言資料型態對應等等問題都需要克服。

首先,要先知道這個架構分成兩大部份,一個是基礎的資料型態系列,另一邊是Resource系列。Resource端比較容易理解,其實就是欄位屬性的宣告與使用。

資料型態端其實要分成四大類型。分別是Primitive Type、General-Purpose Datatypes、Metadata Types與Special Purpose Datatypes。但實踐上只有兩大類,Primitive Type與Complex Type。

Primitive Type就是最底層的值,而這個值就是要對應到程式語言的資料型態。他是沒有欄位,只是值的物件。
Complex Type就是有欄位的物件。
從C#程式語言的角度來說,Primitive Type就是「實質類型」,而Complex Type就是「參考類型」。
Resource類型原則上與Complex Type類似,屬於「參考類型」。
========
講到這,如何覺得很簡單,那你就錯了。
因為FHIR這個Extension架構,讓Primitive Type變得很複雜。因為Primitive Type繼承了Element,他卻是Complex Type結構。這部份FHIR有了新的定義。細節就留待下一篇了。



2024年10月25日 星期五

FHIR SDK (DotNet) - FHIR QB

 FHIR QB採用C#開發,基於DotNet 8。底層採用自行研發的FHIR SDK,目前支援FHIR R5。除了支援Open Server之外,也可透過SMART Backend Services方式,支援OAuth 2.0的FHIR Server。

FHIR QB 使用介面

  1.  輸入FHIR Server URL後,點選[Connect]。順利連線,會取回CapabilityStatement Resource。當然,這部份會丟到FHIR SDK去解析,之後就可以透過物件方式,存取此Resource。
  2. 這邊會列出此FHIR Server寫在CapabilityStatement中有支援的Resurce。圖中是選擇Encounter Resource。後續的內容都會依據CapabilityStatement來決定。
  3. 此為查詢參數,分成一般與Resource兩大類。至於這部份的背景知識請參考官方文件。
  4. 因應不同查詢參數類別,此區會出現相對應的欄位內容。依據自己查詢條件,逐一加入查詢清單。
  5. 此為查詢清單,可透過[Add]新一個查詢條件,或者[Remove]刪除一個查詢條件, [Remove All]刪除所有查詢條件。
  6. 此處可勾選_include或_revinclude的需求。
  7. 點選[Modifying Results]可以設定相關參數。

  8. 點選[Create]就能夠把所有設定參數建立查詢URL。點選[Search]就開始向FHIR Server查詢資料。
  9. 此區為查詢結果。
  10. 可以將結果複製,或者存檔。
如果是連接有支援OAuth 2.0的Server時。

  1. 會出現[Get Token]按鈕。點選後就需要輸入相關資訊。在此走Backend Service情境。(相關技術不在本系列範圍)

  2. 在此顯示SMART on FHIR的metadata。
完成JWT相關資料後,就會向授權服務器來取得Token。

  1. Server回應的Token內容。
  2. Resource部份,只會顯示當初申請時,所要求的Resource (Scope)。

其操過過程與一般無異,只是受到的限制比較多。




FHIR SDK (DotNet) - 背景故事

在於國外EMR奮戰的過程中發現,各家FHIR Server號稱標準,其實都不標準(IG只是理想,Profile只是說說)。為了找到一個最佳資料存取方法,常常需要測試各種查詢參數。一開始透過Postman是不錯的工具,但隨著查詢條件複雜化後,每次得去記那些參數實在有夠累。於是有了開發FHIR Query Builder (FHIR QB)的念頭。

FHIR QB的目的就是希望有點選的方式來產生查詢條件,並向FHIR Server取得資料,來確認是否與自己的預期相符。沒問題後,就可以把這樣子的查詢參數模式放在正式的程式中。

接著問題來了,要能有效發揮FHIR QB的功效,底層解析Resource的程式庫的需求也就浮現出來了。當然,市場上已經有各種程式語言的SDK可以下載,但對我來說,為什麼不自己也寫一套呢?因為,我的理想是建構一個FHIR Ecosystem,未來會有自己的FHIR Server、Authorization Server、EMR Lite、Profile base validater、CDS Hooks Service等。這些服務是不斷堆疊的,唯有掌握好底層技術,一切發展才能隨心所欲。

是的,從底層開始研發的FHIR SDK完成了;應用FHIR SDK的FHIR QB也完成了,其他部份也期望能在「默默」中實踐夢想。但是,看到很多人在努力推動FHIR,也許分享自己開發SDK的概念,會有拋磚引玉的效果,能激起一些漣漪。若能有更多人參與投入,讓國內FHIR的發展更加穩固,朝正確方向前進,那是最好的結果。

本系列文章將會先介紹FHIR QB的使用與SDK開發的思維邏輯與注意事項。至於原始碼是否公開,就一切隨緣吧。