ShowProgramCode

2022年12月30日 星期五

C# .Net Core6 多執行緒處理ThreadPool PartII

從上一篇可知ThreadPool的基本設定了,不過接下來又遇上新的問題。

  1.  如何確認所有執行緒完成?
  2.  將主控台專案改為WinForm專案使用ThreadPool...

首先,先說明如何確認所有執行緒完成工作?
在此我使用者WaitHandle.WaitAll()來確認。
依照官網的說明,必須將doneEvent = new ManualResetEvent(false)帶入ThreadPool,最終使用doneEvent陣列帶入WaitHandle.WaitAll()即可。
二話不說,先上程式碼。

  1. internal class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. const int FibonacciCalculations = 10;
  6. firtstTest(FibonacciCalculations);
  7. }
  8. private static void ThreadPoolCallbackV1(object obj)
  9. {
  10. if(obj == null) return;
  11. ManualResetEvent doneEvent = (ManualResetEvent)obj;
  12. Console.WriteLine(nameof(ThreadPoolCallbackV1));
  13. doneEvent.Set();
  14. }
  15. private static void firtstTest(int theadCount)
  16. {
  17. ThreadPool.SetMinThreads(2, 2);
  18. ThreadPool.SetMaxThreads(3, 3);
  19. ManualResetEvent[] doneEvents = new ManualResetEvent[theadCount];
  20. for(var i = 0; i < theadCount; i++)
  21. {
  22. doneEvents[i] = new ManualResetEvent(false);
  23. Console.WriteLine($"{nameof(firtstTest)} 第 {i} 次執行...");
  24. ThreadPool.QueueUserWorkItem(ThreadPoolCallbackV1, doneEvents[i]);
  25. }
  26. WaitHandle.WaitAll(doneEvents);
  27. Console.WriteLine("All calculations are complete.");
  28. }
  29. }

是的,這個是主控台的程式碼,當所有執行緒完成後,才會印出最後一個句子。

接下來的問題,就是要把這段程式碼改道WomForm的專案了...
最剛開始,我想...在主控台都測通過了,沒有問題。於是,隨便開了個空的Form表單,就把程式碼貼過去了。
結果? 當然是失敗了...

我花了至少三天才搞定它,雖然解法非常簡單...
二話不說,先上程式碼。

Program.cs

  1. internal static class Program
  2. {
  3. ///
  4. /// The main entry point for the application.
  5. ///
  6. //[STAThread] 將STAThread改為MTAThread,WaitHandle.WaitAll不支援STAThread,改為MTAThread就能夠執行...
  7. [MTAThread]
  8. static void Main()
  9. {
  10. // To customize application configuration such as set high DPI settings or default font,
  11. // see https://aka.ms/applicationconfiguration.
  12. ApplicationConfiguration.Initialize();
  13. Application.Run(new Form1());
  14. }
  15. }

Form1.cs

  1. private string _content = string.Empth;
  2. public partial class Form1 : Form
  3. {
  4. public TpcGrpcStressTest()
  5. {
  6. InitializeComponent();
  7. }
  8. private static void ThreadPoolCallbackV1(object obj)
  9. {
  10. if(obj == null) return;
  11. ManualResetEvent doneEvent = (ManualResetEvent)obj;
  12. _content += nameof(ThreadPoolCallbackV1) + Environment.NewLine;
  13. doneEvent.Set();
  14. }
  15.  
  16. private static void firtstTest(int theadCount)
  17. {
  18. ThreadPool.SetMinThreads(2, 2);
  19. ThreadPool.SetMaxThreads(3, 3);
  20. ManualResetEvent[] doneEvents = new ManualResetEvent[theadCount];
  21. for(var i = 0; i < theadCount; i++)
  22. {
  23. doneEvents[i] = new ManualResetEvent(false);
  24. _content += $"{nameof(firtstTest)} 第 {i} 次執行..." + Environment.NewLine;
  25. ThreadPool.QueueUserWorkItem(ThreadPoolCallbackV1, doneEvents[i]);
  26. }
  27. WaitHandle.WaitAll(doneEvents);
  28. }
  29.  
  30. private void SubmitBtn_Click(object sender, EventArgs e)
  31. {
  32. firtstTest(10);
  33. TextBox1.Text = _content + "All calculations are complete.";
  34. }
  35. }

