Quantcast
Channel: ProgrammerXDB Blog - C#

C#10新功能簡介

$
0
0

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號:N220123302
出刊日期: 2022/1/19

在這篇文章中,將要介紹C# 10 提供的一些新語法,以做為開發的參考。

 

隱含全域Using(implicit global using)

使用Visual Studio 2022開發工具建立C# 應用程式時,預設的專案啟用隱含全域Usingimplicit global using功能,在使用到一些常用的.NET類別時,不需要在專案中使用「using」語法。以主控台應用程式(Console Application)時為例,預設隱含引用以下命名空間:

  • using System;
  • using System.IO;
  • using System.Collections.Generic;
  • using System.Linq;
  • using System.Net.Http;
  • using System.Threading;
  • using System.Threading.Tasks;

我們可以從專案的屬性頁看到預設勾選「Implicit global usings」項目,啟用隱含全域Using(implicit global using)功能,請參考下圖所示:

clip_image002

圖 1:預設啟用隱含全域Using(implicit global using)功能。

例如我們想要使用「System.IO」命名空間下的「File」類別,只需要直接使用類別名稱,不必先使用「using」語法引用命名空間,例如以下程式碼:

clip_image004

圖 2:隱含全域Using(implicit global using)。

若想要自行掌控這些命名空間,可以將隱含全域Using(implicit global using)關掉,然後修改程式如下,需要明確使用「using」語法引用所有命名空間:

 

using System;
using System.IO;
Console.WriteLine( File.Exists(@"c:\temp\data.txt") );

 

全域 using 指示詞(Global using directives)

若不啟用隱含全域Using(implicit global using)功能,另一個替代做法是採用全域 using 指示詞(Global using directives),可於應用程式等級引用命名空間,不需要每個檔案中重複引用。我們需要在專案檔案中進行設定,在Visual Studio 2022 「Solution Explorer」視窗中專案名稱上方按滑鼠右鍵,從快捷選單選擇「Edit Project File」項目,請參考下圖所示:

clip_image006

圖 3:編輯專案檔。

在專案檔案內設定<ImplicitUsings>項目為「true」,然後在<ItemGroup>項目內,利用<Using Include..>引用命名空間,或使用「<Using Remove...>」移除專案隱含自動引用的命名空間:

<Project Sdk="Microsoft.NET.Sdk">

    <ItemGroup>
        <Using Include="System.IO" />
        <Using Include="System" />
        <Using Remove="System.Linq" />
    </ItemGroup>

    <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

 

除了在專案檔設定全域 using 指示詞(Global using directives)之外,另外一種作法是使用「global using」關鍵字,我們可以在專案中任一個C# 程式檔案內,使用「global using」引用命名空間,如此在此專案中其它的C#檔案內都可以參照到這個命名空間,參考下圖所示:

clip_image008

圖 4:使用「global using」引用命名空間。

一個專案中可以有多個C# 程式檔案內,使用「global using」引用命名空間,參考下圖範例所示:

clip_image010

圖 5:在多個C# 程式檔案內使用「global using」。

 

using static指示詞

我們也可以使用「global using static」引用型別(Type),這樣在專案中到處都可以使用到它的成員,例如下圖的範例中引用「System.Console」型別:

clip_image012

圖 6:using static指示詞。

使用「global using」指示詞時,也可以為型別取一個別名,以避免名稱衝突問題,例如可能有兩個以上的命名空間下都有同名的「Console」類別,我們可以取別名如下:

global using MyConsole = System.Console;
global using System.IO;

這樣在其它程式中便可以直接使用別名,例如以下程式碼:

MyConsole.WriteLine( File.Exists(@"c:\temp\data.txt") );

特別注意「using static」不能夠和別名一起使用,否則會得到語法錯誤訊息:

A 'using static' directive cannot be used to declare an alias

 

File Scoped Namespaces

File Scoped Namespaces 簡化命名空間的語法,適用於一個CS檔案中只有一個命名空間的情況下,讓你在定義命名空間時,可以省略{...}區塊,以簡化程式碼。例如以下的範例,「MyNS」命名空間下有一個「HR」命名空間,在其中有一個「Employee」類別:

namespace MyNS
{
    namespace HR
    {
        class Employee
        {
            public void Print()
            {
                Console.WriteLine("Employee");
            }

        }
    }
}

 

這段程式碼可以簡寫如下,使用「.」符號減少一層巢狀結構:

namespace MyNS.HR
{
    class Employee
    {
        public void Print()
        {
            Console.WriteLine("Employee");
        }

    }
}

 

 

不過在C# 10,我們可以利用File Scoped Namespaces語法再加以簡寫如下,在CS檔案中第一行程式定義命名空間為「MyNS.HR」,只需以「;」號結尾,不需要大括號 { }:

 

namespace MyNS.HR;
class Employee
{
    public void Print()
    {
        Console.WriteLine("Employee");
    }
}

 

這樣我們就可以在其它CS檔案中引用「MyNS.HR」命名空間,使用到其內的成員:

 

using MyNS.HR;
Employee employee = new Employee();
employee.Print();

 

若使用File Scoped Namespaces功能的CS檔案中有使用到「using」指示詞,應該放在File Scoped Namespaces宣告之上,例如以下程式範例:

 
using System;
namespace MyNS.HR;
class Employee
{
    public void Print() {
        Console.WriteLine("Employee");
    }
}

 

若變更上例程式碼的順序如下:

namespace MyNS.HR;
using System;
class Employee
{
    public void Print() {
        Console.WriteLine("Employee");
    }
}

 

Visual Studio 2022開發工具將會顯示警告訊息,請參考下圖所示:

clip_image014

圖 7:錯誤訊息。

同時要注意,一個CS檔案之中File Scoped Namespaces只能定義一個,例如以下的程式碼會得到錯誤訊息:「Source file can only contain one file-scoped namespace declaration.」:

namespace MyNS.HR;
namespace MyNS.HumanResource;

using System;
class Employee
{
    public void Print() {
        Console.WriteLine("Employee");
    }
}

 

同時File Scoped Namespaces與標準的namespace語法也不能並存,以下程式碼會得到錯誤訊息:「Source file can not contain both file-scoped and normal namespace declarations」:

namespace MyNS.HR;
namespace MyNS.HumanResource
{
    using System;
    class Employee
    {
        public void Print()
        {
            Console.WriteLine("Employee");
        }
    }
}

 

Lambda運算式自然型別(Natural type)

在C# 10之前,我們有時需要使用到Action<T> 或 Func<T> 委派型別(Delegate Type),例如我們想要有一個「Add」Delegate進行兩數相加,並將計算的結果回傳,標準的Lamdba Expression寫法如下:

Func<int, int, int> Subtract = ( int i, int j ) => {
    return i - j;
};
Console.WriteLine( Subtract(1, 2) );

 

而以下的「Subtract」則採用Lambda Statement做兩數相減:

 

Func<int, int, int> Subtract = ( int i, int j ) => {
    return i - j;
};
Console.WriteLine( Subtract(1, 2) );

 

以上的「Add」與「Subtract」都需要明確地指定目標型別(Target Type):「Func<int, int, int>」。

從C# 10,不需目標型別(Target Type),可以使用新的自然型別(Natural type),使用「var」關鍵字來宣告委派,讓C# 編譯器自動推論型別:

var Add = ( int i, int j ) => i + j;
Console.WriteLine( Add(1, 2) );

在Visual Studio 2022開發工具中撰寫程式時,當滑鼠移動到「Add」變數上方,會顯示「Add」的型別是「Func<int, int, int>」,請參考下圖所示:

clip_image016

圖 8:自動推論型別。

結構(Struct)

在C# 10之前,無法為結構定義一個不帶參數的建構函式,現在在C# 10,我們可以為結構定義不帶參數的建構函式,以自行撰寫結構成員的初始化邏輯,設定預設值。不帶參數的建構函式必需宣告為「public」且不可為「partial」,參考以下範例程式碼:

Contact customer = new();
customer.Display(); // DefaultName:DefaultTitle:123456789
Console.WriteLine( customer ); // DefaultName (DefaultTitle)

public struct Contact
{
    public Contact()
    {
        Name = "DefaultName";
        Title = "DefaultTitle";
        Phone = "123456789";
    }
    public string Name;
    public string Title;
    public string Phone;
    public void Display() => Console.WriteLine( $"{Name}:{Title}:{Phone}" );
    public override string ToString() => $"{Name} ({Title})";
};

 

我們需要使用「new ()」來叫用不帶參數的建構函式,以對結構中的欄位進行初始化,這個範例叫用「Display()」方法的執行結果將印出「DefaultName:DefaultTitle:123456789」字串。而當我們印出「customer」變數時,會叫用「ToString」方法,印出「DefaultName (DefaultTitle)」。

若使用「default」關鍵字來初始化結構,則不會叫用建構函式,根據結構成員的型別自動設定初始值,所有參考型別將設為「null」,數值型別則設為「0」;布林型別設為「false」。

Contact customer = default( Contact );
customer.Display(); //::
Console.WriteLine( customer ); // ()

public struct Contact
{
    public Contact()
    {
        Name = "DefaultName";
        Title = "DefaultTitle";
        Phone = "123456789";
    }
    public string Name;
    public string Title;
    public string Phone;
    public void Display() => Console.WriteLine( $"{Name}:{Title}:{Phone}" );
    public override string ToString() => $"{Name} ({Title})";
};

 

C# 10可以在宣告結構實體欄位(instance field)或屬性(Property)成員時,順帶初始化成員,例如以下程式碼範例:

Contact customer = new();
customer.Display(); // DefaultName:DefaultTitle:123456789
Console.WriteLine( customer ); // DefaultName (DefaultTitle)

public struct Contact
{

    public Contact()
    {
        Title = "DefaultTitle";
    }
    public string Name  = "DefaultName";
    public string Title { get; init; }
    public string Phone { get; init; } = "123456789";
    public void Display() => Console.WriteLine( $"{Name}:{Title}:{Phone}" );
    public override string ToString() => $"{Name} ({Title}) ";
};

 

Record struct

C# 記錄(Record)型別是一種類別或結構,通常當作資料模型(Data Model)來使用。C# 10 開始我們可以利用「record struct」來定義記錄,例如以下範例程式碼,定義一個名為「Coords」的record struct,包含兩個可讀寫屬性「X」與「Y」:

var coords = new Coords { X = 10, Y = 20 };
Console.WriteLine( coords ); // Coords { X = 10, Y = 20 }

coords.X = 100;
coords.Y = 200;

Console.WriteLine( coords ); // Coords { X = 100, Y = 200 }

public record struct Coords
{
    public int X { get; set; }
    public int Y { get; set; }
};

 

record struct中可以使用「init」存取子取代「set」存取子,定義只能初始化的屬性(init only property),這代表的含意是只有在建構函式,或宣告順帶初始化Coords成員時才能設定它的值,因此「coords.X = 100」這行程式碼將會發生錯誤:

var coords = new Coords { X = 10, Y = 20 };
Console.WriteLine( coords ); // Coords { X = 10, Y = 20 }

//coords.X = 100;  // Error CS8852    Init-only property or indexer 'Coords.X' can only be assigned in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor.
//coords.Y = 200;  // Error


public record struct Coords
{
    public int X { get; init; }
    public int Y { get; init; }
};

 

若我們改以下面的語法來定義record struct,在「Coords」名稱後方的主要建構函式(primary constructor)呼叫中宣告「X」 與「Y」兩個變數,「X」與「Y」就會自動成為「Coords」的公開自動屬性(public auto-implemented properties),和record class不同的地方在於,屬性是可讀寫的,因此我們可以利用「coords.X = 100」程式來修改成員的值:

 

var coords = new Coords { X = 10, Y = 20 };
Console.WriteLine( coords ); // Coords { X = 10, Y = 20 }

coords.X = 100;
coords.Y = 200;

Console.WriteLine( coords ); // Coords { X = 100, Y = 200 }

public record struct Coords ( int X, int Y );


若在主要建構函式(primary constructor)定義的變數名稱和屬性、欄位名稱相同,那麼就不會自動建立公開自動屬性(public auto-implemented properties),例如以下程式碼:

public record struct Coords( int X, int Y ) {
    public int X { get; set; } = 300;
};

編譯時Visual Studio 2022開發工具就會顯示以下的警告訊息:

Parameter 'X' is unread. Did you forget to use it to initialize the property with that name?

 

不可變的記錄結構(immutable record struct)

若要建立不可變的記錄結構(immutable record struct)可以使用「readonly」,你可以在record struct套用「readonly」或是在屬性的定義上套用。參考以下的範例程式碼,不可變的記錄結構(immutable record struct)不可修改成員的值,因此「coords.X = 100」程式碼將無法編譯:

var coords = new Coords { X = 10, Y = 20 };
Console.WriteLine( coords ); // Coords { X = 10, Y = 20 }

//coords.X = 100; //Error
//coords.Y = 200; //Error


public readonly  record struct Coords
{
    public int X { get; init; }
    public int Y { get; init; }
};

 

檢查參數是否為Null

在程式中常常需要檢查物件變數是否不為Null,再存取其成員,否則會產生「NullReferenceException」例外錯誤,例如以下程式碼範例,當「c」變數為「null」時,「Print(c)」這行程式碼就會產生「NullReferenceException」例外錯誤:

var c = new Contact() { Name = "Mary", Title = "Miss", Phone = "123456789" };
Print( c ); // Mary:Miss:123456789
c = null;
Print( c ); // System.NullReferenceException: Object reference not set to an instance of an object.
void Print( Contact contact )
{
    contact.Display();
}

public class Contact
{
    public string? Name { get; set; }
    public string? Title { get; set; }
    public string? Phone { get; set; }
    public void Display() => Console.WriteLine( $"{Name}:{Title}:{Phone}" );
};

 

為了避免例外錯誤,我們可能需要加上檢查程式碼,例如以下的「if」區塊會在「Print」方法的「contact」參數為「Null」的情況下,丟出「ArgumentNullException」例外錯誤:

 

var c = new Contact() { Name = "Mary", Title = "Miss", Phone = "123456789" };
Print( c ); // Mary:Miss:123456789
c = null;
Print( c! ); // Unhandled exception. System.ArgumentNullException: Value cannot be null. (Parameter 'contact')
void Print( Contact contact )
{
    if (contact is null)
    {
        throw new ArgumentNullException( nameof(contact) );
    }

    contact.Display();
}


public class Contact
{
    public string? Name { get; set; }
    public string? Title { get; set; }
    public string? Phone { get; set; }
    public void Display() => Console.WriteLine( $"{Name}:{Title}:{Phone}" );
};

 

在C# 10中可以改寫如下,使用「ArgumentNullException.ThrowIfNull」靜態方法來簡化程式碼,也不需要使用「nameof」運算子設定參數名稱,而會自動在檢查遇到Null值時產生「ArgumentNullException」例外錯誤,顯示和上例相同的錯誤訊息:

 

var c = new Contact() { Name = "Mary", Title = "Miss", Phone = "123456789" };
Print( c ); // Mary:Miss:123456789
c = null;
Print( c ); // Unhandled exception. System.ArgumentNullException: Value cannot be null. (Parameter 'contact')
void Print( Contact contact )
{
    ArgumentNullException.ThrowIfNull( contact );
    contact.Display();
}

public class Contact
{
    public string? Name { get; set; }
    public string? Title { get; set; }
    public string? Phone { get; set; }
    public void Display() => Console.WriteLine( $"{Name}:{Title}:{Phone}" );
};


ASP.NET Core Razor Pages資料分頁

$
0
0

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號:
N220223401
出刊日期: 2022/2/2

網頁資料分頁的功能每個Web專案可能都會用到,雖然可以從無到有自行撰寫分頁程式碼,不過拿現成的程式來套用也是一種選擇,最近測試幾個資料分頁元件,使用起來蠻方便的,在這篇文章中,我將分享一下如何在ASP.NET Core Razor Pages專案中套用「JW.Pager」套件。

「JW.Pager」套件的説明可參閱:https://github.com/cornflourblue/JW.Pager

以下的步驟說明如何在Visual Studio 2022開發工具建立一個ASP.NET Core Razor Pages專案,並透過「Entity Framewrok Core建立資料存取程式讀取微軟「Pubs」範例資料庫的「Authors」資料表資料,然後利用「JW.Pager」進行分頁。

啟動Visual Studio 2022開發工具,直接點選「Create a New Project」,選取「ASP.NET Core Web App」範本。請參考下圖所示:clip_image002

圖 1:建立ASP.NET Core 應用程式專案。

在「Configure your new project」對話盒中,設定專案名稱與專案存放路徑,然後按下「Next」鍵,請參考下圖所示:

clip_image004

圖 2:設定專案名稱與專案存放路徑。

在「Additional information」視窗,確認左上方的「Target Framework」清單選取「.NET 6.0 (Current)」,確定「Authentication Type」項目設定為「None」,勾選右下方的「Configure for HTTPS」核取方塊,清除勾選下方的「Enable Docker」、「Enable Razor runtime compilation」核取方塊,然後按下「Create」按鈕建立專案,請參考下圖所示:

clip_image006

圖 3:「Additional information」視窗。

安裝套件

在專案安裝「JW.Pager」套件,從「Solution Explorer」視窗 –> 專案名稱上方,按滑鼠右鍵,從快捷選單選擇「Manage NuGet Packages」項目。從對話盒「Browse」分頁上方文字方塊中,輸入查詢關鍵字「JW.Pager」,找到套件後,點選「Install」按鈕進行安裝,請參考下圖所示:

clip_image008

圖 4:安裝「JW.Pager」套件。

使用Entity Framewrok Core建立資料存取程式

接下來我們想透過Entity Framework Core的資料庫優先(Database First)方式存取SQL Server Express「Pubs」範例資料庫「Authors」資料表的資料。要在ASP.NET Core Web App類型的專案之中使用Entity Framewrok Core提供的類別來建立SQL Server Express資料庫的資料存取程式,得重複上個步驟,在專案中安裝以下套件:

  • 「Microsoft.EntityFrameworkCore.SqlServer」套件。

clip_image010

圖 5:安裝「Microsoft.EntityFrameworkCore.SqlServer」套件。

  • 「Microsoft.EntityFrameworkCore.Tools」套件。

clip_image012

圖 6:安裝「Microsoft.EntityFrameworkCore.Tools」套件。

下一步是從現有的「Pubs」資料庫來建立Enity Framework Core實體模型(Entity Model)。「Pubs」資料庫可以從微軟網站下載下來安裝:「https://docs.microsoft.com/zh-tw/dotnet/framework/data/adonet/sql/linq/downloading-sample-databases」。

從Visual Studio 2022開發工具「Tools」- 「Nuget Package Manager」項目開啟選單,從選單選擇「Package Manager Console」選項,開啟「Package Manager Console」對話盒,直接輸入並執行以下指令來指定資料庫連接字串、資料提供者(Provider),以進行反向工程步驟,這樣就可這以從現有的「Pubs」資料庫來建立模型:

Scaffold-DbContext "Server=.\sqlexpress;Database=Pubs;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models

