2024年11月1日 星期五

.NET 8 Blazor Identity與資料庫優先

作者:許薰尹  恆逸教育訓練中心資深講師



ASP.NET Core Identity使用程式碼優先的方式進行開發,要如何搭配資料庫優先的方式,將ASP.NET Core Identity所需的資料表放在指定的現有資料庫之中呢? 這篇文章將一步步詳細說明做法。


為了方便產生存取現有的SQL Server Express「Northwind」資料庫的「Entity」與「DbContext」類別,我們先來安裝「EF Core Power Tools」擴充程式。

安裝EF Core Power Tools擴充程式
首先,打開 Visual Studio 2022 開發工具,然後在上方選單中選擇「Extensions」並點擊「Manage Extensions」。這會開啟「Extensions Manager」的對話盒。在左側的分類列表中,選擇「Browse」,然後在右上角的搜尋方塊中輸入「EF Core Power Tools」來進行搜尋。找到後,點擊「Install」按鈕,系統會自動從網路下載並安裝這個擴充功能。請參考下圖示範:



圖 1:Entity Framework Core Power Tools安裝。


建立Blazor專案


使用 Visual Studio 2022 開發工具來建立一個 .NET 8 Web App 專案,依照以下步驟進行:

  •  啟動 Visual Studio 2022 開發環境。
  •  在「開始」視窗中,選擇「Create a new project」選項。
  •  在「Create a new project」對話盒中,選擇使用 C# 語法的「Blazor Web App」範本。
  •  按下「Next」按鈕繼續設定專案。


請參考下圖以更清楚了解操作步驟:




圖 2:建立「Blazor Web App 」專案。


接下來,在「Configure your new project」視窗中,設定 Blazor 專案的名稱和專案的存放路徑。例如,您可以將專案命名為「BlazorDBFirst」。設定完成後,按下「Next」按鈕繼續。請參考下圖所示:



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



接下來,在「Additional information」視窗中進行以下設定:

  •  將「Target Framework」設定為「.NET 8.0 (Long Term Support)」。
  •  將「Authentication Type」設為「Individual Accounts」。
  •  在「Interactive render mode」中選擇「Auto (Server and WebAssembly)」。
  •  將「Interactivity location」設為「Per page/component」。
  •  勾選「Include sample pages」。

完成這些設定後,按下「Create」按鈕繼續。



圖 4:設定「Target Framework」為「NET 8.0 (Long Term Support)」。


專案建立完成後,您可以在「Solution Explorer」視窗中看到專案的結構。與 ASP.NET Core Identity 相關的元件與程式碼位於「Account」資料夾中,所有功能都被包裝成 Blazor 元件(*.razor)。


逆向工程(Reverse engineering)


我們想要將ASP.NET Core Identity所需的資料表放在SQL Server Express的「Northwind」範例資料庫。先使用 Entity Framework Core 的逆向工程(Reverse Engineering),從現有的資料庫結構生成所需的實體類別程式碼。您可以在 Visual Studio 2022 開發工具中的「Solution Explorer」視窗中,右鍵點擊專案名稱,然後從快捷選單中選擇「EF Core Power Tools」並點擊「Reverse Engineer」選項,請參考下圖所示:




圖 5:逆向工程(Reverse engineering)。

接下來的步驟是連接到資料庫,本範例使用的是「EF Core 8��版本,因此在「Choose Your Database Connection」對話盒中,勾選「EF Core 8」選項,然後按下「Add」>「Add Database Connection」。請參考下圖來確認這些操作步驟。




圖 6:連接到資料庫。

接下來,我們以連接到 Microsoft 開發用的 SQL Server Express為例,在「Connection Properties」視窗中進行以下設定:

  • 資料來源(Data Source):選擇「Microsoft SQL Server (SqlClient)」。
  • 伺服器名稱(Server name):輸入「.\SQLExpress」。
  • 驗證(Authentication):選擇「Windows驗證(Windows Authentication)」。
  • 選取或輸入資料庫名稱(Select or enter a database name):選擇「Northwind」資料庫。

請參考下圖來確認這些設定。



圖 7:連接到微軟開發用的SQL Server Express。


在「Choose your Data Connection」對話盒中,按下「OK」按鈕繼續。




圖 8:設定資料庫連線。

在「Choose your Database Objects」對話盒中,選擇您要使用的資料表(可以選擇多個)。為了簡單起見,本文範例選取所有資料表。選好後,按下「OK」按鈕繼續。請參考下圖來確認這些操作步驟。



圖 9:選擇您要使用的資料表。


下一步,在「Choose Your Settings for Project…」對話盒設定以下內容,然後按下「OK」按鈕,接著就會根據這個步驟的設定,來產生程式碼。:



圖 10:設定要產生的項目。

EF Core Power Tools 會自動在專案中加入「Microsoft.EntityFrameworkCore.SqlServer」套件,並且在「Models」資料夾中自動生成「NorthwindContext.cs」和Entity類別的檔案。請參考下圖來了解這些新增的檔案位置。


圖 11:自動安裝套件與產生實體類別程式碼。


設定連接字串