答案超簡單,但困擾我許久,特別記錄下來,免得下次又發生。

參考網址: AutoResetEvent.WaitAll 等到人生三大事,然后大笑开心。


本以為到此為止,但是當我測試超過100筆執行緒時,又一個問題出現了...
System.NotSupportedException: 'The number of WaitHandles must be less than or equal to 64.'

再去查了資訊,才發現原來超過64個執行緒使用WaitHandle.WaitAll會錯誤...
實際找到解法後,說明不需要使用WaitHandle.WaitAll來檢查程序是否執行完成,那麼二話不說,修改程式碼...

Program.cs

  1. internal static class Program
  2. {
  3. ///
  4. /// The main entry point for the application.
  5. ///
  6. [STAThread]
  7. static void Main()
  8. {
  9. // To customize application configuration such as set high DPI settings or default font,
  10. // see https://aka.ms/applicationconfiguration.
  11. ApplicationConfiguration.Initialize();
  12. Application.Run(new Form1());
  13. }
  14. }

Form1.cs

  1. public partial class Form1 : Form
  2. {
  3. private string _content = string.Empth;
  4. private static int _numerOfThreadsNotYetCompleted = 0;
  5. private static ManualResetEvent _doneEvent = new ManualResetEvent(false);
  6. public TpcGrpcStressTest()
  7. {
  8. InitializeComponent();
  9. }
  10. private static void ThreadPoolCallbackV1(object obj)
  11. {
  12. if(obj == null) return;
  13. try
  14. {
  15. ManualResetEvent doneEvent = (ManualResetEvent)obj;
  16. _content += $"{nameof(ThreadPoolCallbackV1)} 第 {(int)i} 次執行...") + Environment.NewLine;
  17. doneEvent.Set();
  18. }
  19. finally
  20. {
  21. if (Interlocked.Decrement(ref _numerOfThreadsNotYetCompleted) == 0)
  22. _doneEvent.Set();
  23. }
  24. }
  25.  
  26. private static void firtstTest(int theadCount)
  27. {
  28. ThreadPool.SetMinThreads(2, 2);
  29. ThreadPool.SetMaxThreads(3, 3);
  30. for(var i = 0; i < theadCount; i++)
  31. {
  32. _content += nameof(firtstTest) + Environment.NewLine;
  33. ThreadPool.QueueUserWorkItem(ThreadPoolCallbackV1, (object)i);
  34. }
  35. _doneEvent.WaitOne();
  36. }
  37.  
  38. private void SubmitBtn_Click(object sender, EventArgs e)
  39. {
  40. _numerOfThreadsNotYetCompleted = 100;
  41. firtstTest(100);
  42. TextBox1.Text = _content + "All calculations are complete.";
  43. }
  44. }

參考網址: Solved: “The number of WaitHandles must be less than or equal to 64″

2022年12月15日 星期四

C# .Net Core6 多執行緒處理ThreadPool

現行遇到一個狀況,需要使用多執行緒連線TCP Server。
原先我是想使用Thread,不過在查詢如何使用多個執行緒設定時,發現了另外一個可用的方法ThreadPool

首先是Microsoft目前並不建議直接使用Thread控制,加上ThreadPool可以直接在for迴圈內使用,自動增加多個執行緒。對於我目前要做一個壓測用的程式而言,只要在設定檔寫好測試次數或者測試時間,用for迴圈執行ThreadPool就能得到想要的結果。實在是太方便了!

不過,在這當中我遇到了一些問題,為防止自己忘記,先記錄在此。

  1.  依照說明傳入是非固定的object,該如何設為固定的物件?
  2.  設定的執行緒的最大值,但實際程式執行時,同一時間內的執行緒超過上限...

首先是關於第一項的問題,該如何將object轉為固定的物件?
關於這一點,必須先說明ThreadPool如何使用...
下面的範例是我用來測試使用的,不過後來改過,所以不確定這個是否可以執行...

  1. static void Main(string[] args)
  2. {
  3. for(var i=0;i<100;i++)
  4. {
  5. ThreadPool.QueueUserWorkItem(new WaitCallback(countIndex), i);
  6. }
  7. }
  8.  
  9. private static void countIndex(object? obj)
  10. {
  11. string timeStr = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
  12. string msg = $"{timeStr}\t[{obj}] \t[{Thread.CurrentThread.ManagedThreadId}] \t This is Test!!";
  13. Console.WriteLine(msg);
  14. }

