ShowProgramCode

2023年6月14日 星期三

C# Net6 WebAPI或MVC架構下 Nlog+EFCore+MSSQL設定 自動記錄SQL命令

首先建立一個Net6 WebAPI或MVC專案

下面使用WebAPI專案作為示範,不過MVC專案設定大致相同,除了不需要手動增加Model資料夾...

一、設定Nlog

1.使用NuGet管理員,下載NLog.Web.AspNetCore,此次使用版本5.3.0。

2.在專案中手動增加nlog.config檔案。

記得必須選擇"永遠複製"到輸出目標。
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
autoReload="true"
internalLogLevel="Error">
<!-- 啟用 ASP.NET Core layout renderers -->
<extensions>
<add assembly="NLog.Web.AspNetCore"/>
</extensions>
<!-- 設定log根目錄 -->
<variable name="logDirectory" value="C:\TWBank_Log\NlogTest" />
<!-- log 儲存目標 -->
<targets>
<target xsi:type="File" name="allfile" fileName="${logDirectory}\nlog-all-${shortdate}.log"
layout="${date:format=HH\:mm\:ss\.ffff} [thread:${threadid}] ${level:uppercase=true} ${message} ${exception:Format=ToString}" createDirs="true" encoding="UTF-8" />
</targets>
<!-- 設定 logger 名稱與 log 儲存目標的對應 -->
<rules>
<!--將Microsoft與System.Net.Http的錯誤拿掉不紀錄-->
<logger name="Microsoft.*" maxlevel="Info" final="true" />
<logger name="System.Net.Http.*" maxlevel="Info" final="true" />
<logger name="*" minlevel="Trace" writeTo="allfile" />
</rules>
</nlog>

3.修改Program.cs

public class Program
{
	public static void Main(string[] args)
	{
		var builder = WebApplication.CreateBuilder(args);

		//將NLog註冊到此專案內
		builder.Logging.ClearProviders();
		builder.Host.UseNLog();

		...
		var app = builder.Build();
		
		...
		app.Run();
	}
}

4.調整appsettings.json

