作者: paul

c# 冗餘的 await 調用

c# 冗餘的 await 調用

最近在CodeReview一些程式碼的時候,看到這行字

冗餘的 await 調用

• 如果結果已經是 Task,不需要重複 await,這樣可以簡化代碼。

想起來有時候我們確實在一些方法並沒有回傳值的問題而定義了回傳的Task

但這時候使用端仍往往習慣去await這個Task

這時建議的移除await,”似乎”是反習慣性的建議

因此我想說,用一些具體的例子,看一下在async 方法下 不去await一個 Task方法時,會不會有什麼影響

結論: 不會有影響~

簡易的程式碼如下:

Program.cs

Console.WriteLine("1");

var test = new Test();

test.Method1();

Console.WriteLine("2");

Console.ReadLine();


public class Test
{
    async Task Method2()
    {
        Console.WriteLine("3");
    }
    
    async Task<int> Method3()
    {
        Console.WriteLine("6");
        return 7;
    }
    
    public void Method1()
    {
        Method2();
        Console.WriteLine("4");
        var value = Method3();
        Console.WriteLine("5");
        Console.WriteLine(value.Result);
    }
}


Console的執行順序結果會打印 出什麼結果?

[ChatGPT閒聊]自動化測試要關注什麼?

[ChatGPT閒聊]自動化測試要關注什麼?

我問 ChatGPT,它回答我:

在自動化測試中需要關注以下幾個重點:

1. 測試範圍選擇(Test Scope Selection)

選擇適合自動化的測試用例,通常會選擇那些重複性高、穩定、且在多個版本中都會執行的用例,如回歸測試、API測試等。對於容易變動且難以自動化的部分,則可能更適合手動測試

2. 測試腳本的可維護性(Maintainability of Test Scripts)

要確保自動化測試腳本易於維護,編寫腳本時應保持簡潔、模組化並遵循乾淨的代碼原則。當應用程式更新時,測試腳本需要能夠方便地做出調整。

3. 測試用例的穩定性(Stability of Test Cases)

自動化測試應該要具備穩定性,避免 flaky tests(不穩定測試),即某些測試結果偶爾會通過,偶爾會失敗的情況。可以通過確保測試環境穩定、避免依賴外部因素等方式來提高測試的穩定性。

4. 測試數據管理(Test Data Management)

自動化測試需要有效管理測試數據,確保測試數據是獨立且可控的。這意味著測試腳本不應該依賴於生產環境數據,並且應該能夠生成或重置測試數據。

5. 報告和記錄(Reporting and Logging)

測試報告應該能夠詳細展示測試結果,包括測試通過與失敗的用例、失敗的原因、錯誤日誌等,以方便快速定位問題並作出相應調整。

6. 異常處理和重試機制(Exception Handling and Retry Mechanism)

自動化測試腳本應該具有良好的異常處理機制,能夠在遇到錯誤時記錄詳細信息,並且在合適的情況下進行重試。這可以避免因網絡不穩定或暫時性問題導致的測試失敗。

7. 持續集成(Continuous Integration, CI)

自動化測試應與持續集成工具(如 Jenkins、GitLab CI 等)相結合,確保測試能夠在代碼變更時及時執行,並且能自動生成測試報告,以方便追蹤代碼品質。

8. 測試環境的隔離和一致性(Environment Isolation and Consistency)

測試環境應該與開發和生產環境相隔離,並且要保持一致性,以避免測試結果受到環境差異的影響。可以使用 Docker 或虛擬機等工具來確保環境的一致性。

9. 測試效率和執行速度(Test Efficiency and Speed)

自動化測試需要在合理的時間內完成,過長的執行時間可能會影響開發和發布節奏。因此需要平衡測試覆蓋率和測試效率,並嘗試使用並行執行或分佈式測試來加速測試流程。

10. 選擇合適的測試工具和框架(Choosing the Right Tools and Frameworks)

根據項目的需求、技術棧和團隊熟悉程度來選擇合適的自動化測試工具和框架,如 Selenium、Appium、Cypress、Robot Framework 等。不同工具有其優勢和適用場景,選擇適合的工具能提高開發效率。

關注這些重點,可以有效地構建和維護自動化測試流程,提高測試效率和產品質量。

以上是ChatGPT整理的摘要,若能在產品開發階段就關注可測試性,其實 對於後續導入測試框架會有事半功倍的效果

以下是我的觀點,幾個應該前期產品設計與開發就該涉獵的重點:

首先,最基本的是測試獨立性。雖然環境設定或第三方服務可能有些麻煩,但這些通常可以透過 Mocking 或在指定環境下關閉來解決,視測試重點而定。然而,更麻煩的是如何確保測試數據的獨立性,這直接關係到測試案例的穩定性。如果每次生成測試數據時,數據數量不一致(例如,原本只有 1 筆,重跑後變成多筆,甚至超過 10 次導致新分頁出現),那麼原本會通過的案例可能在第 10 次執行時突然失敗。