測試沒有問題後,我發現我要帶入的是一個完整的資料格式。
我用過不少方法,都沒有成功,最後改為下面的方式就可以執行了。
不過必須要用try catch包好,物件轉換錯誤可能會讓程式整個當掉,但因為是內部使用,就沒寫那麼多防呆了。

  1. static void Main(string[] args)
  2. {
  3. for(var i=0;i<100;i++)
  4. {
  5. DTO dto = new dto;
  6. dto.Index = i;
  7. ...
  8. ThreadPool.QueueUserWorkItem(new WaitCallback(countIndex), dto);
  9. }
  10. }
  11.  
  12. private static void countIndex(object? obj)
  13. {
  14. try
  15. {
  16. //這裡的轉換物件須注意
  17. DTO dto = (DTO)obj;
  18. ...
  19. string msg = getMessage(obj.Index, dto.Message);
  20. Console.WriteLine(msg);
  21. }
  22. catch (Exception ex)
  23. {
  24. string msg = getMessage(0, $"error = {ex.ToString()}");
  25. Console.WriteLine(msg);
  26. }
  27. }
  28.  
  29. private static string getMessage(int Index, string msg)
  30. {
  31. string timeStr = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
  32. return $"{timeStr}\t[{Index}] \t[{Thread.CurrentThread.ManagedThreadId}] \t {msg}";
  33. }

其次是關於執行緒上限的設定...
依照我上方的程式碼,執行緒ID與Index是可以做比對的,所以我發現了在同一時間,執行緒超出我設定的上限...
當時的程式碼大致如下:

  1. static void Main(string[] args)
  2. {
  3. ThreadPool.SetMaxThreads(5, 5);
  4. for(var i=0;i<100;i++)
  5. {
  6. DTO dto = new dto;
  7. dto.Index = i;
  8. ...
  9. ThreadPool.QueueUserWorkItem(new WaitCallback(countIndex), dto);
  10. }
  11. }
  12.  
  13. private static void countIndex(object? obj)
  14. {
  15. try
  16. {
  17. //這裡的轉換物件須注意
  18. DTO dto = (DTO)obj;
  19. ...
  20. string msg = getMessage(obj.Index, dto.Message);
  21. Console.WriteLine(msg);
  22. }
  23. catch (Exception ex)
  24. {
  25. string msg = getMessage(0, $"error = {ex.ToString()}");
  26. Console.WriteLine(msg);
  27. }
  28. }
  29.  
  30. private static string getMessage(int Index, string msg)
  31. {
  32. string timeStr = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
  33. return $"{timeStr}\t[{Index}] \t[{Thread.CurrentThread.ManagedThreadId}] \t {msg}";
  34. }

但是實際執行,跳出的執行緒至少超過10個...
這和我當初想像的結果完全不同,測試過許多方法都沒有用,最後才發現只設定SetMaxThreads是不行的!
將程式碼調整成下方後,測試的結果終於與我想的相同了。Thread.CurrentThread.ManagedThreadId出現的號碼只有設定的數量,同一個時間執行緒也不會超過上限...

  1. static void Main(string[] args)
  2. {
  3. //min & max 必須同時設定才有效...
  4. ThreadPool.SetMinThreads(2, 2);
  5. ThreadPool.SetMaxThreads(5, 5);
  6. for(var i=0;i<100;i++)
  7. {
  8. DTO dto = new dto;
  9. dto.Index = i;
  10. ...
  11. ThreadPool.QueueUserWorkItem(new WaitCallback(countIndex), dto);
  12. }
  13. }
  14.  
  15. private static void countIndex(object? obj)
  16. {
  17. try
  18. {
  19. //這裡的轉換物件須注意
  20. DTO dto = (DTO)obj;
  21. ...
  22. string msg = getMessage(obj.Index, dto.Message);
  23. Console.WriteLine(msg);
  24. }
  25. catch (Exception ex)
  26. {
  27. string msg = getMessage(0, $"error = {ex.ToString()}");
  28. Console.WriteLine(msg);
  29. }
  30. }
  31.  
  32. private static string getMessage(int Index, string msg)
  33. {
  34. string timeStr = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
  35. return $"{timeStr}\t[{Index}] \t[{Thread.CurrentThread.ManagedThreadId}] \t {msg}";
  36. }