指令後的「-OutputDir」參數用來指定將產生的類別存放的資料夾,以本例來說為「Models」資料夾,若資料夾不存在,則會自動建立。這個命令執行結果請參考下圖所示:

clip_image014

圖 7:進行反向工程。

完成後,專案中「Models」資料夾將包含許多C#類別程式碼,如下圖所示:

clip_image016

圖 5:工具自動產生的實體類別程式碼。

接下來我們需要在「Program.cs」檔案中註冊「DbContext」物件,以透過「Entity Framework Core」連結到資料庫,在「Program.cs」,加入以下程式碼,叫用「IServiceCollection」的「AddDbContext」方法,註冊「PubsContext」:

Program.cs

using Microsoft.EntityFrameworkCore;
using RPPaging.Models;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();

builder.Services.AddDbContext<PubsContext>(options =>
         options.UseSqlServer("Server=.\\sqlexpress;Database=Pubs;Trusted_Connection=True;"));


var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

 

修改首頁「Index.cshtml.cs」檔案程式碼,加入以下程式碼,查詢特定分頁要顯示的Authos資料:

Pages\Index.cshtml.cs

using JW;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using RPPaging.Models;

namespace RPPaging.Pages
{
    public class IndexModel : PageModel
    {
        private readonly RPPaging.Models.PubsContext _context;

        public IndexModel(RPPaging.Models.PubsContext context)
        {
            _context = context;
        }

        public IList<Author> Author { get; set; }

        public Pager Pager { get; set; }
        public int PageSize { get; set; } = 5;
        public int MaxPages { get; set; } = 10;
        public async Task OnGetAsync( int p = 1 )
        {
            var query = _context.Authors.AsQueryable();

            var count = await _context.Authors.CountAsync();


            Pager = new JW.Pager( count, p, PageSize, MaxPages );

            Author = await query.Skip( (Pager.CurrentPage - 1) * Pager.PageSize ).Take( Pager.PageSize ).ToListAsync();

        }
    }
}

修改「Index.cshtml」檔案程式碼如下,使用表格來呈現資料,下方則是資料分頁程式:

\Pages\Index.cshtml

@page
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>


<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor( model => model.Author[0].AuLname )
            </th>
            <th>
                @Html.DisplayNameFor( model => model.Author[0].AuFname )
            </th>
            <th>
                @Html.DisplayNameFor( model => model.Author[0].Phone )
            </th>
            <th>
                @Html.DisplayNameFor( model => model.Author[0].Address )
            </th>
            <th>
                @Html.DisplayNameFor( model => model.Author[0].City )
            </th>
            <th>
                @Html.DisplayNameFor( model => model.Author[0].State )
            </th>
            <th>
                @Html.DisplayNameFor( model => model.Author[0].Zip )
            </th>
            <th>
                @Html.DisplayNameFor( model => model.Author[0].Contract )
            </th>

        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Author)
        {
            <tr>
                <td>
                    @Html.DisplayFor( modelItem => item.AuLname )
                </td>
                <td>
                    @Html.DisplayFor( modelItem => item.AuFname )
                </td>
                <td>
                    @Html.DisplayFor( modelItem => item.Phone )
                </td>
                <td>
                    @Html.DisplayFor( modelItem => item.Address )
                </td>
                <td>
                    @Html.DisplayFor( modelItem => item.City )
                </td>
                <td>
                    @Html.DisplayFor( modelItem => item.State )
                </td>
                <td>
                    @Html.DisplayFor( modelItem => item.Zip )
                </td>
                <td>
                    @Html.DisplayFor( modelItem => item.Contract )
                </td>

            </tr>
        }
    </tbody>
</table>

<!-- pager -->
@{
    var baseUrl = ".";
}

@if (Model.Pager.Pages.Any())
{
    <nav class="table-responsive">
        <ul class="pagination justify-content-center d-flex flex-wrap">
            @if (Model.Pager.CurrentPage > 1)
            {
                <li class="page-item">
                    <a class="page-link mt-2" href="@baseUrl"> 第一筆 </a>
                </li>
                <li class="page-item">
                    <a class="page-link mt-2" href="@baseUrl/?p=@(Model.Pager.CurrentPage - 1)"> 上一筆 </a>
                </li>
            }

            @foreach (var p in Model.Pager.Pages)
            {
                <li class="page-item @(p == Model.Pager.CurrentPage ? "active" : "")">
                    <a class="page-link mt-2" href="@baseUrl/?p=@p">@p</a>
                </li>
            }

            @if (Model.Pager.CurrentPage < Model.Pager.TotalPages)
            {
                <li class="page-item">
                    <a class="page-link mt-2" href="@baseUrl/?p=@(Model.Pager.CurrentPage + 1)"> 下一筆 </a>
                </li>
                <li class="page-item">
                    <a class="page-link mt-2" href="@baseUrl/?p=@(Model.Pager.TotalPages)"> 最後一筆 </a>
                </li>
            }
        </ul>
    </nav>
}

測試與執行

選取Visual Studio 開發工具「Build」-「Build Solution」項目編譯目前的專案,確認程式碼能正確編譯。在Visual Studio開發工具,按CTRL+F5執行網站首頁,執行結果請參考下圖所示:

clip_image018

圖 6:資料分頁效果。

使用部分檢視

為了方便在不同的頁面重複使用分頁的功能,我們想將分頁程式碼抽離出來。在多個Razor Page要共用Razor程式與HTML,可以使用部分檢視(Partial view)來達成,這不是ASP.NET Core MVC專屬的功能。部分檢視 (Partial View)用來產生HTML,在父頁面(Razor Page)中可以使用「Html.PartialAsync」、「Html.Partial」 方法、或是使用<partial> 標記協助程式來插入部分檢視 (Partial View)。部分檢視 (Partial View)命名原則習慣以底線「_」符號(underscore )開始,通常放在「Pages/Shared」資料夾。

加入「_Pager.cshtml」部分檢視檔案。在「Solution Explorer」視窗, - 「Pages」-「Shared」資料夾項目上方,按滑鼠右鍵,從快捷選單選擇「Add」- 「New Item」項目。在「Add New Item」對話盒左方選取「Visual C#」分類,然後從清單中選取「Razor View - Empty」項目,將檔案名稱設定為「_Pager.cshtml」,然後按下「Add 」按鈕,參考下圖所示:

clip_image020

在「_Pager.cshtml」檔案中加入以下程式碼:

Pages\Shared\_Pager.cshtml

<!-- pager -->
@if (Model.Pages.Any())
{
    <nav class="table-responsive">
        <ul class="pagination justify-content-center d-flex flex-wrap">
            @if (Model.CurrentPage > 1)
            {
                <li class="page-item">
                    <a class="page-link mt-2" href="@baseUrl"> 第一筆 </a>
                </li>
                <li class="page-item">
                    <a class="page-link mt-2" href="@baseUrl/?p=@(Model.CurrentPage - 1)"> 上一筆 </a>
                </li>
            }

            @foreach (var p in Model.Pages)
            {
                <li class="page-item @(p == Model.CurrentPage ? "active" : "")">
                    <a class="page-link mt-2" href="@baseUrl/?p=@p">@p</a>
                </li>
            }

            @if (Model.CurrentPage < Model.TotalPages)
            {
                <li class="page-item">
                    <a class="page-link mt-2" href="@baseUrl/?p=@(Model.CurrentPage + 1)"> 下一筆 </a>
                </li>
                <li class="page-item">
                    <a class="page-link mt-2" href="@baseUrl/?p=@(Model.TotalPages)"> 最後一筆 </a>
                </li>
            }
        </ul>
    </nav>

 

修改「Index.cshtml」檔案,刪除出現在「_Pager.cshtml」檔案中資料分頁的程式,使用<partial>標籤插入「_Pager.cshtml」檔案執行結果,同時將「Pager」物件當作模型傳入:

Pages\Index.cshtml

@page
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>


<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor( model => model.Author[0].AuLname )
            </th>
            <th>
                @Html.DisplayNameFor( model => model.Author[0].AuFname )
            </th>
            <th>
                @Html.DisplayNameFor( model => model.Author[0].Phone )
            </th>
            <th>
                @Html.DisplayNameFor( model => model.Author[0].Address )
            </th>
            <th>
                @Html.DisplayNameFor( model => model.Author[0].City )
            </th>
            <th>
                @Html.DisplayNameFor( model => model.Author[0].State )
            </th>
            <th>
                @Html.DisplayNameFor( model => model.Author[0].Zip )
            </th>
            <th>
                @Html.DisplayNameFor( model => model.Author[0].Contract )
            </th>

        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Author)
        {
            <tr>
                <td>
                    @Html.DisplayFor( modelItem => item.AuLname )
                </td>
                <td>
                    @Html.DisplayFor( modelItem => item.AuFname )
                </td>
                <td>
                    @Html.DisplayFor( modelItem => item.Phone )
                </td>
                <td>
                    @Html.DisplayFor( modelItem => item.Address )
                </td>
                <td>
                    @Html.DisplayFor( modelItem => item.City )
                </td>
                <td>
                    @Html.DisplayFor( modelItem => item.State )
                </td>
                <td>
                    @Html.DisplayFor( modelItem => item.Zip )
                </td>
                <td>
                    @Html.DisplayFor( modelItem => item.Contract)
                </td>

            </tr>
        }
    </tbody>
</table>

<partial name="_Pager" model="@Model.Pager" />

 

目前「Index.cshtml.cs」檔案的內容如下:

Pages\Index.cshtml.cs

 

using JW;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using RPPaging.Models;

namespace RPPaging.Pages
{
    public class IndexModel : PageModel
    {
        private readonly RPPaging.Models.PubsContext _context;

        public IndexModel(RPPaging.Models.PubsContext context)
        {
            _context = context;
        }

        public IList<Author> Author { get; set; }

        public Pager Pager { get; set; }
        public int PageSize { get; set; } = 5;
        public int MaxPages { get; set; } = 10;
        public async Task OnGetAsync( int p = 1 )
        {
            var query = _context.Authors.AsQueryable();

            var count = await _context.Authors.CountAsync();


            Pager = new JW.Pager( count, p, PageSize, MaxPages );

            Author = await query.Skip( (Pager.CurrentPage - 1) * Pager.PageSize ).Take( Pager.PageSize ).ToListAsync();

        }
    }
}

客製化Scaffold樣板

$
0
0

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號:
N220223402
出刊日期: 2022/2/16

俗稱「工欲善其事,必先利其器」,在程式開發的過程中有了好的工具輔助能夠加快產能,在這篇文章中我們將介紹Visual Studio 2022的Scaffold功能,在ASP.NET Core MVC網站中快速設計一個控制器,透過Entity Framework Core存取資料庫,顯示圖書資料。而資料的排序的功能在網站程式中的其它控制器可能都會需要用到,我們希望修改將控制器的程式,加上排序資料功能,並將程式轉換成程式產生器的公版,以便後續利用程式碼產生器來自動產生控制器程式碼,讓產生出來的新控制器程式預設都有排序的功能。

 

使用Visual Studio 2022建立專案

從Visual Studio 2022「開始」視窗選取「Create a new project」選項,請參考下圖所示:

clip_image002

圖 1:「Create a new project」選項。

從開發工具「Create a new project」對話盒中,選取 使用C# 語法的「ASP.NET Core Web App(Model –View - Controller)」項目,然後按一下「Next」按鈕,請參考下圖所示:

clip_image004

圖 2:選取「ASP.NET Core Web App(Model –View - Controller)」項目。

在「Configure your new project」視窗中設定專案名稱、專案存放路徑,然後按下「Next」按鈕,請參考下圖所示:

clip_image006

圖 3:「Configure your new project」視窗。

在「Additional information」視窗,確認左上方的「Target Framework」清單選取「.NET 6.0 」,確定「Authentication Type」項目設定為「None」,勾選右下方的「Configure for HTTPS」核取方塊,清除勾選下方的「Enable Docker」、「Enable Razor runtime compilation」核取方塊,然後按下「Create」按鈕建立專案,請參考下圖所示:

clip_image008

圖 4:「Additional information」視窗。

加入模型類別

下一個步驟是加入「Book」模型描述圖書資料,從「Solution Explorer」視窗 -「Models」資料夾上方,按滑鼠右鍵,從快捷選單選擇「Add」- 「Class」新增一個「Book」類別,請參考下圖所示:

clip_image010

圖 5:加入一個「Book」類別。

在「Book」類別之中加入以下屬性,描述圖書資料:

namespace CustomScaffold.Models
{
    public class Book
    {
        public int Id { get; set; }
        public string? Title { get; set; }
        public int Price { get; set; }
        public DateTime PublishDate { get; set; }
        public bool InStock { get; set; }
        public string? Description { get; set; }

    }
}

 

使用Scaffold功能產生程式碼

接著透過Visual Studio Scaffold功能產生使用Entity Framework Core 存取資料庫的控制器與檢視的程式碼,從「Solution Explorer」視窗「Controllers」資料夾上方按滑鼠右鍵,從快捷選單選擇「Add」- 「New Scaffolded Item」項目,請參考下圖所示:

clip_image012

圖 6:加入控制器。

在「Add New Scaffolded Item」對話盒中選取「MVC Controller with Views, using Entity Framework」,然後按下「Add 」按鈕,請參考下圖所示:

clip_image014

圖 7:加入控制器。

在下一個畫面,設定「Model Class」為「Book」類別,按下「Data context class」後方的「+」按鈕新增一個Data context class,請參考下圖所示:

clip_image016

圖 8:新增一個data context class。

將「New data context type」設定為「BooksContext」,再按下「Add」按鈕,請參考下圖所示:

clip_image018

圖 9:設定Data Context名稱。

回到前一個畫面,設定控制器名稱,然後按下「Add 」按鈕,請參考下圖所示:

clip_image020

圖 10:新增控制器。

接著Visual Studio 2022會自動在「Controllers」資料夾產生「BooksController」的程式碼。同時Visual Studio也會在Views\Books資料夾中產生多個檢視的檔案。此外,「appsettings.json」檔案之中則記錄了資料庫連接字串的資訊,預設會將資料放在LocalDb資料庫:

\CustomScaffold\appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "BooksContext": "Server=(localdb)\\mssqllocaldb;Database=BooksContext-e9e53531-6eb4-4577-b8d7-8ea28faac5fd;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}

 

Visual Studio 2022工具也會幫我們在「Program.cs」檔案中產生註冊Entity Framework Core服務的程式碼:

\CustomScaffold\Program.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using CustomScaffold.Data;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<BooksContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("BooksContext")));

// Add services to the container.
builder.Services.AddControllersWithViews();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

 

 

使用EF Core移轉建立資料庫

這個步驟將使用EF Core移轉(Entity Framework Core Migration)功能建立資料庫。從Visual Studio 2022開發工具「Tools」-「NuGet Package Manager」-「Package Manager Console」開啟「Package Manager Console」視窗,然後在命令提示字元中輸入「add-migration」指令,取一個名稱如「initial」:

Add-Migration initial

接著Visual Studio 2022會在專案中建立一個「Migrations」資料夾,裏頭包含多個C#檔案,包含變更資料庫結構的程式碼。在命令提示字元中輸入「update-database」指令,以更新資料庫:

Update-Database

得到的執行結果如下圖所示:

clip_image002[4]

圖 11:使用EF移轉更新資料庫。

為了方便測試我們在「_Layout.cshtml」檔案中加入「Books」與「Create」兩個選單項目,以連結到「Books」控制器的「Index」與「Create」動作:

\CustomScaffold\Views\Shared\_Layout.cshtml

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - CustomScaffold</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
    <link rel="stylesheet" href="~/CustomScaffold.styles.css" asp-append-version="true" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container-fluid">
                <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">CustomScaffold</a>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Books" asp-action="Index">Books</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Books" asp-action="Create">Create</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    </header>
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2021 - CustomScaffold - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
        </div>
    </footer>
    <script src="~/lib/jquery/dist/jquery.min.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>
    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>

 

在Visual Studio開發工具,按CTRL+F5執行網站,執行結果參考如下,我們可以透過目前的網站程式碼進行圖書資料的新、刪、查、改動作:

clip_image004[4]

圖 12:資料新增。

以下則是圖書清單畫面:

clip_image006[4]

圖 13:資料清單。

設計資料排序程式

修改工具產生出來的「BooksController」類別「Index」方法程式碼,傳入一個「sortOrder」參數來決定排序的欄位:

\CustomScaffold\Controllers\BooksController.cs

#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using CustomScaffold.Data;
using CustomScaffold.Models;

namespace CustomScaffold.Controllers
{
    public class BooksController : Controller
    {
        private readonly BooksContext _context;

        public BooksController(BooksContext context)
        {
            _context = context;
        }

        // GET: Books
        public async Task<IActionResult> Index(string sortOrder)
        {
            ViewData["TitleSortParm"] = String.IsNullOrEmpty(sortOrder) ? "title_desc" : "";
            ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
            var books = from s in _context.Book
                           select s;

            switch (sortOrder)
            {
                case "title_desc":
                    books = books.OrderByDescending(b => b.Title);
                    break;
                case "Date":
                    books = books.OrderBy(b => b.PublishDate);
                    break;
                case "date_desc":
                    books = books.OrderByDescending(s => s.PublishDate);
                    break;
                default:
                    books = books.OrderBy(s => s.Title);
                    break;
            }
            return View(await books.AsNoTracking().ToListAsync());
        }

        // GET: Books/Details/5
        public async Task<IActionResult> Details(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var book = await _context.Book
                .FirstOrDefaultAsync(m => m.Id == id);
            if (book == null)
            {
                return NotFound();
            }

            return View(book);
        }

        // GET: Books/Create
        public IActionResult Create()
        {
            return View();
        }

        // POST: Books/Create
        // To protect from overposting attacks, enable the specific properties you want to bind to.
        // For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create([Bind("Id,Title,Price,PublishDate,InStock,Description")] Book book)
        {
            if (ModelState.IsValid)
            {
                _context.Add(book);
                await _context.SaveChangesAsync();
                return RedirectToAction(nameof(Index));
            }
            return View(book);
        }

        // GET: Books/Edit/5
        public async Task<IActionResult> Edit(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var book = await _context.Book.FindAsync(id);
            if (book == null)
            {
                return NotFound();
            }
            return View(book);
        }

        // POST: Books/Edit/5
        // To protect from overposting attacks, enable the specific properties you want to bind to.
        // For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Edit(int id, [Bind("Id,Title,Price,PublishDate,InStock,Description")] Book book)
        {
            if (id != book.Id)
            {
                return NotFound();
            }

            if (ModelState.IsValid)
            {
                try
                {
                    _context.Update(book);
                    await _context.SaveChangesAsync();
                }
                catch (DbUpdateConcurrencyException)
                {
                    if (!BookExists(book.Id))
                    {
                        return NotFound();
                    }
                    else
                    {
                        throw;
                    }
                }
                return RedirectToAction(nameof(Index));
            }
            return View(book);
        }

        // GET: Books/Delete/5
        public async Task<IActionResult> Delete(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var book = await _context.Book
                .FirstOrDefaultAsync(m => m.Id == id);
            if (book == null)
            {
                return NotFound();
            }

            return View(book);
        }

