顯示具有 C# 標籤的文章。 顯示所有文章
顯示具有 C# 標籤的文章。 顯示所有文章

2026年1月13日 星期二

FHIR 11 如何對FhirClient的API呼叫,看到實際的 HTTP Payload

 

FHIR 11 如何對FhirClient的API呼叫,看到實際的 HTTP Payload

對於.NET的開發者,想要存取 FHIR Server上的資料,透過 FhirClient 物件來操作,是最為方便與簡潔的作法,想要使用這個物件,在 .NET 下主要需要安裝 Firely SDK(前身是 FHIR .NET API),在這裡的例子將會安裝了 Hl7.Fhir.R4 套件。

一旦安裝了安裝了 Hl7.Fhir.R4 套件之後,就可以在程式碼中使用 FhirClient 物件來對 FHIR Server 進行各種操作,例如讀取、查詢、更新、刪除等。可以想像你要去醫院查病歷資料,FhirClient 就像是一個「翻譯員 + 信差」:

  • 翻譯員角色:把你的請求(「我要查某個病人的資料」)翻譯成 FHIR 伺服器能懂的語言
  • 信差角色:幫你把請求送到伺服器,再把結果帶回來給你

因此,面對 FHIR Server 中超過上百個 Resource,操作起來更加輕鬆雨容易,這裡可以做到這些實際用途,例如:讀取病人資料(Patient)、查詢檢驗結果(Observation)、取得用藥記錄(Medication)、新增或更新醫療資料等。

就技術上來說,FhirClient 是一個程式庫(library),封裝了 HTTP 請求、資料格式轉換、錯誤處理等複雜細節,讓開發者可以用簡單的程式碼就能存取 FHIR 醫療資料,不用自己處理那些繁瑣的通訊協定和資料格式。簡單來說,它就是讓程式開發者能輕鬆存取 FHIR 醫療資料標準系統的工具。

有些時候,開發者可能會想要知道 FhirClient 在背後實際傳送了什麼 HTTP 請求,或是伺服器回傳了什麼樣的 HTTP 回應,這對於除錯、效能優化、了解系統行為等都很有幫助。甚至可以透過這些 HTTP 請求與回應,更深入的取學習與理解 FHIR API 的用法。

建立 Http 管道處理器

  • 延續 文章中做做出的程式碼
  • 在 [Program.cs] 檔案中找到 namespace csPatientCRUD;,在其下方加入這個類別定義
