星期四, 12月 28, 2023

[.NET] 在 BackgroundService 內使用 Quartz

在 BackgroundService 內使用 Quartz 3.8 來處理排程需求,Quartz 相關請參考之前筆記

安裝 Quartz 套件

必須安裝兩個套件

建立 Quartz Job

把 Log DI 注入後,使用 LogWarming 在畫面上輸出文字
using Quartz;

namespace QuartzWorkerSample
{
    [DisallowConcurrentExecution]
    public class HelloWorldJob : IJob
    {
        private readonly ILogger<HelloWorldJob> _logger;
        public HelloWorldJob(ILogger<HelloWorldJob> logger)
        {
            _logger = logger;
        }

        public Task Execute(IJobExecutionContext context)
        {
            _logger.LogWarning($"Hello World {DateTime.Now} and Jobtype: {context.JobDetail.JobType}");
            return Task.CompletedTask;
        }
    }
}  

在 Program 中使用 Quartz 

會有兩個文字輸出,分別為
  • QuartzHostedService:每 5 秒輸出
  • BackgroundService:即為 .NET 8 預設 Worker,每 1 秒輸出
QuartzHostedService 有實作 IHostService,可以變成背景服務來使用
C# 語法
using Quartz;

namespace QuartzWorkerSample
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = Host.CreateApplicationBuilder(args);

            builder.Services.AddQuartz(config =>
            {
                config.AddJob<HelloWorldJob>(config => config
                    .WithIdentity(nameof(HelloWorldJob)));

                config.AddTrigger(config => config
                    .ForJob(nameof(HelloWorldJob))
                    .WithIdentity("HelloWorldJob-Trigger")
                    .WithSimpleSchedule(x => x
                        .WithIntervalInSeconds(5) // 每五秒觸發一次
                        .RepeatForever()));
            });

            // QuartzHostedService 實作 IHostService
            builder.Services.AddQuartzHostedService(options => options
                .WaitForJobsToComplete = true);

            // 該 Worker 為 .NET 8 預設範本
            builder.Services.AddHostedService<Worker>();

            var host = builder.Build();
            host.Run();
        }
    }
}
執行結果
網路教學常見語法
builder.Services.AddQuartz(config =>
{
    config.UseMicrosoftDependencyInjectionScopedJobFactory();
    config.UseMicrosoftDependencyInjectionJobFactory();
});
UseMicrosoftDependencyInjectionScopedJobFactory 在 V3.3.2 開始 Job Factory 為 Scoped Jobs,不在需要使用它,詳見官方文章 - Microsoft DI Integration
UseMicrosoftDependencyInjectionJobFactory 在 V3.7 時 MS DI 變成預設 DI,不需要去設定,詳見 V3.7 release討論

看到這兩個語法就把它 pass 掉吧

星期三, 12月 27, 2023

[VFP] 報表預覽在 Form 後方

使用者回報,為什麼報表列印預覽,只有列印控制跳出來而已,沒有看見報表預覽,然後程式就像當機一樣都無法操作,只能強制關閉後重開系統,去操作發現報表預覽竟然在 Form 後方,如下圖


確認 Form 屬性 AlwaysOnTop 有被開啟,關閉後就恢復正常報表預覽效果

星期一, 12月 25, 2023

[SQL] ANSI_WARNINGS

收到某隻排程商業邏輯維護執行失敗通知,錯誤訊息如下
異質性查詢需要為連線設定 ANSI_NULLS 和 ANSI_WARNINGS 選項。這樣才能確保一致的查詢語意。請啟用這些選項再重新發出您的查詢。 [SQLSTATE 42000] (錯誤 7405). 步驟失敗。

公司維護 sp 都是放在 master 底下,script 會指定 dbname,但沒有使用到 Linked Server 才是,且是某天突然收到執行失敗,打開 sp 來觀察,發現有明確關閉 ANSI_WARNINGS,該設定效果如下
  • When set to ON, if null values appear in aggregate functions, such as SUM, AVG, MAX, MIN, STDEV, STDEVP, VAR, VARP, or COUNT, a warning message is generated. When set to OFF, no warning is issued.
  • When set to ON, the divide-by-zero and arithmetic overflow errors cause the statement to be rolled back and an error message is generated. When set to OFF, the divide-by-zero and arithmetic overflow errors cause null values to be returned. The behavior in which a divide-by-zero or arithmetic overflow error causes null values to be returned occurs
  • if an INSERT or UPDATE is tried on a character, Unicode, or binary column in which the length of a new value exceeds the maximum size of the column. If SET ANSI_WARNINGS is ON, the INSERT or UPDATE is canceled as specified by the ISO standard. Trailing blanks are ignored for character columns and trailing nulls are ignored for binary columns. When OFF, data is truncated to the size of the column and the statement succeeds.