        // POST: Books/Delete/5
        [HttpPost, ActionName("Delete")]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> DeleteConfirmed(int id)
        {
            var book = await _context.Book.FindAsync(id);
            _context.Book.Remove(book);
            await _context.SaveChangesAsync();
            return RedirectToAction(nameof(Index));
        }

        private bool BookExists(int id)
        {
            return _context.Book.Any(e => e.Id == id);
        }
    }
}

 

修改「Views\Books\Index.cshtml」檢視的程式碼,在顯示資料的Table表頭使用超連結以切換排序方向性:

\CustomScaffold\Views\Books\Index.cshtml

@model IEnumerable<CustomScaffold.Models.Book>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                <a asp-action="Index" asp-route-sortOrder="@ViewData["TitleSortParm"]">@Html.DisplayNameFor(model => model.Title)</a>
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Price)
            </th>
            <th>
                 <a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]">@Html.DisplayNameFor(model => model.PublishDate)</a>
            </th>
            <th>
                @Html.DisplayNameFor(model => model.InStock)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Description)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.PublishDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.InStock)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Description)
            </td>
            <td>
                <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

 

執行這個網站,現在圖書資料可以根據「Title」與「PublishDate」進行排序動作,請參考下圖所示:

clip_image008[4]

圖 14:圖書資料排序。

客製化程式碼產生器樣板

接下來讓我們來談談客製化動作,目地是後續透過Visual Studio Scaffold功能產生出的控制器程式都可以根據「Title」與「PublishDate」進行排序。

首先要安裝程式碼產生器套件,從「Solution Explorer」視窗 –> 專案名稱上方,按滑鼠右鍵,從快捷選單選擇「Manage NuGet Packages」項目,從對話盒「Browse」分頁上方文字方塊中,輸入查詢關鍵字,找到「Microsoft.VisualStudio.Web.CodeGeneration.Design」套件後,點選「Install」按鈕進行安裝,請參考下圖所示請參考下圖所示:

clip_image010[4]

圖 15:安裝「Microsoft.VisualStudio.Web.CodeGeneration.Design」套件。

從以下資料夾複製「Templates」資料夾中想客製化的檔案到專案,這個資料夾會在你進行文章上述的Scaffold動作時自動建立:

C:\Users\使用者帳號\.nuget\packages\microsoft.visualstudio.web.codegenerators.mvc\6.0.1\Templates

以本文範例而言只會使用到「Templates\ControllerGenerator」與「Templates\ViewGenerator」兩個資料夾的程式碼,目前資料夾看起來如下圖所示:

clip_image012[4]

圖 16:加入樣板。

修改「MvcControllerWithContext.cshtml」檔案產生「Index」方法的程式,約在行號84開始的程式碼,如下:

\CustomScaffold\Templates\ControllerGenerator\MvcControllerWithContext.cshtml

@inherits Microsoft.VisualStudio.Web.CodeGeneration.Templating.RazorTemplateBase
@{
if (@Model.NullableEnabled)
{
@:#nullable disable

}
}
@using System.Collections.Generic;
@using System.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
@{
    foreach (var namespaceName in Model.RequiredNamespaces)
    {
@:using @namespaceName;
    }
}

namespace @Model.ControllerNamespace
{
@{
    string routePrefix;
    if (String.IsNullOrEmpty(Model.AreaName))
    {
        routePrefix = Model.ControllerRootName;
    }
    else
    {
        routePrefix = Model.AreaName + "/" + Model.ControllerRootName;
    }
    var modelProperties = new List<string>();
    foreach (var property in Model.ModelMetadata.Properties)
    {
        if (property.Scaffold)
        {
            modelProperties.Add(property.PropertyName);
        }
    }
    var bindString = string.Join(",", modelProperties);
    var contextTypeName = Model.ContextTypeName;
    var entitySetName = Model.ModelMetadata.EntitySetName;
    var entitySetVar = Model.EntitySetVariable ??
        (String.IsNullOrEmpty(entitySetName)
            ? entitySetName
            : (entitySetName.Substring(0, length: 1).ToLowerInvariant() + entitySetName.Substring(1)));
    var primaryKeyName = Model.ModelMetadata.PrimaryKeys[0].PropertyName;
    var primaryKeyShortTypeName = Model.ModelMetadata.PrimaryKeys[0].ShortTypeName;
    var primaryKeyType = Model.ModelMetadata.PrimaryKeys[0].TypeName;
    var primaryKeyNullableTypeName = GetNullableTypeName(primaryKeyType, primaryKeyShortTypeName);
    var lambdaVar = Model.ModelVariable[0];
    var relatedProperties = new Dictionary<string, dynamic>();
    foreach (var nav in Model.ModelMetadata.Navigations)
    {
        relatedProperties.Add(nav.AssociationPropertyName, nav);

    }

    var inlineIncludes = "";
    foreach (var property in relatedProperties.Values)
    {
        inlineIncludes += string.Format("{0}                .Include({1} => {1}.{2})", Environment.NewLine, lambdaVar, property.AssociationPropertyName);
    }

    if (!string.IsNullOrEmpty(Model.AreaName))
    {
    @:@string.Format("[Area(\"{0}\")]", Model.AreaName)
    }
}
    public class @Model.ControllerName : Controller
    {
        private readonly @Model.ContextTypeName _context;

        public @(Model.ControllerName)(@Model.ContextTypeName context)
        {
            _context = context;
        }

        // GET: @routePrefix
        public async Task<IActionResult> Index(string sortOrder)
        {
            ViewData["TitleSortParm"] = String.IsNullOrEmpty(sortOrder) ? "title_desc" : "";
            ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
            var @Model.ModelVariable = from s in _context.@(entitySetName) select s;
            switch (sortOrder)
            {
                   case "title_desc":
                       @(Model.ModelVariable) = @(Model.ModelVariable).OrderByDescending(b => b.Title);
                       break;
                   case "Date":
                       @(Model.ModelVariable) = @(Model.ModelVariable).OrderBy(b => b.PublishDate);
                       break;
                   case "date_desc":
                       @(Model.ModelVariable) = @(Model.ModelVariable).OrderByDescending(b => b.PublishDate);
                       break;
                   default:
                       @(Model.ModelVariable) = @(Model.ModelVariable).OrderBy(b => b.Title);
                       break;
            }
            return View(await @(Model.ModelVariable).AsNoTracking().ToListAsync());
        }

        // GET: @routePrefix/Details/5
        public async Task<IActionResult> Details(@primaryKeyNullableTypeName id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var @Model.ModelVariable = await _context.@(entitySetName)@inlineIncludes
                .FirstOrDefaultAsync(m => m.@primaryKeyName == id);
            if (@Model.ModelVariable == null)
            {
                return NotFound();
            }

            return View(@Model.ModelVariable);
        }

        // GET: @routePrefix/Create
        public IActionResult Create()
        {
@{
    foreach (var property in relatedProperties.Values)
    {
            @:ViewData["@(property.ForeignKeyPropertyNames[0])"] = new SelectList(_context.@property.EntitySetName, "@property.PrimaryKeyNames[0]", "@property.DisplayPropertyName");
    }
}            return View();
        }

        // POST: @routePrefix/Create
        // To protect from overposting attacks, enable the specific properties you want to bind to.
        // For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create([Bind("@bindString")] @Model.ModelTypeName @Model.ModelVariable)
        {
            if (ModelState.IsValid)
            {
@{
    if (!string.IsNullOrEmpty(primaryKeyType) && IsGuid(primaryKeyType))
    {
                @:@(Model.ModelVariable).@primaryKeyName = Guid.NewGuid();
    }
                @:_context.Add(@Model.ModelVariable);
                @:await _context.SaveChangesAsync();
}                return RedirectToAction(nameof(Index));
            }
@{
    foreach (var property in relatedProperties.Values)
    {
            @:ViewData["@(property.ForeignKeyPropertyNames[0])"] = new SelectList(_context.@property.EntitySetName, "@property.PrimaryKeyNames[0]", "@property.DisplayPropertyName", @(Model.ModelVariable).@property.ForeignKeyPropertyNames[0]);
    }
}
            return View(@Model.ModelVariable);
        }

        // GET: @routePrefix/Edit/5
        public async Task<IActionResult> Edit(@primaryKeyNullableTypeName id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var @Model.ModelVariable = await _context.@(entitySetName).FindAsync(id);
            if (@Model.ModelVariable == null)
            {
                return NotFound();
            }
@{
    foreach (var property in relatedProperties.Values)
    {
            @:ViewData["@(property.ForeignKeyPropertyNames[0])"] = new SelectList(_context.@property.EntitySetName, "@property.PrimaryKeyNames[0]", "@property.DisplayPropertyName", @(Model.ModelVariable).@property.ForeignKeyPropertyNames[0]);
    }
}
            return View(@Model.ModelVariable);
        }

        // POST: @routePrefix/Edit/5
        // To protect from overposting attacks, enable the specific properties you want to bind to.
        // For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Edit(@primaryKeyShortTypeName id, [Bind("@bindString")] @Model.ModelTypeName @Model.ModelVariable)
        {
            if (id != @Model.ModelVariable.@primaryKeyName)
            {
                return NotFound();
            }

            if (ModelState.IsValid)
            {
                try
                {
                    _context.Update(@Model.ModelVariable);
                    await _context.SaveChangesAsync();
                }
                catch (DbUpdateConcurrencyException)
                {
                    if (!@(Model.ModelTypeName)Exists(@Model.ModelVariable.@primaryKeyName))
                    {
                        return NotFound();
                    }
                    else
                    {
                        throw;
                    }
                }
                return RedirectToAction(nameof(Index));
            }
@{
    foreach (var property in relatedProperties.Values)
    {
            @:ViewData["@(property.ForeignKeyPropertyNames[0])"] = new SelectList(_context.@property.EntitySetName, "@property.PrimaryKeyNames[0]", "@property.DisplayPropertyName", @(Model.ModelVariable).@property.ForeignKeyPropertyNames[0]);
    }
}
            return View(@Model.ModelVariable);
        }

        // GET: @routePrefix/Delete/5
        public async Task<IActionResult> Delete(@primaryKeyNullableTypeName id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var @Model.ModelVariable = await _context.@(entitySetName)@inlineIncludes
                .FirstOrDefaultAsync(m => m.@primaryKeyName == id);
            if (@Model.ModelVariable == null)
            {
                return NotFound();
            }

            return View(@Model.ModelVariable);
        }

        // POST: @routePrefix/Delete/5
        [HttpPost, ActionName("Delete")]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> DeleteConfirmed(@primaryKeyShortTypeName id)
        {
            var @Model.ModelVariable = await _context.@(entitySetName).FindAsync(id);
            _context.@(entitySetName).Remove(@Model.ModelVariable);
            await _context.SaveChangesAsync();
            return RedirectToAction(nameof(Index));
        }

        private bool @(Model.ModelTypeName)Exists(@primaryKeyShortTypeName id)
        {
            return _context.@(entitySetName).Any(e => e.@primaryKeyName == id);
        }
    }
}
@functions
{
    // This function converts the primary key short type name to its nullable equivalent when possible. This is required to make
    // sure that an HTTP 400 error is thrown when the user tries to access the edit, delete, or details action with null values.
    string GetNullableTypeName(string typeName, string shortTypeName)
    {
        // The exceptions are caught because if for any reason the type is user defined, then the short type name will be used.
        // In that case the user will receive a server error if null is passed to the edit, delete, or details actions.
        Type primaryKeyType = null;
        try
        {
            primaryKeyType = Type.GetType(typeName);
        }
        catch
        {
        }
        if (primaryKeyType != null && (!Microsoft.VisualStudio.Web.CodeGeneration.Templating.TypeUtilities.IsNullable(primaryKeyType) || IsGuid(typeName)))
        {
            return shortTypeName + "?";
        }
        return shortTypeName;
    }

    bool IsGuid(string typeName) {
        return String.Equals("System.Guid", typeName, StringComparison.OrdinalIgnoreCase);
    }
}

 

修改「Templates\ViewGenerator\List.cshtml」檔案的程式碼,約在行號54開始的程式碼:

\CustomScaffold\Templates\ViewGenerator\List.cshtml

@inherits Microsoft.VisualStudio.Web.CodeGeneration.Templating.RazorTemplateBase
@using Microsoft.VisualStudio.Web.CodeGeneration.EntityFrameworkCore
@using System.Collections.Generic
@using System.Linq
@@model @GetEnumerableTypeExpression(Model.ViewDataTypeName)

@{
    if (Model.IsPartialView)
    {
    }
    else if (Model.IsLayoutPageSelected)
    {
@:@@{
    @:ViewData["Title"] = "@Model.ViewName";
        if (!string.IsNullOrEmpty(Model.LayoutPageFile))
        {
    @:Layout = "@Model.LayoutPageFile";
        }
@:}
@:
@:<h1>@Model.ViewName</h1>
@:
    }
    else
    {
@:@@{
    @:Layout = null;
@:}
@:
@:<!DOCTYPE html>
@:
@:<html>
@:<head>
    @:<meta name="viewport" content="width=device-width" />
    @:<title>@Model.ViewName</title>
@:</head>
@:<body>
        //    PushIndent("    ");
    }
@:<p>
    @:<a asp-action="Create">Create New</a>
@:</p>
@:<table class="table">
    @:<thead>
        @:<tr>
        Dictionary<string, IPropertyMetadata> propertyLookup = ((IModelMetadata)Model.ModelMetadata).Properties.ToDictionary(x => x.PropertyName, x => x);
        Dictionary<string, INavigationMetadata> navigationLookup = ((IModelMetadata)Model.ModelMetadata).Navigations.ToDictionary(x => x.AssociationPropertyName, x => x);

        foreach (var item in Model.ModelMetadata.ModelType.GetProperties())
        {
            if (propertyLookup.TryGetValue(item.Name, out IPropertyMetadata property)
                && property.Scaffold && !property.IsForeignKey && !property.IsPrimaryKey)
            {
                if (item.Name == "Title")
                {
                    <th>
                         <a asp-action="Index" asp-route-sortOrder="@@ViewData["TitleSortParm"]">
                           @@Html.DisplayNameFor(model => model.Title)
                         </a>
                    </th>   
                }
                else if(item.Name == "PublishDate")
                {
                    <th>
                        <a asp-action="Index" asp-route-sortOrder="@@ViewData["DateSortParm"]">
                           @@Html.DisplayNameFor(model => model.PublishDate)
                        </a>
                    </th>   
                }
                else
                {
                    <th>
                        @@Html.DisplayNameFor(model => model.@GetValueExpression(property))
                    </th>   
                }
           
            }
            else if (navigationLookup.TryGetValue(item.Name, out INavigationMetadata navigation))
            {
            <th>
                @@Html.DisplayNameFor(model => model.@GetValueExpression(navigation))
            </th>
            }
        }
            @:<th></th>
        @:</tr>
    @:</thead>
    @:<tbody>
@:@@foreach (var item in Model) {
        @:<tr>
        foreach (var item in Model.ModelMetadata.ModelType.GetProperties())
        {
            if (propertyLookup.TryGetValue(item.Name, out IPropertyMetadata property)
                && property.Scaffold && !property.IsForeignKey && !property.IsPrimaryKey)
            {
            <td>
                @@Html.DisplayFor(modelItem => item.@GetValueExpression(property))
            </td>
            }
            else if (navigationLookup.TryGetValue(item.Name, out INavigationMetadata navigation))
            {
            <td>
                @@Html.DisplayFor(modelItem => item.@GetValueExpression(navigation).@navigation.DisplayPropertyName)
            </td>
            }
        }
        string pkName = GetPrimaryKeyName();
        if (pkName != null)
        {
            @:<td>
                @:<a asp-action="Edit" asp-route-id="@@item.@pkName">Edit</a> |
                @:<a asp-action="Details" asp-route-id="@@item.@pkName">Details</a> |
                @:<a asp-action="Delete" asp-route-id="@@item.@pkName">Delete</a>
            @:</td>
        }
        else
        {
            <td>
                @@Html.ActionLink("Edit", "Edit", new { /* id=item.PrimaryKey */ }) |
                @@Html.ActionLink("Details", "Details", new { /* id=item.PrimaryKey */ }) |
                @@Html.ActionLink("Delete", "Delete", new { /* id=item.PrimaryKey */ })
            </td>
        }
        @:</tr>
@:}
    @:</tbody>

@:</table>
    if(!Model.IsPartialView && !Model.IsLayoutPageSelected)
    {
        //ClearIndent();
@:</body>
@:</html>
    }
}
@functions
{
    string GetPrimaryKeyName()
    {
        return (Model.ModelMetadata.PrimaryKeys != null && Model.ModelMetadata.PrimaryKeys.Length == 1)
        ? Model.ModelMetadata.PrimaryKeys[0].PropertyName
        : null;
    }

    string GetValueExpression(IPropertyMetadata property)
    {
        return property.PropertyName;
    }

    string GetValueExpression(INavigationMetadata navigation)
    {
        return navigation.AssociationPropertyName;
    }

    string GetEnumerableTypeExpression(string typeName)
    {
        return "IEnumerable<" + typeName + ">";
    }
}

 

好了,現在可以來測試我們客製化的程式碼產生器樣板,刪除「BooksController.cs」檔案,重複前述文章的步驟,透過Visual Studio Scaffold功能重新產生程式碼。

從「Solution Explorer」視窗「Controllers」資料夾上方按滑鼠右鍵,從快捷選單選擇「Add」- 「New Scaffolded Item」項目。在「Add New Scaffolded Item」對話盒中選取「MVC Controller with Views, using Entity Framework」,然後按下「Add 」按鈕。

在下一個畫面,設定Model Class為「Book」類別,並設定控制器名稱,請參考下圖所示:

clip_image014[4]

圖 17:使用Scaffold功能產生程式碼。

若一切順利的話,便可重新執行程式碼,這個範例應該都能正常運作。產生出來的「BooksController」程式碼如下:

\CustomScaffold\Controllers\BooksController.cs

#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using CustomScaffold.Data;
using CustomScaffold.Models;

namespace CustomScaffold.Controllers
{
    public class BooksController : Controller
    {
        private readonly BooksContext _context;

        public BooksController(BooksContext context)
        {
            _context = context;
        }

        // GET: Books
        public async Task<IActionResult> Index(string sortOrder)
        {
            ViewData["TitleSortParm"] = String.IsNullOrEmpty(sortOrder) ? "title_desc" : "";
            ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
            var book = from s in _context.Book select s;
            switch (sortOrder)
            {
                   case "title_desc":
book = book.OrderByDescending(b => b.Title);
                       break;
                   case "Date":
book = book.OrderBy(b => b.PublishDate);
                       break;
                   case "date_desc":
book = book.OrderByDescending(b => b.PublishDate);
                       break;
                   default:
book = book.OrderBy(b => b.Title);
                       break;
            }
            return View(await book.AsNoTracking().ToListAsync());
        }

        // GET: Books/Details/5
        public async Task<IActionResult> Details(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var book = await _context.Book
                .FirstOrDefaultAsync(m => m.Id == id);
            if (book == null)
            {
                return NotFound();
            }

            return View(book);
        }