修改「appsettings.json」檔案中的連接字串設定,用來指定ASP.NET Core Identity要使用的資料庫,我們將之設定為現有的「Northwind」資料庫:

1
2
3
4
5
6
7
8
9
10
11
12
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=.\\sqlexpress;Database=Northwind;Trusted_Connection=True;TrustServerCertificate=true;"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}
 

 


測試網站


在 Visual Studio 2022 開發工具中,按下 CTRL+F5 執行網站。執行後,網站首頁的畫面如下圖所示,選單中包含「Register」、「Login」等功能。點擊「Register」來註冊一個新帳號,請參考下圖以了解操作步驟。




圖 12:註冊新帳號。


此時,會出現一個錯誤畫面,說明資料庫錯誤。這是因為我們尚未建立存放註冊帳號的資料庫。根據錯誤頁面的提示,點擊「Apply Migrations」來套用移轉,使用 Entity Framework 來建立資料庫。請參考下圖來了解詳細操作。



圖 13:套用移轉。


回到瀏覽器並重整網頁,註冊的帳號資料會成功寫入資料庫,接著會導向「Register confirmation」確認頁面。點擊「Click here to confirm your account」以完成帳號確認,這樣才能登入網站。



圖 14:「Register confirmation」畫面。


接下來,系統會顯示電子郵件確認畫面。



圖 15:電子郵件確認。


從「Server Explorer」視窗中,可以看到「Northwind」資料庫已成功建立AspNet開頭的資料表,且在「AspNetUsers」資料表中包含我們註冊的帳號資訊:



圖 16:檢視資料庫資料。


註冊服務


參考下列程式碼,修改「Program.cs」檔案,加入叫用「AddDbContextFactory」方法的程式碼,它是用來註冊「DbContextFactory」的擴充方法。「AddDbContextFactory」方法需要傳入一個委派當參數。接著,我們使用「options.UseSqlServer」方法來設定「DbContext」將使用 SQL Server 作為資料庫的儲存方式。這個方法需要一個連接字串,從「appsettings.json」檔讀取名為 「DefaultConnection」 的連接字串。這樣一來,我們就可以在需要時輕鬆地建立「NorthwindContext」的實例,並使用它來存取資料庫。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
using BlazorDBFirst.Client.Pages;
using BlazorDBFirst.Components;
using BlazorDBFirst.Components.Account;
using BlazorDBFirst.Data;
using BlazorDBFirst.Models;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
 
var builder = WebApplication.CreateBuilder( args );
 
// Add services to the container.
builder.Services.AddRazorComponents( )
    .AddInteractiveServerComponents( )
    .AddInteractiveWebAssemblyComponents( );
 
builder.Services.AddCascadingAuthenticationState( );
builder.Services.AddScoped<IdentityUserAccessor>( );
builder.Services.AddScoped<IdentityRedirectManager>( );
builder.Services.AddScoped<AuthenticationStateProvider , PersistingRevalidatingAuthenticationStateProvider>( );
 
builder.Services.AddAuthentication( options => {
  options.DefaultScheme = IdentityConstants.ApplicationScheme;
  options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
} )
    .AddIdentityCookies( );
 
var connectionString = builder.Configuration.GetConnectionString( "DefaultConnection" ) ?? throw new InvalidOperationException( "Connection string 'DefaultConnection' not found." );
builder.Services.AddDbContext<ApplicationDbContext>( options =>
    options.UseSqlServer( connectionString ) );
builder.Services.AddDatabaseDeveloperPageExceptionFilter( );
 
builder.Services.AddIdentityCore<ApplicationUser>( options => options.SignIn.RequireConfirmedAccount = true )
    .AddEntityFrameworkStores<ApplicationDbContext>( )
    .AddSignInManager( )
    .AddDefaultTokenProviders( );
 
builder.Services.AddSingleton<IEmailSender<ApplicationUser> , IdentityNoOpEmailSender>( );
 
<span style="background-color: #ffff00;">builder.Services.AddDbContextFactory<NorthwindContext>(
  options => options.UseSqlServer(
    builder.Configuration.GetConnectionString( "DefaultConnection" ) ) );</span>
 
var app = builder.Build( );
 
