星期一, 11月 25, 2024

[C#] DataGridView 和 DoubleBuffered - 避免閃爍

延續 [C#] DataGridView 和 DoubleBuffered 筆記,從官方文章 - Control.DoubleBuffered 屬性 可以理解開啟 DoubleBuffered 可以減少或防止重繪閃動,驗證方式是 DataGridView.CellFormatting 內針對 Cell 進行顏色變化,移動捲軸來觀察顏色變化

有開 DoubleBuffered

沒有開 DoubleBuffered,每次卷軸往下就可以看出畫面閃爍
不知道是文件太舊還是對於內容有所誤會,開啟 DoubleBuffered 效果還蠻顯著

星期六, 11月 23, 2024

[C#] DataGridView 和 DoubleBuffered

在官方文章 - DataGridView.ColumnHeadersHeightSizeMode Property 發現這段文字說明
The DataGridView control does not support double buffering. If DoubleBuffered is set to true in a derived DataGridView control, users will not receive visual feedback when resizing rows, columns, or headers or when reordering columns.
所在環境也是有自訂 DataGridView 並開啟 DoubleBuffered 來使用,一直以來沒有出現大問題,測試理解何謂 visual feedback 效果

自訂 DataGridView 並開啟 DoubleBuffered
using System.Windows.Forms;

namespace AvoidDoubleBuffer
{
    public class UCDataGridView : DataGridView
    {
        public UCDataGridView()
        {
            DoubleBuffered = true;
        }
    }
}

調整欄位寬度

DoubleBuffered = true
未指定 DoubleBuffered
調整欄位順序

DoubleBuffered = true
DoubleBuffered = true

星期三, 11月 13, 2024

[Shopify] 新版顧客帳號頁面網域設定

開一個新商城時注意到,在結帳設定有下面訊息提醒



點擊 [變更網域] 過去後被導向 [客戶帳號] => [新版客戶帳號]

點擊 [變更網域] 按鈕就會進入顧客帳號網域設定,預設是 account,可自行修改

決定好子網域後,按下 [繼續] 按鈕,會出現 Shopify DNS CName 設定,看網域是託管在哪,把該 CName 設定進去就行,DNS 設定生效後按下 [驗證] 按鈕,就等兩邊驗證完成

在 Hinet 上輸入 CName 後大概 2 hr 後設定生效,Shopify 驗證大約是 1 hr 完成

驗證完成後就可以在 [網域] 內看到顧客帳號網域設定
最後也是最神奇的地方在於根本就沒有使用新版客戶帳號功能,但是會提醒要進行網域設定

星期二, 11月 12, 2024

[SSRS] 首頁頂端空白

在 SSRS 報表上常常會發生第一頁控件上方會出現空白,但第二頁又沒有的情況,專屬於第一頁的空白,後來是設定頁首來控制該情況

Tablix 控件離報表上方有段距離

實際執行,第一頁離報表上方有段空白,但第二頁卻又貼合報表上方

把頁首加入並把把背景設定為灰色,Tablix 則是完全貼合頁首下緣
實際執行第一頁空白就不會出現且第一、第二頁就會一致

目前都是是把頁首當成 SSRS 報表一定要存在設計,也一併處理該情況
  • 參考資料
  • 論壇討論 12

星期三, 11月 06, 2024

[SSMS] 索引標籤文字

發現 SSMS 索引標籤文字是可以設定,下圖為預設顯示,包含
  • Login 名稱
  • Server 名稱
  • Database 名稱
  • File 名稱

選項 => 文字編輯器 => 編輯器索引標籤和狀態列 => 索引標籤文字內可以進行更改
下圖為保留 Database 名稱和 File 名稱效果


星期一, 11月 04, 2024

[EFCore] SqlQuery、SqlQueryRaw - 欄位對應

延續 [EFCore] SqlQuery、SqlQueryRaw 筆記,在 Raw SQL queries for unmapped types 內還有提到 Data Annotations - ColumnAttribute 應用,之前把 Product Name 欄位在 PurchaseOrderDetailViewModel class 內轉為 ProductName,造成寫 TSQL 時,一定會需要 P.[Name]  AS ProductName 這段轉換,要不然會拋出錯誤。

TSQL Name 欄位未轉換為 ProductName

CASE 1:沒有任何 where 條件
string TSQL10 = @"
    SELECT 
        P.ProductID ,
        P.[Name] ,
        POD.OrderQty ,
        POD.UnitPrice ,
        POD.LineTotal 
    FROM [Purchasing].[PurchaseOrderHeader] AS POH
        JOIN [Purchasing].[PurchaseOrderDetail] AS POD ON POH.PurchaseOrderID = POD.PurchaseOrderID
        JOIN Production.[Product] AS P ON POD.ProductID = P.ProductID
    ";

var result10 = dbContext.Database
    .SqlQueryRaw<PurchaseOrderDetailViewModel>(TSQL10);

var result = await result10.ToListAsync();
Console.WriteLine(result.Count().ToString());
// TSQL 語法正常輸出
//    SELECT
//        P.ProductID ,
//        P.[Name] , 
//        POD.OrderQty ,
//        POD.UnitPrice ,
//        POD.LineTotal
//    FROM[Purchasing].[PurchaseOrderHeader] AS POH
//        JOIN[Purchasing].[PurchaseOrderDetail] AS POD ON POH.PurchaseOrderID = POD.PurchaseOrderID
//        JOIN Production.[Product] AS P ON POD.ProductID = P.ProductID
執行後會拋出該錯誤,原來 SqlQueryRaw 底層是 FromSql 去執行的,所以每個 Property 都要有值才行,在 TSQL pass 但卡在 EFCore 上。
Unhandled exception. System.InvalidOperationException: The required column 'ProductName' was not present in the results of a 'FromSql' operation.

CASE2:TSQL WHERE 沒有條件,但 LINQ WHERE 內有
string TSQL11 = @"
    SELECT 
        P.ProductID ,
        P.[Name] , 
        POD.OrderQty ,
        POD.UnitPrice ,
        POD.LineTotal 
    FROM [Purchasing].[PurchaseOrderHeader] AS POH
        JOIN [Purchasing].[PurchaseOrderDetail] AS POD ON POH.PurchaseOrderID = POD.PurchaseOrderID
        JOIN Production.[Product] AS P ON POD.ProductID = P.ProductID
    ";

var result11 = dbContext.Database
    .SqlQueryRaw<PurchaseOrderDetailViewModel>(TSQL11)
    .Where(vw => vw.ProductID == productID);

var result = await result11.ToListAsync();
Console.WriteLine(result.Count().ToString());
產生 TSQL
exec sp_executesql N'
	SELECT 
	    [e].[LineTotal], 
	    [e].[OrderQty], 
	    [e].[ProductID], 
	    [e].[ProductName], -- 外層為 ProductName
	    [e].[UnitPrice]
	FROM
		(
			SELECT
				P.ProductID,
				P.[Name], -- 內層為 Name
				POD.OrderQty,
				POD.UnitPrice,
				POD.LineTotal
			FROM[Purchasing].[PurchaseOrderHeader] AS POH
				JOIN[Purchasing].[PurchaseOrderDetail] AS POD ON POH.PurchaseOrderID = POD.PurchaseOrderID
				JOIN Production.[Product] AS P ON POD.ProductID = P.ProductID
		) AS[e]
	WHERE[e].[ProductID] = @__productID_1',
	N'@__productID_1 int',@__productID_1=1
執行後會拋出該錯誤,從 TSQL 語法可以觀察到,LINQ WHERE 條件,會根據 PurchaseOrderDetailViewModel Property 轉成對應 TSQL 欄位,因為 TSQL 內層沒有把 Name 轉成 ProductName,導致外層抓不到 ProductName。
Unhandled exception. Microsoft.Data.SqlClient.SqlException (0x80131904): 無效的資料行名稱 'ProductName'。

Data Annotations - ColumnAttribute

在 PurchaseOrderDetailViewModel 上把 ProductName 加上 ColumnAttribute
namespace EFCoreQuery.Models
{
    public class PurchaseOrderDetailViewModel
    {
        public int ProductID { get; set; }

        /// <summary>
        /// 把 Name 轉為 ProductName
        /// </summary>
        [Column("Name")]
        public string ProductName { get; set; }

        public Int16 OrderQty { get; set; }

        public decimal UnitPrice { get; set; }

        public decimal LineTotal { get; set; }
    }
}
CASE 1:沒有任何 where 條件
string TSQL13 = @"
    SELECT 
        P.ProductID ,
        P.[Name] ,
        POD.OrderQty ,
        POD.UnitPrice ,
        POD.LineTotal 
    FROM [Purchasing].[PurchaseOrderHeader] AS POH
        JOIN [Purchasing].[PurchaseOrderDetail] AS POD ON POH.PurchaseOrderID = POD.PurchaseOrderID
        JOIN Production.[Product] AS P ON POD.ProductID = P.ProductID
    ";

var result13 = dbContext.Database
    .SqlQueryRaw<PurchaseOrderDetailViewModel>(TSQL13);

var result = await result13.ToListAsync();
Console.WriteLine(result.Count().ToString());
// TSQL 語法正常輸出
//    SELECT
//        P.ProductID ,
//        P.[Name] , 
//        POD.OrderQty ,
//        POD.UnitPrice ,
//        POD.LineTotal
//    FROM[Purchasing].[PurchaseOrderHeader] AS POH
//        JOIN[Purchasing].[PurchaseOrderDetail] AS POD ON POH.PurchaseOrderID = POD.PurchaseOrderID
//        JOIN Production.[Product] AS P ON POD.ProductID = P.ProductID
正常執行,沒有拋出 FromSql ProductName 沒有值的錯誤,從中斷點去觀察,也發現值有塞進去,TSQL Name 欄位和 PurchaseOrderDetailViewModel ProudctName 屬性會自動對應。

CASE2:TSQL WHERE 沒有條件,但 LINQ WHERE 內有
int productID = 1;

string TSQL14 = @"
    SELECT 
        P.ProductID ,
        P.[Name] , 
        POD.OrderQty ,
        POD.UnitPrice ,
        POD.LineTotal 
    FROM [Purchasing].[PurchaseOrderHeader] AS POH
        JOIN [Purchasing].[PurchaseOrderDetail] AS POD ON POH.PurchaseOrderID = POD.PurchaseOrderID
        JOIN Production.[Product] AS P ON POD.ProductID = P.ProductID
    ";

var result14 = dbContext.Database
    .SqlQueryRaw<PurchaseOrderDetailViewModel>(TSQL14)
    .Where(vw => vw.ProductID == productID);

var result = await result14.ToListAsync();
Console.WriteLine(result.Count().ToString());
產生 TSQL
exec sp_executesql N'
	SELECT 
	    [e].[LineTotal], 
	    [e].[OrderQty], 
	    [e].[ProductID], 
	    [e].[Name], -- 外層為 Name
	    [e].[UnitPrice]
	FROM
		(
			SELECT
				P.ProductID,
				P.[Name], -- 內層為 Name
				POD.OrderQty,
				POD.UnitPrice,
				POD.LineTotal
			FROM[Purchasing].[PurchaseOrderHeader] AS POH
				JOIN[Purchasing].[PurchaseOrderDetail] AS POD ON POH.PurchaseOrderID = POD.PurchaseOrderID
				JOIN Production.[Product] AS P ON POD.ProductID = P.ProductID
		) AS[e]
	WHERE[e].[ProductID] = @__productID_1',
	N'@__productID_1 int',@__productID_1=1
外層 TSQL 產生 Name 而不是 ProductName

在 EF 學習 Data Annotations 時,是在 Code First FluentAPI 內設定給 Migration 使用,沒想到在 EFCore DbFirst 已經可以影響 TSQL 產生。

星期日, 11月 03, 2024

[EFCore] SqlQuery、SqlQueryRaw

延續 [EFCore] FromSql、FromSqlRaw 筆記,根據 SQL QueriesRaw SQL queries for unmapped types 文章內容,來記錄 SqlQuery 和 SqlQueryRaw,使用 EFCore 8、AdventureWorks2022 Purchasing 相關 Table 為資料來源並開啟 Sql Profile 來觀察產生的 TSQL 語法。

SqlQuery 和 SqlQueryRaw
  • 兩者都是在 EFore 7.0 新功能
  • EFCore 7.0:只能回傳 Scalar Value,EX:string、datetime、int
  • EFCore 8.0:能回傳 unmapped types,EX:DTO、ViewModel

PurchaseOrderDetailViewModel

用來承接 SqlQuery、SqlQueryRaw 結果
namespace EFCoreQuery.Models
{
    public class PurchaseOrderDetailViewModel
    {
        public int ProductID { get; set; }

        /// <summary>
        /// 把 Name 轉為 ProductName
        /// </summary>
        public string ProductName { get; set; }

        public Int16 OrderQty { get; set; }

        public decimal UnitPrice { get; set; }

        public decimal LineTotal { get; set; }
    }
}
EFCore 7.0 實際測試
namespace EFCoreQuery
{
    internal class Program
    {
        static async Task Main(string[] args)
        {
            using var dbContext = new AdventureWorks2022Context();

            // CASE 1:使用 EFCore 7.0,只能回傳 Scalar Value
            var productID = new SqlParameter("@ProductID", SqlDbType.Int);
            productID.Value = 403;

            var result6 = await dbContext.Database.SqlQuery<int>(
                $"SELECT ProductID FROM Production.[Product] WHERE ProductID = {productID}")
                .ToListAsync();
            Console.WriteLine(result6.Count().ToString());
            // TSQL:exec sp_executesql
            //     N'SELECT ProductID FROM Production.[Product] WHERE ProductID = @ProductID',
            //     N'@ProductID int',@ProductID=403

            // CASE 2:EFCore 7.0 不支援回傳 unmapped types
            var purchaseOrderID = new SqlParameter("@PurchaseOrderID", SqlDbType.SmallInt);
            purchaseOrderID.Value = 8;

            string TSQL7 = @"
                SELECT 
                    P.ProductID ,
                    P.[Name] AS ProductName , 
                    POD.OrderQty ,
                    POD.UnitPrice ,
                    POD.LineTotal 
                FROM [Purchasing].[PurchaseOrderDetail] AS POD
                    JOIN Production.[Product] AS P ON POD.ProductID = P.ProductID
                WHERE POD.PurchaseOrderID = @PurchaseOrderID
                ";

            var result7 = await dbContext.Database.SqlQueryRaw<PurchaseOrderDetailViewModel>(TSQL7, purchaseOrderID).ToListAsync();
            Console.WriteLine(result7.Count().ToString());
            // EFCore 7.0 會拋出下列錯誤,EFCore 8.0 才能正常執行
            // Unhandled exception. System.InvalidOperationException:
            // The element type 'EFCoreQuery.Models.PurchaseOrderDetailViewModel' used in 'SqlQuery' method is not natively supported by your database provider.
            // Either use a supported element type, or use ModelConfigurationBuilder.DefaultTypeMapping to define a mapping for your type.
        }
    }
}
EFCore 8.0 實際測試

TSQL 和 LINQ WHERE 條件混搭方式在 EFCore 7.0 就可以,該筆記寫在 EFCore 8.0 內紀錄而已,SqlQuery 和 SqlQueryRaw 回傳 IQueryable,所以後續才能再使用 LINQ WHERE 來篩選資料
namespace EFCoreQuery
{
    internal class Program
    {
        static async Task Main(string[] args)
        {
            using var dbContext = new AdventureWorks2022Context();

            // CASE 1::EFCore 8.0 支援 unmapped types
            var orderDate = new SqlParameter("@OrderDate", SqlDbType.DateTime);
            orderDate.Value = new DateTime(2014, 1, 1);

            FormattableString TSQL8 = $@"
                SELECT 
                    P.ProductID ,
                    P.[Name] AS ProductName , 
                    POD.OrderQty ,
                    POD.UnitPrice ,
                    POD.LineTotal 
                FROM [Purchasing].[PurchaseOrderHeader] AS POH
                    JOIN [Purchasing].[PurchaseOrderDetail] AS POD ON POH.PurchaseOrderID = POD.PurchaseOrderID
                    JOIN Production.[Product] AS P ON POD.ProductID = P.ProductID
                WHERE POH.OrderDate >= {orderDate}
                ";

            var result8 = await dbContext.Database
                .SqlQuery<PurchaseOrderDetailViewModel>(TSQL8)
                .Where(vw => vw.UnitPrice > 80)
                .ToListAsync();

            Console.WriteLine(result8.Count().ToString());
            // TSQL:exec sp_executesql
            //     N'SELECT [e].[LineTotal], [e].[OrderQty], [e].[ProductID], [e].[ProductName], [e].[UnitPrice]
            //       FROM(
            //              SELECT
            //                  P.ProductID,
            //                  P.[Name] AS ProductName,
            //                  POD.OrderQty,
            //                  POD.UnitPrice,
            //                  POD.LineTotal
            //              FROM[Purchasing].[PurchaseOrderHeader] AS POH
            //                  JOIN[Purchasing].[PurchaseOrderDetail] AS POD ON POH.PurchaseOrderID = POD.PurchaseOrderID
            //                  JOIN Production.[Product] AS P ON POD.ProductID = P.ProductID
            //              WHERE POH.OrderDate >= @OrderDate
            //           ) AS[e]
            //       WHERE[e].[UnitPrice] > 80.0',
            //     N'@OrderDate datetime',@OrderDate='2014 - 01 - 01 00:00:00'

            // CASE 2:模擬條件串接情況
            int? productID = 1;
            short? orderQty = 4;

            string TSQL9 = @"
                SELECT 
                    P.ProductID ,
                    P.[Name] AS ProductName , 
                    POD.OrderQty ,
                    POD.UnitPrice ,
                    POD.LineTotal 
                FROM [Purchasing].[PurchaseOrderHeader] AS POH
                    JOIN [Purchasing].[PurchaseOrderDetail] AS POD ON POH.PurchaseOrderID = POD.PurchaseOrderID
                    JOIN Production.[Product] AS P ON POD.ProductID = P.ProductID
                ";

            var result = dbContext.Database
                .SqlQueryRaw<PurchaseOrderDetailViewModel>(TSQL9);

            if (productID.HasValue)
                result = result.Where(vw => vw.ProductID == productID.Value);

            if (orderQty.HasValue)
                result = result.Where(vw => vw.OrderQty >= orderQty.Value);

            Console.WriteLine((await result.ToListAsync()).Count().ToString());

            // TSQL:exec sp_executesql 
            //     N'SELECT [e].[LineTotal], [e].[OrderQty], [e].[ProductID], [e].[ProductName], [e].[UnitPrice]
            //       FROM(
            //              SELECT
            //                  P.ProductID,
            //                  P.[Name] AS ProductName,
            //                  POD.OrderQty,
            //                  POD.UnitPrice,
            //                  POD.LineTotal
            //              FROM[Purchasing].[PurchaseOrderHeader] AS POH
            //                  JOIN[Purchasing].[PurchaseOrderDetail] AS POD ON POH.PurchaseOrderID = POD.PurchaseOrderID
            //                  JOIN Production.[Product] AS P ON POD.ProductID = P.ProductID
            //           ) AS[e]
            //       WHERE[e].[ProductID] = @__productID_Value_1
            //              AND[e].[OrderQty] >= @__orderQty_Value_2',
            //       N'@__productID_Value_1 int, @__orderQty_Value_2 smallint',@__productID_Value_1=1,@__orderQty_Value_2=4

        }
    }
}
從上述兩段語法就可以發現 LINQ WHERE 語法產生條件,會在主要的 TSQL 語法外再包一層

星期六, 11月 02, 2024

[RV] 影像控件 - QRCode 顯示

論壇問題,基本上是在 SSRS 上顯示 QRCode,剛好可以拿來練習影像控件 (Image) 顯示圖檔。

SSRS Image 控件有三種影像來源模式,分別為
該筆記是使用 [資料庫 (data-bound)] ,來製作財產標籤,標籤內包含 QRCode

資料來源

在 AdventureWork2022 內建立 Asset
USE [AdventureWorks2022]
GO

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[Asset](
	[ID] [int] IDENTITY(1,1) NOT NULL,
	[NO] [char](5) NOT NULL,
	[Name] [nvarchar](100) NOT NULL,
	[PurchDate] [date] NULL,
	[Location] [nvarchar](20) NOT NULL,
	[Department] [nvarchar](20) NOT NULL,
 CONSTRAINT [PK_Asset] PRIMARY KEY CLUSTERED 
(
	[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[Asset] ADD  CONSTRAINT [DF_Asset_NO]  DEFAULT ('') FOR [NO]
GO

ALTER TABLE [dbo].[Asset] ADD  CONSTRAINT [DF_Asset_Name]  DEFAULT (N'') FOR [Name]
GO

ALTER TABLE [dbo].[Asset] ADD  CONSTRAINT [DF_Asset_Location]  DEFAULT (N'') FOR [Location]
GO

ALTER TABLE [dbo].[Asset] ADD  CONSTRAINT [DF_Asset_Department]  DEFAULT (N'') FOR [Department]
GO

-- 建立假資料
INSERT INTO [dbo].[Asset] ([NO],[Name],[PurchDate],[Location],[Department]) VALUES
	('A0001' , N'商用電腦' , '20240101' , N'職員一' , N'行政') ,
	('A0002' , N'事務機'   , '20230707' , N'職員一' , N'行政') ,
	('A0003' , N'油壓台車' , '20200820' , N'職員二' , N'運輸') ,
	('A0004' , N'貨車'     , '20190505' , N'職員二' , N'運輸') ,
	('A0005' , N'手機'     , '20190607' , N'職員二' , N'運輸')
GO

Model 和 ReportViewModel

使用 Entity Framework 來存取資料並建立 ReportViewModel 來呈現 QRCode
namespace ZXingQRCode
{
    public class AssetReportModel : Asset
    {
        public byte[] QRcodeInfo { get; set; }
    }
}

QRCode

使用 ZXing 套件來產生 QRCode 圖檔
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using ZXing;

namespace ZXingQRCode
{
    internal static class QRCodeGenerator
    {
        public static byte[] Create(string content)
        {
            if (string.IsNullOrWhiteSpace(content))
                throw new ArgumentNullException(content);

            BarcodeWriter writer = new BarcodeWriter
            {
                Format = BarcodeFormat.QR_CODE,
                Options = new ZXing.QrCode.QrCodeEncodingOptions
                {
                    Margin = 0,
                    Width = 200,
                    Height = 200
                }
            };

            using (Bitmap bitmap = writer.Write(content))
            using (MemoryStream stream = new MemoryStream())
            {
                bitmap.Save(stream, ImageFormat.Png);
                return stream.ToArray();
            }
        }
    }
}

報表設計

設計 8 X 5 cm 大小標籤,並使用清單來呈現每一筆資料,同時把自訂編號資訊轉成 QRCode 來呈現
影像控件設定
程式執行
using Microsoft.Reporting.WinForms;
using System;
using System.Data;
using System.Linq;
using System.Windows.Forms;

namespace ZXingQRCode
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            var dbContext = new AdventureWorks2022Entities();
            var dataSources = dbContext.Asset.ToList();
            var reportSource = dataSources.Select(s => new AssetReportModel()
            {
                NO = s.NO,
                Name = s.Name,
                PurchDate = s.PurchDate,
                Location = s.Location,
                Department = s.Department,
                QRcodeInfo = QRCodeGenerator.Create(s.NO)
            }).ToList();

            reportViewer1.LocalReport.DataSources.Add(new ReportDataSource(nameof(AssetReportModel), reportSource));
            reportViewer1.RefreshReport();
        }
    }
}

星期五, 11月 01, 2024

[SQL] 彙總函數應用 - 字串欄位分組

網路問題,基本上和 [SQL] 彙總函數應用 - 資料分組 是類似問題但相同解法,透過累計加總 (Running Total) 來做到字串欄位的資料分組
use tempdb
GO

DROP TABLE IF EXISTS #Temp

CREATE TABLE #Temp 
(
    ID int IDENTITY(1,1),
    Data char(1) null
)

INSERT INTO #Temp VALUES
    ('1') , (null) , (null) ,
    ('5') , (null) , (null) ,
    ('8') , (null) , (null) 

SELECT * , 
    SUM(IIF(Data IS NULL , 0 , 1)) OVER 
        (
            ORDER BY ID 
            ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW 
        ) AS GroupNo
FROM #Temp

星期二, 10月 29, 2024

[EFCore] FromSql、FromSqlRaw

根據 SQL Queries 文章內容,來記錄 FromSql 和 FromSqlRaw,使用  EFCore 8、AdventureWorks2022 Production.Product Table 為資料來源並開啟 Sql Profile  來觀察產生的 TSQL 語法

FromSql

using EFCoreQuery.Models;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using System.Data;

namespace EFCoreQuery
{
    internal class Program
    {
        static async Task Main(string[] args)
        {
            using var dbContext = new AdventureWorks2022Context();
            
            // CASE 1:常數
            var result1 = await dbContext.Product.FromSql(
                $"SELECT * FROM Production.Product WHERE Class = N'H'").ToListAsync();
            Console.WriteLine(result1.Count().ToString());
            // TSQL:SELECT * FROM Production.Product WHERE Class = N'H'

            // CASE2:C# 字串插補傳遞變數,TSQL 會參數化,資料型態、長度為 nvarchar(4000)
            string data = "H";
            var result2 = await dbContext.Product.FromSql(
                $"SELECT * FROM Production.Product WHERE Class = {data}").ToListAsync();
            Console.WriteLine(result2.Count().ToString());
            // TSQL:exec sp_executesql 
            //     N'SELECT * FROM Production.Product WHERE Class = @p0' ,
            //     N'@p0 nvarchar(4000)',@p0=N'H'

            // CASE3:C# 字串插補傳遞 SqlParameter 並對應欄位資料型態、長度 nchar(2),TSQL 會參數化
            var para = new SqlParameter("@Class", SqlDbType.NChar, 2);
            para.Value = "H";
            var result3 = await dbContext.Product.FromSql(
                $"SELECT * FROM Production.Product WHERE Class = {para}").ToListAsync();
            Console.WriteLine(result3.Count().ToString());
            // TSQL:exec sp_executesql 
            //     N'SELECT * FROM Production.Product WHERE Class = @Class',
            //     N'@Class nchar(2)',@Class=N'H '            

        }
    }
}

CASE2 雖然是字串插補傳遞變數,但會將變數包裝於 SqlParameter 內,產生的 TSQL 會參數化,不會是組合字串傳進去,可以避免 SqlInjection

FromSqlRaw

可以用它動態改變欄位名稱,但有 Sqlnjection 風險,不適合應用在實務上
using EFCoreQuery.Models;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using System.Data;

namespace EFCoreQuery
{
    internal class Program
    {
        static async Task Main(string[] args)
        {
            using var dbContext = new AdventureWorks2022Context();

            // CASE1:C# 字串插補傳遞欄位名稱、SqlParameter 參數值
            string columnName = "Class";

            var para = new SqlParameter("@Class", SqlDbType.NChar, 2);
            para.Value = "H";

            var result4 = await dbContext.Product.FromSqlRaw(
                $"SELECT * FROM Production.Product WHERE {columnName} = @Class", para).ToListAsync();
            Console.WriteLine(result4.Count().ToString());
            // TSQL:exec sp_executesql
            //     N'SELECT * FROM Production.Product WHERE Class = @Class',
            //     N'@Class nchar(2)',@Class=N'H ' 

            // CASE2:C# 字串插補傳遞變數只能應用在欄位
            string data = "H";
            var result5 = await dbContext.Product.FromSqlRaw(
                $"SELECT * FROM Production.Product WHERE Class = {data}").ToListAsync();
            Console.WriteLine(result5.Count().ToString());
            // Exception:Unhandled exception. 
            // Microsoft.Data.SqlClient.SqlException (0x80131904):
            // 無效的資料行名稱 'H'
        }
    }
}

FromSql 和 FromSqlRaw 限制

官方文件上說明
  • The SQL query must return data for all properties of the entity type.
  • The column names in the result set must match the column names that properties are mapped to. Note that this behavior is different from EF6; EF6 ignored property-to-column mapping for SQL queries, and result set column names had to match those property names.
  • The SQL query can't contain related data. However, in many cases you can compose on top of the query using the Include operator to return related data (see Including related data).
個人理解是回傳的 Entity 每個 Property 都要對應到,只要有一個 Property 沒有對應到就會拋出  InvalidOperationException 且抓取關聯 Entity 時一定得透過 Include
namespace EFCoreQuery
{
    internal class Program
    {
        static async Task Main(string[] args)
        {
            using var dbContext = new AdventureWorks2022Context();

            // CASE:回傳的 Entity 每個 Property 都要對應到,故意只抓 ProductID 而已
            var result = await dbContext.Product.FromSql(
                $"SELECT ProductID FROM Production.Product WHERE Class = N'H'").ToListAsync();
            Console.WriteLine(result.Count().ToString());
            // Unhandled exception. System.InvalidOperationException:
            // The required column 'Class' was not present in the results of a 'FromSql' operation.
        }
    }
}
函式演變

整理如下
  • EFCore 2:只有 FromSql
  • EFCore 3:FromSql 被移除,取而代之的是 FromSqlRaw 和 FromSqlInterpolated。
  • EFCore 7: FromSql 強勢回歸,用來取代 FromSqlInterpolated,但 FromSqlRaw 仍然存在
EFCore 3 BreakChange 內關於 FromSql 被取代的相關章節
EFCore 7 FromSql 回歸取代 FromSqlInterpolated 則是在 SQL Queries 內的備註上看到,EFCore 7 What's NewBreakChange 內反而沒有提到


星期六, 10月 26, 2024

[C#] DefaultEventAttribute

DefaultEventAttribute 可以用來指定元件預設事件,以 C# WinForm ComboBox 為例說明,當把 ComboBox 拉近 Form 內後,直接點擊控件,會直接在 cs 檔案內建立 SelectedIndexChanged 事件,SelectedIndexChanged 為其預設事件
ComboBox 事件觸發時機
  • SelectedIndexChanged:使用程式變更 Index、使用者點選
  • SelectionChangeCommitted:使用者點選
實務上是希望使用者點選時才觸發事件,但常常在畫面顯示上需要指定 ComboBox 顯示時的預設值,使用 Code 設定預設值時會觸發 SelectedIndexChanged,必須改用 SelectionChangeCommitted 才行。

建立自訂 ComboBox 控件並透過 DefaultEventAttribute 來自訂預設事件,把預設事件設定為 SelectionChangeCommitted,這樣直接點選 ComboBox 就會在 cs 檔案內產生 SelectionChangeCommitted 事件

UserControl 預設事件截圖

星期一, 10月 21, 2024

[C#] Path - GetExtension()

實務上有檔案檔名是 [日期 + 時間] 的情況,在使用 Path.GetExtension() 時發生異常

C# Code
namespace PathSample
{
    internal class Program
    {
        static void Main(string[] args)
        {
            List<string> target = new List<string>();
            target.Add(@"\2024-10-1817.29.37.777.jpg");   // 正常路徑
            target.Add(@"\2024-10-1817.29.37.777");       // 尾端沒有副檔名
            target.Add(@"\2024-10-1817.29.37.777.jpg.");  // 尾端有 . 符號
            target.Add(@"\2024-10-1817.29.37.777.jpg\");  // 尾端有 \ 符號

            string extension = string.Empty;
            foreach (var path in target)
            {
                extension = Path.GetExtension(path);
                extension = string.IsNullOrWhiteSpace(extension) ? "空值" : extension;
                Console.WriteLine($"{path} 副檔名:{extension}");
            }
        }
    }
}
顯示結果

官方文件內容

文件備註
This method obtains the extension of path by searching path for a period (.), starting with the last character in path and continuing toward the first character. If a period is found before a DirectorySeparatorChar or AltDirectorySeparatorChar character, the returned string contains the period and the characters after it; otherwise, String.Empty is returned.
C# SourceCode
基本上 GetExtension() 就是找 [最後的 \ 符號] 至尾端文字,該端文字內 [最後的 . 符號] 至尾端文字內容

星期日, 10月 20, 2024

[C#] Path - 預設路徑

看見一段 C# 語法
Path.GetExtension("FileExtensiom.png");
有點意外這樣可以單獨取出副檔名,腦海裡 Path class 都是針對路徑來取得相對應資料,實際查詢時發現,原來使用 Path 時會有預設路徑的存在,不是真的直接對 FileExtensiom.png 這名稱去進行解析
namespace PathSample
{
    internal class Program
    {
        static void Main(string[] args)
        {
            string targetFileName = @"FileExtension.png";
            string currentDirectory = Environment.CurrentDirectory;
            string fullPath = Path.GetFullPath(targetFileName);
            string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(targetFileName);
            string extension = Path.GetExtension(targetFileName);

            Console.WriteLine($"CurrentDirectory:{currentDirectory}");
            Console.WriteLine($"FullPath:{fullPath}");
            Console.WriteLine($"fileName:{fileNameWithoutExtension}");
            Console.WriteLine($"extension:{extension}");
        }
    }
}

星期六, 10月 19, 2024

[C#] File.Copy - UnauthorizedAccessException

C# File.Copy overwrite 參數,一直以為把它設定為 true,目的端檔案就一定可以覆蓋過去,沒想到它也有例外情況,實務上遇上指定 overwrite 情況下,拋出 [UnauthorizedAccessException 拒絕存取路徑] Exception,肯定有對應權限情況下,在 File.Copy 官方文件 上找到線索 UnauthorizedAccessException 有下列三種情況會發生
  • 呼叫端沒有對應權限
  • 目的端檔案屬性為唯讀 (ReadOnly)
  • overwrite 為 true,目的端檔案存在且檔案屬行為隱藏 (hidden),但是來源端檔案屬性不是隱藏 (hidden)
遇上第二種情況,下面簡易紀錄

唯讀檔案

C# Code
using System.IO;

namespace ConsoleApp1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            string sourceFile = @"D:\FileCopyException\Source\FileCopyException.txt";
            string DestFile = @"D:\FileCopyException\Dest\FileCopyException.txt";

            // 目的端沒有檔案,可以正常複製過去
            File.Copy(sourceFile, DestFile, true);
            // 第一次複製過去後,複製檔案會保留唯讀屬性,
            // 所以第二次複製過去,就算有 overrite 也會拋出 Exception
            File.Copy(sourceFile, DestFile, true);
        }
    }
}
執行後拋出的 Exception
未處理的例外狀況: System.UnauthorizedAccessException: 拒絕存取路徑 'D:\FileCopyException\Dest\FileCopyException.txt'。
    於 System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)
    於 System.IO.File.InternalCopy(String sourceFileName, String destFileName, Boolean overwrite, Boolean checkHost)
    於 System.IO.File.Copy(String sourceFileName, String destFileName, Boolean overwrite)
    於 ConsoleApp1.Program.Main(String[] args) 於 D:\ConsoleApp1\Program.cs: 行 20
一開始看到 System.IO.__Error.WinIOError 以為是 IOException,還想說不是有 overrite,被自己誤導。

星期五, 10月 18, 2024

IBM x3250 M3 - 指示橘燈

要拿這台 12 年的 [IBM x3250 M3] 來當成特定機台的 File Server,一開機就發現指示燈亮橘燈且風扇狂轉,發現是 BIOS System Log 滿造成亮橘燈,最新的 System Log 還在 2016 年,把 System Log 清除並完全斷電後再重啟,指示橘燈才熄滅且風扇轉速恢復正常

指示橘燈
第一直覺是 Disk 出問題,但 MegaRAID 顯示正常
發現 BIOS System Log 已滿訊息

星期三, 10月 16, 2024

[SQL] 視窗函數應用 - 連續字串分組

延續 [SQL] 分析函數應用 - 排班班表,班別基本上是連續字串,想要直接處理字串就產生群組,而不依賴日期欄位,資料來源和 TSQL 流程如下圖
TSQL
DECLARE @Temp TABLE
(
    ID int identity(1,1) ,
    Col1 char(1)
)

INSERT INTO @Temp VALUES
    ('A') , ('A') , ('A') , ('B') , ('C') , ('C') , ('A') , ('A') ,  ('A')

;
WITH T1 AS
(
    SELECT 
        * , 
        -- 第三參數:null 的預設值
        LAG(Col1 , 1 , Col1) OVER (ORDER BY ID)AS PreValue
    FROM @Temp
)
, T2 AS
(
    SELECT
        * ,
        -- 判斷連續字串是否有變
        CAST(IIF(Col1 = PreValue , 0 , 1) AS int) AS GroupChange
    FROM T1
)
, T3 AS
(
    SELECT
        * ,
        -- 使用彙總視窗函數跑 Running Total
        SUM(GroupChange) OVER (ORDER BY ID ROWS UNBOUNDED PRECEDING) AS GroupNO 
	FROM T2
)
SELECT * 
FROM T3
ORDER BY ID
LAG default 參數

官方文件說明
The value to return when offset is beyond the scope of the partition. If a default value is not specified, NULL is returned. default can be a column, subquery, or other expression, but it cannot be an analytic function. default must be type-compatible with scalar_expression.
該範例就是把第一筆資料的 null 塞 A 來取代

星期二, 10月 15, 2024

[SQL] 分析函數應用 - 排班班表

社群問題,簡化為下圖來理解,資料來源只顯示單一位員工資訊
相關商業邏輯
  • WorkWay 班別會混雜 [特、休] 休假資訊,A 代表 08-16 早班時段、B 代表 13-21 午班時段 
  • [特、休] 所屬班別判斷邏輯:班別是若遇當月休假換班別,就歸屬前一個班別,例如 10/13 休 10/14 換成 A 班,那麼 10/13 會往前抓到最後一天的班別,但是像月初特休就會往後抓班別來判斷

資料來源
USE TempDB
GO

DROP TABLE IF EXISTS #Attend

CREATE TABLE #Attend(
	Name nvarchar(20) ,
	WorkDate date ,
	WorkWay nvarchar(5))

INSERT INTO #Attend
VALUES 
	(N'小明','2024/10/1' ,N'A' ) , (N'小明','2024/10/2' ,N'A' ) , (N'小明','2024/10/3' ,N'A' ) , (N'小明','2024/10/4' ,N'A' ) , (N'小明','2024/10/5' ,N'休') , 
	(N'小明','2024/10/6' ,N'休') , (N'小明','2024/10/7' ,N'A' ) , (N'小明','2024/10/8' ,N'A' ) , (N'小明','2024/10/9' ,N'A' ) , (N'小明','2024/10/10',N'休') ,
	(N'小明','2024/10/11',N'A' ) , (N'小明','2024/10/12',N'休') , (N'小明','2024/10/13',N'休') , (N'小明','2024/10/14',N'B' ) , (N'小明','2024/10/15',N'B' ) ,
	(N'小明','2024/10/16',N'B' ) , (N'小明','2024/10/17',N'B' ) , (N'小明','2024/10/18',N'B' ) , (N'小明','2024/10/19',N'休') , (N'小明','2024/10/20',N'休') ,
	(N'小明','2024/10/21',N'A' ) , (N'小明','2024/10/22',N'A' ) , (N'小明','2024/10/23',N'A' ) , (N'小明','2024/10/24',N'A' ) , (N'小明','2024/10/25',N'A' ) ,
	(N'小明','2024/10/26',N'休') , (N'小明','2024/10/27',N'休') , (N'小明','2024/10/28',N'A' ) , (N'小明','2024/10/29',N'A' ) , (N'小明','2024/10/30',N'A' ) , (N'小明','2024/10/31',N'A') ,
	(N'大白','2024/10/1' ,N'特') , (N'大白','2024/10/2' ,N'特') , (N'大白','2024/10/3' ,N'B' ) , (N'大白','2024/10/4' ,N'B' ) , (N'大白','2024/10/5' ,N'休') ,	(N'大白','2024/10/6' ,N'休') , (N'大白','2024/10/7' ,N'B' ) , (N'大白','2024/10/8' ,N'B' ) , (N'大白','2024/10/9' ,N'B' ) , (N'大白','2024/10/10',N'休') ,
	(N'大白','2024/10/11',N'B' ) , (N'大白','2024/10/12',N'休') , (N'大白','2024/10/13',N'休') , (N'大白','2024/10/14',N'A' ) , (N'大白','2024/10/15',N'A' ) ,
	(N'大白','2024/10/16',N'A' ) , (N'大白','2024/10/17',N'A' ) , (N'大白','2024/10/18',N'A' ) , (N'大白','2024/10/19',N'休') , (N'大白','2024/10/20',N'休') ,
	(N'大白','2024/10/21',N'B' ) , (N'大白','2024/10/22',N'B' ) , (N'大白','2024/10/23',N'B' ) , (N'大白','2024/10/24',N'B' ) , (N'大白','2024/10/25',N'B' ) , 
	(N'大白','2024/10/26',N'休') , (N'大白','2024/10/27',N'休') , (N'大白','2024/10/28',N'B' ) , (N'大白','2024/10/29',N'B' ) , (N'大白','2024/10/30',N'B ') , (N'大白','2024/10/31',N'B')
TSQL 寫法

主要分為三個步驟,分別為
  • 把 [特、休] 字樣改成所屬班別,使用分析視窗函數參數 IGNORE NULLS 來整理
  • 透過連續日期、連續班別來判斷群組
  • 產生對應需求資料
; 
WITH T1 AS 
( 
    SELECT * ,
        IIF(WorkWay IN ('A' , 'B') , WorkWay , NULL) AS NewWorkWay
    FROM #Attend
)
, T2 AS
(
    SELECT * ,
        COALESCE(
            NewWorkWay ,
            LAG(NewWorkWay) IGNORE NULLS OVER (PARTITION BY Name ORDER BY WorkDate) ,
            LEAD(NewWorkWay) IGNORE NULLS OVER (PARTITION BY Name ORDER BY WorkDate)) AS GroupNO
    FROM T1
)
, T3 AS 
(
    SELECT 
        * ,
        DATEADD(
            d , 
            ROW_NUMBER() OVER (PARTITION BY Name , GroupNO ORDER BY WorkDate) * -1 , 
            WorkDate) AS GroupDate
        FROM T2
)
, T4 AS
(
    SELECT 
        T3.Name , 
        T3.GroupDate ,
        T3.GroupNO ,
        MIN(WorkDate) AS MinDate ,
        T3.GroupNO + ':' + CAST(DAY(MIN(WorkDate)) AS varchar(2)) + '~' + CAST(DAY(MAX(WorkDate)) AS varchar(2)) AS WorkInfo
    FROM T3
    GROUP BY Name , GroupDate , GroupNO
)
SELECT
    Name ,
    STRING_AGG(WorkInfo , ' , ') WITHIN GROUP (ORDER BY MinDate)
FROM T4
GROUP BY Name
分析視窗函數參數 IGNORE NULLS

因為 WorkWay 有 [特、休] 字樣來呈現該員工休假狀態,所以要把 [特、休] 改為 null,方便後續分析函數搭配 IGNORE NULLS,下面語法故意把 LAG 產生的 PreviousValue 和 LEAD 產生的 NextValue 資料獨立出來顯示,從圖片就可以觀察到 T1 和 T2 這兩個階段處理事項
; 
WITH T1 AS 
( 
    SELECT * ,
        IIF(WorkWay IN ('A' , 'B') , WorkWay , NULL) AS NewWorkWay
    FROM #Attend
    WHERE Name = N'大白'
)
, T2 AS
(
    SELECT * ,
        -- 顯示資料來理解
        LAG(NewWorkWay) IGNORE NULLS OVER (PARTITION BY Name ORDER BY WorkDate) AS PreviousValue ,
        -- 顯示資料來理解
        LEAD(NewWorkWay) IGNORE NULLS OVER (PARTITION BY Name ORDER BY WorkDate) AS NextValue ,
        COALESCE(
            NewWorkWay ,
            LAG(NewWorkWay) IGNORE NULLS OVER (PARTITION BY Name ORDER BY WorkDate) ,
            LEAD(NewWorkWay) IGNORE NULLS OVER (PARTITION BY Name ORDER BY WorkDate)) AS GroupNO
    FROM T1
)
SELECT * FROM T2
ORDER BY Name , WorkDate

透過連續日期、連續班別來判斷群組

透過 Name 和 GroupNO 來跑 Row_Number() 並和工作日期運算來判斷群組
, T3 AS 
(
    SELECT 
        * ,
        -- 顯示資料來理解
        ROW_NUMBER() OVER (PARTITION BY Name , GroupNO ORDER BY WorkDate) * -1 AS DisplayRowNO,
        DATEADD(
            d , 
            ROW_NUMBER() OVER (PARTITION BY Name , GroupNO ORDER BY WorkDate) * -1 , 
            WorkDate) AS GroupDate
    FROM T2
)
SELECT * FROM T3
ORDER BY Name , WorkDate

產生對應需求資料

分出員工連續工作日後,就可以找出每個群組起始和結束日期,再把資料整理成問題需求,最後使用 STRING_AGG 把資料串在一起呈現
, T4 AS
(
    SELECT 
        T3.Name , 
        T3.GroupDate ,
        T3.GroupNO ,
        MIN(WorkDate) AS MinDate ,
        T3.GroupNO + ':' + CAST(DAY(MIN(WorkDate)) AS varchar(2)) + '~' + CAST(DAY(MAX(WorkDate)) AS varchar(2)) AS WorkInfo
    FROM T3
    GROUP BY Name , GroupDate , GroupNO
)
SELECT
    Name ,
    STRING_AGG(WorkInfo , ' , ') WITHIN GROUP (ORDER BY MinDate)
FROM T4
GROUP BY Name

星期六, 10月 12, 2024

[SQL] 伺服器定序 - 變數大小寫

Line 討論發現到,原來定序會影響 TSQL 撰寫時的大小寫,原以為定序只對資料排序有影響而已,在官方文件 - 設定或變更伺服器定序 上發現文字說明
變數、資料指標和 GOTO 標籤的名稱。 例如,如果伺服器層級定序區分大小寫,變數 @pi 和 @PI 會視為不同的變數;如果伺服器層級定序不區分大小寫,則會視為相同的變數。
下列 TSQL 語法和圖示可以觀察到伺服器定序為 Chinese_Taiwan_Stroke_CS_AS 區分大小寫,可以看見變數大小寫在 IDE 上會有錯誤提示
SELECT SERVERPROPERTY('collation')

星期四, 10月 10, 2024

[C#] DataGridViewColumn.Clone()

延續 [C#] 提供資料行已經屬於 DataGridView 控制項 筆記,發現原來 DataGridViewColumn.Clone() 產生 DataGridViewColumn 是可以再放進其他 DataGridViw

C# Code
private void btnClone_Click(object sender, EventArgs e)
{
    // 寫法一
    List<DataGridViewColumn> cloneColumn = new List<DataGridViewColumn>();
    foreach (DataGridViewColumn col in dataGridView1.Columns)
        cloneColumn.Add(col.Clone() as DataGridViewColumn);

    // 寫法二
    List<DataGridViewColumn> cloneColumn = dataGridView1.Columns.OfType<DataGridViewColumn>()
        .Select(col => col.Clone() as DataGridViewColumn)
        .ToList();

    dataGridView2.Columns.AddRange(cloneColumn.ToArray());
}
從中斷點內可以觀察到 DataGridViewColumn.DataGridView 屬性變成 null
實際執行

星期二, 10月 08, 2024

[CCNA] 動態路由 - OSPF - 優先權

延續 [CCNA] 動態路由 - OSPF[CCNA] 動態路由 - OSPF - 負載平衡 筆記,從 router0 => router1 有兩條路由,如下圖
上述兩條路由 AD 相同,但 Cost 不同,優先是以 router0 => router1 為主,當 router0 => router1 不通時,才會改成 router0 => router3 => router2 => router1 ,Cost 計算可以參考 認識OSPF路由協定 活用相關指令設定參數值 內的公式介紹,兩條路由成本分別為
  • router0 => router1 :65
  • router0 => router3 => router2 => router1:193
OSPF AD 值預設為 110

router0 => router1

router0 => router3 => router2 => router1

故意關閉 router0 上往 router1 網卡

星期一, 10月 07, 2024

[CCNA] 動態路由 - OSPF - 負載平衡

延續 [CCNA] 動態路由 - OSPF 筆記,從 router0 至 router2 有兩條路由可以到達,可以達到負載平衡效果
show ip route

重新開啟 Packet Tracer 進入 router0 CLI 時發現 OSPF 自動進行同步,Router IP 以 192.168.120.1 (深綠) 和 192.168.140.1 (淺綠) 來表示,是因為該 IP 為 Router 上最大的 IPAddress 

紫框為 router0 至 router2 的 OSPF 路由設定

星期日, 10月 06, 2024

[CCNA] 動態路由 - OSPF

OSPF (Open Shortest Path First),簡單說就是讓 router 自行交換路由,不用每個都手動建立
在 Router 上啟動 OSPF 並進行設定

紀錄 router0、router1、router2 啟用和設定方式,router3 就省略囉,主要設定語法為
router ospf ProcessID => ProcessID 為識別用
network IPAddress wildcard area AreaID => 該範例會設定在同一個 area 內
wildcard 請參考該篇文章 - Wild Card反遮罩

router0 上啟用 OSPF, network 192.168.0.0 0.0.255.255 area 0,代表 192.168 開頭都會啟用 OSPF
router1 啟用 OSPF,network 192.0.0.0 0.255.255.255 area 0,代表 192 開頭都會啟用 OSPF
router2 啟用 OSPF,network 0.0.0.0 255.255.255.255 area 0,代表全部 IP 都會啟用 OSPF 
當 router OSPF 啟動後,每個 router 會出現同步訊息,Full 代表已經交換完,之後就是定期發出 Hello 封包確認 router 路由設定是否有異動
show ip route

四個 router 都啟用 OSPF 後,在 router0 上確認 OSPF 路由
show ip protocols

該語法可以查詢 Router 上 OSPF 設定,下圖以 router0 為例紀錄
Cient 端 PC 進行測試 OSPF 設定檢查