        // GET: Books/Create
        public IActionResult Create()
        {
            return View();
        }

        // POST: Books/Create
        // To protect from overposting attacks, enable the specific properties you want to bind to.
        // For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create([Bind("Id,Title,Price,PublishDate,InStock,Description")] Book book)
        {
            if (ModelState.IsValid)
            {
                _context.Add(book);
                await _context.SaveChangesAsync();
                return RedirectToAction(nameof(Index));
            }
            return View(book);
        }

        // GET: Books/Edit/5
        public async Task<IActionResult> Edit(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var book = await _context.Book.FindAsync(id);
            if (book == null)
            {
                return NotFound();
            }
            return View(book);
        }

        // POST: Books/Edit/5
        // To protect from overposting attacks, enable the specific properties you want to bind to.
        // For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Edit(int id, [Bind("Id,Title,Price,PublishDate,InStock,Description")] Book book)
        {
            if (id != book.Id)
            {
                return NotFound();
            }

            if (ModelState.IsValid)
            {
                try
                {
                    _context.Update(book);
                    await _context.SaveChangesAsync();
                }
                catch (DbUpdateConcurrencyException)
                {
                    if (!BookExists(book.Id))
                    {
                        return NotFound();
                    }
                    else
                    {
                        throw;
                    }
                }
                return RedirectToAction(nameof(Index));
            }
            return View(book);
        }

        // GET: Books/Delete/5
        public async Task<IActionResult> Delete(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var book = await _context.Book
                .FirstOrDefaultAsync(m => m.Id == id);
            if (book == null)
            {
                return NotFound();
            }

            return View(book);
        }

        // POST: Books/Delete/5
        [HttpPost, ActionName("Delete")]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> DeleteConfirmed(int id)
        {
            var book = await _context.Book.FindAsync(id);
            _context.Book.Remove(book);
            await _context.SaveChangesAsync();
            return RedirectToAction(nameof(Index));
        }

        private bool BookExists(int id)
        {
            return _context.Book.Any(e => e.Id == id);
        }
    }
}

 

「Index.cshtml」檔案是根據「List.cshtml」樣板來產生程式碼,產生出來的「Index.cshtml」檔案程式如下:

\CustomScaffold\Views\Books\Index.cshtml

@model IEnumerable<CustomScaffold.Models.Book>

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>
</head>
<body>
<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
                    <th>
                         <a asp-action="Index" asp-route-sortOrder="@ViewData["TitleSortParm"]">
                           @Html.DisplayNameFor(model => model.Title)
                         </a>
                    </th>   
                    <th>
                        @Html.DisplayNameFor(model => model.Price)
                    </th>   
                    <th>
                        <a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]">
                           @Html.DisplayNameFor(model => model.PublishDate)
                        </a>
                    </th>   
                    <th>
                        @Html.DisplayNameFor(model => model.InStock)
                    </th>   
                    <th>
                        @Html.DisplayNameFor(model => model.Description)
                    </th>   
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.PublishDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.InStock)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Description)
            </td>
            <td>
                <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>
</body>
</html>

 

使用aspnet-codegenerator

在開發過程中重複使用Visual Studio 2022 選單的「Add」>「 New Scaffolded Item」功能來測試程式碼產生器產生的程式碼有一點煩雜。我們可以利用指令來加快這個開發流程。若要重複產生多個控制器與檢視程式碼,直接執行「aspnet-codegenerator」會比手動操作Visual Studio快很多,將之儲存成批次檔以方便重複執行。

例如我們可以在專案根目錄下加入一個「run.bat」批次檔,然後加入以下指令,先透過「del」刪除先前產生的「BooksController.cs」檔案,然後使用「dotnet」執行「aspnet-codegenerator」指定以下參數:「controller」表示要產生控制器程式碼;「-name」指定控制器名稱為「BooksController」;「-m」指定模型類別為「Book」;「-dc」指定DbContext類別為「BooksContext 」;「-outDir」指定要將檔案放在「Controllers」資料夾;「-f」則是在檔案存在時,強迫覆寫檔案:

\CustomScaffold\run.bat

del "Controllers\BooksController.cs" 2>NUL

dotnet aspnet-codegenerator controller -name BooksController -m Book -dc BooksContext -outDir Controllers -f

我們可以在「Package Manager Console」執行這個批次檔,這個命令執行結果,請參考下圖所示:

clip_image016[4]

圖 18:使用批次檔產生程式。

若要套用指定的版面配置頁,可以修改批次檔,設定「--layout」參數:

del "Controllers\BooksController.cs" 2>NUL

dotnet aspnet-codegenerator controller -name BooksController -m Book -dc BooksContext -outDir Controllers -f --layout _Layout.cshtml

產生的「Index」檢視程式如下,程式上方會加入「Layout」屬性的設定:

@model IEnumerable<CustomScaffold.Models.Book>