// Configure the HTTP request pipeline.
if ( app.Environment.IsDevelopment( ) ) {
  app.UseWebAssemblyDebugging( );
  app.UseMigrationsEndPoint( );
}
else {
  app.UseExceptionHandler( "/Error" , createScopeForErrors: true );
  // 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.UseAntiforgery( );
 
app.MapRazorComponents<App>( )
    .AddInteractiveServerRenderMode( )
    .AddInteractiveWebAssemblyRenderMode( )
    .AddAdditionalAssemblies( typeof( BlazorDBFirst.Client._Imports ).Assembly );
 
// Add additional endpoints required by the Identity /Account Razor components.
app.MapAdditionalIdentityEndpoints( );
 
app.Run( );

 



設計選單


為了方便測式,在「NavMenu.razor」加入一個選單項目,可導向「regionlist」元件:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
@implements IDisposable
 
@inject NavigationManager NavigationManager
 
<div class="top-row ps-3 navbar navbar-dark">
    <div class="container-fluid">
        <a class="navbar-brand" href="">BlazorDBFirst</a>
    </div>
</div>
 
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
 
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
    <nav class="flex-column">
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
            </NavLink>
        </div>
    <div class="nav-item px-3">
      <NavLink class="nav-link" href="regionlist">
        <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Regions
      </NavLink>
    </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="counter">
                <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
            </NavLink>
        </div>
 
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="weather">
                <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
            </NavLink>
        </div>
 
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="auth">
                <span class="bi bi-lock-nav-menu" aria-hidden="true"></span> Auth Required
            </NavLink>
        </div>
 
        <AuthorizeView>
            <Authorized>
                <div class="nav-item px-3">
                    <NavLink class="nav-link" href="Account/Manage">
                        <span class="bi bi-person-fill-nav-menu" aria-hidden="true"></span> @context.User.Identity?.Name
                    </NavLink>
                </div>
                <div class="nav-item px-3">
                    <form action="Account/Logout" method="post">
                        <AntiforgeryToken />
                        <input type="hidden" name="ReturnUrl" value="@currentUrl" />
                        <button type="submit" class="nav-link">
                            <span class="bi bi-arrow-bar-left-nav-menu" aria-hidden="true"></span> Logout
                        </button>
                    </form>
                </div>
            </Authorized>
            <NotAuthorized>
                <div class="nav-item px-3">
                    <NavLink class="nav-link" href="Account/Register">
                        <span class="bi bi-person-nav-menu" aria-hidden="true"></span> Register
                    </NavLink>
                </div>
                <div class="nav-item px-3">
                    <NavLink class="nav-link" href="Account/Login">
                        <span class="bi bi-person-badge-nav-menu" aria-hidden="true"></span> Login
                    </NavLink>
                </div>
            </NotAuthorized>
        </AuthorizeView>
    </nav>
</div>
 
@code {
    private string? currentUrl;
 
    protected override void OnInitialized()
    {
        currentUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri);
        NavigationManager.LocationChanged += OnLocationChanged;
    }
 
    private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
    {
        currentUrl = NavigationManager.ToBaseRelativePath(e.Location);
        StateHasChanged();
    }
 
    public void Dispose()
    {
        NavigationManager.LocationChanged -= OnLocationChanged;
    }
}

 

 


設計元件讀取資料庫資料


從「Solution Explorer」視窗 「Pages」資料夾上方按���鼠右鍵,從快捷選單選擇「Add」- 「Razor Component」選項加入Razor 元件,將名稱設定為「RegionList.razor」:



圖 17:加入Razor 元件。


在「RegionList.razor」加入以下程式碼,在元件初始化時,使用資料庫內容物件從資料庫中查詢「Regions」清單,並將結果放到「Regions」屬性。這樣,在元件渲染時,使用「Regions」屬性來顯示地區清單的內容:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@page "/regionlist"
@using BlazorDBFirst.Models
@using Microsoft.EntityFrameworkCore
 
<h1> Regin List</h1>
 
<PageTitle> Region List </PageTitle>
 
@if ( Regions is not null ) {
  <ul class="list-group ">
    @foreach ( var item in Regions ) {
      <li class="list-group-item list-group-item-primary">
        @item.RegionId - @item.RegionDescription
      </li>
    }
  </ul>
}
 
@code {
  [Inject]
  public IDbContextFactory<NorthwindContext> DbFactory { get; set; } = null!;
 
  public IEnumerable<Region> Regions { get; set; } = null!;
 
  protected override async Task OnInitializedAsync() {
    using var context = DbFactory.CreateDbContext( );
    Regions = await context.Regions.ToListAsync( )!;
  }
}

 



這個元件執行結果參考如下圖:


圖 18:查詢並顯示資料庫資料。

 


授權


修改「RegionList.razor」元件的程式碼,加入「@attribute [Authorize]」。這樣一來,使用者必須先登入網站,才能存取和執行該元件。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@page "/regionlist"
@using BlazorDBFirst.Models
@using Microsoft.EntityFrameworkCore
@using Microsoft.AspNetCore.Authorization
@attribute [StreamRendering]
@attribute [Authorize]
 
<h1> Regin List</h1>
 
<PageTitle> Region List </PageTitle>
 
@if ( Regions is not null ) {
  <ul class="list-group ">
    @foreach ( var item in Regions ) {
      <li class="list-group-item list-group-item-primary">
        @item.RegionId - @item.RegionDescription
      </li>
    }
  </ul>
}
 
@code {
  [Inject]
  public IDbContextFactory<NorthwindContext> DbFactory { get; set; } = null!;
 
  public IEnumerable<Region> Regions { get; set; } = null!;
 
  protected override async Task OnInitializedAsync( ) {
    using var context = DbFactory.CreateDbContext( );
    Regions = await context.Regions.ToListAsync( )!;
  }
}



執行網站程式,當點選到「Regions」選單,便會要求從「Login」元件進行登入,請參考下圖所示,登入成功後,才會看到「RegionList.razor」元件的資料清單:



圖 19:登入。

0 意見:

張貼留言