該 sp 是彙總統計整理相關,可能是為了避免彙總時有 null 出現發出警告才把 ANSI_WARNINGS 關閉。

仔細閱讀後發現到真正原因在於一個 view,它最近變成 Linked Server 必須跨 Server 去抓資料,所以才導致該錯誤訊息發生,現況去檢查也看不出會彙總時會有 null 發生,把 ANSI_WARNINGS ON 起來結案

星期三, 12月 20, 2023

SATA 線

維修 PC 時看電源偵測器說明書時發現,才發現 SATA 線有 4 條和 5 條的差異

說明書上的文字說明
有個別客戶反應:插上 SATA 介面 測試器的 +3.3V 燈不亮,因為現在市場上的電源 SATA 介面有 4 根線和 5 根線的哦!首先要確認您的電源 SATA 介面是 4 根線還是 5 根線,如果是 4 根線 那是沒有 +3.3V 輸出的,要 5 根線的才有 +3.3V 輸出的。

SATA 4 條線
SATA 5 條線
看說明書長知識

星期二, 12月 19, 2023

[.NET] 在 BackgroundService 內使用 NLog

在 BackgroundService 內使用 NLog 5.2.7 來處理 Log,主要進行三種輸出,分別為 txt 檔案、DB 和 Console,NLog 相關請參考之前筆記
NLog 5.2.7 在筆記時官方說明還沒有 .NET 8 在上面,但可以進行安裝,DB 連線部分則是安裝 Microsoft.Data.SqlClinet 來使用,而非 System.Data.SqlClient,下圖為安裝套件



Programes
using NLog.Config;
using NLog.Extensions.Logging;
using NLog.Targets;

namespace NLogWorkerSample
{
    public class Program
    {
        public static void Main(string[] args)
        {
            HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
            builder.Services.AddHostedService<Worker>();

            builder.Logging.ClearProviders();
            builder.Logging.AddNLog(NLogConfigurationInit());

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

        private static LoggingConfiguration NLogConfigurationInit()
        {
            LoggingConfiguration config = new LoggingConfiguration();

            // Target 1:把錯誤訊息輸出至 txt 檔案 
            FileTarget errorFile = new FileTarget("errorfile") { FileName = @"D:\ErrorFile.txt" };
            config.AddRule(NLog.LogLevel.Trace, NLog.LogLevel.Fatal, errorFile);

            // Target 2:把錯誤訊息 insert 至 DB 
            DatabaseTarget target = new DatabaseTarget();
            target.ConnectionString = "Data Source=.;Initial Catalog=AdventureWorks2022;Integrated Security=True;TrustServerCertificate=True";
            target.CommandText = "insert into LogTable(LogTime,Message) values(@LogTime, @Message);";
            target.Parameters.Add(new DatabaseParameterInfo() { Name = "@LogTime", Layout = "${date}" });
            config.AddRule(NLog.LogLevel.Trace, NLog.LogLevel.Fatal, target);

            // Target 3:把錯誤訊息輸出至 Console
            ColoredConsoleTarget coloredConsole = new ColoredConsoleTarget("ColoredConsole");
            config.AddRule(NLog.LogLevel.Trace, NLog.LogLevel.Fatal, coloredConsole);

            return config;
        }
    }
}

.NET 8 預設 Worker 範本
namespace NLogWorkerSample
{
    public class Worker : BackgroundService
    {
        private readonly ILogger<Worker> _logger;

        public Worker(ILogger<Worker> logger)
        {
            _logger = logger;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                if (_logger.IsEnabled(LogLevel.Information))
                {
                    _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
                }
                await Task.Delay(1000, stoppingToken);
            }
        }
    }
}

執行結果

寫進 txt 檔案


  DB insert
 


Console 輸出

星期二, 12月 12, 2023

[EF] 此憑證鏈結是由不受信任的授權單位發出的

操作 edmx 時連線 local SQL Server 時突然出現這個錯誤訊息


了解該錯誤訊息相關資訊後發現,原來 Microsoft.Data.SqlClient 4.0 開始會強制加密,官方文件 - Introduction to Microsoft.Data.SqlClient namespace 
進 VS 伺服器總管內觀察連線,發現 [加密選項] 已經被設定為 [Mandatory (True)],把該選項設定為 [Optional (False)] 或是勾選下方 [信任伺服器憑證] 都可以正常連線