@{
    ViewData["Title"] = "Index";
    Layout = "_Layout.cshtml";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
//以下略


 

若要套用預設的版面配置頁,可以修改批次檔,設定「-- useDefaultLayout」參數:

del "Controllers\BooksController.cs" 2>NUL

dotnet aspnet-codegenerator controller -name BooksController -m Book -dc BooksContext -outDir Controllers -f --useDefaultLayout

產生的Index檢視程式如下,會移除<Html>、<Body>等HTML標籤:

@model IEnumerable<CustomScaffold.Models.Book>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table"
//以下略


 

更多aspnet-codegenerator參數請參考官方文件:

https://docs.microsoft.com/en-us/aspnet/core/fundamentals/tools/dotnet-aspnet-codegenerator?view=aspnetcore-6.0

記錄

$
0
0

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號: N220423601
出刊日期: 2022/4/13

不可變物件(Immutable object)是一種建立之後就不能夠變更的物件,讓物件在多執行緒的應用程式中達到執行緒安全(Thread Safe),有助於改善應用程式記憶體管理,程式也比較容易維護。

C# 提供多種語法來達到物件的不變性,例如僅供初始化的屬性(Init only Property)、記錄(Record型別等等。僅供初始化的屬性(Init only Property)可讓物件的個別屬性唯讀、不可變;而記錄(Record)型別則可以讓整個物件不可變,非常適合在要求執行緒安全(Thread Safe)的應用程式之中使用。在這篇文章中,將要介紹記錄(Record)型別。

 

使用Visual Studio 2022新增主控台應用程式專案

為了便於說明,我們將使用Visual Studio 2022新增主控台應用程式專案,Visual Studio 2022主控台應用程式採用最上層語句(Top-level Statements)這個功能,簡化範例程式碼。先用Visual Studio建立一個「Console App」的專案:

clip_image002[4]

圖 1:建立新專案。

設定專案名稱:

clip_image004[4]

圖 2:設定專案名稱。

並將Framework設為「.NET 6」:

clip_image006[4]

圖 3:設定Framework。

預設「Console App」專案樣板會在新建的專案中,自動加入一個名為「Program.cs」的程式檔,我們可以在這個檔案中加入程式碼。

僅供初始化的屬性(Init only Property)

僅供初始化的屬性(Init only Property)是一個只有在物件初始化時才可以指派值的屬性。在C#可以使用init存取子(Accessor)來定義僅供初始化的屬性(Property),屬性的值不可以在其它地方修改,僅可在初始化物件時,使用物件初始設定式語法或在建構函式之中設定屬性值。

以下範例中的「Book」物件之屬性初始化之後就不可以修改屬性的值:

var book = new Book { Id = 1 , Title = "Programming C#" , Publisher = "APublisher" , Price = 500 };
Console.WriteLine($@"
Id        : {book.Id}
Title     : {book.Title}
Publisher : {book.Publisher}
Price     : {book.Price}"
);

book.Id = 2; //Error
book.Title = "C# Programming"; //Error
book.Publisher = "New Publisher"; //Error
book.Price = 600; //Error

public class Book {
  public int Id { get; init; }
  public string? Title { get; init; }
  public string? Publisher { get; init; }
  public int Price { get; init; }
};


若加入試圖修改任一屬性的程式碼,Visual Studio 開發工具馬上會顯示錯誤的警示訊息:

clip_image008[4]

圖 4:不可以修改屬性。

僅供初始化的屬性(Init only Property)允許在建構函式中進行初始化,例如以下程式碼:

var book = new Book( 1 , "Programming C#" , "APublisher" , 500 );
Console.WriteLine($@"
Id        : {book.Id}
Title     : {book.Title}
Publisher : {book.Publisher}
Price     : {book.Price}"
);

public class Book {
  public Book( int id , string title , string publisher , int price ) {
    Id = id;
    Title = title;
    Publisher = publisher;
    Price = price;
  }
  public int Id { get; init; }
  public string? Title { get; init; }
  public string? Publisher { get; init; }
  public int Price { get; init; }
};

這個範例的執行結果如下圖所示:

clip_image010[4]

圖 5:範例的執行結果。

記錄(Record)型別

C# 的記錄(Record)型別提供簡潔的語法來宣告只具備唯讀屬性、不可變動成員值的型別。同時記錄(Record)型別會根據物件的屬性值來決定物件是否相等。

因為記錄型別(Record)具備不變性,因此它屬於執行緒安全(Thread Safe),且一旦建立之後,不可做任何異動,只可在建構函式中初始化記錄型別。

你可以使用「record」關鍵字來宣告記錄(Record)型別。在宣告記錄(Record)型別時可以使用「record class」關鍵字來表示要建立一個參考型別(Reference Type);而使用「record struct」關鍵字來表示要建立一個實值型別(Value Type)。

宣告記錄(Record)型別

要宣告記錄(Record)型別最精簡的語法是使用位置記錄(positional records),以下程式碼範例示範如何宣告一個record class:

var book = new Book( 1 , "Programming C#" , "APublisher" , 500 );
Console.WriteLine( $@"
Id        : {book.Id}
Title     : {book.Title}
Publisher : {book.Publisher}
Price     : {book.Price}"
);
public record class Book( int Id , string? Title , string? Publisher , int Price );

宣告記錄(Record)型別時「class」關鍵字可以省略,可將上述「Book」記錄範例改為:

public record Book( int Id , string? Title , string? Publisher , int Price );

使用位置引數(positional arguments)這個語法建立的記錄(Record)型別實體預設是不可變的,「Id」、「Title」、「Publisher」與「Price」被編譯成僅供初始化的屬性Init only Property,若試著修改屬性值,則無法編譯。例如以下程式碼,建立物件之後試著修改屬性值會發生錯誤:

book.Id = 2; //Error
book.Title = "C# Programming"; //Error
book.Publisher = "New Publisher"; //Error
book.Price = 600; //Error


 

使用標準屬性語法定義記錄

僅供初始化的屬性Init only Property是只有在物件初始化時才可以指派值的屬性,上述「Book」記錄範例也可以改寫為以下比較冗長的標準屬性語法:

var book = new Book( 1 , "Programming C#" , "APublisher" , 500 );
Console.WriteLine( $@"
Id        : {book.Id}
Title     : {book.Title}
Publisher : {book.Publisher}
Price     : {book.Price}"
);
public record class Book( int Id , string? Title , string? Publisher , int Price ) {
  public int Id { get; init; } = Id;
  public string? Title { get; init; } = Title;
  public string? Publisher { get; init; } = Publisher;
  public int Price { get; init; } = Price;
};

建立可變更屬性值的記錄

若要建立可變更屬性值的記錄,可以在記錄中使用「get」、「set」存取子明確地定義可讀寫屬性,例如以下程式碼:

var book = new Book( 1 , "Programming C#" , "APublisher" , 500 );

book.Id = 2;
book.Title = "C# Programming";
book.Publisher = "New Publisher";
book.Price = 600;

Console.WriteLine( $@"
Id        : {book.Id}
Title     : {book.Title}
Publisher : {book.Publisher}
Price     : {book.Price}"
);
public record class Book( int Id , string? Title , string? Publisher , int Price ) {
  public int Id { get; set; } = Id;
  public string? Title { get; set; } = Title;
  public string? Publisher { get; set; } = Publisher;
  public int Price { get; set; } = Price;
};


這個範例的執行結果如下圖所示:

clip_image012[4]

圖 6:範例的執行結果。

記錄結構(Record Struct)

C# 記錄(Record)型別是一種類別或結構,通常當作資料模型(Data Model)來使用。C# 10 開始我們可以利用「record struct」來定義記錄,將記錄宣告成實值型別,稱為記錄結構(Record Struct),例如「Book」記錄的範例可以改為以下程式碼:

var book = new Book( 1 , "Programming C#" , "APublisher" , 500 );

Console.WriteLine( $@"
Id        : {book.Id}
Title     : {book.Title}
Publisher : {book.Publisher}
Price     : {book.Price}"
);
public record struct Book( int Id , string? Title , string? Publisher , int Price ) {
  public int Id { get; init; } = Id;
  public string? Title { get; init; } = Title;
  public string? Publisher { get; init; } = Publisher;
  public int Price { get; init; } = Price;
};

 

同樣地,「Id」、「Title」、「Publisher」與「Price」被編譯成僅供初始化的屬性(Init only Property),若試著修改屬性值,則編譯會失敗:

book.Id = 2; //Error
book.Title = "C# Programming"; //Error
book.Publisher = "New Publisher"; //Error
book.Price = 600; //Error

記錄比較

記錄(Record)型別和類別、結構不同的地方在於:記錄(Record)型別在進行比較時,是針對成員的值來比較是否相等,而類別(class)是比物件的參考。例如以下「Book」記錄程式碼範例,「book」與「book2」被視為相同的物件:

var book = new Book( 1 , "Programming C#" , "APublisher" , 500 );
var book2 = new Book( 1 , "Programming C#" , "APublisher" , 500 );

Console.WriteLine(book == book2); // true
Console.WriteLine(book.Equals(book2)); // true
public record class Book( int Id , string? Title , string? Publisher , int Price );

 

而以下「Book」類別範例,「book」與「book2」被視為不相同的物件:

var book = new Book( 1 , "Programming C#" , "APublisher" , 500 );
var book2 = new Book( 1 , "Programming C#" , "APublisher" , 500 );

Console.WriteLine( book == book2 ); // false
Console.WriteLine( book.Equals( book2 ) ); // false
public class Book {
  public Book( int id , string? title , string? publisher , int price ) {
    Id = id;
    Title = title;
    Publisher = publisher;
    Price = price;
  }

  public int Id { get; init; }
  public string? Title { get; init; }
  public string? Publisher { get; init; }
  public int Price { get; init; }
}

可為 Null 的參考型別

$
0
0

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號: N220423602
出刊日期: 2022/4/27

C# 在第8版新增可為 Null 的參考型別(Nullable reference types,NRT),可以將參考型別明確標註為可以設定為「null」,以便於在開發階段讓工具協助檢查參考型別變數是否可以設定為「null」,如此可以減少應用程式擲出「System.NullReferenceException」例外錯誤的機率。

例如以下範例程式碼:

string s = null;

Console.WriteLine( s.Length );

若直接執行程式,則會直接擲出「System.NullReferenceException」例外錯誤,請參考下圖所示:

clip_image002

圖 1:「System.NullReferenceException」例外錯誤。

啟用與停用Nullable

預設使用Visual Studio 2022建立的.NET 6專案,已經自動在專案等級啟用「Nullable」,以檢查專案中所有檔案的程式碼。你可以使用屬性(Properties)視窗,或是編輯專案檔「*.csproj」來啟用或停用之。

以主控台應用程式為例,從Visual Studio 2022「Solution Explorer」視窗 – 專案名稱上方,按滑鼠右鍵,從快捷選單選擇「Properties」項目。在「Build」分頁中,從「Nullable」下拉式清單方塊中啟用或停用,請參考下圖所示:

clip_image004

圖 2:啟用與停用Nullable。

專案等級的設定會儲存在專案檔案(*.csproj)中,我們可以在Visual Studio 2022「Solution Explorer」視窗 – 專案名稱上方,按滑鼠右鍵,從快捷選單選擇「Edit Project File」項目,請參考下圖所示:

clip_image006

圖 3:編輯專案檔案。

專案檔案中包含<Nullable>enable</Nullable>的設定:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>

 

若要停用可以直接將「Nullable」項目設定為「disable」,例如以下程式碼:

<Nullable>disable</Nullable>

在啟用「Nullable」的情況下,可為 Null 的參考型別Nullable reference types變數的值若為「null」,在開發階段Visual Studio 2022工具會顯示警告訊息,例如以下範例程式碼:

string s = null;

Console.WriteLine( s.Length );

第一行程式會顯示「CS8600」警告訊息,請參考下圖所示:

clip_image008

圖 4:「CS8600」警告訊息。

而第二行程式會顯示「CS8602」警告訊息,請參考下圖所示:

clip_image010

圖 5:「CS8602」警告訊息。

加上檢查程式碼

啟用Nullable功能之後,開發工具在編譯階段就可以幫你檢查程式碼,顯示警告訊息,提示程式執行時可能會出錯,要移除這些警告訊息的方式有很多種,其中一種變是加上檢查程式碼,避免在變數或屬性值為「null」時,直接進行操作。

例如修改程式碼如下,加上「?」號,讓「s」變數的型別為可為 Null 的參考型別(Nullable reference types,NRT),再加上「if」陳述式進行判斷,Visual Studio 2022將透過程式碼靜態分析功能為你檢視程式碼,並移除警告訊息:

string? s = null;
if ( s is not null ) {
  Console.WriteLine( s.Length );
}

 

「?.」運算子

使用「?.」運算子,讓判斷程式碼更為簡短,將上述程式碼修改如下:

string? s = null;

Console.WriteLine( s?.Length );

 

「!」運算子

另一種做法是使用C# !(null-forgiving) 運算子,搭配不可為 Null 的參考型別(Non-nullable reference types將程式改寫如下,將「s」設定為「null!」:

string s = null!;

Console.WriteLine( s.Length );

 

「??」運算子

有時可能需要將可能為Null的運算式指派給變數,這時可能會得到編譯器提示的警告訊息,例如以下程式碼:

string s = GetString();

Console.WriteLine( s?.Length );

static string? GetString( ) => "Hello";

第一行程式碼會出現「CS8600」警告訊息,請參考下圖所示:

clip_image012

圖 6:「CS8600」警告訊息。

我們同樣可以使用「??」運算子,在「GetString()」函式回傳「null」時,給予一個預設值,例如以下程式碼:

string s = GetString() ?? "";

Console.WriteLine( s?.Length ); //5

static string? GetString( ) => "Hello";

如此當「GetString」傳回「null」,程式執行時也不會發生例外,參考以下程式碼:

string s = GetString() ?? "";

Console.WriteLine( s?.Length ); //0

static string? GetString( ) => null;

也可以在方法呼叫語法之後使用C# !(null-forgiving) 運算子,參考以下程式碼:

string s = GetString()!;

Console.WriteLine( s?.Length ); //empty

static string? GetString( ) => null;

 

「#nullable」指示詞

如果你可以確保變數或屬性值不為「null」,那麼可以利用「#nullable」指示詞通知編譯器忽略這個警告,例如以下程式碼,第二行的警告會消失;而第四行的警告依然存在:

#nullable disable warnings

string s = null;

#nullable enable warnings

Console.WriteLine( s.Length );

請參考下圖所示:

clip_image014

圖 7:使用「#nullable」指示詞。

 

null狀態靜態分析Attribute

啟用Nullable之後,C#編譯器會進行程式碼靜態分析來檢查變數的null狀態來決定是否顯示警示訊息。不過有時後我們要適當給C#編譯器一些暗示,才能準確地辨識出變數的狀態。例如以下程式碼範例:

string? s = null;
if ( IsNotNull(s) ) {
  Console.WriteLine( s.Length );
}
static bool IsNotNull( string? str ) => str is not null;

 

雖然在第二行程式碼中,我們已叫用自訂的「IsNotNull」函式檢查null狀態,在第三行程式中還會顯示警示訊息,請參考下圖所示:

clip_image002[1]

圖 8:自訂「IsNotNull」函式檢查。

參考以下程式碼,我們可以使用「NotNullWhen」Attribute來知會編譯器,當此函式回傳「true」時,便表示變數不為「null」,如此警示訊息便會自動消失:

using System.Diagnostics.CodeAnalysis;

string? s = null;
if ( IsNotNull( s ) ) {
  Console.WriteLine( s.Length );
}

static bool IsNotNull( [NotNullWhen( true )] string? str ) => str is not null;

Entity Framework Core與可為 Null 的參考型別(NRT)

若在Entity Framework Core使用到可為 Null 的參考型別(NRT,需要特別的小心。Entity Framework Core Enitty類別的屬性(Property)值若可以包含「null」,便表示它是選擇性的(Optional),若Enitty類別的屬性(Property)值不可以包含「null」,則表示它應該是必要的(Required)。

停用可為 Null 的參考型別(NRT時,所有.NET參考型別都視為選擇性的(可以包含「null」);啟用可為 Null 的參考型別(NRT時,NRT被視為選擇性的(如string?,可以包含「null」),其它參考型別(如string)是必要的,不可以包含「null」。

Entity Framework Core建議應該使用可為 Null 的參考型別(Nullable reference types,NRT),這樣就可以不必使用Fluent API或Data Annotations來做重複的事情,例如設定[Required] Attribute來表示此屬性是否不可為「null」。

參考以下程式碼範例,在啟用「Nullable」功能時,根據慣例「Title」是必要的(Required);而「Description」是選擇性的:

public class Book {
  public int Id { get; set; }
  [StringLength( 50 )]
  public string Title { get; set; }
  public int Price { get; set; }
  public DateTime PublishDate { get; set; }
  public bool InStock { get; set; }
  public string? Description { get; set; }
}

 

當然我們也可以明確地使用[Required] Attribute 來指明「Description」是必要的,參考以下程式碼:

public class Book {
  public int Id { get; set; }
  [StringLength( 50 )]
  public string Title { get; set; }
  public int Price { get; set; }
  public DateTime PublishDate { get; set; }
  public bool InStock { get; set; }
  [Required]
  public string? Description { get; set; }
}

設定預設值

上述程式碼會讓編譯器產生「CS8618」警告:

Non-nullable property 'Title' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

請參考下圖所示:

clip_image016

圖 9:「CS8618」警告。

除去警告的最簡單做法就是賦予string型別的「Title」屬性一個初始值,例如以下程式碼:

public class Book {
   public int Id { get; set; }
   [StringLength( 50 )]
   [Required]
   public string Title { get; set; } = string.Empty;
   public int Price { get; set; }
   public DateTime PublishDate { get; set; }
   public bool InStock { get; set; }
   public string? Description { get; set; }
}

 

不過這不是一個好做法,這樣C# 8發明的Nullable Reference Types就沒有意義了,Nullable Reference Types 用來確保屬性或變數要包含一個有用的值,並且在使用到屬性或變數時能偵測它是否為Null。

 

建構函式繫結(Constructor Binding)

為了避免編譯器會抱怨「CS8618」警告,可使用建構函式繫結(Constructor Binding)避免沒有初始化不可為 Null 的參考型別Non-nullable reference types的警告訊息,修改程式碼如下,加入建構函式:

public class Book {
  public int Id { get; set; }
  [StringLength( 50 )]
  public string Title { get; set; }
  public int Price { get; set; }
  public DateTime PublishDate { get; set; }
  public bool InStock { get; set; }
  public string? Description { get; set; }
  public Book( string title, string? description = null ) {
    Title = title;
    Description = description;
  }
}

 

建構函式繫結(Constructor Binding)有一個限制,不適用於導覽屬性(Navigation properties)的初始化。

!(null-forgiving) 運算子

上述問題的其中一種簡潔的解法是使用C# !(null-forgiving) 運算子,將程式改寫如下,將「Title」屬性值設定為「null!」:

public class Book {
  public int Id { get; set; }
  [StringLength( 50 )]
  public string Title { get; set; } = null!;
  public int Price { get; set; }
  public DateTime PublishDate { get; set; }
  public bool InStock { get; set; }
  public string? Description { get; set; }
}

 

DbContext與DbSet

當啟用可為 Null 的參考型別(Nullable reference types,NRT)時,在「DbContext」類別定義DbSet<T>屬性時也會遇到初始化的問題,例如以下程式碼:

public class BookContext : DbContext {
  public BookContext( DbContextOptions<BookContext> options ) : base( options ) {
  }

  public DbSet<Book> Books { get; set; }
}

 

編譯器會提示發生「CS8618」警告,請參考下圖所示:

clip_image018

圖 10:「CS8618」警告。

實際上「DbContext」基礎類別(Base Class)會負責初始化這個屬性,但編譯器目前無法偵測到這一點,因此我們可以將程式改寫如下,使用自動屬性語法設計DbSet<T>屬性,然後使用C# !(null-forgiving) 運算子知會編譯器這個DbSet<T>屬性會在其它程式碼中進行初始化,它會被設為非Null值:

public class BookContext : DbContext {
    public BookContext( DbContextOptions<BookContext> options ) : base( options ) {
    }
    public DbSet<Book> Books { get; set; } = null!;
  }

 

另一種作法是設定屬性的初始值為Set<T>,例如以下程式碼:

public class BookContext : DbContext {
    public BookContext( DbContextOptions<BookContext> options ) : base( options ) {
    }
    public DbSet<Book> Books => Set<Book>( );
  }

 

注意事項

逆向工程目前不支援可為 Null 的參考型別NRT,EF Core在產生C#程式碼時預設會假設停用這個功能。

C# 11版新功能介紹 - 1

$
0
0

.NET Magazine國際中文電子雜誌
許薰尹
稿張智凱
文章編號 N230324701
出刊日期: 2023/3/8

這篇文章中將介紹2022年11月發表的C# 11版幾個新功能,包含原始字串常值(Raw string literals)、字串插值(String interpolations)、UTF-8 字串常值(UTF-8 string literals)、「required」等新功能。

若要使用C# 11版進行開發,則Visual Studio 2022開發工具需要 17.2 以上版本,本篇文章使用以下步驟建立主控台程式專案進行測試:

1. 從Visual Studio 2022開發工具「Create a new project」對話盒中,選取「Console App」項目。

2. 在「Configure your new project」視窗中設定專案名稱,與專案存放路徑。

3. 在「Additional information」視窗,確認左上方的「Target Framework」清單選取「.NET 7.0 (Standard-term support)。

4. 按下「Create」按鈕建立專案。

原始字串常值(Raw string literals)

原始字串常值(Raw string literals)是一種C# 新的字串語法,以前後三個「"」符號將文字包起來。「"""」符號包起來的內容可以包含一般的文數字、空白、換行符號,引號等等,特殊符號不需要再加上跳脫序列(Escape sequence)。

參考以下包含多行文字的原始字串常值(Raw string literals)範例程式碼,你可以將程式碼直接加入「Program.cs」檔案中進行測試:

 

 

string mystring = """
string line 1
string line 2
I'm good
"Hello World"
""";
Console.WriteLine( mystring );

 

這個範例執行後會輸出以下訊息到主控台:

string line 1

string line 2

I'm good

"Hello World"

使用原始字串常值(Raw string literals)語法時,前後三個「"」符號要單獨出現在一行,不要和其它字元出現在同一行,以下的寫法是錯誤的,開頭的「"」符號和「string line 1」字串出現在同一行:

 

string mystring = """ string line 1
string line 2
I'm good
"Hello World"
""";

 

編譯時會顯示出現錯誤訊息:「Error CS9000 Raw string literal delimiter must be on its own line.」。以下的寫法也是錯誤的,結尾的「"」符號和「string line 1」字串出現在同一行:

 

 

string mystring = """
string line 1
string line 2
I'm good
"Hello World" """;
Console.WriteLine( mystring );

 

另外要特別注意的是,每一行文字的前方若有空白,則每行前置空白的數量必需少於、等於結尾行的空白,例如以下範例,「string line 1」這行前方有兩個空白;而結尾的這行「"""」符號前有四個空白

 

string mystring = """
  string line 1
        string line 2
             I'm good
        "Hello World"       
    """;

 

 

為方便文章的閱讀,我們將上面程式碼的空白用「*」符號表示:

 

string mystring = """
**string line 1
        string line 2
             I'm good
        "Hello World"       
****""";

 

 

這會導致編譯失敗,錯誤編號「CS8999」,訊息如下:「Line does not start with the same whitespace as the closing line of the raw string literal.」

而以下程式碼,每一行文字前方都有四個空白,這行程式才能順利編譯:

 

string mystring = """
    string line 1
    string line 2
    I'm good
    "Hello World" 
    """;

 

 

為方便文章的閱讀,我們將上面程式碼的空白用「*」符號表示:

 

string mystring = """
****string line 1
****string line 2
****I'm good
****"Hello World" 
****""";

 

 

原始字串常值(Raw string literals)還可以與字串插值(string interpolation)語法混用,例如以下範例程式碼所示:

 

string userName = "Mary";
string now = DateTime.Now.ToString();
string mystring = $"""
    string line 1
    string line 2
    I'm good
    "Hello World" , {userName}
    Now is "{now}"
    """;

Console.WriteLine( "[" + mystring + "]" );

 

 

這個範例的執行結果參考如下,主控台會輸出以下文字:

[string line 1

string line 2

I'm good

"Hello World" , Mary

Now is "2022/12/6 下午 03:48:17"]

「$」符號的數量決定了用來做字串插補的大括弧數量,例如上例的程式碼,可以改寫如下,「mystring」以兩個「$」符號開始,因此用來做字串插補的大括弧數量便是2個,:

 

 

string userName = "Mary";
string now = DateTime.Now.ToString( );
string mystring = $$"""
    string line 1
    string line 2
    I'm good
    "Hello World" , {{userName}}
    Now is "{{now}}"
    """;
Console.WriteLine( "[" + mystring + "]" );

 

所以上例「mystring」字串中的「userName」若想要使用大括號包起來,則搭配字串插值語法時,大括號要寫3次,例如以下程式碼:

 

 

string userName = "Mary";
string now = DateTime.Now.ToString( );
string mystring = $$"""
    string line 1
    string line 2
    I'm good
    "Hello World" , {{{userName}}}
    Now is "{{now}}"
    """;
Console.WriteLine( "[" + mystring + "]" );

 

 

這個範例的執行結果參考如下:

[string line 1

string line 2

I'm good

"Hello World" , {Mary}

Now is "2022/12/6 下午 04:04:37"]

依此類推,以下範例執行結果同上例:

 

string userName = "Mary";
string now = DateTime.Now.ToString( );
string mystring = $$$"""
    string line 1
    string line 2
    I'm good
    "Hello World" , {{{{userName}}}}
    Now is "{{{now}}}"
    """;
Console.WriteLine( "[" + mystring + "]" );

 

 

在字串插值(tring interpolations)的語法中「:」號有特別用途,若要使用條件運算子(?:),需要將程式利用小括號「( )」包起來,例如以下程式碼:

 

string userName = "Mary";
DateTime now = DateTime.Now;
string mystring = $$$"""
    string line 1
    string line 2
    I'm good
    "Hello World" , {{{{userName}}}}
    Now is "{{{now}}}"
    Greeting : "{{{(now.Hour < 12 ? "早安" : "今天好") }}}"
    """;
Console.WriteLine( mystring );

 

 

這個範例的執行結果參考如下:

string line 1

string line 2

I'm good

"Hello World" , {Mary}

Now is "2022/12/8 下午 02:07:07"

Greeting : "今天好"

字串插值(String interpolations)

字串插值(String interpolations)的 「{」 和「}」 符號內的程式碼現在可以跨越多行了,其中也可包含換行符號。「{」 和「}」 符號內的文字都會被視做C# 程式碼,新的語法提高程式碼的可閱讀性,例如以下範例程式碼:

 

 

DateTime now = DateTime.Now;

string mystring = $" Greeting : {now} , {now.Hour switch {
  < 12 => "早安",
  < 18 => "午安",
  < 21 => "晚安",
  _ => "今天好",
}}";

Console.WriteLine( mystring );

 

UTF-8 字串常值(UTF-8 string literals)

預設.NET中的字串是以UTF-16格式儲存,在C# 11版之後,可以在字串後綴「u8」來指定使用UTF-8編碼。這種類型的字串會以「ReadOnlySpan<byte>」型別的物件來儲存。「ReadOnlySpan<byte>」很適合用於網站應用程式以處理遠端下載而來的UTF8格式的資料。

舉例來說,以下範例程式碼展示將「ABCDE」字串利用「UTF8.GetBytes」轉成byte[] 型別之後,搭配「FileStream」物件將資料寫入文字檔中,然後將再文字檔的內容讀出:

 

 

byte [ ] data = System.Text.Encoding.UTF8.GetBytes( "ABCDE" );
byte [ ] data2 = new byte [ 5 ];
FileStream file;
file = new FileStream( "data.txt" , FileMode.Create , FileAccess.Write );
file.Write( data , 0 , 5 );
file.Close( );
file = new FileStream( "data.txt" , FileMode.Open , FileAccess.Read );
file.Read( data2 , 0 , 5 );
for ( int i = 0 ; i < data2.Length ; i++ ) {
  Console.WriteLine( data2 [ i ] );
}
file.Close( );

 

 

範例執行後產生的「data.txt」文字檔的內容參考如下:

ABCDE

而這個範例程式執行時,主控台將印出以下資料:

65

66

67

68

69

上述範例若改用C# 11版UTF-8 字串常值(UTF-8 string literals)語法,則程式可改寫如下,這將會得到相同執行結果:

 

 

ReadOnlySpan<byte> data = "ABCDE"u8;
byte [ ] data2 = new byte [ 5 ];
FileStream file;
file = new FileStream( "data.txt" , FileMode.Create , FileAccess.Write );
file.Write( data );
file.Close( );
file = new FileStream( "data.txt" , FileMode.Open , FileAccess.Read );
file.Read( data2 );
for ( int i = 0 ; i < data2.Length ; i++ ) {
  Console.WriteLine( data2 [ i ] );
}
file.Close( );

 

「required」修飾詞

C# 11版新增「required」修飾詞來增強物件初始化功能,讓你更容易初始化類別成員。「required」修飾詞可以套用在類別(class)、結構(struct)、紀錄(record)或紀錄結構(record struct)的欄位成員或屬性成員,用來指明此成員必需明確透過建構函式,或物件初始設定式(object initializer)進行初始化。簡單的說,可以強迫呼叫端一定要設定成員的初始值。此外「required」修飾詞不能夠套用在介面(Interface)的成員。

參考以下程式碼,編譯時會產生CS9035錯誤,「Employee」類別的「EmployeeId」與「EmployeeName」屬性使用了「required」修飾詞,卻沒初始化:

 

Employee employee = new Employee( ); //Error	CS9035	Required member 'Employee.EmployeeId' must be set in the object initializer or attribute constructor

public class Employee {
  public required int EmployeeId;
  public required string EmployeeName { get; set; }
  public DateTime BirthDay { get; set; }
  public bool IsMarried { get; set; }
  public string? Department { get; set; }
}

 

 

而以下建立「Employee」物件的程式碼才能正確編譯,明確在物件初始設定式(object initializer)初始化「EmployeeId」、與「EmployeeName」屬性:

 

 

Employee employee = new Employee( ) {
  EmployeeId = 1 ,
  EmployeeName = "Mary"
};

 

套用「required」修飾詞的成員可以被初始化為「null」,例如以下範例程式碼,「EmployeeName」屬性的型別是「string?」(可為 Null 的參考型別, nullable reference type),則建立物件實例時,可將屬性初始化為「null」:

 

Employee employee = new Employee( ) {
  EmployeeId = 1 ,
  EmployeeName = null
};

Console.WriteLine($" Employee Id : {employee.EmployeeId} , Employee Name : {employee.EmployeeName}");
public class Employee {
  public required int EmployeeId;
  public required string? EmployeeName { get; set; }
  public DateTime BirthDay { get; set; }
  public bool IsMarried { get; set; }
  public string? Department { get; set; }
}

 

 

若「EmployeeName」屬性別是「string」(不可為 Null 的參考型別,non-nullable reference type),則將「EmployeeName」屬性初始化為「null」時,編譯器會產生一個CS8625號的警示訊息,參考以下範例程式碼:

 

Employee employee = new Employee( ) {
  EmployeeId = 1 ,
  EmployeeName = null //warning CS8625: Cannot convert null literal to non-nullable reference type.
};

Console.WriteLine($" Employee Id : {employee.EmployeeId} , Employee Name : {employee.EmployeeName}");
public class Employee {
  public required int EmployeeId;
  public required string EmployeeName { get; set; }
  public DateTime BirthDay { get; set; }
  public bool IsMarried { get; set; }
  public string? Department { get; set; }
}

 

 

套用「required」修飾詞的成員之成員存取修飾詞(Access Modifier)不可以小於包含它的型別,例如以下程式碼將無法編譯,「EmployeeName」成員之成員存取修飾詞為「protected」小於包含它的「Employee」型別的「public」存取修飾詞:

 

 

public class Employee {
    public required int EmployeeId;
    protected required string EmployeeName { get; set; } // Error CS9032  Required member 'Employee.EmployeeName' cannot be less visible or have a setter less visible than the containing type 'Employee'.	

    public DateTime BirthDay { get; set; }
    public bool IsMarried { get; set; }
    public string? Department { get; set; }
  }

 

套用「required」修飾詞的屬性成員必需包含「setter」或「init」存取子,例如以下程式碼將無法編譯,因為「EmployeeName」為唯讀屬性:

 

public class Employee {
  public required int EmployeeId;
  public required string EmployeeName { get; } // Error CS9034  Required member 'Employee.EmployeeName' must be settable.
  public DateTime BirthDay { get; set; }
  public bool IsMarried { get; set; }
  public string? Department { get; set; }
}

 

套用「required」修飾詞的「EmployeeName」屬性成員可以定義init 存取子:

 

public class Employee {
  public required int EmployeeId;
  public required string EmployeeName { get; init; } 
  public DateTime BirthDay { get; set; }
  public bool IsMarried { get; set; }
  public string? Department { get; set; }
}

 

 

因為新的「required」修飾詞的緣故,型別的名稱便不能命為「required」,例如以下程式碼便無法編譯:

 

class required {   //Error CS9029	Types and aliases cannot be named 'required'.

}

 

 

當然C#程式碼大小寫視為不同,所以類別名稱仍可命為「Required」,參考以下範例程式碼:

 

class Required {   

}

 

 

「SetsRequiredMembers」Attribute

若類別的建構函式(Constructor)中已經包含初始化「required」成員的程式碼,你可以明確在建構函式上方套用「SetsRequiredMembers」Attribute,來通知編譯器停用檢查「required」成員是否被初始化。這個「SetsRequiredMembers」Attribute 可便於升級已利用舊版C#語法設計好的類別到C# 11版。參考以下程式碼所示:

 

using System.Diagnostics.CodeAnalysis;

Employee employee = new Employee( 1 , " Mary " );
Console.WriteLine($" Employee Id : {employee.EmployeeId} , Employee Name : {employee.EmployeeName}");
public class Employee {
  [SetsRequiredMembers]
  public Employee( int id , string name ) {
    EmployeeId = id;
    EmployeeName = name;
  }
  public required int EmployeeId;
  public required string EmployeeName { get; set; }
  public DateTime BirthDay { get; set; }
  public bool IsMarried { get; set; }
  public string? Department { get; set; }
}

 

 

若沒有加上「SetsRequiredMembers」Attribute,則建立物件實例時,編譯會失敗,例如以下程式碼所示:

 

Employee employee = new Employee( 1 , " Mary " ); //Error CS9035	Required member 'Employee.EmployeeId' must be set in the object initializer or attribute constructor.

Console.WriteLine( $" Employee Id : {employee.EmployeeId} , Employee Name : {employee.EmployeeName}" );
public class Employee {
  public Employee( int id , string name ) {
    EmployeeId = id;
    EmployeeName = name;
  }
  public required int EmployeeId;
  public required string EmployeeName { get; set; }
  public DateTime BirthDay { get; set; }
  public bool IsMarried { get; set; }
  public string? Department { get; set; }
}

 

 

需要注意的是,編譯器不會檢查套用「SetsRequiredMembers」Attribute的建構函式是否真的初始化了所有「required」成員,例如以下程式碼只在建構函式初始化「EmployeeName」屬性,沒有初始化「EmployeeId」欄位,雖然可以順利編譯跟執行,但「EmployeeId」欄位的值會是「0」,這可能不是你想要的結果:

 

using System.Diagnostics.CodeAnalysis;

Employee employee = new Employee( 1 , " Mary " ); //Employee Id : 0 , Employee Name :  Mary

Console.WriteLine( $" Employee Id : {employee.EmployeeId} , Employee Name : {employee.EmployeeName}" );
public class Employee {
  [SetsRequiredMembers]
  public Employee( int id , string name ) {
    EmployeeName = name;
  }
  public required int EmployeeId;
  public required string EmployeeName { get; set; }
  public DateTime BirthDay { get; set; }
  public bool IsMarried { get; set; }
  public string? Department { get; set; }
}

 

 

另外要特別注意的是:「SetsRequiredMembers」Attribute只能在建構函式套用,不能夠在類別的屬性、欄位上方套用。

下列的程式範例包含一個沒有參數的建構函式,這個建構函式並未套用[SetsRequiredMembers] Attribute,因此使用「Employee」型别時,呼叫端可利用物件初始設定式(object initializers)進行「required」成員的初始化動作,否則編譯將會失敗:

 

using System.Diagnostics.CodeAnalysis;
//Employee employee1 = new Employee( ); //Error CS9035	Required member 'Employee.EmployeeId' must be set in the object initializer or attribute constructor.
Employee employee = new Employee( ) { EmployeeId = 1 , EmployeeName = "Mary" };
Console.WriteLine( $" Employee Id : {employee.EmployeeId} , Employee Name : {employee.EmployeeName}" );
public class Employee {
  public Employee( ) {

  }
  [SetsRequiredMembers]
  public Employee( int id , string name ) {
    EmployeeId = id;
    EmployeeName = name;
  }
  public required int EmployeeId;
  public required string EmployeeName { get; set; }
  public DateTime BirthDay { get; set; }
  public bool IsMarried { get; set; }
  public string? Department { get; set; }
}

 

C# 11版新功能介紹 - 2

$
0
0

.NET Magazine國際中文電子雜誌
許薰尹
稿張智凱
文章編號 N230324702
出刊日期: 2023/3/22

這篇文章中將延續本站《C# 11版新功能介紹 - 1》一文的情境,介紹C# 11版「file」修飾詞與List模式(List Pattern)、自動預設結構(Auto-default struct)等等新功能。

「file」修飾詞

「file」修飾詞可以套用到多種型別,例如class、interface、record、struct、enum、delegate等,限定此型別只能在目前這個cs檔案之中使用,這帶來的好處是:一個專案中可有多個相同名稱的類別。在沒有「file」修飾詞之前,你可能會使用命名空間(namespace)來解決名稱衝突的問題,但在很多種情況下,例如程式碼產生器想動態產生程式碼,或定義擴充方法,你還是得取一個命名空間或類別名稱,「file」修飾詞可以簡化這部分的程式碼。

預設一個C#專案之中,在相同命名空間下若有多個cs檔案,檔案內包含相同名稱的類別定義,這個程式將無法編譯。舉例來說,若專案中有一個「Employee.cs」檔案,包含以下程式碼,其中定義一個「Employee」類別:

Employee.cs
namespace CSharp11 {
  public class Employee {
    public Employee( int id , string name ) {
      EmployeeId = id;
      EmployeeName = name;
    }

    public int EmployeeId { get; set; }
    public string? EmployeeName { get; set; }

    public override string ToString( ) {
      return $"Employee.cs Employee Id : {this.EmployeeId} , Employee Name : {this.EmployeeName}";
    }
  }
}

而相同專案中的「HR.cs」檔案內,包含以下程式碼,其中也有一個「Employee」類別:

HR.cs
namespace CSharp11 {
  class Employee {
    public Employee( int id , string name ) {
      EmployeeId = id;
      EmployeeName = name;
    }

    public int EmployeeId { get; set; }
    public string? EmployeeName { get; set; }

    public override string ToString( ) {
      return $"HR.cs Employee Id : {this.EmployeeId} , Employee Name : {this.EmployeeName}";
    }
  }
  class Sales : Employee {
    public Sales( int id , string name ) : base( id , name ) {
    }
  }
}

 

編譯時專案將會出現相當多錯誤,其中會包含以下訊息,表示「Employee」類別名稱重複了:「CS0101 The namespace 'CSharp11' already contains a definition for 'Employee'」。

「file」修飾詞可限定此型別(如類別),只能夠在某一個檔案之中的程式碼才可以使用,我們可以將上例的「HR.cs」檔案中的程式碼修改如下,限定此檔案中的「Employee」類別只有在這個檔案中的程式碼中才可使用,如「Sales」類別:

HR.cs
namespace CSharp11 {
  file class Employee {
    public Employee( int id , string name ) {
      EmployeeId = id;
      EmployeeName = name;
    }
    public int EmployeeId { get; set; }
    public string? EmployeeName { get; set; }

    public override string ToString( ) {
      return $"HR.cs Employee Id : {this.EmployeeId} , Employee Name : {this.EmployeeName}";
    }
  }
  file class Sales : Employee {
    public Sales( int id , string name ) : base( id , name ) {
    }
  }
}

 

這樣我們就可以在主程式「Program.cs」檔案中加入以下程式碼,建立「Employee.cs」檔案中定義的「Employee」物件:

 

Program.cs
using CSharp11; //專案名稱命名空間

Employee employee = new Employee( 1 , " Mary " ); 
Console.WriteLine( employee.ToString( ) ); // Employee.cs Employee Id : 1 , Employee Name :  Mary

若想要在「Program.cs」檔案中使用到「HR.cs」檔案中的物件,我們可以利用靜態方法來間接取得它們,我們可以修改「HR.cs」檔案程式如下,透過「GetSales」方法來取得「Sales」物件:

 

HR.cs
namespace CSharp11 {
  file class Employee {
    public Employee( int id , string name ) {
      EmployeeId = id;
      EmployeeName = name;
    }
    public int EmployeeId { get; set; }
    public string? EmployeeName { get; set; }

    public override string ToString( ) {
      return $"HR.cs Employee Id : {this.EmployeeId} , Employee Name : {this.EmployeeName}";
    }
  }
  file class Sales : Employee {
    public Sales( int id , string name ) : base( id , name ) {
    }
  }
  static class MyStaticClass {
    internal static object GetSales( ) => new Sales( 2 , "Candy" );
  }
}

 

這樣在「Program.cs」檔案中便可加入以下程式碼,同時取得「Employee.cs」檔案與「HR.cs」檔案中定義的型別所建立的物件:

 

Program.cs

using CSharp11;

Employee employee = new Employee( 1 , " Mary " );
Console.WriteLine( employee.ToString( ) ); // Employee.cs Employee Id : 1 , Employee Name :  Mary

var sales = MyStaticClass.GetSales( );
Console.WriteLine( sales.ToString( ) ); // HR.cs Employee Id : 2 , Employee Name : Candy

成員存取修飾詞限制

「file」修飾詞在使用上也有一些限制,例如不能與「public」、「private」、「internal」等成員存取修飾詞一起使用,例如修改「HR.cs」檔案程式如下,則程式將無法編譯:

HR.cs
namespace CSharp11 {
  public file class Employee { // Error CS9052  File-local type 'Employee' cannot use accessibility modifiers.
    public Employee( int id , string name ) {
      EmployeeId = id;
      EmployeeName = name;
    }
    public int EmployeeId { get; set; }
    public string? EmployeeName { get; set; }

    public override string ToString( ) {
      return $"HR.cs Employee Id : {this.EmployeeId} , Employee Name : {this.EmployeeName}";
    }
  }
  internal file class Sales : Employee { //Error CS9052  File-local type 'Sales' cannot use accessibility modifiers.

    public Sales( int id , string name ) : base( id , name ) {
    }
  }
  static class MyStaticClass {
    internal static object GetSales( ) => new Sales( 2 , "Candy" );
  }
}

 

最上層語句(Top-level statements)

「file」修飾詞也可以和「partial」修飾詞合併一起使用,不過所有的定義要放在同一個cs檔案中,例如在使用最上層語句(Top-level statements)的情況下,我們可以在檔案中加入以下程式碼:

 

Program.cs
Employee employee = new Employee( 1 , " Mary " );
Console.WriteLine( employee.ToString( ) ); // Employee.cs Employee Id : 1 , Employee Name :  Mary
file partial class Employee {
  public int EmployeeId { get; set; }
}
file partial class Employee {
  public Employee( int id , string name ) {
    EmployeeId = id;
    EmployeeName = name;
  }
  public string? EmployeeName { get; set; }
  public override string ToString( ) {
    return $"Employee.cs Employee Id : {this.EmployeeId} , Employee Name : {this.EmployeeName}";
  }
}

 

因為新的「file」修飾詞的緣故,型別的名稱便不能命為「file」,例如以下程式碼便無法編譯:

class file {  // Error CS9056  Types and aliases cannot be named 'file'.	

}

 

List模式(List Pattern)

從 C# 11版開始,你不必寫迴圈程式碼,逐一針對陣列或集合中的項目進行比對,現在可以使用List模式(List Pattern)逐一比對陣列或清單中的項目,例如以下範例程式碼所示,我們建立一個字串集合,利用「is」來比較集合中的每一個項目是否相符:

List<string> names = new() { "Mary" , "Candy" , "LuLu" , "victor" , "Jack" };
Console.WriteLine(names is [ "Mary", "Candy", "LuLu", "victor", "Jack" ] ); // True
Console.WriteLine( names is [ "Mary" , "Candy" , "LuLu" , "victor" , "Jack" , "Susan" ] ); // False
Console.WriteLine( names is [ "Mary" , "Candy" , "LuLu" , "victor"  ] ); // False
Console.WriteLine( names is [ "Lisa" or "Mary" or "Betty" , "Candy" , "LuLu" , "victor" , "Jack" ] ); // True

 

相同的情境可用來比對陣列中的項目,改寫上例程式如下:

string [ ] names = { "Mary" , "Candy" , "LuLu" , "victor" , "Jack" };
Console.WriteLine(names is [ "Mary", "Candy", "LuLu", "victor", "Jack" ] ); // True
Console.WriteLine( names is [ "Mary" , "Candy" , "LuLu" , "victor" , "Jack" , "Susan" ] ); // False
Console.WriteLine( names is [ "Mary" , "Candy" , "LuLu" , "victor"  ] ); // False
Console.WriteLine( names is [ "Lisa" or "Mary" or "Betty" , "Candy" , "LuLu" , "victor" , "Jack" ] ); // True

 

discard 模式(-)

在List模式(List Pattern)可以搭配discard 模式(-)讓程式碼更為簡潔,我們要比對清單中所有項目是否相同,或部分項目是否相同時,可以使用以下程式碼:

string [ ] names = { "Mary" , "Candy" , "LuLu" , "victor" , "Jack" };
Console.WriteLine( names is [ _ , _ , _ , _ , _ ] ); // True
Console.WriteLine( names is not [ _ , "Betty" , _ , _ , _ ] ); // True
Console.WriteLine( names is not [ _ , "Candy" , _ , _ , _ ] ); // False
Console.WriteLine( names is not [ _ , "Candy" or "Betty" , _ , _ , _ ] ); // False
Console.WriteLine( names is not [ _ , "LuLu" or "Betty" , _ , _ , _ ] ); // True

 

var 模式(var Pattern)

若想要擷取清單中的項目放到變數之中,可搭配var 模式(var Pattern),參考以下範例程式碼,將比對清單中的第一個名字放到「user」變數中:

string [ ] names = { "Mary" , "Candy" , "LuLu" , "victor" , "Jack" };
if ( names is [ var user , _ , _ , _ , _ ] ) {
  Console.WriteLine( "First user  is : " + user );
}

 

這個範例執行結果參考如下:

First user is : Mary

配量模式(.. ,Slice Pattern)

若我們只想比部分的項目是否和清單中的項目相符,還可以搭配配量模式(Slice Pattern),例如以下程式碼,只比對前兩個項目是否和清單中前兩個項目相符:

string [ ] names = { "Mary" , "Candy" , "LuLu" , "victor" , "Jack" };
Console.WriteLine( names is [ "Mary" , "Candy" , .. ] ); // True

 

若想比對的是第一個與最後一個項目要相符,其餘不管,則可撰寫程式如下:

string [ ] names = { "Mary" , "Candy" , "LuLu" , "victor" , "Jack" };
Console.WriteLine( names is [ "Mary" , .. , "Jack" ] ); // True

 

以下程式則比對第二個項目要為「Candy」或「Betty」:

string [ ] names = { "Mary" , "Candy" , "LuLu" , "victor" , "Jack" };
Console.WriteLine( names is [ _ , "Candy" or "Betty" , .. ] ); // True

以下程式則比對第二個項目要為「Candy」或「Betty」,但清單中只有兩個項目:

string [ ] names = { "Mary" , "Candy" , "LuLu" , "victor" , "Jack" };
Console.WriteLine( names is [ _ , "Candy" or "Betty" ] ); // False

自動預設結構(Auto-default struct)

自動預設結構(Auto-default struct),讓C# 編譯器來確保「struct」型別中的成員,都可以透過建構函式進行初始化,換句話說,任何的欄位成員與自動屬性若沒有明確透過建構函式初始化的話,那麼將由編譯器會負起初始化的任務,將成員設定為型別預設值。

參考以下範例程式碼,「e1」與「e2」物件分別叫用建構函式進行「Employee」結構成員初始化動作,而「e3」則利用「default」運算子,將所有成員初始化成型別預設值,而非叫用建構函式來進行初始化,這個範例程式碼在.NET 6的環境中可以順利編譯及執行:

 

var e1 = new Employee( );
Console.WriteLine("Employee 1 : "); 
Console.WriteLine( e1.EmployeeId ); // -1
Console.WriteLine( e1.EmployeeName ); // None
Console.WriteLine( e1.BirthDay ); // 2022/12/9 下午 01:19:18
Console.WriteLine( e1.IsMarried ); //False
Console.WriteLine( e1.Department ); //None
Console.WriteLine();

var e2 = new Employee( 2 , "Candy" , new DateTime( 2000 , 01 , 01 ) , false , "IT" );
Console.WriteLine( "Employee 2 : " );
Console.WriteLine( e2.EmployeeId ); // 2
Console.WriteLine( e2.EmployeeName ); // Candy
Console.WriteLine( e2.BirthDay ); // 2000/1/1 上午 12:00:00
Console.WriteLine( e2.IsMarried ); // False
Console.WriteLine( e2.Department ); //IT
Console.WriteLine( );

var e3 = default( Employee );
Console.WriteLine( "Employee 3 : " );
Console.WriteLine( e3.EmployeeId ); // 0
Console.WriteLine( e3.EmployeeName ); // Empty String
Console.WriteLine( e3.BirthDay ); // 0001/1/1 上午 12:00:00
Console.WriteLine( e3.IsMarried ); // False
Console.WriteLine( e3.Department ); // Empty String

public struct Employee {
  public Employee( ) {
    EmployeeId = -1;
    EmployeeName = "None";
    BirthDay = DateTime.Now;
    IsMarried = false;
    Department = "None";
  }

  public Employee( int id , string name , DateTime birth , bool isMarried , string? dept ) {
    EmployeeId = id;
    EmployeeName = name;
    BirthDay = birth;
    IsMarried = isMarried;
    Department = dept;
  }
  public int EmployeeId;
  public string EmployeeName { get; set; }
  public DateTime BirthDay { get; set; }
  public bool IsMarried { get; set; }
  public string? Department { get; set; }
}

 

在C# 11版中,結構的建構函式中不必初始化所有欄位與自動屬性成員,編譯器會自動將他們初始化為型別預設值,參考以下範例程式碼,當然這個範例程式碼在.NET 6的環境中無法編譯及執行:

var e1 = new Employee( );
Console.WriteLine("Employee 1 : "); 
Console.WriteLine( e1.EmployeeId ); // -1
Console.WriteLine( e1.EmployeeName ); // Empty String
Console.WriteLine( e1.BirthDay ); // 0001/1/1 上午 12:00:00
Console.WriteLine( e1.IsMarried ); // False
Console.WriteLine( e1.Department ); // Empty String
Console.WriteLine();

var e2 = new Employee( 2 , "Candy" , new DateTime( 2000 , 01 , 01 ) , false , "IT" );
Console.WriteLine( "Employee 2 : " );
Console.WriteLine( e2.EmployeeId ); // 2
Console.WriteLine( e2.EmployeeName ); // Candy
Console.WriteLine( e2.BirthDay ); // 0001/1/1 上午 12:00:00
Console.WriteLine( e2.IsMarried ); // False
Console.WriteLine( e2.Department ); // Empty String
Console.WriteLine( );

var e3 = default( Employee );
Console.WriteLine( "Employee 3 : " );
Console.WriteLine( e3.EmployeeId ); // 0
Console.WriteLine( e3.EmployeeName ); // Empty String
Console.WriteLine( e3.BirthDay ); // 0001/1/1 上午 12:00:00
Console.WriteLine( e3.IsMarried ); // False
Console.WriteLine( e3.Department ); // Empty String

public struct Employee {
  public Employee( ) {
    EmployeeId = -1;
  }

  public Employee( int id , string name , DateTime birth , bool isMarried , string? dept ) {
    EmployeeId = id;
    EmployeeName = name;
  }
  public int EmployeeId;
  public string EmployeeName { get; set; }
  public DateTime BirthDay { get; set; }
  public bool IsMarried { get; set; }
  public string? Department { get; set; }
}

Entity Framework(EF)7簡介 - 1

$
0
0

.NET Magazine國際中文電子雜誌
許薰尹
稿張智凱
文章編號N230524901
出刊日期 2023/5/3

Entity Framework Core(EF Core)7 是Entity Framework Core (EF Core) 6的下一版,而不是Entity Framework的下一版。從這個版本開始,名稱可以再精簡一些,直接之稱為Entity Framework(EF)7,雖然名稱之中沒有「Core」的字眼,並不代表它可以在.NET Framework的環境中執行,它需要在.NET的環境中執行。

這篇文章將介紹Entity Framework Core(EF Core)7的新功能。為了說明,我們將以「Console App」為例,搭配資料庫優先(Database First)方式來存取Northwind資料庫。按照以下步驟建立專案:

  • 從Visual Studio開發工具「File」-「New」-「Project」項目,在「Create a New Project」對話盒中,選取「C#」程式語言,選取「Console App」。
  • 在「Configure your new project 」視窗設定專案名稱與專案存放路徑。
  • 在「Additional information 」視窗選取Framework為「NET 7」,然後按下「Create」按鈕建立專案。
  • 從Visual Studio 2022開發工具「Tools」-「NuGet Package Manager」-「Package Manager Console」開啟「Package Manager Console」視窗,然後在提示字元中輸入指令安裝Entity Framework 7套件:

Install-Package Microsoft.EntityFrameworkCore.SqlServer

Install-Package Microsoft.EntityFrameworkCore.Tools

連接字串

和前版不相同的地方在於:Entity Framework 7預設連接字串中的「Encrypt」項目預設值為「True」,因此資料庫伺服器必需設定一個有效的憑證,而用戶端則要信任這個憑證,若有任何一個條件沒有滿足,會得到例外錯誤。

A connection was successfully established with the server, but then an error occurred during the login process. (provider: SSL Provider, error: 0 - 此憑證鏈結是由不受信任的授權單位發出的。 )

要處理這個問題有三個做法,透過「SQL Server 2019 Configuration Manager」管理工具在SQL Server伺服器安裝憑證,詳細步驟可參考微軟文件:

https://learn.microsoft.com/en-us/sql/database-engine/configure-windows/configure-sql-server-encryption?view=sql-server-ver16#step-1-configure-sql-server-to-use-certificates

第二個做法是在連接字串中,加上「TrustServerCertificate=True」來跳過信任的步驟,例如以下連接字串範例:

Server=.\sqlexpress;Database=Northwind;Trusted_Connection=True;TrustServerCertificate=True

第三個做法是在連接字串中,加上「Encrypt=False」,參考以下連接字串範例:

Server=.\sqlexpress;Database=Northwind;Trusted_Connection=True;Encrypt=False;

這樣開發機器連接到本機資料庫時,就不需要一個有效的憑證。使用第二與第三種做法較不安全,但足夠我們開發階段測試用了。

資料庫優先

首先我們需要利用「Scaffold-DbContext」命令由現有的「Northwind」資料庫建立DbContext與相關的Entity類別。在「Package Manager Console」視窗輸入以下指令:

Scaffold-DbContext "Server=.\sqlexpress;Database=Northwind;Trusted_Connection=True;TrustServerCertificate=True" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models

或者以下指令:

Scaffold-DbContext "Server=.\sqlexpress;Database=Northwind;Trusted_Connection=True;Encrypt=False;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models

完成後專案中會多出一個「Models」資料夾,包含DbContext與相關的Entity類別的檔案,預設會有一個「NorthwindContext」類別可以讓你連接到資料庫。我們可以在專案「Program.cs」檔案中,直接加入以下程式查詢出「Northwind」資料庫「Employees」資料表的筆數:

using var context = new NorthwindContext( );
Console.WriteLine(context.Employees.ToList().Count);

 

使用「ExecuteUpdate」與「ExecuteDelete」方法進行大量更新與刪除

Entity Framework(EF)7新增「ExecuteUpdate」與「ExecuteDelete」兩個方法,用於進行大量資料的更新與刪除作業,由於不牽涉到實體變更的追蹤動作,也不必把所有實體載入記憶體,執行起來較一次處理一個實體有效率的多。從名稱可得知「ExecuteUpdate」方法用於更新資料庫中的實體;「ExecuteDelete」方法則用於刪除資料庫中的實體。「ExecuteUpdate」與「ExecuteDelete」方法還可以搭配LINQ查詢來篩選想異動的資料。不過這兩個方法有一個限制,只能針對單一資料表做動作,無法一次修改或刪除多個資料表。

使用「ExecuteDelete」方法刪除資料

以下是使用「ExecuteDelete」方法刪除「Region」資料中所有資料的範例程式碼:

using Microsoft.EntityFrameworkCore;
using var context = new NorthwindContext( );

try {

  var affected =  context.Regions
        .ExecuteDelete( );

  Console.WriteLine( "影響筆數 : " + affected );
}
catch ( Exception ex ) {
  Console.WriteLine( ex.Message );
}

 

這段程式碼執行時,Entity Framework 7會產生以下T-SQL語法進行資料刪除動作:

DELETE FROM Region AS r

SELECT 1

FROM Region AS r

不過這個範例在執行時會得到以下錯誤訊息:

發生例外錯誤 ,訊息 : The DELETE statement conflicted with the REFERENCE constraint "FK_Territories_Region". The conflict occurred in database "Northwind", table "dbo.Territories", column 'RegionID'.

這是因為「Region」資料表與「Territories」資料表之間有一對多的關聯性,因此無法直接刪除「Region」資料表資料。若要修正這個問題,其中做法是,先刪除相關的「Territories」資料表資料,再刪除「Region」資料表資料。

我們可以使用非同步的「ExecuteDeleteAsync」方法來進行刪除,參考以下範例程式碼:

using Microsoft.EntityFrameworkCore;
using var context = new NorthwindContext( );

try {

  var affected = await context.Regions
        .ExecuteDeleteAsync( );

  Console.WriteLine( "影響筆數 : " + affected );
}
catch ( Exception ex ) {
  Console.WriteLine( ex.Message );
}

 

加上註解

使用「TagWith」方法可以為產生出的SQL語法加上文字性的描述,修改上例程式如下:

using Microsoft.EntityFrameworkCore;
using var context = new NorthwindContext( );

try {

  var affected = await context.Regions
    .TagWith( "*****Deleting regions...*****" )
    .ExecuteDeleteAsync( );

  Console.WriteLine( "影響筆數 : " + affected );
}
catch ( Exception ex ) {
  Console.WriteLine( ex.Message );
}
Console.ReadLine( );

 

這段程式碼執行時,Entity Framework 7會產生以下T-SQL語法,包含「TagWith」產生的註解:

-- *****Deleting regions...*****

DELETE FROM Region AS r

SELECT 1

FROM Region AS r

設定篩選條件

「ExecuteDeleteAsync」方法可以再搭配LINQ語法,叫用「Where」方法進一步設定篩選條件,例如以下範例程式碼刪除「RegionDescription」欄位包含「test」字串的資料:

using Microsoft.EntityFrameworkCore;
using var context = new NorthwindContext( );

try {
  var affected = await context.Regions
          .Where( r => r.RegionDescription.Contains( "test" ) )
          .ExecuteDeleteAsync( );
  Console.WriteLine( "影響筆數 : " + affected );
}
catch ( Exception ex ) {
  Console.WriteLine("發生例外錯誤 ,訊息 : "+ ex.Message );
}

 

這段程式碼執行時,Entity Framework 7會產生以下T-SQL語法,搭配「like」關鍵字篩選出要刪除的料:

DELETE FROM Region AS r

SELECT 1

FROM Region AS r

WHERE r.RegionDescription LIKE N'%test%'

此外你還可以搭配導覽屬性(Navigation Property)來進一步設定相關聯資料的篩選條件,例如以下範例程式碼,將「Region」相關聯的「Territories」資料表中「TerritoryDescription」資料為「New York」的資料刪除:

using Microsoft.EntityFrameworkCore;
using var context = new NorthwindContext( );

try {
  var affected = await context.Regions
          .Where( r => r.Territories.Any( t => t.TerritoryDescription.Trim() == "New York" ) )
          .ExecuteDeleteAsync( );

  Console.WriteLine( "影響筆數 : " + affected );
}
catch ( Exception ex ) {
  Console.WriteLine( "發生例外錯誤 ,訊息 : " + ex.Message );
}

 

 

這段程式碼執行時,Entity Framework 7會產生以下T-SQL語法來刪除資料:

DELETE FROM Region AS r

SELECT 1

FROM Region AS r

WHERE EXISTS (

SELECT 1

FROM Territories AS t

WHERE (( r.RegionID != NULL ) && ( r.RegionID == t.RegionID )) && ( LTRIM( RTRIM( t.TerritoryDescription ) ) == N'New York' ) )

 

使用「ExecuteUpdate」方法修改資料

要一次修改多筆資料,可以叫用「ExecuteUpdate」方法,參考以下範例程式碼,一次修改「Employees」資料表中的「Title」欄位值:

 

using Microsoft.EntityFrameworkCore;
using var context = new NorthwindContext( );

try {
  var affected =  context.Employees
          .ExecuteUpdate( e =>
          e.SetProperty( t => t.Title , t => t.Title + "!" )
          );

  Console.WriteLine( "影響筆數 : " + affected );
}
catch ( Exception ex ) {
  Console.WriteLine( "發生例外錯誤 ,訊息 : " + ex.Message );
}

 

這個方法有一個非同步版本,可改寫上例程式如下:

 

using Microsoft.EntityFrameworkCore;
using var context = new NorthwindContext( );

try {
  var affected = await context.Employees
          .ExecuteUpdateAsync( e =>
          e.SetProperty( t => t.Title , t => t.Title + "!" )
          );

  Console.WriteLine( "影響筆數 : " + affected );
}
catch ( Exception ex ) {
  Console.WriteLine( "發生例外錯誤 ,訊息 : " + ex.Message );
}

 

這段程式碼執行時,Entity Framework 7會產生以下T-SQL語法來更新資料:

UPDATE Employees AS e

SET

e.Title = e.Title + N'!'

SELECT 1

FROM Employees AS e

更新多個欄位

使用「ExecuteUpdateAsync」方法時,你可以叫用「SetProperty」方法多次, 一次更新多個欄位,例如以下範例程式碼,一次修改「Employees」資料表中的「Title」與「LastName」欄位值:

 

using Microsoft.EntityFrameworkCore;
using var context = new NorthwindContext( );

try {
  var affected = await context.Employees
          .ExecuteUpdateAsync( e =>
          e.SetProperty( t => t.Title , t => t.Title!.Substring( 0 , t.Title.Length - 1 ) )
           .SetProperty( t => t.LastName , t => t.LastName + "!" )
          );

  Console.WriteLine( "影響筆數 : " + affected );
}
catch ( Exception ex ) {
  Console.WriteLine( "發生例外錯誤 ,訊息 : " + ex.Message );
}

 

這段程式碼執行時,Entity Framework 7會產生以下T-SQL語法來更新資料:

UPDATE Employees AS e

SET

e.LastName = e.LastName + N'!',

e.Title = SUBSTRING( e.Title, 0 + 1, CAST( LEN( e.Title ) AS int ) - 1 )

SELECT 1

FROM Employees AS e

設定篩選條件

「ExecuteUpdateAsync」方法可以搭配LINQ語法設定篩選條件來修改資料,例如以下範例程式碼只修改「EmployeeId」欄位值大於5的資料:

using Microsoft.EntityFrameworkCore;
using var context = new NorthwindContext( );

try {
  var affected = await context.Employees
          .Where( e => e.EmployeeId > 5 )
          .ExecuteUpdateAsync( e =>
             e.SetProperty( t => t.LastName , t => t.LastName + "!" )
          );

  Console.WriteLine( "影響筆數 : " + affected );
}
catch ( Exception ex ) {
  Console.WriteLine( "發生例外錯誤 ,訊息 : " + ex.Message );
}

 

這段程式碼執行時,Entity Framework 7會產生以下T-SQL語法來更新資料:

UPDATE Employees AS e

SET

e.LastName = e.LastName + N'!'

SELECT 1

FROM Employees AS e

WHERE e.EmployeeID > 5

 

再修改程式碼,將上例更新的資料改回來:

 

using Microsoft.EntityFrameworkCore;
using var context = new NorthwindContext( );

try {
  var affected = await context.Employees
          .Where(e=>e.EmployeeId > 5)
          .ExecuteUpdateAsync( e =>
          e.SetProperty( t => t.LastName , t => t.LastName!.Substring( 0 , t.LastName.Length - 1 ) )
          );

  Console.WriteLine( "影響筆數 : " + affected );
}
catch ( Exception ex ) {
  Console.WriteLine( "發生例外錯誤 ,訊息 : " + ex.Message );
}

 

這段程式碼執行時,Entity Framework 7會產生以下T-SQL語法來更新資料:

UPDATE Employees AS e

SET

e.LastName = SUBSTRING( e.LastName, 0 + 1, CAST( LEN( e.LastName ) AS int ) – 1 )

SELECT 1

FROM Employees AS e

WHERE e.EmployeeID > 5


Entity Framework(EF)7簡介 - 2

$
0
0

.NET Magazine國際中文電子雜誌
許薰尹
稿張智凱
文章編號N230524902
出刊日期2023/5/17

本文將延續本站《Entity Framework(EF)7簡介 - 1》一文的情境,介紹Entity Framework Core(EF Core)7 的新增特性,這篇文章將延用上文建立的專案來進行說明,讓您了解效能提升的優化方式,交易的使用,以及如何使用 T4(Text Template Transformation Toolkit)模板自訂 Scaffolded 程式碼。

提升效能

為了確保資料的完整性,異動資料庫的動作通常會透過交易機制加以保護,為了加快執行效能Entity Framework 7會在執行單一SQL陳述句時,避免啟動交易。參考以下新增一筆資料到「Region」資料表的程式碼:

 

 

using var context = new NorthwindContext( );
try {

  var affected = await context.AddAsync( new Region { RegionId = 5 , RegionDescription = "Middle" } );
  await context.SaveChangesAsync( );

  Console.WriteLine( "影響筆數 : " + affected );
}
catch ( Exception ex ) {
  Console.WriteLine( ex.Message );
}

 

 

這段程式碼執行時,Entity Framework 7會產生以下T-SQL語法來更新資料,將「IMPLICIT_TRANSACTIONS」設定為「OFF」:

SET IMPLICIT_TRANSACTIONS OFF;

SET NOCOUNT ON;

INSERT INTO [Region] ( [RegionID], [RegionDescription] )

VALUES ( @p0, @p1 );

再看一個例子,使用以下程式碼,一次新增兩筆資料到「Region」資料表:

 

using var context = new NorthwindContext( );
try {

  await context.AddAsync( new Region { RegionId = 6 , RegionDescription = "Middle1" } );
  await context.AddAsync( new Region { RegionId = 7 , RegionDescription = "Middle2" } );
  var affected = await context.SaveChangesAsync( );

  Console.WriteLine( "影響筆數 : " + affected );
}
catch ( Exception ex ) {
  Console.WriteLine( ex.Message );
}
Console.ReadLine( );

 

 

 

這段程式碼執行時,Entity Framework 7會產生以下T-SQL語法:

SET IMPLICIT_TRANSACTIONS OFF;

SET NOCOUNT ON;

INSERT INTO [Region] ( [RegionID], [RegionDescription] )

VALUES ( @p0, @p1 ),

( @p2, @p3 );

使用「SaveChangesAsync」更新多筆資料

若在程式中叫用「SaveChangesAsync」方法多次來更新多筆資料,這種情況下是沒有交易保護作用的,參考以下範例程式碼:

 

using var context = new NorthwindContext( );

try {

  await context.AddAsync( new Region { RegionId = 6 , RegionDescription = "Middle1" } );
  await context.SaveChangesAsync( );

  await context.AddAsync( new Region { RegionId = 7 , RegionDescription = "Middle2" } );
  await context.SaveChangesAsync( );


}
catch ( Exception ex ) {
  Console.WriteLine( ex.Message );
}

 

 

 

使用交易

若要使用交易來防止資料錯誤,可以使用「IDbContextTransaction」,參考以下範例程式碼: 

 

using var context = new NorthwindContext( );
using var transaction = context.Database.BeginTransaction( );

try {

  await context.AddAsync( new Region { RegionId = 6 , RegionDescription = "Middle1" } );
  await context.SaveChangesAsync( );

  await context.AddAsync( new Region { RegionId = 7 , RegionDescription = "Middle2" } );
  await context.SaveChangesAsync( );

  transaction.Commit( );

}
catch ( Exception ex ) {
  Console.WriteLine( ex.Message );
  transaction.Rollback( );
}

 

 

 

客製化模板(Custom Template)

T4(Text Template Transformation Toolkit)模板是一種可用來產生程式碼的生成引擎,它能夠根據一個模板文件來產生指定的Scaffolded程式碼。

在 Entity Framework 7 中,您現在可以使用 T4來自訂資料庫優先(Database First)工作流程,透過 T4 模板自行定義DbContext 和實體(Entity)類別的程式碼該如何產生,以滿足開發人員的需求,這樣可以節省大量時間和精力來設計資料存取程式碼。

不過T4 模板可能需要一些學習成本,你得花一些時間去了解 T4 模板語言,同時T4 模板產生的程式碼可能過於標準化,無法滿足特定的需求和場景,因此開發人員可能還是需要手動去修改生成的程式碼。

接下來讓我們看看如何透過Entity Framework 7中的反向工程來自訂 Scaffolded 程式碼,首先需要先使用「dotnet new」安裝「Microsoft.EntityFrameworkCore.Templates套件,安裝之前可以先在作業系統命令提示字元下指令確認是否已安裝「Entity Framework Core Scaffolding Templates」範本:

dotnet new list

指令執行後看起來的結果如下:

C:\Users\Admin>dotnet new list
這些範本符合您的輸入:

範本名稱              簡短名稱                    語言        標記
--------------------  --------------------------  ----------  ---------------------------------------------------------
.NET MAUI Blazor ...  maui-blazor                 [C#]        MAUI/Android/iOS/macOS/Mac Catalyst/Windows/Tizen/Blazor
.NET MAUI Content...  maui-page-csharp            [C#]        MAUI/Android/iOS/macOS/Mac Catalyst/WinUI/Tizen/Xaml/Code
...略
Entity Framework Core Scaffolding Templates  ef-templates  [C#]  EFCore/Scaffolding
...略

若範本名稱沒有列出「Entity Framework Core Scaffolding Templates」,那麼可下以下指令安裝:

dotnet new install Microsoft.EntityFrameworkCore.Templates

 

完成之後可以使用「dotnet new」或Visual Studio 2022建立一個專案(如Console),然後在專案檔案所在資料夾中執行以下指令:

dotnet new ef-templates

 

接著專案中會建立一個「CodeTemplates」資料夾,其「EFCore」子資料夾下,將新增兩個檔案「DbContext.t4」與「EntityType.t4」,其中「DbContext.t4」用來產生DbContext類別的程式碼,而「EntityType.t4」則是用來產生Entity類別的程式碼。

 

讓我們試著修改「DbContext.t4」程式碼,體驗一下客製化,在約28行的地方,加入引用「System.Data.SqlClient」命名空間的程式碼:

 

var usings = new List<string>
    {
        "System",
        "System.Collections.Generic",
        "Microsoft.EntityFrameworkCore",
        "System.Data.SqlClient"
    };

在約50行的地方,我們讓「DbContext」類別實作「ILogger」介面(在此僅舉例說明客製化模板的方式,並沒有真的要實作這個介面),參考以下範例程式碼:

public partial class <#= Options.ContextName #> : DbContext , ILogger
{<#
    if (!Options.SuppressOnConfiguring)
    {
#>
...略

 

同樣修改「EntityType.t4」檔案程式碼,在27行的地方引用「System.Data.SqlClient」命名空間的程式碼,參考以下範例程式碼:

 

var usings = new List<string>
    {
        "System",
        "System.Collections.Generic",
        "System.Data.SqlClient"
    };

接著我們利用「Scaffold-DbContext」指令從現有資料庫來建立DbContext與Entity類別程式碼,參考以下範例程式碼:

Scaffold-DbContext "Server=.\sqlexpress;Database=Northwind;Trusted_Connection=True;TrustServerCertificate=True" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models

檢視產生的「NorthwindContext.cs」程式碼參考如下:

 

using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using Microsoft.EntityFrameworkCore;

namespace ConsoleApp2.Models;

public partial class NorthwindContext : DbContext , ILogger
{
    public NorthwindContext()
    {
    }
以下略...

 

檢視產生的「Category.cs」程式碼參考如下:

using System;
using System.Collections.Generic;
using System.Data.SqlClient;

namespace ConsoleApp2.Models;

public partial class Category
{
    public int CategoryId { get; set; }

以下略...

 

這些T4模板存放於Github網站,參考以下網址:

https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.Templates

未來若網站有新版本,可以在專案中下以下指令更新T4模板:

dotnet new update

Entity Framework(EF)7簡介 - 3

$
0
0

.NET Magazine國際中文電子雜誌
許薰尹
稿張智凱
文章編號N230524903
出刊日期: 2023/5/31

本文將延續本站本站《Entity Framework(EF)7簡介 - 1》、《Entity Framework(EF)7簡介 - 2》文章的情境,介紹Entity Framework Core(EF Core)7 的新增特性,這篇文章將延用上文建立的專案來進行說明,了解如何使用Entity Framework(EF)7來呼叫預存程序。

預存程序

在Entity Framework 7之中要如何使用事先定義在資料庫中的預存程式呢? 接下來這個小節就讓我們來談談使用的方式。以Northwind範例資料庫為例,我們先使用以下T-SQL建立預存程序,針對「Northwind」「Region」資料表的資料做新增、刪除、修改、查詢資料的動作:

 

Create PROCEDURE usp_GetRegion 
AS
	select RegionID,RegionDescription from Region
GO

Create PROCEDURE usp_GetOneRegion (
	@RegionID int
)
AS
	select RegionID,RegionDescription from Region where RegionID=@RegionID
GO

Create PROCEDURE usp_RegionDescription 
AS
	select RegionDescription from Region
GO

Create PROCEDURE usp_InsertRegion (
	@RegionID int,
	@RegionDescription varchar(50)
)
AS
	insert into region(RegionID,RegionDescription) values (@RegionID,@RegionDescription)
GO

Create PROCEDURE usp_UpdateRegion (
	@RegionID int,
	@RegionDescription varchar(50)
)
AS
UPDATE Region
       SET RegionDescription=@RegionDescription
	   where RegionID = @RegionID


go

Create PROCEDURE usp_DeleteRegion (
	@RegionID int
)
AS
Delete Region
	   where RegionID = @RegionID
go

 

 

 

目前專案中「Region」類別的程式碼如下:

 

 

using System;
using System.Collections.Generic;

namespace EFCDemo.Models;

public partial class Region {
  public int RegionId { get; set; }

  public string RegionDescription { get; set; } = null!;

  public virtual ICollection<Territory> Territories { get; } = new List<Territory>( );
}

 

 

「DbSet」類別提供多個方法可以進行資料的查詢,包含「FromSql」、「FromSqlInterpolated」與「FromSqlRaw」等等,若要透過Entity Framework 7異動資料可以利用「DbContext.Database」物件的「ExecuteSql」、「ExecuteSqlInterpolated」與「ExecuteSqlRaw」方法。

「FromSql」方法

若要使用預存程序來查詢資料,查詢需要取回對應到模型屬性的所有欄位,這樣Entity Framework 7才能夠將資料對應用模型實體類別。「FromSql」方法是Entity Framework 7 版新增的語法,只接收一個「FormattableString」型別的參數,參數值會被包裝在DbParameter物件中以防止SQL隱碼攻擊(SQL injection)。以下是使用「FromSql」方法呼叫預存程序的範例:

 

using Microsoft.EntityFrameworkCore;
using var context = new NorthwindContext( );

var regions = context?.Regions?.FromSql( $"usp_GetRegion" ).ToList( )
  ?? Enumerable.Empty<Region>( );

foreach ( var item in regions ) {
  Console.WriteLine( $"RegionId : { item.RegionId } , RegionDescription : { item.RegionDescription }" );
}

 

 

 

若要使用「FromSql」方法呼叫需要參數的「usp_GetOneRegion」預存程式,可以使用以下程式碼:

 

 

using Microsoft.EntityFrameworkCore;
using var context = new NorthwindContext( );
var regionid = 1;
var regions = context?.Regions?.FromSql( $"usp_GetOneRegion { regionid }" ).ToList( )
  ?? Enumerable.Empty<Region>( );

foreach ( var item in regions ) {
  Console.WriteLine( $"RegionId : { item.RegionId } , RegionDescription : { item.RegionDescription }" );
}

 

 

若叫用預存程式只取回部分對應到模型屬性的欄位,例如以下範例程式碼:

 

using Microsoft.EntityFrameworkCore;
using var context = new NorthwindContext( );

var regions = context?.Regions?.FromSql( $"usp_RegionDescription" ).ToList( )
  ?? Enumerable.Empty<Region>( );

foreach ( var item in regions ) {
  Console.WriteLine( $" RegionDescription : { item.RegionDescription }" );
}

 

 

 

則執行時會得到以下例外錯誤,訊息如下:

Unhandled exception. System.InvalidOperationException: The required column 'RegionID' was not present in the results of a 'FromSql' operation.

「FromSqlInterpolated」方法

「FromSqlInterpolated」方法是EF Core 3新增的語法,使用方式和「FromSql」方法差不多,參考以下範例程式碼:

 

 

using Microsoft.EntityFrameworkCore;
using var context = new NorthwindContext( );

var regions = context?.Regions?.FromSqlInterpolated( $"usp_GetRegion" ).ToList( )
  ?? Enumerable.Empty<Region>( );

foreach ( var item in regions ) {
  Console.WriteLine( $"RegionId : { item.RegionId } , RegionDescription : { item.RegionDescription }" );
}

 

 

若要使用「FromSqlInterpolated」方法呼叫需要參數的「usp_GetOneRegion」預存程式,可以使用以下程式碼:

 

 

using Microsoft.EntityFrameworkCore;
using var context = new NorthwindContext( );
var regionid = 1;
var regions = context?.Regions?.FromSqlInterpolated( $"usp_GetOneRegion { regionid }" ).ToList( )
  ?? Enumerable.Empty<Region>( );

foreach ( var item in regions ) {
  Console.WriteLine( $"RegionId : { item.RegionId } , RegionDescription : { item.RegionDescription }" );
}

 

 

若叫用預存程式只取回部分對應到模型屬性的欄位,例如以下範例程式碼:

 

 

using Microsoft.EntityFrameworkCore;
using var context = new NorthwindContext( );

var regions = context?.Regions?.FromSqlInterpolated( $"usp_RegionDescription" ).ToList( )
  ?? Enumerable.Empty<Region>( );

foreach ( var item in regions ) {
  Console.WriteLine( $" RegionDescription : {item.RegionDescription}" );
}

 

 

則執行時會得到以下例外錯誤,訊息如下:

Unhandled exception. System.InvalidOperationException: The required column 'RegionID' was not present in the results of a 'FromSql' operation.

「FromSqlRaw」方法

「FromSqlRaw」較不安全,可能會有SQL隱碼攻擊(SQL injection)問題,因此要小心使用,若要使用這個方法來呼叫預存程序,最好將參數包裝在參數物件,不要使用串字串的方式來傳遞參數,例如以下範例程式碼:

 

 

using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using var context = new NorthwindContext( );
var regionid = 1;
var regions = context?.Regions?
  .FromSqlRaw( $"usp_GetOneRegion @regionid" ,new SqlParameter( "@regionid" , regionid ))
  .ToList( )
  ?? Enumerable.Empty<Region>( );

foreach ( var item in regions ) {
  Console.WriteLine( $"RegionId : { item.RegionId } , RegionDescription : { item.RegionDescription }" );
}

 

 

當然你可以使用組字串的方式來傳遞參數,例如以下範例程式碼,但這種做法是不安全的,參數沒有包裝成DbParameter參數物件傳遞,容易造成SQL隱碼攻擊(SQL injection):

 

 

using Microsoft.EntityFrameworkCore;
using var context = new NorthwindContext( );
var regionid = 1;
var regions = context?.Regions?
  .FromSqlRaw( $" usp_GetOneRegion @regionid = { regionid } "  )
  .ToList( )
  ?? Enumerable.Empty<Region>( );

foreach ( var item in regions ) {
  Console.WriteLine( $"RegionId : { item.RegionId } , RegionDescription : {I tem.RegionDescription }" );
}

 

 

「Database.ExecuteSql」方法

若要進行資料的新增、修改、刪除動作,可以利用「Database」的「ExecuteSql」方法,以下是呼叫預存程式新增資料的範例程式碼:

 

 

using Microsoft.EntityFrameworkCore;
using var context = new NorthwindContext( );
var regionID = 5;
var regionDescription = "Test";

context.Database.ExecuteSql( $"usp_InsertRegion @RegionID={ regionID }, @RegionDescription={ regionDescription }" );

 

 

以下是呼叫預存程式修改資料的範例程式碼:

 

 

using Microsoft.EntityFrameworkCore;
using var context = new NorthwindContext( );
var regionID = 5;
var regionDescription = "Test!!!";

context.Database.ExecuteSql( $"usp_UpdateRegion @RegionID={ regionID }, @RegionDescription={ regionDescription }" );

 

 

以下是呼叫預存程式刪除資料的範例程式碼:

 

using Microsoft.EntityFrameworkCore;
using var context = new NorthwindContext( );
var regionID = 5;
context.Database.ExecuteSql( $"usp_DeleteRegion @RegionID={ regionID }" );
Console.WriteLine( "Finish!" );

 

 

還有一個「ExecuteSqlAsync」方法使用非同步方式執行SQL命令,例如以下呼叫預存程式刪除資料的範例程式碼:

 

using Microsoft.EntityFrameworkCore;
using var context = new NorthwindContext( );
var regionID = 5;
await context.Database.ExecuteSqlAsync( $"usp_DeleteRegion @RegionID={ regionID }" );
Console.WriteLine( "Finish!" );

 

 

 

動態產生SQL

「ExecuteSql」較安全,不能用串字串方式來執行SQL命令,例如以下呼叫預存程序的程式碼是不能執行的:

 

 

using Microsoft.EntityFrameworkCore;
using var context = new NorthwindContext( );
var regionID = 5;
var regionDescription = "Test";
var regionIDField = "@RegionID";
var regionDescriptionField = "@RegionDescription";

context.Database.ExecuteSql( $" usp_InsertRegion { regionIDField }={ regionID },{ regionDescriptionField }={ regionDescription }" );

Console.WriteLine( "Finish!" );

 

 

這段程式一執行會得到以下例外錯誤:

Unhandled exception. Microsoft.Data.SqlClient.SqlException (0x80131904): Procedure or function 'usp_InsertRegion' expects parameter '@RegionID', which was not supplied.

要動態組SQL指令可以改用「ExecuteSqlRaw」方法,參考以下範例程式碼,為了安全起見,參數要包成「SqlParameter」參數物件:

 

 

using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using var context = new NorthwindContext( );
var regionID = 5;
var regionDescription = "Test";
var regionIDField = "@RegionID";
var regionDescriptionField = "@RegionDescription";

 context.Database.ExecuteSqlRaw( $"usp_InsertRegion {regionIDField}=@rid,{regionDescriptionField}=@rdesc" ,
  new SqlParameter( "@rid" , regionID ) ,
  new SqlParameter( "@rdesc" , regionDescription )
  );

Console.WriteLine( "Finish!" );

 

 

若改用非同步的「ExecuteSqlRawAsync」,可修改程式如下:

 

using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using var context = new NorthwindContext( );
var regionID = 5;
var regionDescription = "Test";
var regionIDField = "@RegionID";
var regionDescriptionField = "@RegionDescription";

await context.Database.ExecuteSqlRawAsync( $"usp_InsertRegion { regionIDField }=@rid,{ regionDescriptionField }=@rdesc" ,
  new SqlParameter( "@rid" , regionID ) ,
  new SqlParameter( "@rdesc" , regionDescription )
  );

Console.WriteLine( "Finish!" );

 

 

 

「ExecuteSqlInterpolated」方法

「ExecuteSqlInterpolated」方法使用方式和「ExecuteSql」是一樣的,以下是呼叫預存程式新增資料的範例程式碼:

 

 

using Microsoft.EntityFrameworkCore;
using var context = new NorthwindContext( );
var regionID = 5;
var regionDescription = "Test";

context.Database.ExecuteSqlInterpolated( $"usp_InsertRegion @RegionID={ regionID }, @RegionDescription={ regionDescription }" );
Console.WriteLine( "Finish!" );

 

 

以下是呼叫預存程式修改資料的範例程式碼:

 

 

using Microsoft.EntityFrameworkCore;
using var context = new NorthwindContext( );
var regionID = 5;
var regionDescription = "Test!!!";

context.Database.ExecuteSqlInterpolated( $"usp_UpdateRegion @RegionID={ regionID }, @RegionDescription={ regionDescription }" );
Console.WriteLine( "Finish!" );

 

 

以下是呼叫預存程式刪除資料的範例程式碼:

 

 

using Microsoft.EntityFrameworkCore;
using var context = new NorthwindContext( );
var regionID = 5;
context.Database.ExecuteSqlInterpolated( $"usp_DeleteRegion @RegionID={ regionID }" );
Console.WriteLine( "Finish!" );

 

 

還有一個「ExecuteSqlInterpolatedAsync」方法使用非同步方式執行SQL命令,例如以下範例程式碼:

 

 

using Microsoft.EntityFrameworkCore;
using var context = new NorthwindContext( );
var regionID = 5;
await context.Database.ExecuteSqlInterpolatedAsync( $"usp_DeleteRegion @RegionID={ regionID }" );
Console.WriteLine( "Finish!" );

 

 

 

Database.ExecuteSqlRaw方法

「ExecuteSqlRaw」方法在使用上和「ExecuteSql」方法相同,但要小心可能會有SQL隱碼攻擊的問題。最好使用「SqlParameter」物件來傳遞參數,以下是呼叫預存程式新增資料的範例程式碼:

 

using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using var context = new NorthwindContext( );
var regionID = 5;
var regionDescription = "Test";

context.Database.ExecuteSqlRaw( $"usp_InsertRegion @RegionID, @RegionDescription" ,
   new SqlParameter( "@RegionID" , regionID ) ,
   new SqlParameter( "@RegionDescription" , regionDescription )
  );

以下是呼叫預存程式修改資料的範例程式碼:

using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using var context = new NorthwindContext( );
var regionID = 5;
var regionDescription = "Test!!!";

context.Database.ExecuteSqlRaw( $"usp_UpdateRegion @RegionID, @RegionDescription" ,
   new SqlParameter( "@RegionID" , regionID ) ,
   new SqlParameter( "@RegionDescription" , regionDescription )
  );

 

以下是呼叫預存程式刪除資料的範例程式碼:

using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using var context = new NorthwindContext( );
var regionID = 5;

context.Database.ExecuteSqlRaw( $"usp_DeleteRegion @RegionID" ,
   new SqlParameter( "@RegionID" , regionID )
);

Console.WriteLine( "Finish!" );

還有一個「ExecuteSqlRawAsync」方法使用非同步方式執行SQL命令,例如以下範例程式碼:

 

using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using var context = new NorthwindContext( );
var regionID = 5;

await context.Database.ExecuteSqlRawAsync( $"usp_DeleteRegion @RegionID" ,
   new SqlParameter( "@RegionID" , regionID )
);

Console.WriteLine( "Finish!" );

「FromSql」混用LINQ語法

「FromSql」方法可以混用LINQ語法,參考以下範例程式碼,先使用SQL查詢出資料表資料,再利用LINQ的「Where」方法篩選資料:

 

using Microsoft.EntityFrameworkCore;
using var context = new NorthwindContext( );

var regions = context?.Regions?
  .FromSql( $"select * from Region" )
  .Where( r => r.RegionId > 3 )
  .ToList( )
  ?? Enumerable.Empty<Region>( );

foreach ( var item in regions ) {
  Console.WriteLine( $"RegionId : { item.RegionId } , RegionDescription : { item.RegionDescription }" );
}

但不可使用「FromSql」方法叫用預存程序,再混用LINQ語法,例如以下範例程式碼:

 

var regions = context?.Regions?
  .FromSql( $"usp_GetRegion" )
  .Where( r => r.RegionId > 3 )
  .ToList( )
  ?? Enumerable.Empty<Region>( );

這段程式一執行會得到以下例外錯誤:

Unhandled exception. System.InvalidOperationException: 'FromSql' or 'SqlQuery' was called with non-composable SQL and with a query composing over it. Consider calling 'AsEnumerable' after the method to perform the composition on the client side.

需要將其改寫如下:

using Microsoft.EntityFrameworkCore;
using var context = new NorthwindContext( );

var regions = context?.Regions?
  .FromSql( $"usp_GetRegion" ).AsEnumerable()
  .Where( r => r.RegionId > 3 )
  .ToList( )
  ?? Enumerable.Empty<Region>( );

foreach ( var item in regions ) {
  Console.WriteLine( $"RegionId : { item.RegionId } , RegionDescription : { item.RegionDescription }" );
}