public class HttpLoggingHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var startTime = DateTime.UtcNow;

        // 記錄請求
        Console.WriteLine("===== HTTP 請求 =====");
        Console.WriteLine($"{request.Method} {request.RequestUri}");
        Console.WriteLine("標頭:");
        foreach (var header in request.Headers)
        {
            Console.WriteLine($"  {header.Key}: {string.Join(", ", header.Value)}");
        }

        if (request.Content != null)
        {
            foreach (var header in request.Content.Headers)
            {
                Console.WriteLine($"  {header.Key}: {string.Join(", ", header.Value)}");
            }

            var requestBody = await request.Content.ReadAsStringAsync(cancellationToken);
            if (!string.IsNullOrEmpty(requestBody))
            {
                Console.WriteLine("請求內容:");
                Console.WriteLine(requestBody);
            }
        }

        // 發送請求
        var response = await base.SendAsync(request, cancellationToken);

        var duration = DateTime.UtcNow - startTime;

        // 記錄回應
        Console.WriteLine("\n===== HTTP 回應 =====");
        Console.WriteLine($"狀態: {(int)response.StatusCode} {response.StatusCode}");
        Console.WriteLine("標頭:");
        foreach (var header in response.Headers)
        {
            Console.WriteLine($"  {header.Key}: {string.Join(", ", header.Value)}");
        }

        if (response.Content != null)
        {
            foreach (var header in response.Content.Headers)
            {
                Console.WriteLine($"  {header.Key}: {string.Join(", ", header.Value)}");
            }

            var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
            if (!string.IsNullOrEmpty(responseBody))
            {
                Console.WriteLine("回應內容:");
                Console.WriteLine(responseBody);
            }
        }

        Console.WriteLine($"耗時: {duration.TotalMilliseconds:F2} ms");
        Console.WriteLine("".PadRight(50, '='));

        return response;
    }
}
  • [DelegatingHandler 類別] (https://learn.microsoft.com/zh-tw/dotnet/api/system.net.http.delegatinghandler?view=net-8.0) 是 .NET 提供的一個抽象類別,可以用來建立自訂的 HTTP 處理器(Handler)。這些處理器可以用來攔截、修改、記錄或處理 HTTP 請求和回應。透過繼承 DelegatingHandler,開發者可以實現自己的邏輯,並將其插入到 HTTP 請求管道中。
  • 這裡新建立的 [HttpLoggingHandler] 類別將會繼承了這個 [DelegatingHandler] 類別,因此,可以用來攔截 HTTP 請求與回應,並記錄相關的資訊,例如請求方法、URL、標頭、內容,以及回應的狀態碼、標頭、內容等。
  • 此類別覆寫了 [SendAsync] 方法,這個方法會在每次發送 HTTP 請求時被呼叫。
  • 在這個方法中,我們先記錄請求的詳細資訊,然後呼叫 base.SendAsync 方法來發送請求,接著再記錄回應的詳細資訊。最後,將回應物件返回。
  • 這裡也會將 HTTP Header 的資訊與內容(Content)都記錄下來,方便後續查看。
  • 當 [SendAsync] 方法被呼叫後,會得到一個 [HttpResponseMessage] 物件,代表伺服器的回應。
  • 有了這個物件,便可以取得此次 HTTP 回應的原始內容,例如狀態碼、標頭、內容等,並將這些資訊記錄下來。
  • 最後,這個類別也會計算出每次呼叫 FHIR API 需要花費的時間成本。
  • 這樣一來,每次透過 FhirClient 發送的 HTTP 請求與回應,都會被這個處理器攔截並記錄,方便開發者查看實際的 HTTP Payload。

在 FhirClient 中使用 HttpLoggingHandler

  • 在 [Program.cs] 檔案中,找到 var httpClient = new HttpClient(); 這一行程式碼,將其修改為以下內容:
var httpHandler = new HttpClientHandler();
var loggingHandler = new HttpLoggingHandler { InnerHandler = httpHandler };
var httpClient = new HttpClient(loggingHandler);
  • 這段程式碼中,我們先建立了一個 [HttpClientHandler] 物件,這是 .NET 提供的預設 HTTP 處理器,負責處理實際的 HTTP 通訊。
  • 接著,我們建立了一個 [HttpLoggingHandler] 物件,並將剛剛建立的 [HttpClientHandler] 設定為它的 [InnerHandler],這樣當 [HttpLoggingHandler] 收到請求時,就會將請求傳遞給內部的 [HttpClientHandler] 來處理。
  • 最後,我們使用這個 [HttpLoggingHandler] 來建立 [HttpClient] 物件,這樣所有透過這個 [HttpClient] 發送的請求,都會先經過我們的日誌處理器,從而記錄下詳細的 HTTP 請求與回應資訊。
  • 這樣一來,當我們使用 FhirClient 進行各種操作時,例如讀取病人資料、查詢檢驗結果等,都會觸發我們的 [HttpLoggingHandler],從而在控制台中看到詳細的 HTTP 請求與回應內容,方便我們進行除錯與分析。

測試 FhirClient 的 API 呼叫

  • 執行 [csPatientCRUD] 專案,觀察控制台輸出
Creating Patient ...
JSON: {"resourceType":"Patient","identifier":[{"system":"http://example.org/mrn","value":"MRN-20240814A1"}],"active":true,"name":[{"family":"Lee","given":["Vulcan20250814111"]}],"telecom":[{"system":"phone","value":"0912-345-678","use":"mobile"}],"gender":"female","birthDate":"1990-01-01"}
===== HTTP 請求 =====
POST https://hapi.fhir.org/baseR4/Patient
標頭:
  Accept: application/fhir+json
  Accept-Charset: utf-8
  User-Agent: firely-sdk-client/5.12.1
  Content-Type: application/fhir+json; charset=utf-8
請求內容:
{"resourceType":"Patient","identifier":[{"system":"http://example.org/mrn","value":"MRN-20240814A1"}],"active":true,"name":[{"family":"Lee","given":["Vulcan20250814111"]}],"telecom":[{"system":"phone","value":"0912-345-678","use":"mobile"}],"gender":"female","birthDate":"1990-01-01"}

===== HTTP 回應 =====
狀態: 201 Created
標頭:
  Server: nginx/1.24.0, (Ubuntu)
  Date: Tue, 13 Jan 2026 08:29:11 GMT
  Transfer-Encoding: chunked
  Connection: keep-alive
  X-Powered-By: HAPI FHIR 8.5.3-SNAPSHOT/e3a3c5f741/2025-08-28 REST Server (FHIR Server; FHIR 4.0.1/R4)
  ETag: W/"1"
  X-Request-ID: I9kYzrNbqdbmsueL
  Location: https://hapi.fhir.org/baseR4/Patient/53805146/_history/1
  Content-Type: application/fhir+json; charset=utf-8
  Content-Location: https://hapi.fhir.org/baseR4/Patient/53805146/_history/1
  Last-Modified: Tue, 13 Jan 2026 08:29:11 GMT
回應內容:
{
  "resourceType": "Patient",
  "id": "53805146",
  "meta": {
    "versionId": "1",
    "lastUpdated": "2026-01-13T08:29:11.623+00:00",
    "source": "#I9kYzrNbqdbmsueL"
  },
  "text": {
    "status": "generated",
    "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><div class=\"hapiHeaderText\">Vulcan20250814111 <b>LEE </b></div><table class=\"hapiPropertyTable\"><tbody><tr><td>Identifier</td><td>MRN-20240814A1</td></tr><tr><td>Date of birth</td><td><span>01 January 1990</span></td></tr></tbody></table></div>"
  },
  "identifier": [ {
    "system": "http://example.org/mrn",
    "value": "MRN-20240814A1"
  } ],
  "active": true,
  "name": [ {
    "family": "Lee",
    "given": [ "Vulcan20250814111" ]
  } ],
  "telecom": [ {
    "system": "phone",
    "value": "0912-345-678",
    "use": "mobile"
  } ],
  "gender": "female",
  "birthDate": "1990-01-01"
}
耗時: 1122.91 ms
==================================================
Created: id=53805146, version=1
Press any key to continue...
 Reading Patient by id ...
===== HTTP 請求 =====
GET https://hapi.fhir.org/baseR4/Patient/53805146
標頭:
  Accept: application/fhir+json
  Accept-Charset: utf-8
  User-Agent: firely-sdk-client/5.12.1

===== HTTP 回應 =====
狀態: 200 OK
標頭:
  Server: nginx/1.24.0, (Ubuntu)
  Date: Tue, 13 Jan 2026 08:29:14 GMT
  Transfer-Encoding: chunked
  Connection: keep-alive
  X-Powered-By: HAPI FHIR 8.5.3-SNAPSHOT/e3a3c5f741/2025-08-28 REST Server (FHIR Server; FHIR 4.0.1/R4)
  ETag: W/"1"
  X-Request-ID: 6eD6YAfNWkKzdFl5
  Content-Type: application/fhir+json; charset=utf-8
  Content-Location: https://hapi.fhir.org/baseR4/Patient/53805146/_history/1
  Last-Modified: Tue, 13 Jan 2026 08:29:11 GMT
回應內容:
{
  "resourceType": "Patient",
  "id": "53805146",
  "meta": {
    "versionId": "1",
    "lastUpdated": "2026-01-13T08:29:11.623+00:00",
    "source": "#I9kYzrNbqdbmsueL"
  },
  "text": {
    "status": "generated",
    "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><div class=\"hapiHeaderText\">Vulcan20250814111 <b>LEE </b></div><table class=\"hapiPropertyTable\"><tbody><tr><td>Identifier</td><td>MRN-20240814A1</td></tr><tr><td>Date of birth</td><td><span>01 January 1990</span></td></tr></tbody></table></div>"
  },
  "identifier": [ {
    "system": "http://example.org/mrn",
    "value": "MRN-20240814A1"
  } ],
  "active": true,
  "name": [ {
    "family": "Lee",
    "given": [ "Vulcan20250814111" ]
  } ],
  "telecom": [ {
    "system": "phone",
    "value": "0912-345-678",
    "use": "mobile"
  } ],
  "gender": "female",
  "birthDate": "1990-01-01"
}
耗時: 200.41 ms
==================================================
Read: Vulcan20250814111 Lee | active=True
Press any key to continue...
 Updating Patient (add email, set active=false) ...
===== HTTP 請求 =====
PUT https://hapi.fhir.org/baseR4/Patient/53805146
標頭:
  Accept: application/fhir+json
  Accept-Charset: utf-8
  User-Agent: firely-sdk-client/5.12.1
  Content-Type: application/fhir+json; charset=utf-8
  Last-Modified: Tue, 13 Jan 2026 08:29:11 GMT
請求內容:
{"resourceType":"Patient","id":"53805146","meta":{"versionId":"1","lastUpdated":"2026-01-13T08:29:11.623+00:00","source":"#I9kYzrNbqdbmsueL"},"text":{"status":"generated","div":"<div xmlns=\"http://www.w3.org/1999/xhtml\"><div class=\"hapiHeaderText\">Vulcan20250814111 <b>LEE </b></div><table class=\"hapiPropertyTable\"><tbody><tr><td>Identifier</td><td>MRN-20240814A1</td></tr><tr><td>Date of birth</td><td><span>01 January 1990</span></td></tr></tbody></table></div>"},"identifier":[{"system":"http://example.org/mrn","value":"MRN-20240814A1"}],"active":false,"name":[{"family":"Lee","given":["Vulcan20250814111"]}],"telecom":[{"system":"phone","value":"0912-345-678","use":"mobile"},{"system":"email","value":"Vulcan20250814111.Lee@example.org"}],"gender":"female","birthDate":"1990-01-01"}

===== HTTP 回應 =====
狀態: 200 OK
標頭:
  Server: nginx/1.24.0, (Ubuntu)
  Date: Tue, 13 Jan 2026 08:29:17 GMT
  Transfer-Encoding: chunked
  Connection: keep-alive
  X-Powered-By: HAPI FHIR 8.5.3-SNAPSHOT/e3a3c5f741/2025-08-28 REST Server (FHIR Server; FHIR 4.0.1/R4)
  ETag: W/"2"
  X-Request-ID: etoip1zPped1yLq4
  Content-Type: application/fhir+json; charset=utf-8
  Content-Location: https://hapi.fhir.org/baseR4/Patient/53805146/_history/2
  Last-Modified: Tue, 13 Jan 2026 08:29:17 GMT
回應內容:
{
  "resourceType": "Patient",
  "id": "53805146",
  "meta": {
    "versionId": "2",
    "lastUpdated": "2026-01-13T08:29:17.196+00:00",
    "source": "#etoip1zPped1yLq4"
  },
  "text": {
    "status": "generated",
    "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><div class=\"hapiHeaderText\">Vulcan20250814111 <b>LEE </b></div><table class=\"hapiPropertyTable\"><tbody><tr><td>Identifier</td><td>MRN-20240814A1</td></tr><tr><td>Date of birth</td><td><span>01 January 1990</span></td></tr></tbody></table></div>"
  },
  "identifier": [ {
    "system": "http://example.org/mrn",
    "value": "MRN-20240814A1"
  } ],
  "active": false,
  "name": [ {
    "family": "Lee",
    "given": [ "Vulcan20250814111" ]
  } ],
  "telecom": [ {
    "system": "phone",
    "value": "0912-345-678",
    "use": "mobile"
  }, {
    "system": "email",
    "value": "Vulcan20250814111.Lee@example.org"
  } ],
  "gender": "female",
  "birthDate": "1990-01-01"
}
耗時: 232.04 ms
==================================================
Updated: version=2, telecom=Phone:0912-345-678, Email:Vulcan20250814111.Lee@example.org
Press any key to continue...
 Searching Patient by identifier 'MRN-20240814A1' ...
===== HTTP 請求 =====
GET https://hapi.fhir.org/baseR4/Patient?_count=5&identifier=MRN-20240814A1
標頭:
  Accept: application/fhir+json
  Accept-Charset: utf-8
  User-Agent: firely-sdk-client/5.12.1

===== HTTP 回應 =====
狀態: 200 OK
標頭:
  Server: nginx/1.24.0, (Ubuntu)
  Date: Tue, 13 Jan 2026 08:29:18 GMT
  Transfer-Encoding: chunked
  Connection: keep-alive
  X-Powered-By: HAPI FHIR 8.5.3-SNAPSHOT/e3a3c5f741/2025-08-28 REST Server (FHIR Server; FHIR 4.0.1/R4)
  X-Request-ID: UbbmBpPRELgqIKJ1
  Content-Type: application/fhir+json; charset=utf-8
  Last-Modified: Tue, 13 Jan 2026 08:29:18 GMT
回應內容:
{
  "resourceType": "Bundle",
  "id": "3b672bdf-058e-4580-bab8-dbf3e6335188",
  "meta": {
    "lastUpdated": "2026-01-13T08:29:18.646+00:00"
  },
  "type": "searchset",
  "total": 1,
  "link": [ {
    "relation": "self",
    "url": "https://hapi.fhir.org/baseR4/Patient?_count=5&identifier=MRN-20240814A1"
  } ],
  "entry": [ {
    "fullUrl": "https://hapi.fhir.org/baseR4/Patient/53805146",
    "resource": {
      "resourceType": "Patient",
      "id": "53805146",
      "meta": {
        "versionId": "2",
        "lastUpdated": "2026-01-13T08:29:17.196+00:00",
        "source": "#etoip1zPped1yLq4"
      },
      "text": {
        "status": "generated",
        "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><div class=\"hapiHeaderText\">Vulcan20250814111 <b>LEE </b></div><table class=\"hapiPropertyTable\"><tbody><tr><td>Identifier</td><td>MRN-20240814A1</td></tr><tr><td>Date of birth</td><td><span>01 January 1990</span></td></tr></tbody></table></div>"
      },
      "identifier": [ {
        "system": "http://example.org/mrn",
        "value": "MRN-20240814A1"
      } ],
      "active": false,
      "name": [ {
        "family": "Lee",
        "given": [ "Vulcan20250814111" ]
      } ],
      "telecom": [ {
        "system": "phone",
        "value": "0912-345-678",
        "use": "mobile"
      }, {
        "system": "email",
        "value": "Vulcan20250814111.Lee@example.org"
      } ],
      "gender": "female",
      "birthDate": "1990-01-01"
    },
    "search": {
      "mode": "match"
    }
  } ]
}
耗時: 207.54 ms
==================================================
Search total (if provided): 1
 - 53805146 | Vulcan20250814111 Lee | active=False
Press any key to continue...
 Deleting Patient ...
===== HTTP 請求 =====
DELETE https://hapi.fhir.org/baseR4/Patient/53805146
標頭:
  Accept: application/fhir+json
  Accept-Charset: utf-8
  User-Agent: firely-sdk-client/5.12.1

===== HTTP 回應 =====
狀態: 200 OK
標頭:
  Server: nginx/1.24.0, (Ubuntu)
  Date: Tue, 13 Jan 2026 08:29:21 GMT
  Transfer-Encoding: chunked
  Connection: keep-alive
  X-Powered-By: HAPI FHIR 8.5.3-SNAPSHOT/e3a3c5f741/2025-08-28 REST Server (FHIR Server; FHIR 4.0.1/R4)
  X-Request-ID: 483EQAcOrq348Tca
  Content-Type: application/fhir+json; charset=utf-8
  Content-Location: https://hapi.fhir.org/baseR4/Patient/53805146/_history/3
回應內容:
{
  "resourceType": "OperationOutcome",
  "text": {
    "status": "generated",
    "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><h1>Operation Outcome</h1><table border=\"0\"><tr><td style=\"font-weight: bold;\">INFORMATION</td><td>[]</td><td>Successfully deleted 1 resource(s). Took 18ms.</td></tr></table></div>"
  },
  "issue": [ {
    "severity": "information",
    "code": "informational",
    "details": {
      "coding": [ {
        "system": "https://hapifhir.io/fhir/CodeSystem/hapi-fhir-storage-response-code",
        "code": "SUCCESSFUL_DELETE",
        "display": "Delete succeeded."
      } ]
    },
    "diagnostics": "Successfully deleted 1 resource(s). Took 18ms."
  } ]
}
耗時: 226.93 ms
==================================================
Deleted.
===== HTTP 請求 =====
GET https://hapi.fhir.org/baseR4/Patient/53805146
標頭:
  Accept: application/fhir+json
  Accept-Charset: utf-8
  User-Agent: firely-sdk-client/5.12.1

===== HTTP 回應 =====
狀態: 410 Gone
標頭:
  Server: nginx/1.24.0, (Ubuntu)
  Date: Tue, 13 Jan 2026 08:29:22 GMT
  Transfer-Encoding: chunked
  Connection: keep-alive
  X-Powered-By: HAPI FHIR 8.5.3-SNAPSHOT/e3a3c5f741/2025-08-28 REST Server (FHIR Server; FHIR 4.0.1/R4)
  X-Request-ID: bxMyna52dXXlZoKB
  Location: https://hapi.fhir.org/baseR4/Patient/53805146/_history/3
  Content-Type: application/fhir+json; charset=utf-8
回應內容:
{
  "resourceType": "OperationOutcome",
  "text": {
    "status": "generated",
    "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><h1>Operation Outcome</h1><table border=\"0\"><tr><td style=\"font-weight: bold;\">ERROR</td><td>[]</td><td>Resource was deleted at 2026-01-13T08:29:21.412+00:00</td></tr></table></div>"
  },
  "issue": [ {
    "severity": "error",
    "code": "processing",
    "diagnostics": "Resource was deleted at 2026-01-13T08:29:21.412+00:00"
  } ]
}
耗時: 977.67 ms
==================================================
Confirmed 410 Gone after delete.
Press any key to continue...





FHIR 10 使用 FHIR API 對病人資料 CRUD 操作範例

FHIR 10 使用 FHIR API 對病人資料 CRUD 操作範例

想要對 FHIR Server 進行資料的存取與管理,通常會使用 FHIR API 來進行各種 CRUD(Create、Read、Update、Delete)操作。當然,想要在 .NET C# 環境內進行這樣的操作,是可以自行建立一個 [HttpClient] 物件,然後手動撰寫各種 HTTP 請求來呼叫 FHIR API。不過,這樣的做法會比較繁瑣,且需要自行處理許多細節。

為了簡化相關的操作,Hl7.Fhir.R4 套件提供了一個方便的 [FhirClient] 類別,讓開發者可以更輕鬆地與 FHIR Server 進行互動。這個類別封裝了許多常用的 FHIR API 操作,讓我們可以透過簡單的方法呼叫來完成各種 CRUD 任務。

首先,FHIR R4 有多少種 Resource 呢?FHIR R4 正式定義了 157 種 Resource(不含 Extension / DataType / ComplexType),涵蓋臨床、管理、財務、公共衛生等領域。

最常見、最重要的基本 Resource,大約有 20 多種,這些 Resource 是我們在日常醫療資訊系統中最常會用到的。以下將常用 Resource 分為幾類,並描述用途與彼此關聯:

病人與照護相關

Resource用途跟其他的關聯
Patient病人核心資料(姓名、性別、出生年月、識別碼)其他所有與病人相關的記錄都會 reference Patient
Practitioner醫療人員資料(醫師、護理、物理治療師等)Encounter, Procedure, CarePlan 等會 reference
PractitionerRole醫療人員角色(哪科、哪機構)連結 Practitioner 與 Organization
RelatedPerson與病人有關的親屬等Patient 的關聯者

照護事件與行為

Resource用途關聯
Encounter就診事件(門診、住院、急診)Patient, Practitioner, Location
Appointment約診Patient, Practitioner, Schedule
Procedure醫療處置(手術、檢查等)Encounter, Patient
CarePlan照護計畫Patient, Practitioner

簡單例子:

  • Encounter → 描述一次病人在院內的流動
  • Appointment → 有時病人需先掛號
  • Procedure → 真正做的檢查或手術

3. 醫療資料(主要)

Resource用途常跟患者關聯
Observation測量 / 檢驗值vitals, lab result 等
Condition疾病 / 健康問題記錄病人目前的診斷
Medication藥品資訊供用於 Rx
MedicationRequest開立處方Patient, Practitioner
MedicationAdministration實際給藥狀態Patient, Practitioner
ServiceRequest檢查/治療請求Patient, Practitioner
DiagnosticReport檢驗報告Observation 集合
AllergyIntolerance過敏資訊Patient

簡單例子:

  • Observation → 量血壓、血糖
  • Condition → 糖尿病診斷
  • MedicationRequest → 醫師開立藥物
  • DiagnosticReport → 實驗室報告

4. 組織與地點

Resource用途
Organization醫院 / 診所
Location醫療地點(病房、診間)
HealthcareService該地方提供什麼服務

5. 身分與安全

Resource用途
Consent病人同意/拒絕資料使用
AuditEvent記錄誰做過什麼(稽核)

6. 支援性的管理與財務

Resource用途
OrganizationAffiliation醫療機構的合作
Claim / ClaimResponse保險理賠請求與回覆
Coverage保險方案(是否有給付)

簡單的使用流程與實際意義(用例)

情境:門診看診

  1. Patient 進入門診
  2. 建立 Encounter(這次就診)
  3. 醫師用 Observation 記錄血壓
  4. 開立 MedicationRequest 處方
  5. 可能建立 Condition(病名)
  6. 若做檢查,會有 ServiceRequest 與後續 DiagnosticReport

給初學者的建議學習順序

你可以按照以下順序理解與練習:

步驟重點
1先掌握 Patient / Practitioner
2再理解 Encounter / Appointment
3再熟悉 Observation / Condition
4最後看 MedicationRequest / DiagnosticReport

在初步了解了 FHIR Server 中的資源類型與應用之後,接下來我們將透過一個簡單的範例程式碼,示範如何使用 Hl7.Fhir.R4 套件中的 FhirClient 類別,先來針對單一資源來了解如何做到 CRUD : 新增、修改、刪除、查詢的操作,要如何能夠在 .NET C# 中來完成。為了要簡化體驗開發過程,這裡將會採用 主控台應用程式 (Console App) 的方式來進行示範。

建立 主控台應用程式 專案

  • 開啟 Visual Studio 2026
  • 選擇「建立新專案」
  • 在 [建立新專案] 視窗中,在右方清單內,找到並選擇 [主控台應用程式] 項目
  • 然後點擊右下方「下一步」按鈕
  • 此時將會看到 [設定新的專案] 對話窗
  • 在該對話窗的 [專案名稱] 欄位中,輸入專案名稱,例如 "csPatientCRUD"
  • 然後點擊右下方「下一步」按鈕
  • 接著會看到 [其他資訊] 對話窗
  • 在這個對話窗內,確認使用底下的選項
    • 架構:.NET 10.0 (或更新版本)
    • 勾選 不要使用最上層陳述式 (這是我的個人習慣)
  • 然後點擊右下方「建立」按鈕
  • 現在,已經完成了這個 主控台應用程式 專案的建立

安裝 Hl7.Fhir.R4 套件

Hl7.Fhir.R4 套件是由 HL7 官方 FHIR 團隊發佈的 .NET SDK(程式開發套件),用來讓開發者在 C# / .NET 中以「物件模型」的方式存取與操作 FHIR R4(Release 4) 的所有資源。

根據在 .NET NuGet 上看到的資訊,其作者是 Firely(原名 Furore),其為 HL7 FHIR 官方 .NET 參考實作團隊,也是 寫 FHIR 規格的人,同時寫 .NET SDK,這正是目前 .NET 世界中標準且官方等級的 FHIR R4 SDK。

  • 在 Visual Studio 的「方案總管」視窗中,右鍵點擊專案名稱
  • 從右鍵選單中,選擇「管理 NuGet 套件」
  • 在 NuGet 套件管理器視窗中,切換到「瀏覽」標籤頁
  • 在搜尋框中,輸入 "Hl7.Fhir.R4" 並按下 Enter 鍵
  • 從搜尋結果中,找到 "Hl7.Fhir.R4" 套件 並點擊它
  • 在這裡的範例中,使用該套件的版本為 5.12.1
  • 在右側的詳細資訊面板中,點擊「安裝」按鈕

撰寫程式碼

  • 打開 Program.cs 檔案,並將其內容替換為以下程式碼:
using Hl7.Fhir.Model;
using Hl7.Fhir.Rest;
using Hl7.Fhir.Serialization;

namespace csPatientCRUD;

internal class Program
{
    private const string FhirBaseUrl = "https://hapi.fhir.org/baseR4";

    static async System.Threading.Tasks.Task Main()
    {
        string GivenName = "Vulcan20250814111";
        string FamilynName = "Lee";

        var settings = new FhirClientSettings
        {
            PreferredFormat = ResourceFormat.Json,
            Timeout = 60_000
        };

        var httpClient = new HttpClient();

        var client = new FhirClient(FhirBaseUrl, httpClient, settings);

        try
        {
            #region ============== Create ==============
            string identityValue = $"MRN-{Guid.NewGuid():N}".Substring(0, 12);
            identityValue = "MRN-20240814A1";
            var newPatient = new Patient
            {
                Identifier =
                {
                    new Identifier("http://example.org/mrn", identityValue)
                },
                Name = { new HumanName().WithGiven(GivenName).AndFamily(FamilynName) },
                Gender = AdministrativeGender.Female,
                BirthDate = "1990-01-01",
                Telecom = { new ContactPoint(ContactPoint.ContactPointSystem.Phone, ContactPoint.ContactPointUse.Mobile, "0912-345-678") },
                Active = true
            };

            Console.WriteLine("Creating Patient ...");
            var json = newPatient.ToJson();
            Console.WriteLine($"JSON: {json}");
            var created = await client.CreateAsync(newPatient); // POST /Patient
            Console.WriteLine($"Created: id={created.Id}, version={created.Meta?.VersionId}");

            PressAnyKeyToContinue();
            #endregion

            #region ============== Read ==============
            Console.WriteLine("Reading Patient by id ...");
            var readBack = await client.ReadAsync<Patient>($"Patient/{created.Id}"); // GET /Patient/{id}
            Console.WriteLine($"Read: {readBack.Name?.FirstOrDefault()} | active={readBack.Active}");

            PressAnyKeyToContinue();
            #endregion

            #region ============== Update ==============
            Console.WriteLine("Updating Patient (add email, set active=false) ...");
            readBack.Active = false;
            readBack.Telecom.Add(new ContactPoint(ContactPoint.ContactPointSystem.Email, null, $"{GivenName}.{FamilynName}@example.org"));
            var updated = await client.UpdateAsync(readBack); // PUT /Patient/{id}
            Console.WriteLine($"Updated: version={updated.Meta?.VersionId}, telecom={string.Join(", ", updated.Telecom.Select(t => $"{t.System}:{t.Value}"))}");

            PressAnyKeyToContinue();
            #endregion

            #region ============== Search ==============
            // 使用 identifier 精準搜尋剛建立的 Patient
            Console.WriteLine($"Searching Patient by identifier '{identityValue}' ...");
            var bundle = await client.SearchAsync<Patient>(new string[]
            {
                $"identifier={identityValue}",
                "_count=5"
            }); // GET /Patient?identifier={identityValue}&_count=5
            Console.WriteLine($"Search total (if provided): {bundle.Total}");
            foreach (var entry in bundle.Entry ?? Enumerable.Empty<Bundle.EntryComponent>())
            {
                if (entry.Resource is Patient p)
                    Console.WriteLine($" - {p.Id} | {p.Name?.FirstOrDefault()} | active={p.Active}");
            }

            PressAnyKeyToContinue();
            #endregion

            #region ============== Delete ==============
            Console.WriteLine("Deleting Patient ...");
            await client.DeleteAsync($"Patient/{created.Id}"); // DELETE /Patient/{id}
            Console.WriteLine("Deleted.");

            // 驗證刪除(預期 404)
            try
            {
                await client.ReadAsync<Patient>($"Patient/{created.Id}");
                Console.WriteLine("⚠️ Still readable (server may be eventual consistent).");
            }
            catch (FhirOperationException foe) when ((int)foe.Status == 404 || (int)foe.Status == 410)
            {
                Console.WriteLine($"Confirmed {(int)foe.Status} {foe.Status} after delete.");
            }
            PressAnyKeyToContinue();
            #endregion
        }
        catch (FhirOperationException foe)
        {
            Console.WriteLine($"FHIR error: HTTP {(int)foe.Status} {foe.Status}");
            if (foe.Outcome is OperationOutcome oo)
            {
                foreach (var i in oo.Issue)
                    Console.WriteLine($" - {i.Severity} {i.Code}: {i.Details?.Text}");
            }
            else
            {
                Console.WriteLine(foe.Message);
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine("ERR: " + ex.Message);
        }
    }

    // press any key to continue
    private static void PressAnyKeyToContinue()
    {
        Console.WriteLine("Press any key to continue...");
        Console.ReadKey();
        Console.WriteLine("");
        Console.WriteLine("");
        Console.WriteLine("");

    }
}

執行程式

首先先來看這個專案的執行結果:

  • 按下 F5 鍵或點擊「開始」按鈕來執行程式
  • 這個專案將會依序針對 Patient 這個資源進行 Create 新增、Retrive 查詢、Update 修改、Search 搜尋、Delete 刪除 等操作
  • 每個步驟都會在主控台視窗中顯示相關的訊息,並等待使用者按下任意鍵後繼續下一步
  • 底下為實際操作過程的輸出文字
Creating Patient ...
JSON: {"resourceType":"Patient","identifier":[{"system":"http://example.org/mrn","value":"MRN-20240814A1"}],"active":true,"name":[{"family":"Lee","given":["Vulcan20250814111"]}],"telecom":[{"system":"phone","value":"0912-345-678","use":"mobile"}],"gender":"female","birthDate":"1990-01-01"}
Created: id=53797442, version=1
Press any key to continue...



Reading Patient by id ...
Read: Vulcan20250814111 Lee | active=True
Press any key to continue...



Updating Patient (add email, set active=false) ...
Updated: version=2, telecom=Phone:0912-345-678, Email:Vulcan20250814111.Lee@example.org
Press any key to continue...



Searching Patient by identifier 'MRN-20240814A1' ...
Search total (if provided): 1
 - 53797442 | Vulcan20250814111 Lee | active=False
Press any key to continue...



Deleting Patient ...
Deleted.
Confirmed 410 Gone after delete.
Press any key to continue...

如何進行程式碼設計說明

底下的操作,根據這個變數宣告:

private const string FhirBaseUrl = "https://hapi.fhir.org/baseR4";

將會採用 HAPI FHIR Server 的 R4 版本作為呼叫對象。

  • 在系統一開始執行前
  • 將會建立一個 [FhirClientSettings] & [HttpClient] 物件
  • 前者 [FhirClientSettings] 將用於設定 FHIR 用戶端與伺服器溝通行為 的組態物件,例如資料格式(JSON/XML)、逾時時間、是否允許重新導向與錯誤處理方式。
  • 後者 [HttpClient] 則是用於實際發送 HTTP 請求與接收回應的物件
  • 上面提到的三個物件,都會傳送給 [FhirClient] 建構式
  • [FhirClient] 這個物件將用於與 FHIR 伺服器進行互動,並提供各種方法來執行 CRUD 操作

新增

  • 為了方便日後反覆操作與展示,這裡的 Patient Identifier 將會採用固定職 identityValue = "MRN-20240814A1"; 來進行
  • 建立一個新的 [Patient] 物件,並設定其屬性,例如 Identifier、Name、Gender、BirthDate、Telecom、Active 等
  • 這些屬性的意義為:
    • Identifier:病人的識別碼,這裡使用一個自訂的系統 URI 與剛剛設定的值
    • Name:病人的姓名
    • Gender:病人的性別
    • BirthDate:病人的出生日期
    • Telecom:病人的聯絡資訊,這裡設定一個手機號碼
    • Active:表示病人是否為活躍狀態
  • 當然,對於 Patient 這個 FHIR Resource 還有其他屬性可以使用,這裡僅點綴說明而已
  • 有了 Patient 這個物件之後,就可以透過 wait client.CreateAsync(newPatient); 來進行寫入到遠端 FHIR Server 上了
  • 這個方法會發送一個 HTTP POST 請求到 FHIR 伺服器的 /Patient 端點,並將 Patient 物件序列化為 JSON 格式的請求主體
  • 當伺服器成功處理請求後,會回傳一個包含新建立的 Patient 資源的回應
  • 回傳的 Patient 物件會包含伺服器分配的唯一識別碼(ID)與版本資訊(VersionId)
  • 顯示該 Patient 的 ID & 版本資訊

查詢

  • 在新增 Patient 之後,可以使用其 ID 來查詢該病人的資料
  • 使用 await client.ReadAsync<Patient>($"Patient/{created.Id}"); 方法來根據剛剛建立的 Patient ID 來查詢該病人資料
  • 這個方法會發送一個 HTTP GET 請求到 FHIR 伺服器的 /Patient/{id} 端點
  • 當伺服器成功處理請求後,會回傳一個包含該 Patient 資源的回應
  • 一旦取得了 Patient 之後,就會在螢幕上顯示該 Patient 的姓名與活躍狀態

修改

  • 查詢到 Patient 之後,可以對其進行修改
  • 這裡的範例是將 Patient 的 Active 屬性設為 false,並新增一個 Email 聯絡方式
  • 在此,將會使用底下程式碼來修改 Patient 物件的屬性
readBack.Active = false;
readBack.Telecom.Add(new ContactPoint(ContactPoint.ContactPointSystem.Email, null, $"{GivenName}.{FamilynName}@example.org"));
  • 使用 await client.UpdateAsync(readBack); 方法來將修改後的 Patient 物件更新到 FHIR 伺服器
  • 這個方法會發送一個 HTTP PUT 請求到 FHIR 伺服器的 /Patient/{id} 端點,並將修改後的 Patient 物件序列化為 JSON 格式的請求主體
  • 當伺服器成功處理請求後,會回傳一個包含更新後的 Patient 資源的回應
  • 顯示更新後的 Patient 版本資訊與聯絡方式

搜尋

  • 所謂的搜尋,是指根據特定條件來查找符合條件的資源。在這個範例中,我們將使用 Patient 的 Identifier 來進行搜尋。
  • 使用底下的程式碼來進行搜尋條件的設定
var bundle = await client.SearchAsync<Patient>(new string[]
{
    $"identifier={identityValue}",
    "_count=5"
}); // GET /Patient?identifier={identityValue}&_count=5
  • 這裡使用 [SearchAsync] 方法來搜尋 Patient 資源,並傳入一個字串陣列作為搜尋參數
  • 在這個例子中,我們使用 identifier={identityValue} 作為搜尋條件,這表示我們要搜尋具有特定 Identifier 的 Patient 資源
  • 同時,我們也使用 _count=5 來限制回傳的結果數量為最多 5 筆
  • 當伺服器成功處理搜尋請求後,會回傳一個包含符合條件的 Patient 資源的 Bundle 回應
  • 在螢幕上顯示搜尋結果的總數量
  • 所謂的 Bundle,是 FHIR 中用來封裝多個資源的容器,例如,在這裡將會檢查 bundle.Entry 項目,篩選出其中的 Patient 資源
  • 然後逐一列出每個 Patient 的 ID、姓名與活躍狀態
  • 這樣就可以看到所有符合搜尋條件的 Patient 資源

刪除

  • 最後,我們可以刪除剛剛建立的 Patient 資源
  • 使用 await client.DeleteAsync($"Patient/{created.Id}"); 方法來刪除該 Patient 資源
  • 這個方法會發送一個 HTTP DELETE 請求到 FHIR 伺服器的 /Patient/{id} 端點
  • 當伺服器成功處理刪除請求後,該 Patient 資源將會被移除
  • 為了確認刪除是否成功,我們可以嘗試再次查詢該 Patient 資源
  • 預期會收到 404 Not Found 或 410 Gone 的回應,表示 該資源已經不存在
  • 如果收到預期的回應,則表示刪除操作成功
  • 顯示刪除確認的訊息

結論

對於一個 FHIR Resource 要對其進行新增、查詢、更新、篩選、刪除的操作,使用 Hl7.Fhir.R4 套件中的 FhirClient 類別,可以大幅簡化程式碼的撰寫與維護工作。 FhirClient 物件提供了相對應的方法,CreateAsync、ReadAsync、UpdateAsync、SearchAsync、DeleteAsync 等,讓開發者可以輕鬆地與 FHIR 伺服器進行互動,而不需要手動處理 HTTP 請求與回應的細節。

這些方法將會轉換成為 FHIR API 的呼叫方式,並自動處理資源的序列化與反序列化,讓開發者可以專注於業務邏輯的實現,而不需要擔心底層的通訊細節。