設計及維護測試數據本身是一項高成本的活動,因此在開發初期理清數據流相當重要。數據往往存在相依性,例如,有人員資訊但沒有權限資訊,某些功能就無法運作。因此,確定最小可動範圍是功能性測試的第一步,通常專案中有整合可重覆與重置的資料庫整合測試框架,我認為極有助於這一塊議題的發展(以我們是使用了.Net Core + EntityFramework + Respawn套件 (https://www.nuget.org/packages/respawn)達成數據可測試性框架,有興趣可以自行再深入了解。接著,測試的獨立性要求在同一版本或不同版本間能重複執行持續執行。例如,若隨版本更新而需要同步更新測試數據,否則新功能可能會因不相容的數據而壞掉。因此,功能測試時也要考慮數據的相容性。雖然設計上沒有絕對的好壞,但一致性與簡單易懂是設計的原則。

接著,關於程式數據生成,在日常功能測試時,RD 可以設計資料生成機制,將測試任務和迴歸測試任務拓展至 RD 層級,要求他們在各環境中確保重複驗證功能性。這樣 RD 就會設計出透過程式生成數據的機制(大家都怕麻煩),對測試資料的建立與生成很有幫助。例如,我們近期產品開發時,我會規劃一個「示範客戶/公司」,由示範客戶下 來建立獨立的使用者,自有的權限,自有的相關數據,而不會影響其他客戶,並透過程式生成示範產品的基本可動數據,確保每次上版時能自動更新,這樣就能銜接更進階的測試,特別是涉及外部同步的資料。最好隨時備妥可重複的模擬資料,確保測試流程順暢。

註:以上示範了在 Multi-Tenant 架構下,如何在前期設計中獨立控制資料欄位。如果不是採用這種架構,也可以從資料庫層面切割客戶,這樣做其實沒有問題,主要還是跟架構成本有關。但一旦確立了標準的 schema,接下來就是如何確保系統在最小可行範圍內運作。

總結來說,一切還是圍繞在成本效益比(C/P 值)上。多數專案或產品因為看不到短期效益而猶豫不前,這種情況很常見。就像決定一家餐廳的存亡時,還在考慮菜單字體好不好看,這樣很難說服自己行動。因此,效益和成本必須一起考慮。建立自動化測試也是如此,如果產品在撰寫和執行測試案例時不夠靈活,造成困難,這就會成為一大門檻。大多數阻礙你前進的問題,不是工具怎麼用,而是當你開始動手時,不知道能不能處理後續的麻煩。

Playwright在Azure Pipeline配置

Playwright在Azure Pipeline配置

2024年10月2日,颱風天雖然無風無雨,但還是放了颱風假,只好待在家裡研究最近上保哥的 Playwright 課程後的一些心得和小成果。

從在本地環境使用 Node.js 和 TypeScript 建立測試案例到執行,已經有很多文件和參考資料,所以這部分就先略過。

不過對於自動化框架已經有這麼多套了後,為什麼目前選擇playwright呢?上完課初步瞭解後,我自己整理有以下幾點

  1. playwright簡化了selector,重新設計了定位器,符合無障礙網頁設計規範,與autowait,並可以透過 playwright生態與Api支援下 ,測試可讀性也大幅增加,配合強大工具進行錄製,大幅降低了e2e測試案例寫code的困難度,也降低了維護上的成本。
  2. 強大的Mocking的機制,還有諸多黑科技機制 ,不旦可以mocking api , browser 的storage, 有機會達成更多深入的驗證情境(例如pass 人類、簡訊等雙因子驗證)
  3. 支持所有顯示主流語言,包含java , c# , nodejsd的type script,且可以透過 框架一鍵切換,文件也很完整。
  4. 微軟爸爸金主萬歲

其實,E2E的測試,還有一個就是文化面的觀點,就是所有的測試都必須跟RD雙向奔赴,一面測試能夠做到反饋,產品主動優化,這樣正向循環才有助於產品品質的發展。

作為一個自動化測試框架,通常後續整合到專案或產品上有兩個實作方向:

1. 作為前端專案的介面整合測試

在這種情況下,測試案例的版控權限需要開放到前端專案中。因此,如果開發人員以外的人(如 QA、PM)要協助編修測試案例,可能會頻繁變動專案。而且可能還需要在建置過程中處理 localhost 的啟動及套件測試驗證。這樣的測試其實有點像界面的單元測試,只是更依賴於前端專案,並且還要考慮相關的服務或其他環境的設定。

當然這種方式的好處在於,測試流程可以隨著介面的變更一起調整,與前端工程師的工作息息相關。因此,測試案例的防守範圍包含了 RD,可以隨著功能特性(feature)的調整,達到與版本部署的同步。

但維護成本也相對高(跟專案綁在一起的關係)

2. 作為產品介面的回歸測試

這種方式著重於將較為穩定且不常變動的介面優先建置測試案例。目前,我們專案正朝著這樣的 E2E 測試流程發展,希望能針對不同環境(如 tst、uat、preprod、prod)進行測試。藉由Azure Pileine配置獨立的 E2E 測試專案及執行指令,可以靈活地測試不同環境的標的。

獨立了 test 專案後,測試team(當然也可以RD、PM都一起來寫XD)可以依據不同環境規劃測試案例的目錄。透過 npx playwright test 指令,可以方便地測試指定的案例目錄及不同瀏覽器參數。這種方式相當便捷(先前也玩過 Cypress),並且獨立版控的測試專案也帶來了一些好處,例如可以在 CI 流程中靈活分配,無論是每次上版到不同環境時進行或是定期測試、在指定條件下執行測試,都能自由安排。

這次我希望達到的是上述第二種切入點的做法,因為我們的 E2E 測試希望從使用者操作的角度進行,以 “指定環境” 的 Web 站台為測試對象。

因此我們將BASE_URL(測試站台的進入點)作為 參數由外部傳入

另外,因為考慮了playwright的覆用性,因此也把playwright的大部分Task包成另一個 Azure Template

這樣一來,不同環境就可以建置不同的Pipeline去定義 參數後再傳入PipelineTemplate執行標準的測試 Task

Main Pipeline

(以下範例移除了公司的標記,可以依有抽出參數的部分自由參考運用)


pool:
  vmImage: ubuntu-latest

trigger:
  branches:
    include:
      - 'main'

resources:
  repositories:
  - repository: PipelineTemplates
    type: git
    name: {版控}/Pipeline-Templates
  - repository: E2ETesting
    type: git
    name: {專案}-End2EndTesting

name: $(Build.BuildId)

stages:

  - stage: qa
    displayName: 'Run Automation Test - QA'
    dependsOn: []
    jobs:
      - template: template/playwright-template.yml@PipelineTemplates
        parameters:
          BASE_URL: 'https://tst-{站台}/zh-TW/home' #測試站點
          End2EndProjectKey: E2ETesting  #測試專案版控Key
          End2EndProjectName: {專案}-End2EndTesting #測試專案目錄名稱(用於組合路徑用)

PipelineTemplates

(作為Function可以被不同的pipeline參考執行,注意,這個是 playwright-template.yml的內容,另外一個檔案)


parameters:
  - name: BASE_URL
    type: string
  - name: End2EndProjectKey
    type: string
  - name: End2EndProjectName
    type: string

jobs:

  - job: test
    displayName: Run Playwright Tests
    steps:
      - download: none
      - checkout: self
        persistCredentials: true    
      - checkout: ${{ parameters.End2EndProjectKey }}
        persistCredentials: true

      - script: |
          ls $(System.DefaultWorkingDirectory)
        displayName: "Print Working Dir"

      - task: NodeTool@0
        displayName: 'Use Node version 18'
        inputs:
          versionSpec: 18.x

      - script: |
          cd ${{ parameters.End2EndProjectName }}
          ls .
        displayName: "Cd E2E Folder and ls folder "

      - script: |
          cd ${{ parameters.End2EndProjectName }}
          ls .
          npm ci
        displayName: "NPM Install"

      - script: |
          cd ${{ parameters.End2EndProjectName }}
          ls .
          pwd
          echo $(Pipeline.Workspace)/s/${{ parameters.End2EndProjectName }}/browsers
          cd /home/vsts/
          PLAYWRIGHT_BROWSERS_PATH=/home/vsts/
          npx playwright install --with-deps
           #note 不要用sudo安裝,會裝到/root去專案跑的時候會找不到browser driver
        displayName: "Playwright Install"

      - script: |
          echo "BASE_URL= ${{ parameters.BASE_URL }}"
        displayName: "Print Variables"

      - script: |
          cd ${{ parameters.End2EndProjectName }}
          BASE_URL=${{ parameters.BASE_URL }} CI=true PLAYWRIGHT_JUNIT_OUTPUT_NAME=test-results/results.xml npx playwright test
        displayName: "Run Playwright Tests"
        continueOnError: true

      - task: ArchiveFiles@2
        displayName: 'Add playwright-report to Archive'
        inputs:
          rootFolderOrFile: '$(Pipeline.Workspace)/s/${{ parameters.End2EndProjectName }}/playwright-report/'
          archiveFile: '$(Agent.TempDirectory)/$(Build.BuildId)_$(System.JobAttempt)$(System.StageAttempt).zip'

      - task: ArchiveFiles@2
        displayName: 'Add test-results to Archive'
        inputs:
          rootFolderOrFile: '$(Pipeline.Workspace)/s/${{ parameters.End2EndProjectName }}/test-results/'
          archiveFile: '$(Agent.TempDirectory)/$(Build.BuildId)_$(System.JobAttempt)$(System.StageAttempt).zip'
          replaceExistingArchive: false

      - task: PublishPipelineArtifact@1
        displayName: 'Publish Pipeline Artifacts'
        inputs:
          targetPath: '$(Agent.TempDirectory)/$(Build.BuildId)_$(System.JobAttempt)$(System.StageAttempt).zip'
          artifact: pipeline-artifacts

      - task: PublishTestResults@2
        inputs:
          testResultsFormat: 'JUnit'
          testResultsFiles: '*.xml'
          searchFolder: '$(Pipeline.Workspace)/s/${{ parameters.End2EndProjectName }}/test-results'
          testRunTitle: 'Playwright ADO Demo - $(System.StageName)'
        displayName: 'Publish Test Results'

成果

壓力測試到底在關注什麼?

壓力測試到底在關注什麼?

最近請同事開始使用工具做壓力測試,一開始講目的是為了”知道”我們的API能否承受一定的使用者使用我們的系統,至於多少使用者呢?假定是1000個線上用戶為目標。但過了一陣子,發現大家似乎 對於要測什麼,怎麼測,測完後的行動,壓力測試跟效能測試差異,都還有一些疑慮。導致無法有效的進行。

先講”相對”最簡單的議題,怎麼測,首先應該是依賴工具,有各種技術方案,有的用JMeter,有的用Script的Tool例如K6。端看想要如何測試往下延續或保持這種壓測的節奏。

JMeter的考量可能是有GUI的壓測劇情設定編排的畫面,對於比較不寫程式的,也有機會去達成壓測的工作。一樣可以做到算是API間的相依,像是從Login拿Token,再拿Token去打 其他API做驗證後的邏輯。這些都做的到,報表也有整合好可以看到一些圖表數據。這些數據我就先不討論了,都是有定義的。

其他工具例如K6,就可以讓你用JavaScript的方式寫腳本,方便用程式的方式 自動化 的,重覆使用化的去設計你的壓測流程,這是好處,不過就會有一點點門檻。(這對程式有點基礎的就學習上相對簡易很多)

再來一層就是我們要討論 為什麼要壓力測試,壓力測試的對象從不同的面象又可以做一個粗淺的分類,例如:

  1. front-end 或是 back-end
  2. 元件或是 end 2 end 測試
  3. 單一協定架構或混合架構。

上述分類不代表什麼,只是說明你可能可以整體來看你測試對象的分類為何 ,因為能錨定你的對象跟你選擇的工具,腳本的寫法都有些關係。不過我認為,這些還是可以概念上分成由上而下拆解,還是由下而上堆疊來驗證。

由上而下,不外乎還是把你的測試對象視為一個黑盒子。可能你知道中間的構成是什麼,也可能不知道或不確定,因此透過 測試,可以幫助你推論 瓶頸在何處,但都要數據來證明。愈複雜的組成有可能有愈多的依賴。因此你一開始愈來,拆的成本就愈高。

由下而上,我認為是你可以透過很多碎片/單元化的可驗證組件逐一測試。這些分開的成本也不小。疊到符合”使用場景”要花的時間,是你需要考慮的。例如假如知道一隻極度單純的API查詢 資料庫1次的數據(先知道環境依賴的跟與業務邏輯無關的部分),其實你可以往下驗證到,例如你的登入,背後可能是1隻API結合查詢 3次 ,寫入1次 ,這樣的子的邏輯複雜度。在你設定的壓力狀況下的成績如何。再來逐步的疊加可能登入Api+查詢Api複合性的商業邏輯,不過注意必須合理,若網站所有Api不考慮相關性,相依性 平均的去壓測他們,這樣的測試也是既耗時也浪費的。

因此從上述的方向,並不是說你只能走一個方向,而是你要怎麼透過”不同的角度”去找答案!

以API的部分,舉個例子:

一開始 可能設想由上而下,我們現在電商網站最常被使用是登入嗎?可能不是,大家要嘛就已經登入了,user資訊存在cookie中 ,要嘛大家就在逛網站,而不用急著登入。那麼登入真的是最需要被驗證的接口嗎?其實可能不是。這些都端看你網站內的流程如何設計。那往對應到流程後,背後有哪些”最相關的Api分析出來後,就可能是你首要的驗證對象。

因此 使用者場景=分析=>關鍵路徑(常用不一定關鍵)=分解=>背後關鍵的API順序。這個過程去識別出測試對象,就是很重要的。

大則流程,細則程式,你也可以想象,這是一個識別風險的過程,並透過科學的方式證明他們是或不是風險,每次開發好一個API功能後,可能先單獨 驗證這隻API的效能,再接著從這次 新增的Api去往外推論影響的範圍。

最後,聊一下由下而上的概念,這邊舉個例子,就是測試參數到底怎麼決定比較好,看了k6的vus跟duration還有iteration後,我發現測試上會跟現實狀況會有脫勾的情況,例如你要測 2000 concurrent user,你的想像是一秒內同時打 2000個請求,都能被消耗,並能被快速的執行完成,是這樣嗎?往往 使用者情境不見得是這樣,就算是搶票,我們還是可以區分出讀寫比重不一定一樣的行為模式,因此結合使用者情境或是能透過你的情境去解釋,就會各自有差別,不過我想說的以本機測試為例,我們其實是希望知道受測單元的極限。例如1個直接回傳 200的api接口,在一台PC等級的筆電上,能驗證的vus同時併發量就是440vus(因為超過可能就會”環境”failed,是server被reset connection的那種),然後平均每秒 處理請求數是多少,這些都可以先針對你的技術框架有一個單元極限值,然後再往 疊加業務邏輯的方式,去收斂那個參數邊界,最後找到一個合理 測試參數,接著就是固定這個變數,往再去尋找其他可控 變數,例如組合不同邏輯下,對效能、可用性的影響為何 。

知道作法跟想做的方向後,再來看我們要怎麼解讀壓力測試API的結果來回答我們的疑問,現學現賣一下,不外乎想知道兩個面向

  1. Latency(延遲)
  2. Availability(可用性)

從Latency 延遲角度,可以結合Performance的角度就是為了知道有壓力源下的回應時間的分佈狀況。例如你可以想像在早上8:00的時候,使用系統的人最多,可能有1000人,平均等待時間會從100人的平均3秒成長到平均10秒 ,而這樣的延遲是否可以”忍受”。若不能忍受就要提高服務器的性能或是做其他的分流機制。

從Availability可用性的角度,主要是想知道API是否會中止服務,而導致關鍵流程無法完成。因此我認為就是在看 Total跟Failed的比例。假如Fail是3次 ,但是是100萬次的3次,跟10次 裡的3次 ,狀況可能完全不一樣。(但這不代表 100萬的3次 不重要)

可以想像一個情境,假如你有一個搶票的API,公告12:00開始可以進來搶100張票,結果假如你只有一台主機服務,結果因為開搶那一瞬間,湧進來10000個人來搶。瞬間把API打到當機,背後的資料庫也阻塞中止新連線,結果整個網站不要說進來了,連前面有成功進來的人也無法往下走到”金流”的流程,那麼就是典型的可用性出了很大的問題。

而這些都是可以”提前”大膽的假設,透過壓測工具來求證。那麼如何使用工具來驗證場景呢?K6官網有整理了幾種壓測的情境,來因應上述的場景
1.Smoke Test
2.Average Test
3.Stress Test
4.Spike Test
5.Soak Test

其中或許我們只要分辨出,SmokeTest目的是用小單位去驗收你的測試腳本是否正確,先行排除在重點之外。

而Average, Stress, Spike都是在給定一個壓力源(這個也是要設計的)下 ,壓力可能是量,可能是時間長(Soak),到底回應時間以及可用性如何。

上面先啦賽了這麼多,就是希望有助於結構性的先理解我們面對的東西是什麼,概念跟術語是什麼,先做好定義同步共識後,往下做的Action才有理有據,而每一次做的壓測都有如一場實驗,留下的數據,就算是沒有瓶頸的發現,那保留下來的可能那組”安全”參數、數據上的經驗都應該要能夠被延續,而不是把每個壓測都視為那麼單一的事件去操作。每個產品,每個階段,每個功能都不盡相同,但分析與解決問題的脈落其實都有一套科學思維的pattern,數據也有解讀的方式。解讀出來後,如何往下分析瓶頸(依賴)跟重構,那屬於壓測後的獨立工作,就先不在這邊一併討論了。

Server Set Cookie Cross Domain的雷

Server Set Cookie Cross Domain的雷

繼API的CORS的問題後

現在又遇到了一個set cookie的問題,下面的測試順序是反過來的,最早是遇到無法設定到Browser/Application的Cookie,後來發現可以設定的方式後,改實驗多種Domain的組合…

首先本機測試Host先改成模擬前後端分離站台的部署模式

      response.cookies.set('refreshToken', refreshToken, {
        path: '/',
        maxAge: 60 * 60 * 24 * 7,
      });

      response.cookies.set('domainTokenByMiddleWare', refreshToken, {
        path: '/',
        domain: '.azure-paul.com',
        maxAge: 60 * 60 * 24 * 7,
        secure: true,
      });

set-cookie成功!

接著改換 azure-paul.net

      response.cookies.set('refreshToken', refreshToken, {
        path: '/',
        domain: '.azure-paul.net',   
        maxAge: 60 * 60 * 24 * 7,
      });

      response.cookies.set('domainTokenByMiddleWare', refreshToken, {
        path: '/',
        domain: '.azure-paul.net',
        maxAge: 60 * 60 * 24 * 7,
        secure: true,
      });

一樣可以

透過 client端直接呼叫api server response的set-cookie也work

最後我改成這個domain

     response.cookies.set('refreshToken', refreshToken, {
        path: '/',
        domain: '.azurewebsites.net',   
        maxAge: 60 * 60 * 24 * 7,
      });

      response.cookies.set('domainTokenByMiddleWare', refreshToken, {
        path: '/',
        domain: '.azurewebsites.net',
        maxAge: 60 * 60 * 24 * 7,
        secure: true,
      });

結果無情的被拒絕set-cookie

真的沒有寫進去…驚嘆號上的hint是寫著..”current host url invalid”

這樣看來跟.net or .com沒有關係 ,倒是跟azurewebsites.net的domain被列入了卡控 名單有關係

似乎..這跟我”一開始”在/etc/hosts改成dev.azure.com 一樣,會直接被chrome卡掉

繞了一大圈後…

小結論如下
CORS跟Cookie ,若以不同domain要做到CrossDomain的存取,必須繞路避開大網站的domain
(如上azure-paul.com, azure-paul.net passed)

或是直接以NGINX Proxy建成xxxx.yyyy.com + xxxx.yyyy.com/backend,直接就可以不用遇到這種雷…

若有其他發現或知道真相的人歡迎一起分享指教~~

自行備註:

針對azurewebsites.net的set sub-domain的問題其實有人分享過 ,只是一開始哪會找這個方向XD

https://learn.microsoft.com/en-us/answers/questions/360645/cannot-set-a-subdomain-friendly-cookie

竟然是真的跟這邊的名單有關?(半信半疑)

https://publicsuffix.org/list/public_suffix_list.dat

此文的解法 真的也是綁到名單之外的Domain

透過Azure DataStudio / SSMS查詢效能不佳的SQL語法

透過Azure DataStudio / SSMS查詢效能不佳的SQL語法

這兩天遇到一個情境,一個早上突然所有的請求都卡住,平均回應 時間都要1~2分鐘。當下一時間沒啥方向,往登入那邊查,結果找錯了方向,會這樣想純粹是因為認為那個是一條測試的必經之路。而我們還在開發的資料庫中,最多也才幾百筆,最大 也不到一萬筆,為什麼會突然 這麼慢呢?

後來請 DBA看了一下後,馬上就找到問題了,大至記錄如下:

情境:
假如現在有兩張表
分別為
表 1 Master(Id)
表 2 Detail(Id, MasterId)

當時Master表大概有9xx筆,而Detail表有7千筆左右

select * from Master m
left join Detail d on m.Id = d.MasterId
where m.xxxxxx = xxxxxx

結果這樣不得了,一查發現大概執行個5、6百次 ,總執行時間就超過4小時以上了..
一般來說,PrimaryKey需設是Unique+Index(Cluster)因此只以Id作搜尋條件,效能不會差
但此時,Foreign Key若子表的Column沒有建Index,則對搜尋效能仍無影響,FK只作為 限制功能

補充以上觀點後

再補上SqlServer這次如何查找有問題的效能語法的方法(我是Mac,突然很羨慕ssms完整版功能)
不過我也立馬補上Azure Data Studio的方法,不無小補 一下。

1.Azure Portal對有問題的database進行 “查詢效能深入解析” (Azure Portal上的DB查詢權限要開放)

ex: 透過下列清單,可以查詢到Top5慢的查詢,透過查詢ID,可以再往 展開

註 : 這個畫面出不來的話代表 Azure Portal上對DB的權限不夠,不過至少可以對到查詢 ID,再請DBA或用 SSMS/Azure Data Studio去細追

2.透過SSMS,需要DBA針對db user進行開放

Query Store(查詢存放區)
-> Top Resource Consuming
-> Tracked Queries
-> QueryId

SSMS功能就是強大 ,可以針對 時間/CPU/IO統計

對於我們的查詢 ,在一個時間區間內,Where 條件不一樣 ,可能就QueryId就會不同(Plan Id就會不一樣)
-> ViewQueryTest查詢其問題語法分析 執行計劃

透過 查詢計劃可以看到發生了兩次Scan

3.透過Azure Data Studio,加入dashboard圖表

最後一種,若你是用Mac的環境(M1 Core),沒有辦法安裝SSMS這套工具的話

教學:https://learn.microsoft.com/en-us/azure-data-studio/tutorial-qds-sql-server

設定完重啟,在Database-> Manage就可以看到以下的圖表,也可以對齊上述方法的查詢Id

能看到非常 明顯 的峰點,那就還算是好的情境~(特別是還在 測試區)

針對該圖表 可以往下 看Insight的清單,可以對到1525就是問題查詢的所在

從query_sql_text就可以拿到問題語法,一樣貼到Azure Data Studio的Query窗

就可以Estimated Plan來顯示查詢計劃囉

DateTimeOffset在ToString()的神秘事件

DateTimeOffset在ToString()的神秘事件

最近早上08:xx多的時候,跑本機整測突然一個測試跑不過 。

下來Debug查說是比對值驗證 錯誤

測試情境是 datetimeoffset的value會存進資料庫,再查出來直接 做比對是否 正確

突然就這麼錯了覺得很怪,往下 追查,其比對的值出現了
2024/1/1 12:00:00 +00:00 vs 2024/1/1 00:00:00 +00:00

的情況,想說應該是24小時制的問題,因此寫了一小段程式驗證

        var valueHour12 = new DateTimeOffset(2024, 1, 1, 12, 0, 0, 0, offset: TimeSpan.FromHours(0));
        var displayToHour12 = new DateTimeOffset(2024, 1, 1, 0, 0, 0, 0, offset: TimeSpan.FromHours(0));
        
        var s = valueHour12.ToString("o");
        var sWithO = valueHour12.ToString();
        
        Console.WriteLine("valueHour12");
        Console.WriteLine(s);
        Console.WriteLine(sWithO);
        
        Console.WriteLine("displayToHour12");
        s = displayToHour12.ToString("o");
        sWithO = displayToHour12.ToString();
		
        object boxingHour12Am = displayToHour12;
        
        Console.WriteLine(displayToHour12.Hour);
        Console.WriteLine(s);
        Console.WriteLine(sWithO);
        Console.WriteLine(boxingHour12Am);
        //tostring 失真點
        var parsedDTOffset = DateTimeOffset.Parse(boxingHour12Am.ToString());
        Console.WriteLine(parsedDTOffset.Hour);
        
        //明確格式 不會失真
        parsedDTOffset = DateTimeOffset.ParseExact(displayToHour12.ToString("yyyy/MM/dd hh:mm:ss t z"), "yyyy/MM/dd hh:mm:ss t z", new CultureInfo("zh-TW"));
        Console.WriteLine(parsedDTOffset.Hour);

輸出結果如下,會發現指定12點的 跟指定0點的,在ToString()後,再解回Datetimeoffset的結果,hour會飄掉,具體來看是他現顯示成如下

試著把Console印出來結果如下

valueHour12
2024-01-01T12:00:00.0000000+00:00
2024/1/1 12:00:00 +00:00
displayToHour12
0
2024-01-01T00:00:00.0000000+00:00
2024/1/1 12:00:00 +00:00
2024/1/1 12:00:00 +00:00
12 //不明確格式解成了 12的數字
0 //明確格式可以正確解成0的Hour

這個知道為什麼後,解法很簡單,就是明確轉字串格式

不過 為什麼說神秘?

因為首先Windows的OS上不會有這個情況,ToString()至少會有PM, AM或上午, 下午,所以這個案例在Windows的PC上跑測試,目前看起來都 測不出來。目前是我是在Mac的Rider上測試,或許會有OS 的環境、語言差異,造成這個影響,不過我是從Rider上直接看到他的變數可視化 就是直接忽略了上午/下午,這個IDE上差異就比較特別,在Windows的Rider跟VS則都有上午/下午。IDE與OS之餘,可能也可以在Windows上安裝 Docker跑Linux看看,不過 若ToString()可以重現 0點變 成12的Hour屬性 ,就代表 非Windows系統就會有這種ToString()失真的情況!

第二個,只要過了 12點這個臨界點,例如台灣時間9點,代表 UTC凌晨1:00,再ToString(),就 不會有這個問題,可以解出 1的Hour,但是若是指定13點,就又會出現這個情況,一樣會解出1,分別會變成1點PM跟 13點,這樣的測試時間點若加上台灣時間的shifting(-8),很容易混淆,或是誤以為沒問題,且若固定 時間,或隨機時間導致測試測不出來,而不知哪天會有重大的狀況。

設定為 13的情境如下:

例如我們都設定 是 測試資料為 早上10點,那這個問題不會炸,不然就是我們設定 固定 CICD整測 跑的時間都是早上11點,那可能抓到的時間也都沒問題

因此需要小心程式中任何 地方 datetimeoffset,tostring(), 序列化 或 檔案化 的部分會不會有失真的情節 ,將其格式明確統一地定好。

而今天 因為9:00前上班被抓到而探討一下這個問題也實屬運氣成份吧?

在Azure AppService Docker Alpine Container上連線MSSQL遇到 網路不通的問題

在Azure AppService Docker Alpine Container上連線MSSQL遇到 網路不通的問題

最近使用AppService佈署Container服務

結果在連線資料庫的時候 一直掛掉

A network-related or instance-specific error occurred while establishing a connection to SQL Server.

Container的Linux版本是Alpine

因此安裝套件的指令是改用用apk add,不是apt-get

以後telnet套件無法直接安裝,而是被封裝進 busybox-extras 

Ref: https://github.com/gliderlabs/docker-alpine/issues/397

apk add busybox-extras

但實測Telnet下

app# busybox-extras telnet sea-dev-xxxx-sqlsrv.database.windows.net 1433

是通的,但程式還是連不過去

後來終於在這篇找到了一點蛛絲馬跡

https://github.com/dotnet/dotnet-docker/issues/2346

馬上試了一下
apk add icu-libs

果然通了,浪費了我二個小時XD

實作RSA + AES 的檔案加密算法POC

實作RSA + AES 的檔案加密算法POC

為了做到檔案的加密(後來才知道其實用GPG就可以了)

一開始我只是想說用現成套件不同的 金鑰key值作為參數,結合ChatGpt就開始做POC看看了

大不了是換演算法的問題,結果其實數位簽章 除了格式、作業系統管理的模式、公私鑰檔案其實也有不同的規定

但大至上心得是一開始想說讓 檔案加密,而且是透過Public Key進行,然後對方用 Private Key解密,所以我出發點就是用RSA演算法去進行加密,結果發現怎麼都會發生長度錯誤的問題。

後來深究後才發現RSA的加密無法對長度太大的東西進行,所以後來換成短字串果然就可以了

因此思考了加解密該做的事情後,就做了一個AES + RSA的POC,RSA主要用來加密 AES的Key

後來果然發現這個跟GPG的作法有點像~~

實作POC如下∶
using System.Security.Cryptography;

string keyBase64 = string.Empty;
using (Aes aesAlgorithm = Aes.Create())
{
aesAlgorithm.KeySize = 256;
aesAlgorithm.GenerateKey();
keyBase64 = Convert.ToBase64String(aesAlgorithm.Key);
Console.WriteLine($”Aes Key Size : {aesAlgorithm.KeySize}”);
Console.WriteLine(“Here is the Aes key in Base64:”);
Console.WriteLine(keyBase64);
Console.ReadKey();
}

string filePath = “/Users/paul_huang/Desktop/FileEncryptTest”;
GenerateKeys(filePath);

// 設定公鑰和私鑰檔案路徑
string publicKeyPath = Path.Combine(filePath, “publicKey.pem”);
string privateKeyPath = Path.Combine(filePath, “privateKey.pem”);

// 設定金鑰的位元數
var keyString = keyBase64;

// 檢查檔案是否存在
if (!File.Exists(publicKeyPath) || !File.Exists(privateKeyPath))
{
Console.WriteLine(“找不到金鑰檔案。請確保金鑰檔案存在。”);
return;
}

// 讀取公鑰
string publicKey = File.ReadAllText(publicKeyPath);

// 讀取私鑰
string privateKey = File.ReadAllText(privateKeyPath);

// 設定要加密的檔案路徑

// 加密檔案
var keyFileName = “key.txt”;
var keyFilePath = Path.Combine(filePath, keyFileName );
if (File.Exists(keyFilePath) == false)
{
File.WriteAllText(keyFilePath, keyString);
}
else
{
keyString = File.ReadAllText(keyFilePath);
}

EncryptKeyFile(keyFilePath, publicKey);

DecryptKeyFile($”{keyFilePath}.encrypted”, privateKey);


// var encryptedKey = $”{keyFilePath}.encrypted”;

var encryptFile = “paul.jpg”;
var encryptFilePath = Path.Combine(filePath, encryptFile);


var encryptedFileContentString = Encrypt(Convert.ToBase64String(File.ReadAllBytes(encryptFilePath)), keyString);
File.WriteAllText(encryptFilePath+”.encrypted”, encryptedFileContentString);

File.WriteAllBytes(encryptFilePath + “decrypted”, Convert.FromBase64String(Decrypt(encryptedFileContentString, keyString)));

static void EncryptKeyFile(string filePath, string publicKey)
{
try
{
byte[] dataToEncrypt = File.ReadAllBytes(filePath);

using var rsa = new RSACryptoServiceProvider(2048);
rsa.FromXmlString(publicKey);

byte[] encryptedData = rsa.Encrypt(dataToEncrypt, false);

// 寫入加密後的檔案
File.WriteAllBytes($”{filePath}.encrypted”, encryptedData);

Console.WriteLine(“檔案已成功加密。”);
}
catch (Exception ex)
{
Console.WriteLine($”加密時發生錯誤: {ex.Message}”);
}
}

static void DecryptKeyFile(string encryptedFilePath, string privateKey)
{
try
{
byte[] encryptedData = File.ReadAllBytes(encryptedFilePath);

using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(2048))
{
rsa.FromXmlString(privateKey);

byte[] decryptedData = rsa.Decrypt(encryptedData, false);

// 寫入解密後的檔案
File.WriteAllBytes($”{encryptedFilePath}.decrypted”, decryptedData);

Console.WriteLine(“檔案已成功解密。”);
}
}
catch (Exception ex)
{
Console.WriteLine($”解密時發生錯誤: {ex.Message}”);
}
}

static string Encrypt(string plainText, string key)
{
using (Aes aesAlg = Aes.Create())
{
aesAlg.KeySize = 256; // Set key size to 256 bits
aesAlg.Key = Convert.FromBase64String(key);
aesAlg.IV = new byte[16]; // AES uses a 128-bit IV

ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);

using (MemoryStream msEncrypt = new MemoryStream())
{
using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
{
using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
{
swEncrypt.Write(plainText);
}
}

return Convert.ToBase64String(msEncrypt.ToArray());
}
}
}