參考網址:
Microsoft官網說明
kinanson的技術回憶
安德魯的部落格
余小章 @ 大內殿堂
玩轉C#之【執行序-實際實作】
C# .NET Blazor MAUI Xamarin Research

C# .net Core6 主控台使用強行別讀取 AppSetting.json

在主控台讀取設定檔有兩種格式:
  1.  .Xml
  2.  .Json
現在我要記錄的是第二種.json的格式該如何在主控台的專案使用強型別讀取。
首先,在專案的最上層新增一個appsetting.json檔案
接著,新增一個物件檔,處理設定檔的讀取。
  1. internal class ConfigHelp
  2. {
  3. private readonly IConfigurationRoot config;
  4. public ConfigHelp()
  5. {
  6. config = new ConfigurationBuilder()
  7. .SetBasePath(Directory.GetCurrentDirectory())
  8. .AddJsonFile("appsetting.json", true)
  9. .Build();
  10. }
  11. public void Get<T>(string configName, ref T resp)
  12. {
  13. config.GetSection(configName).Bind(resp);
  14. }
  15. public string? Get(string configName)
  16. {
  17. return config[configName];
  18. }
  19. }
appsetting.json檔案
  1. {
  2. "Setting": {
  3. "MinThreads": 2,
  4. "MaxThreads": 3,
  5. "TestTimes": 300
  6. }
  7. }
設定檔的DTO
  1. public class Setting
  2. {
  3. public int MinThreads { get; set; }
  4. public int MaxThreads { get; set; }
  5. public int TestTimes { get; set; }
  6. }
實際主控台程式讀取。
  1. internal class Program
  2. {
  3. private static Setting setting = new Setting();
  4. static void Main(string[] args)
  5. {
  6. ConfigHelp config = new ConfigHelp();
  7. config.Get("Setting", ref setting);
  8. }
  9. }

2022年9月15日 星期四

c# 無須遞迴取得資料夾下包含子目錄的所有檔案

之前我知道取得特定資料夾所有檔案的方式:

  1. //*.*或*代表所有檔案,如果有特定檔名或檔案格式可在此設定
  2. //ex:*.txt
  3. List< string> files = System.IO.Directory.GetFiles(dirPath, "*.*").ToList();

但之後我需要將整個資料夾內所有檔案加密,正在尋找如何取得子目錄的檔案,甚至在考慮如何寫遞迴函式,才找到原來GetFiles就可以取得。

  1. //加入System.IO.SearchOption.AllDirectories即可取得資料夾包含子目錄的檔案
  2. List< string> files = System.IO.Directory.GetFiles(dirPath, "*.*", System.IO.SearchOption.AllDirectories).ToList();

特別記錄下來,以免日後需要時忘記。

2022年9月7日 星期三

c# 讀取中文檔案亂碼

簡言之,使用File.ReadAllLines開啟內容有中文的檔案,內容產生亂碼的處理方式。

原始程式碼:

  1. List< string> csvDiffs = File.ReadAllLines(path).ToList();

更新程式碼:

  1. //Encoding.GetEncoding(950)因.net core簡化會產生錯誤,必須加入此行
  2. Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
  3. //將編碼轉為big5
  4. List< string> csvDiffs = File.ReadAllLines(path, Encoding.GetEncoding(950)).ToList();

參考網站:

http://dog0416.blogspot.com/2019/11/c-read-text-file-with-encoding.html

2022年8月5日 星期五

C# 連結Golang的Grpc Server

Grpc Client專案要如何設定,請參考之前的文章
這裡主要說明的是如何使用C# .NetCore作為Client連結Golang的Grpc Server。

Golang Grpc Server proto調整

  1. syntax = "proto3"; // 定義要使用的 protocol buffer 版本
  2.  
  3. option csharp_namespace = "GrpcTestApi";
  4.  
  5. package grpcServer; // for name space
  6. //option go_package = "./;grpcServer"; // generated code 的 full Go import path
  7.  
  8. message SumRequest {
  9. repeated int64 input = 1 [packed=true];
  10. }
  11.  
  12. message SumResponse {
  13. int64 result = 1;
  14. }
  15.  
  16. message RemainderRequest{
  17. int64 a = 1;
  18. int64 b = 2;
  19. }
  20.  
  21. message RemainderResponse {
  22. int64 result = 1;
  23. }
  24.  
  25. service grpcService {
  26. rpc Sum(SumRequest) returns (SumResponse) {};
  27. rpc Remainder(RemainderRequest) returns (RemainderResponse) {};
  28. }