詳細查該連線使用的 provider 發現沒有使用到  Microsoft.Data.SqlClient,看起來就是加密被啟用,不知道為什麼突然被啟用就是


星期六, 12月 09, 2023

[.NET] 在 BackgroundService 內讀取 Windows 事件

管理 Windows 事件檢視器 內有紀錄使用 .NET Framework Windows Service 來進行管理,該筆記要改為 .NET BackgroundService 來達到,原以為 [C#] EventLog 在 .NET 上會有所變化必須重新學習,但發現是無縫接軌
操作時一直會有綠蚯蚓的提示訊息,說明只能用 Windows 系統上而已
改寫 Worker

抓取各 EventLog 的最新錯誤訊息來顯示,該 ExecuteAsync 執行後,只會 run 一次而已
using System.Diagnostics;

namespace EventWorkerSample
{
    public class Worker : BackgroundService
    {
        private readonly ILogger<Worker> _logger;

        public Worker(ILogger<Worker> logger)
        {
            _logger = logger;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            foreach (EventLog el in EventLog.GetEventLogs())
            {
                EventLogEntry ele = el.Entries
                    .OfType<EventLogEntry>()
                    .Where(entry => entry.EntryType == EventLogEntryType.Error)
                    .OrderByDescending(o => o.TimeWritten)
                    .Take(1)
                    .SingleOrDefault();

                if (ele == null)
                    continue;

                string errorMessage = $@"
                    Log 名稱:{el.Log} - {el.LogDisplayName}
                    錯誤訊息:{ele.Message.Substring(0, 50)}
                    ";

                _logger.LogError(errorMessage);
            }

            await Task.CompletedTask;
        }
    }
}

星期五, 12月 08, 2023

[.NET] 在 BackgroundService 內使用 EFCore

延續 [.NET] 使用 BackgroundService 建立 Windows Service 筆記,該文章內的 JokeService 隨機抓取資料顯示行為改用 EFCore 讀取資料庫來取代,EF Core 安裝和連線字串調整請參考該筆記 - [EFCore] 在 Console 專案上安裝設定

因為 AddHostedService 和 DbContext 生命週期因素,一併把 .NET DI 知識補齊,參考文章如下
理解 DI 生命週期後就知道為什麼要透過 IServiceProvider 來存取 DbContext 囉

JobService

透過 IServiceProvider 來抓取 DbContext,而非直接把 DbContext DI 進 JokeService 內
using EFCoreWorkerSample.Models;
using Microsoft.EntityFrameworkCore;

namespace EFCoreWorkerSample
{
    public class JokeService
    {
        private readonly IServiceProvider _serviceProvider;

        public JokeService(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }

        public string GetJoke()
        {
            using var scope = _serviceProvider.CreateScope();
            var dbContext = scope.ServiceProvider.GetRequiredService<EfcoreDbContext>();

            // 從 DB 隨機抓取一個 Joke 出來顯示
            string tsql = @"
                    SELECT TOP 1 *
                    FROM Jokes
                    ORDER BY NEWID()
                    ";

            var joke = dbContext.Jokes.FromSqlRaw(tsql).Single();

            return $@"
                輸出時間:{DateTime.Now}
                問題:{joke.Q}
                答案:{joke.A}";
        }

    }
}

改寫 Worker 內容

namespace EFCoreWorkerSample
{
    public class Worker : BackgroundService
    {
        private readonly ILogger<Worker> _logger;
        private readonly JokeService _jokeService;

        public Worker(ILogger<Worker> logger, JokeService jokeService)
        {
            _logger = logger;
            _jokeService = jokeService;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                string message = _jokeService.GetJoke();

                _logger.LogInformation(message);

                await Task.Delay(1000, stoppingToken);
            }
        }
    }
}

Program.cs 

Service 擴充方法預設生命週期
  • AddHostedService:singletion
  • AddDbContext:scoped
using EFCoreWorkerSample.Models;

namespace EFCoreWorkerSample
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = Host.CreateApplicationBuilder(args);

            // 使用 AddHostedService 來註冊實作 IHostedService 類別,其生命週期為 Singleton
            builder.Services.AddHostedService<Worker>();

            builder.Services.AddScoped<EfcoreDbContext>();

            builder.Services.AddSingleton<JokeService>();

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

執行結果

DI 生命週期驗證

因為 AddHostedService 註冊 Worker 生命週期為 singletion,所以 JokeService DI 進去時也只能使用 singletion,換成其他生命週期 (scoped 或 transient),就會拋出 exception 啦
Unhandled exception. System.AggregateException: Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: Microsoft.Extensions.Hosting.IHostedService Lifetime: Singleton ImplementationType: EFCoreWorkerSample.Worker': Cannot consume scoped service 'EFCoreWorkerSample.JokeService' from singleton 'Microsoft.Extensions.Hosting.IHostedService'.)