static string Decrypt(string cipherText, string key)
{
using (Aes aesAlg = Aes.Create())
{
aesAlg.KeySize = 256; // Set key size to 256 bits
aesAlg.Key = Convert.FromBase64String(key);
aesAlg.IV = new byte[16];

ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);

using (MemoryStream msDecrypt = new MemoryStream(Convert.FromBase64String(cipherText)))
{
using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
{
using (StreamReader srDecrypt = new StreamReader(csDecrypt))
{
return srDecrypt.ReadToEnd();
}
}
}
}
}

static void GenerateKeys(string path)
{
try
{
using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(2048))
{
// 公鑰
string publicKey = rsa.ToXmlString(false);
Console.WriteLine(“公鑰:”);
Console.WriteLine(publicKey);

// 私鑰
string privateKey = rsa.ToXmlString(true);
Console.WriteLine(“\n私鑰:”);
Console.WriteLine(privateKey);

// 將金鑰寫入檔案
File.WriteAllText(Path.Combine(path, “publicKey.pem”), publicKey);
File.WriteAllText(Path.Combine(path, “privateKey.pem”), privateKey);

Console.WriteLine(“\n金鑰已成功生成並保存至檔案。”);
}
}
catch (Exception ex)
{
Console.WriteLine($”生成金鑰時發生錯誤: {ex.Message}”);
}
}