連線Grpc Client Help物件

  1. public class GrpcServerConnectHelp
  2. {
  3. private string serverUrl;
  4. private grpcService.grpcServiceClient client;
  5.  
  6. public string ServerUrl { get { return serverUrl; } }
  7.  
  8. public GrpcServerConnectHelp()
  9. {
  10. Init(System.Configuration.ConfigurationManager.AppSettings.Get("GrpcServerUrl"));
  11. }
  12.  
  13. public void Init(string url = null)
  14. {
  15. if (string.IsNullOrEmpty(url))
  16. return;
  17.  
  18. serverUrl = url;
  19. //.NetCore 3.*版本必須加入此行,否則會錯誤。
  20. AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
  21. var channel = GrpcChannel.ForAddress(serverUrl);
  22. client = new grpcService.grpcServiceClient(channel);
  23. }
  24.  
  25. public string CommonApi(string method, string input)
  26. {
  27. string resp = "查無此函式";
  28.  
  29. if (method.Equals("Sum"))
  30. {
  31. return Sum(input);
  32. }
  33.  
  34. if(method.Equals("Remainder"))
  35. {
  36. return Remainder(input);
  37. }
  38.  
  39. return resp;
  40. }
  41.  
  42. private string Sum(string input)
  43. {
  44. dynamic dyn = JsonConvert.DeserializeObject(input);
  45. SumRequest request = new SumRequest();
  46.  
  47. foreach(var value in dyn.Input)
  48. {
  49. long temp = Convert.ToInt64(value);
  50. request.Input.Add(temp);
  51. }
  52. var reply = client.Sum(request);
  53. return reply.Result.ToString();
  54. }
  55.  
  56. private string Remainder(string input)
  57. {
  58. dynamic dyn = JsonConvert.DeserializeObject(input);
  59. RemainderRequest request = new RemainderRequest();
  60. request.A = dyn.A;
  61. request.B = dyn.B;
  62.  
  63. var reply = client.Remainder(request);
  64. return reply.Result.ToString();
  65. }
  66. }

AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);

重點在於.Net Core 3.*的版本再傳入URL前,必須加入上面的語法,否則會產生錯誤

C# Grpc Server&Client範例

使用Grpc的預設專案,建立一個C#的GrpcServer+Client在同一個專案下。

 所以大致有以下三個動作:

  1. 建立範例Grpc Server專案
  2. 在同一個方案內建立Grpc Client專案
  3. 將共用的.proto檔案copy到Client中
  4. 測試專案

建立Grpc Server

  • 建立Grpc服務專案
  • 設定專案名稱
  • 選擇 .Net Core 3.1版本
  • Server的方案總管畫面
  • 更名Server專案名稱
  • 更改資料夾名稱
  • 編輯方案檔
  • 改寫Server專案路徑
GrpcDemo\Grpc.Server.csproj => Grpc.Server\Grpc.Server.cspro
  • 加入第三方套件參考
Server設定完成,執行沒有問題。

建立Grpc Client

  • 在同一方案中加入新專案(Client)
  • 專案類型選擇主控台
  • 設定專案名稱
  • 同樣選擇 .Net Core 3.1版本
  • 專案建置完成的方案總管截圖
  • 將Proto檔Copy到此
  • 改寫 .proto檔案內容
Server端:改寫option內容
GrpcDemo => Grpc.Server
Server端:加入參考
Client端:改寫option內容
GrpcDemo => Grpc.Client
  • 調整Grpc.Client.csproj
  • 重建方案
  • 修改Client程式碼
  1. static async Task Main(string[] args)
  2. {
  3. await Task.Delay(3000);
  4. using var channel = GrpcChannel.ForAddress("https://localhost:5001");
  5. var client = new Greeter.GreeterClient(channel);
  6. Console.WriteLine("請輸入你的名字...");
  7. string name = Console.ReadLine();
  8. var reply = client.SayHello(new HelloRequest { Name = name });
  9. Console.WriteLine("問候語 : " + reply.Message);
  10. await channel.ShutdownAsync();
  11. Console.WriteLine("按任何一個鍵退出...");
  12. Console.ReadKey();
  13. }
  • 從方案屬性調整起始專案
  • 開始測試