星期四, 12月 07, 2023

[EFCore] 在 Console 專案上安裝設定

在 .NET 8 Console 專案上安裝 EFCore 並調整資料庫連線,該筆記以 DBFirst 為主

EFCore 套件安裝

使用 EFCore 相關 nuget 套件,分別為
  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.Tool
安裝 Microsoft.EntityFrameworkCore.Tool 才能使用 Scaffold-DbContext 
套件管理器主控台內進行安裝
Install-Package Microsoft.EntityFrameworkCore.SqlServer
Install-Package Microsoft.EntityFrameworkCore.Tools


使用 Scaffold-DbContext 來建立 entity

在 [套件管理器主控台] 內輸入下列語法
Scaffold-DbContext "Server=.;Database=AdventureWorks2022;Trusted_Connection=True;TrustServerCertificate=true" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models -Force
參數說明
  • -OutputDir:產生的 Model 要放在哪一個資料夾內
  • -Force :強制覆蓋
Scaffold-DbContext  建立 entity 後,可以在 DbContext OnConfiguring() 內發現連線字串警告
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see https://go.microsoft.com/fwlink/?LinkId=723263.
=> optionsBuilder.UseSqlServer("Server=.;Database=AdventureWorks2022;Trusted_Connection=True;TrustServerCertificate=true");

使用 appsetings.json 來儲存資料庫連線


原本是想找 [JSON 檔案] 範本來新增 appsettings.json 檔案,但是發現在 Console Project 內不會有它,最後是選擇 [JavaScript JSON 組態檔] 來新增並進行修改,參考資料 - can't add appsettings.json

把 appsettings.json 檔案屬性 [複製到輸出目錄] 修改為 [永遠複製]
把連線字串從 OnConfiguring 內移至 appsettings.json 內
{
  "ConnectionStrings": {
    "SqlServer": "Server=.;Database=AdventureWorks2022;Trusted_Connection=True;TrustServerCertificate=true"
  }
}

主程式

在 Console Project 內安裝
  • Microsoft.Extensions.Configuration
  • Microsoft.Extensions.Configuration.Json
下述程式會從 appsettings.json 內抓出連線字串,並把連線字串塞進 DbContext 內
using CRUDSimple.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;

namespace CRUDSimple
{
    internal class Program
    {
        static void Main(string[] args)
        {

            string connectionString = GetConnectionString();

            var options = new DbContextOptionsBuilder<AdventureWorks2022Context>()
                .UseSqlServer(connectionString)
                .Options;

            using (AdventureWorks2022Context dbContext = new AdventureWorks2022Context(options))
            {
                var source = dbContext.Employees
                    .OrderBy(e => e.HireDate)
                    .Take(10)
                    .ToList();

                foreach (Employee e in source)
                    Console.WriteLine(e.JobTitle);
            }
        }

        private static string GetConnectionString()
        {
            IConfiguration configuration = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
                .Build();

            // 從 appsettings 取得連線字串
            // 寫法一
            // string connectionString = configuration.GetSection("ConnectionString").GetSection("SqlServer").Value;
            // 寫法二
            string connectionString = configuration.GetConnectionString("SqlServer");

            return connectionString;
        }
    }
} 

星期三, 12月 06, 2023

GodexEZ 1300 Plus 本機分享

在使用者 PC 上有台用 USB 連接的標籤機 - GodexEZ 1300 Plus,因為機器本身沒有網路功能,一直被定位在單機使用,但現況是需要分享出來給其他 PC 來進行列印

在本機上設定標籤機分享後,連進來 PC 上安裝標籤機時出現下列該錯誤訊息
完全不知所云的錯誤訊息,反正就是沒有成功連線,把本機上的 Godex Driver 重新安裝至最新版本,其他 PC 就可以正常連線進分享標籤機了

星期五, 12月 01, 2023

[.NET] 使用 BackgroundService 建立 Windows Service

根據官方文章 - Create Windows Service using BackgroundService 的筆記,該內容為使用 BackgroundService 建立 Windows Service,並從 JobService 內隨機取出資料塞進 Windows 事件內

建立背景工作服務 (Worker Service Template) 專案

輸入關鍵字 Worker 就可以找到 [背景工作服務範本]
輸入專案名稱並決定檔案位置
架構為 [.NET 7.0] 和 [不要使用最上層陳述式] 為預設值
nuget 上安裝 Microsoft.Extensions.Hosting.WindowsServices