static string GenerateRandomKey(int length)
{
// 使用 RNGCryptoServiceProvider 產生安全的亂數
using (var rng = new RNGCryptoServiceProvider())
{
// 建立位元組陣列來存放隨機資料
byte[] randomBytes = new byte[length];

// 將亂數填入位元組陣列
rng.GetBytes(randomBytes);

// 將位元組轉換成十六進位字串
string randomKey = BitConverter.ToString(randomBytes).Replace(“-“, “”);

return randomKey;
}
}

[資訊安全] 在Mac上用GPG套件將檔案加密(231108 更新)

[資訊安全] 在Mac上用GPG套件將檔案加密(231108 更新)

Gpg是一個數位簽章的檔案加密套件

其原理可以從他的祖先PGP去略知一二

利用了RSA的演算法與各種雜湊演算法對檔案進行處理,但若直接 使用RSA演算法無法對檔案大小的長度(我的照片6xx kb)內容加密,所以RSA主要是用在對檔案進行對稱加密的金鑰進行保護加密。
其原理可以再自行看一下相關連結。

下載 GPG套件

https://sourceforge.net/projects/gpgosx/

https://formulae.brew.sh/formula/gnupg

基本上,若是採用這個協定進行加密,應該會提供pgp檔案,pgp通常為對方可公開的公鑰檔案。匯入我們這端的系統後,就可以讓我們以gpg指定對方公鑰作為收件者進行檔案加密。照上圖,檔案加密的RandomKey就是由對方的公鑰做加密,以確保只有對方解的開。