測試成功!!!

2022年7月8日 星期五

goLang grpc 安裝問題proto或protoc-gen-go不是可執行的程式

最近被要求寫一個grpc base的server範例,之後可能需要用到。

基本上範例所有動作都是參考下面網址:

https://pjchender.dev/golang/grpc-getting-started/


但是針對這個網址的說明,對我而言不明確的部分,加上紀錄。以防止下一次需要用到相關功能時,發生相同的問題。

一、 'protoc' 不是內部或外部命令、可執行的程式或批次檔。

首先剛開始時我沒有找到上面的網址,所以我先執行了go get -u google.golang.org/grpc、go get github.com/golang/protobuf/protoc-gen-go。執行go get github.com/golang/protobuf/protoc-gen-go時還發生另一個錯誤,go get github.com/golang/protobuf/protoc-gen-go已經不使用了,要求我改用google.golang.org/protobuf。

然後我照了另外一篇文章的說明,先寫proto檔案,在檔案路徑下執行protoc *.proto --go_out=plugins=grpc:. --go_opt=paths=source_relative,就出現了上面的錯誤: 'protoc' 不是內部或外部命令、可執行的程式或批次檔。

這個比較簡單,因為我沒有安裝protoc,另外最上方網址的文章提供的指令並非window使用的,我參考了下面網址,先解壓縮資料夾後,放入我慣用的路徑,最後在去環境變數中設定PATH,執行protoc --version就正常可以使用了。

https://hoohoo.top/blog/mac-linux-windows-install-protocol-buffer-protobuf-compiler-way/


 二、'protoc-gen-go' 不是內部或外部命令、可執行的程式或批次檔。

這個就比較麻煩了,當protoc安裝完畢,我再次在檔案路徑下執行protoc *.proto --go_out=plugins=grpc:. --go_opt=paths=source_relative,就出現了上面的錯誤:'protoc-gen-go' 不是內部或外部命令、可執行的程式或批次檔。

我花了不少時間查詢,包含改變環境參數中的PATH、GOPATH...,都沒有成功執行指令,最終我查到了下面的網址,將GOPATH下bin的檔案copy一份到GOROOT的bin底下就成功了(汗)@@

https://icode.best/i/35480546158182

但有個地方和上方網址的說明不同,protoc-gen-go.exe檔案有,protoc-gen-go-grpc.exe檔案是沒有的,可能因為我安裝的並非github.com/golang/protobuf/protoc-gen-go版本。

不過目前為止,我的測試範本的Server功能可正常執行,之後如果有問題將再繼續補充。

2022年6月24日 星期五

C# 靜態物件在不同執行緒產生的問題

 工作中遇到了一個問題,為了加快速度,我將某個基礎函式定義為靜態物件,這個靜態物件中會送POST到某個機器上,並在裡面夾帶Cookie作為認證機制。

例如:

Public static class Test
{
private string cookie;
public void Send(string url, string content )
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.Method = "POST";
WebHeaderCollection headerCollection = request.Headers;
headerCollection.Add("Cookie", cookie);
request.ContentType = "text/xml";
using (var streamWriter = new StreamWriter(request.GetRequestStream()))
{
streamWriter.Write(content);
streamWriter.Flush();
streamWriter.Close();
}

using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
{
resp.httpStatus = response.StatusCode;
if (response.StatusCode == HttpStatusCode.OK)
{
using (StreamReader sr = new StreamReader(response.GetResponseStream()))
{
resp.xmlRespone = sr.ReadToEnd();
}

if (!string.IsNullOrEmpty(response.GetResponseHeader("Set-Cookie")))
{
cookie = response.Headers.GetValues("Set-Cookie").ToList().FirstOrDefault();
}
}
response.Close();
}
}
}


TestController:
建構式:
private Test test = new Test();
其他呼叫函式TestSend():
test .Send(<<傳送機台網址>>,"sendContent");

一般情況是測不出問題的,可是某天客戶突然說出現了401回覆。

去看了Log才發現,當頻繁的呼叫這個函式(TestSend()),傳送機台那邊回覆401(沒有權限)。也就是雖然我有設定cookie值作為驗證,但是機器不認得這個驗證值,或者認定這個驗證值不屬於我,因此我沒有權限將資料送到機台處理。