Logging.LogLevel.Default可以控制預設紀錄的Log層級,目前Trace是最低層級,也就是什麼都會記錄。
{
  "Logging": {
    "LogLevel": {
      "Default": "Trace",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

5.修改範本Controller程式碼

Nlog支援將List、物件等,轉換成json string顯示,參考下方程式碼。
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
	IEnumerable<WeatherForecast> temp = Enumerable.Range(1, 5).Select(index => new WeatherForecast
	{
		Date = DateTime.Now.AddDays(index),
		TemperatureC = Random.Shared.Next(-20, 55),
		Summary = Summaries[Random.Shared.Next(Summaries.Length)]
	})
	.ToList();
	_logger.LogDebug("WeatherForecast List = {@temp}", temp);
	return temp;
}

6.確認Log內容

二、設定EFCore,此處設定MSSQL,如果要設定不同資料庫,下載與設定方式會略有不同。

1.使用NuGet管理員,下載EFCore。

  • Microsoft.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.Sqlite

2.手動增加Model資料夾,並在內部加入MyDbContext繼承DbContext

public class MyDbContext: DbContext
{
	public MyDbContext(DbContextOptions<MyDbContext> options) : base(options)
	{
	}
	public DbSet<TestUser> User { get; set; }
}

[Table("TestUser")]
public class TestUser
{
	[Key]
	public int Id { get; set; }
	[Required]
	[StringLength(20)]
	public string Name { get; set; } = string.Empty;
	[Required]
	[StringLength(15)]
	public string Phone { get; set; } = string.Empty;
}

3.修改appsettings.json

設定遠端資料庫連線。
開發期間資料庫證書無法設定時,可以添加這個設定,TrustServerCertificate=true,將無條件信任IIS預設證書。
此外,還需要加上Logging設定中Microsoft.EntityFrameworkCore.Database.Command設定,並且在nlog.config必須一起設定。
{
  "ConnectionStrings": {
    "DefaultConnection": "server=資料庫IP;user id=資料庫帳號;password=資料庫密碼;database=資料庫名稱;TrustServerCertificate=true"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Trace",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.EntityFrameworkCore.Database.Command": "Information"
    }
  },
  "AllowedHosts": "*"
}

4.調整nlog.config檔案。

針對Microsoft.EntityFrameworkCore.Database.Command設定
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
autoReload="true"
internalLogLevel="Error">
...
<!-- 設定 logger 名稱與 log 儲存目標的對應 -->
<rules>
<!--寫入SQL-->
<logger name="Microsoft.EntityFrameworkCore.Database.Command" minlevel="Info" writeTo="allfile" />
...
</rules>
</nlog>

5.修改Program.cs

增加EFCore設定。
public class Program
{
	public static void Main(string[] args)
	{
		var builder = WebApplication.CreateBuilder(args);
		
		//註冊EFCoreContext
		//先注入EFCore再注入Nlog才會記錄SQL命令
		string connectString = builder.Configuration.GetConnectionString("DefaultConnection");
		builder.Services.AddDbContext<MyDbContext>(options => options.UseSqlServer(connectString));

		//將NLog註冊到此專案內
		builder.Logging.ClearProviders();
		builder.Host.UseNLog();

		...
		var app = builder.Build();
		
		...
		app.Run();
	}
}

6.修改範本Controller程式碼

private readonly ILogger<WeatherForecastController> _logger;
private readonly MyDbContext _dbContext;

public WeatherForecastController(MyDbContext dbContext,ILogger<WeatherForecastController> logger)
{
	_dbContext = dbContext;
	_logger = logger;
}

public IEnumerable<WeatherForecast> Get()
{
IEnumerable<WeatherForecast> temp = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToList();

IEnumerable<TestUser> temp2 = _dbContext.User.ToList();

_logger.LogDebug("WeatherForecast List = {@temp}", temp);
_logger.LogDebug("TestUser List = {@temp}", temp2);
return temp;
}

7.確認Log內容與資料庫比對

Log內容:
資料庫內容:

由此可以確認資料庫中的內容與Log TestUser是一致的。
此外,如果Log內容不正確,可以將專案清除再重建,可能是什麼被cache造成的。

2022年12月30日 星期五

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

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

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

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

internal class Program
{
    static void Main(string[] args)
    {
        const int FibonacciCalculations = 10;
        firtstTest(FibonacciCalculations);
    }
    
    private static void ThreadPoolCallbackV1(object obj)
    {
        if(obj == null) return;
        ManualResetEvent doneEvent = (ManualResetEvent)obj;
        Console.WriteLine(nameof(ThreadPoolCallbackV1));
        doneEvent.Set();
    }
    
    private static void firtstTest(int theadCount)
    {
        ThreadPool.SetMinThreads(2, 2);
        ThreadPool.SetMaxThreads(3, 3);
        ManualResetEvent[] doneEvents = new ManualResetEvent[theadCount];
        for(var i = 0; i < theadCount; i++)
        {
            doneEvents[i] = new ManualResetEvent(false);
            Console.WriteLine($"{nameof(firtstTest)} 第 {i} 次執行...");
            ThreadPool.QueueUserWorkItem(ThreadPoolCallbackV1, doneEvents[i]);
        }
        
        WaitHandle.WaitAll(doneEvents);
        Console.WriteLine("All calculations are complete.");
    }
}

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

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

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

Program.cs

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

Form1.cs

private string _content = string.Empth;
public partial class Form1 : Form
{
    public TpcGrpcStressTest()
    {
        InitializeComponent();
    }
    
    private static void ThreadPoolCallbackV1(object obj)
    {
    	if(obj == null) return;
    	ManualResetEvent doneEvent = (ManualResetEvent)obj;
    	_content += nameof(ThreadPoolCallbackV1) + Environment.NewLine;
    	doneEvent.Set();
    }

    private static void firtstTest(int theadCount)
    {
    	ThreadPool.SetMinThreads(2, 2);
    	ThreadPool.SetMaxThreads(3, 3);
    	ManualResetEvent[] doneEvents = new ManualResetEvent[theadCount];
    	for(var i = 0; i < theadCount; i++)
    	{
    		doneEvents[i] = new ManualResetEvent(false);
    		_content += $"{nameof(firtstTest)} 第 {i} 次執行..." + Environment.NewLine;
    		ThreadPool.QueueUserWorkItem(ThreadPoolCallbackV1, doneEvents[i]);
    	}
        
        WaitHandle.WaitAll(doneEvents);
    }

    private void SubmitBtn_Click(object sender, EventArgs e)
    {
        firtstTest(10);
        TextBox1.Text = _content + "All calculations are complete.";
    }
}

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

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


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

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

Program.cs

internal static class Program
{
    /// 
    ///  The main entry point for the application.
    /// 
    [STAThread]
    static void Main()
    {
        // To customize application configuration such as set high DPI settings or default font,
        // see https://aka.ms/applicationconfiguration.
        ApplicationConfiguration.Initialize();
        Application.Run(new Form1());
    }
}

Form1.cs

public partial class Form1 : Form
{
    private string _content = string.Empth;
    private static int _numerOfThreadsNotYetCompleted = 0;
    private static ManualResetEvent _doneEvent = new ManualResetEvent(false);
    
    public TpcGrpcStressTest()
    {
        InitializeComponent();
    }
    
    private static void ThreadPoolCallbackV1(object obj)
    {
    	if(obj == null) return;
        try
        {
            ManualResetEvent doneEvent = (ManualResetEvent)obj;
            _content += $"{nameof(ThreadPoolCallbackV1)} 第 {(int)i} 次執行...") + Environment.NewLine;
            doneEvent.Set();
        }
        finally
        {
            if (Interlocked.Decrement(ref _numerOfThreadsNotYetCompleted) == 0)
                _doneEvent.Set();
        }
    }

    private static void firtstTest(int theadCount)
    {
    	ThreadPool.SetMinThreads(2, 2);
    	ThreadPool.SetMaxThreads(3, 3);
    	for(var i = 0; i < theadCount; i++)
    	{
    		_content += nameof(firtstTest) + Environment.NewLine;
    		ThreadPool.QueueUserWorkItem(ThreadPoolCallbackV1, (object)i);
    	}
        _doneEvent.WaitOne();
    }

    private void SubmitBtn_Click(object sender, EventArgs e)
    {
        _numerOfThreadsNotYetCompleted = 100;
        firtstTest(100);
        TextBox1.Text = _content + "All calculations are complete.";
    }
}