然而對方應該不會提供自己的私鑰,通常是會用於加簽(後面指令中的參數為–sign),若你用自己的私鑰加簽,就要提供自己的公鑰,對方才能做驗簽,可以透過gpg –list-secret-key檢查自己的系統是否有私鑰,若沒有的話,可以上網申請一組。

ex:透過RSA對私鑰進行簡易密語設定,格式也可以指定,匯入時gpg套件都會幫你封裝好這些細節

這邊我們在mac上實際 用 gpg一樣可以匯入pgp的檔案,所以基本上應該是有向下支援了。

gpg import xxxx.asc或skr (注意,是兩個減號) => 此為私鑰檔案,通常只有一份要管理/備份好,若都沒有的話,可以透過指令/金鑰工具產生。

gpg import yyyy.pgp (注意,是兩個減號) => 此為公鑰檔案,每個訊息交換端都可以散佈公鑰檔提供對方做加密傳給自己,而只有自己的私鑰可以解開。

匯入yyyy.gpg的時候會需要輸入password

匯入後,使用以下指令就可以進行檔案加密

gpg –output “/Users/paul_huang/Desktop/{加密後的檔案}.encrypted” -u “paul(Test)” –pinentry-mode loopback –always-trust -r “收檔案者(收件者)的公鑰ID” –s2k-digest-algo “SHA1” –sign –passphrase “加簽章時使用的私鑰密語” –encrypt “/Users/paul_huang/Desktop/{欲加密的檔案}.xml”