為什麼呢?我百思不得其解,甚至還在考慮是否客戶的網路結構跟公司不同,在分流IP時產生了問題,造成去回不同路?

看了N個log,我才驚愕地發現一個事實。

我的驗證cookie在不同執行緒下,被蓋掉了!!!

從來不知道原來靜態物件是這樣設定的,在Controller中呼叫靜態物件後,它會占用同樣的儲存位置,因此不同執行緒執行時,靜態物件內部的設定值,即使是私有變數也可能會被蓋掉。

果然,當我取消靜態物件的設定,即使呼叫方式沒有改變,也不再出現驗證失敗401的錯誤了。

為防自己忘記,特別寫一下。並非設定私有變數就一定沒有問題,靜態物件的使用、呼叫必須謹慎。以上就是這次的教訓了。


2022年2月21日 星期一

EF Core SELECT WHERE IN 的用法

 很不幸的,最近支援一個專案,平時直接下SQL語法的我,對所謂的EF Core沒啥研究,但是卻遇到這次的架構使用了這個技術。然後,更不幸的是,居然要我想辦法幫忙修改查詢指令。

主要的問題是,每個使用者擁有不同權限,進入後看到的資料也不同,必須查詢多張表格才能整理出來,最終我提供了Table的Id List,讓他們把舊的查詢再加上一個條件,必須是Id List內相同的Id才顯示。

當然,最終這個語法還是我查出來了,特別紀錄起來以免下次遇到。

OrderIdList = List<int>{orderId1,orderId2...};
orders = _context.Orders.Include(o => o.OrdersPlaceMapping).Where(row => OrderIdList.Contains(row.Id)).OrderBy(o => o.Id).ToList();

2022年1月20日 星期四

ASP.NET CORE3.1 使用EF Core處理資料庫問題

建立書本的範例,書本使用的是Asp.Net Core 2.2版本,但考量實際專案改使用Asp.Net Core 3.1版本,然後自然會開始遇到一連串狀況了。目前的進度只到使用EF Core自動建立資料庫與資料表,後續的新增、修改、刪除、查詢功能都還沒開始處理,先記錄目前為止的問題。

  • EF Core需要安裝:
第一個狀況就是EF Core安裝,在書中並沒有說明這一段,猜測可能Asp.Net Core 2.2包含EF Core不須安裝此套件,總之第一個遇上的問題就是該安裝EF Core哪一個版本?
之後在同事的建議下,安裝了5.04版,這邊特別說明6.01需要.NET Core 6以上版本才能安裝,我目前的版本裝不了。

EF Core安裝截圖






安裝之後我開始依照範例處理DTO、資料庫連線設定等等,然後問題出現了,在Startup.cs的ConfigureServices中加入SQL服務時Error了,原因是找不到我使用的函式,如下圖:

設定使用EF Core做資料庫連線錯誤










  • EF Core相關套件需要安裝:
後來在同事的幫忙下,了解這是因為我少裝了一個套件,EF Core.SqlServer,如下圖:

安裝EF Core的SqlServer相關套件





安裝版本基本是跟EF Core的版本,安裝完剛剛在Startup.cs的錯誤就沒問題了。
接著我按照書上的說明,準備使用Code First方式,通過Migration建立資料庫與資料表。
首先要先找到使用指令的地方,工具/NuGet事件管理員/套件管理器主控台,順道一提因為書籍是簡體版,兩邊翻譯的不同,這個位置讓我找了很久。

使用命令列位置
















接著下達命令來建置Migration參考檔案,命令如下:

Add-Migration InitalCreation
然後當然又錯誤了(實際的錯誤訊息忘了,這是我將參考拿掉產生的)。

命令錯誤資訊







查了一陣子,發現又少裝一個套件,EF Core Tools。

安裝EF Core的Tools相關套件





緊接著,執行Add Migration沒有問題成功了,得到一個Migrations的資料夾,裡面設定了資料庫與資料表的建置方式。

於是依照範例繼續準備寫入資料庫中,命令如下:

Update-Database

然後又錯誤了,原來的錯誤訊息好像是和Initial Catalog相關,因為已經無法重現原來的問題,講一下印象中大意是說不可使用Initial Catalog字元,或者使用Initial Catalog有問題之類的。