參考網址: 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如何使用...
下面的範例是我用來測試使用的,不過後來改過,所以不確定這個是否可以執行...

static void Main(string[] args)
{
    for(var i=0;i<100;i++)
    {
        ThreadPool.QueueUserWorkItem(new WaitCallback(countIndex), i);
    }
}

private static void countIndex(object? obj)
{
    string timeStr = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
    string msg = $"{timeStr}\t[{obj}] \t[{Thread.CurrentThread.ManagedThreadId}] \t This is Test!!";
    Console.WriteLine(msg);
}

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

static void Main(string[] args)
{
    for(var i=0;i<100;i++)
    {
        DTO dto = new dto;
        dto.Index = i;
        ...
        ThreadPool.QueueUserWorkItem(new WaitCallback(countIndex), dto);
    }
}

private static void countIndex(object? obj)
{
    try
    {
        //這裡的轉換物件須注意
        DTO dto = (DTO)obj;
        ...
    	string msg = getMessage(obj.Index, dto.Message);
    	Console.WriteLine(msg);
    }
    catch (Exception ex)
    {
        string msg = getMessage(0, $"error = {ex.ToString()}");
        Console.WriteLine(msg);
    }
}

private static string getMessage(int Index, string msg)
{
    string timeStr = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
    return $"{timeStr}\t[{Index}] \t[{Thread.CurrentThread.ManagedThreadId}] \t {msg}";
}

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

static void Main(string[] args)
{
    ThreadPool.SetMaxThreads(5, 5);
    for(var i=0;i<100;i++)
    {
        DTO dto = new dto;
        dto.Index = i;
        ...
        ThreadPool.QueueUserWorkItem(new WaitCallback(countIndex), dto);
    }
}