成功加密的話,就會有以下的檔案產生  因為涉及加簽,加簽關係到驗簽,證明發送端是我進行的,因此還需要我的公鑰進行驗簽。

註:  

-u, –local-user 使用者-ID,拿指定使用者 ID 來簽署或解密 , 這一步置關重要,要帶入這個參數,對方 才知道你是用 這個人的私key加簽,對方在驗簽的時候,需要你的公鑰進行驗證簽章 ,代表保證檔案的合法性

-r 可以多組 ,代表這個加密檔案,有在收件者當中的人都可以用他的私鑰解密 

可以透過以下指令進行匯出提供接收端做驗簽

gpg –armor –export {公鑰ID} > PublicKey.pgp ( -a, –armor  會以建立以 ASCII 封裝過的輸出)

公鑰ID可以用gpg –list-keys查詢一下私鑰對應的公鑰是哪個  (因為是公鑰,所以不怕公開)

公私鑰使用上的原理如下

加密者,以RamdonKey進行對稱式加密檔案,接著以 解密端(收件者)的 “公鑰” 將Random Key進行檔案加密,對方可以用自己的私鑰解密randomkey,進行檔案解密。其中加密者會在加密後,再用私鑰進行一次簽章,此簽章只能用加密者的公鑰進行驗簽,此驗證就是確保,是加密 者進行上述加密行為,無其他人篡改過。

Reference

https://blog.miniasp.com/post/2022/05/11/gnupg

https://xeodou.me/how-pgp-works/