這個錯誤大概是我找最久的,我當時不再公司只能自己尋找問題,偏偏對這個不熟悉找到的資訊也大多有問題,沒有人幫忙的情況下,最終才發現錯誤訊息的問題跟我考慮和尋找的方向有落差。

原因是我資料庫連線設定錯誤,把設定值放上來。

"DefaultConnection": "Data Source=localhost\\資料庫連線;Initial Catalog=資料庫名稱;Integrated Security=SSPI"

這是我從各式各樣的網頁找的答案拼湊出來的,這個設定值並不是給我目前的狀況使用,但範例原先的設定讓我新增成功卻找不到新增的資料庫,總之結論我把正確的設定值貼上。

"DefaultConnection": "server=localhost\\資料庫連線;database=資料庫名稱;Integrated Security=SSPI"

到此為止終於新增成功,且在資料庫連線工具中找到新增的資料庫了,可喜可賀。

繼續補充之後Migration自動建立假資料的動作

在連線設定繼承DbContext的物件中複寫OnModelCreating函式

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
            base.OnModelCreating(modelBuilder);
            modelBuilder.Entity<AuthorDBDto>().HasData(List<AuthorDBDto>authors);
            modelBuilder.Entity<BookDBDto>().HasData(List<AuthorDBDto>books);
}

工具/NuGet事件管理員/套件管理器主控台

在此輸入指令 Add-Migration SeedData,在Migration資料夾中會多出SeedData的檔案,內容是List中想要新增的資料。然後在輸入指令Update-Database,將資料新增到資料庫中。如下圖:








要注意的是Add-Migration SeedData之後即便把SeedData檔案刪除,仍然已經將資料寫入某個地方,重新填寫資料時,剛剛已經寫入的資料不會寫入新的SeedData檔案。

接著測試自動刪除檔案,先輸入指令Add-Migration RemoveSeededData,在Migration資料夾中會多出RemoveSeededData的檔案,內容是剛剛新增的資料。然後在輸入指令Update-Database,將資料從資料庫中刪除。如下圖:








到此EF Core的測試完畢,真是太好了。

2022年1月6日 星期四

ASP.NET C# Fotify Cross-Site Scripting: Poor Validation(XSS)問題解決

 這次客戶使用Fortify進行掃描,多數的問題都已經解決,或者將公用函式做成.dll檔後,也都通過掃描標準。唯獨標題的問題,從開始到前日一直沒有解決,今天終於處理完畢,所以特別記錄下來。

依照我之前查詢的諸多參考網頁,多數說明解決這個問題的方法就是使用HtmlEncode()處理input,不讓程式執行含有HTML標籤的內容,也避免資料庫存入。

因此我將被掃描出來的地方,只要有被呼叫到的部分,不管input、output全部都使用HtmlEncode()處理,然而這沒有用,Fortify依舊堅持把這段程式掃出來了。

目前解決方法:
input:依舊使用HttpUtility.HtmlEncode()處理。
output:
HtmlSanitizer sanitizer = new HtmlSanitizer();
output = sanitizer.Sanitize(HttpUtility.HtmlDecode(data));

程式碼無法放上來,只能這樣說明,希望下次要是再遇到這樣的問題能夠提供參考。

後續發生一個意想不到的問題,這個第三方套件和Framework4.6.2的版本居然有參考檔會衝突。

原因是客戶不希望保留Nuget參考,希望所有第三方套件全部轉成.dll來參考,加上客戶的Framework版本是4.5升上來的,所以本機執行一直沒有發現錯誤,等到程式發佈到IIS上後居然無法執行。今天找了一整天終於知道有兩個套件參考的dll與4.6.2衝突了,需要改寫web.config設定。

將web.config修改部分放上來,真是始料未及的問題。

  <runtime>

          <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">

<dependentAssembly>

<assemblyIdentity name="AngleSharp.Css" publicKeyToken="XXXXX" culture="neutral"/>

<bindingRedirect oldVersion="0.0.0.0-0.16.3.0" newVersion="0.16.3.0"/>

</dependentAssembly>

                  <dependentAssembly>

                          <assemblyIdentity name="AngleSharp" publicKeyToken="XXXXX" culture="neutral"/>

                          <bindingRedirect oldVersion="0.0.0.0-0.16.1.0" newVersion="0.16.1.0"/>

                  </dependentAssembly>

          </assemblyBinding>

  </runtime>