private static void countIndex(object? obj)
{
    try
    {
        //這裡的轉換物件須注意
        DTO dto = (DTO)obj;
        ...
    	string msg = getMessage(obj.Index, dto.Message);
    	Console.WriteLine(msg);
    }
    catch (Exception ex)
    {
        string msg = getMessage(0, $"error = {ex.ToString()}");
        Console.WriteLine(msg);
    }
}

private static string getMessage(int Index, string msg)
{
    string timeStr = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
    return $"{timeStr}\t[{Index}] \t[{Thread.CurrentThread.ManagedThreadId}] \t {msg}";
}

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

static void Main(string[] args)
{
    //min & max 必須同時設定才有效...
    ThreadPool.SetMinThreads(2, 2);
    ThreadPool.SetMaxThreads(5, 5);
    for(var i=0;i<100;i++)
    {
        DTO dto = new dto;
        dto.Index = i;
        ...
        ThreadPool.QueueUserWorkItem(new WaitCallback(countIndex), dto);
    }
}

private static void countIndex(object? obj)
{
    try
    {
        //這裡的轉換物件須注意
        DTO dto = (DTO)obj;
        ...
    	string msg = getMessage(obj.Index, dto.Message);
    	Console.WriteLine(msg);
    }
    catch (Exception ex)
    {
        string msg = getMessage(0, $"error = {ex.ToString()}");
        Console.WriteLine(msg);
    }
}

private static string getMessage(int Index, string msg)
{
    string timeStr = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
    return $"{timeStr}\t[{Index}] \t[{Thread.CurrentThread.ManagedThreadId}] \t {msg}";
}

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

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

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

2022年9月15日 星期四

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

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

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

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

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

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

2022年9月7日 星期三

c# 讀取中文檔案亂碼

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

原始程式碼:

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

更新程式碼:

//Encoding.GetEncoding(950)因.net core簡化會產生錯誤,必須加入此行
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
//將編碼轉為big5
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調整

syntax = "proto3";  // 定義要使用的 protocol buffer 版本

option csharp_namespace = "GrpcTestApi";

package grpcServer;  // for name space
//option go_package = "./;grpcServer";  // generated code 的 full Go import path

message SumRequest {
  repeated int64 input = 1 [packed=true];
}

message SumResponse {
  int64 result = 1;
}

message RemainderRequest{
  int64 a = 1;
  int64 b = 2;
}

message RemainderResponse {
  int64 result = 1;
}

service grpcService {
  rpc Sum(SumRequest) returns (SumResponse) {};
  rpc Remainder(RemainderRequest) returns (RemainderResponse) {};
}

連線Grpc Client Help物件

public class GrpcServerConnectHelp
{
    private string serverUrl;
    private grpcService.grpcServiceClient client;

    public string ServerUrl { get { return serverUrl; } }

    public GrpcServerConnectHelp()
    {
        Init(System.Configuration.ConfigurationManager.AppSettings.Get("GrpcServerUrl"));
    }

    public void Init(string url = null)
    {
        if (string.IsNullOrEmpty(url))
            return;

        serverUrl = url;
        //.NetCore 3.*版本必須加入此行,否則會錯誤。
        AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
        var channel = GrpcChannel.ForAddress(serverUrl);
        client = new grpcService.grpcServiceClient(channel);
    }

    public string CommonApi(string method, string input)
    {
        string resp = "查無此函式";

        if (method.Equals("Sum"))
        {
            return Sum(input);
        }

        if(method.Equals("Remainder"))
        {
            return Remainder(input);
        }

        return resp;
    }

    private string Sum(string input)
    {
        dynamic dyn = JsonConvert.DeserializeObject(input);
        SumRequest request = new SumRequest();

        foreach(var value in dyn.Input)
        {
            long temp = Convert.ToInt64(value);
            request.Input.Add(temp);
        }
        var reply = client.Sum(request);
        return reply.Result.ToString();
    }

    private string Remainder(string input)
    {
        dynamic dyn = JsonConvert.DeserializeObject(input);
        RemainderRequest request = new RemainderRequest();
        request.A = dyn.A;
        request.B = dyn.B;

        var reply = client.Remainder(request);
        return reply.Result.ToString();
    }
}

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

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