C# cơ bản .NET Core

Bài thực hành này tiếp tục trên ví dụ cũ mvcblog: Route trong MVC, tải mã nguồn về mở ra tiếp tục phát triển tag/ex068-v1

Sau khi dự án MVC đơn giản trên được tạo ra, dùng Visual Studio Code mở ra để bắt đầu thực hành.

ASP.NET MVC với Entity Framework làm việc với SQL Server

Việc tích hợp Entity Framework vào APS.NET MVC thực hoàn hoàn toàn giống với các bài đã hướng dẫn ở Razor Page - quy trình thực hiện theo các bước tại (ASP.NET Razor) Ứng dụng EF làm việc với cơ sở dữ liệu

Thực hiện thêm các package để làm việc với EF kết nối đến MS SQL Server và các công cụ trợ giúp phát sinh code:

dotnet tool install --global dotnet-ef
dotnet tool install --global dotnet-aspnet-codegenerator
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package MySql.Data.EntityFramework

Nếu chưa có máy chủ MS SQL Server có thể làm theo hướng dẫn: chuẩn bị MS SQL Server , sau đó viết chuỗi kết nối vào file appsettings.json để sau này EF sử dụng.

Ứng dụng này ta chọn đặt tên CSDL trong MS SQL Server là myblog, nên cấu hình chuỗi kết nối có thể là:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",  

  "ConnectionStrings": {
    "MyBlogContext": "Data Source=localhost,1433; Initial Catalog=myblog; User ID=SA;Password=Password123"
  }

    
}

Lúc này bạn có thể tạo ra các Model, DbContext sau đó đăng ký vào hệ thống theo kiến thức các bài đã trình bày như: Tạo DbContext trong EF , tạo DbContext EF trong Razor Page ...

Tuy nhiên ta sẽ không tạo database context kế thừa trực tiếp lớp DbContext mà sẽ kế thừa từ lớp phái sinh từ DbContextIdentityDbContext - để tích hợp vào ứng dụng Identity, thư viện về xác thực trong ASP.NET

Tích hợp Identity vào ASP.NET MVC

Việc tích hợp Identity vào MVC thực hiện hoàn toàn giống với các bài viết về Identity trong Razor Page, bắt đầu từ bài Sử dụng Identity trong Razor Page

Hãy cài đặt các gói cần thiết

dotnet add package System.Data.SqlClient
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Logging.Console

dotnet add package Microsoft.AspNetCore.Identity
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.AspNetCore.Identity.UI
dotnet add package Microsoft.AspNetCore.Authentication
dotnet add package Microsoft.AspNetCore.Http.Abstractions
dotnet add package Microsoft.AspNetCore.Authentication.Cookies
dotnet add package Microsoft.AspNetCore.Authentication.Facebook
dotnet add package Microsoft.AspNetCore.Authentication.Google
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package Microsoft.AspNetCore.Authentication.MicrosoftAccount
dotnet add package Microsoft.AspNetCore.Authentication.oAuth
dotnet add package Microsoft.AspNetCore.Authentication.OpenIDConnect
dotnet add package Microsoft.AspNetCore.Authentication.Twitter

dotnet add package MailKit
dotnet add package MimeKit

Tạo model AppUser

Lớp này kế thừa IdentityUser, nó có các trường định nghĩa sẵn (xem Model Identity ), định nghĩa thêm FullName, Address, Birthday

using System;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Identity;

namespace mvcblog.Models {

    public class AppUser : IdentityUser {

        [MaxLength (100)]
        public string FullName { set; get; }

        [MaxLength (255)]
        public string Address { set; get; }

        [DataType (DataType.Date)]
        public DateTime? Birthday { set; get; }

    }
}

Tạo model AppDbContext

Tạo một lớp kế thừa từ IdentityDbContext (có sẵn các bảng về Identity), ánh xạ nội dụng với CSDL (xem tạo AppDbContext )

Nội dung AppDbContext ở thời điểm này có thể như sau:

using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using mvcblog.Models;

namespace mvcblog.Data {
        public class AppDbContext : IdentityDbContext<AppUser> {

        public AppDbContext (DbContextOptions<AppDbContext> options) : base (options) { }

        protected override void OnModelCreating (ModelBuilder builder) {

            base.OnModelCreating (builder);
            // Bỏ tiền tố AspNet của các bảng: mặc định
            foreach (var entityType in builder.Model.GetEntityTypes ()) {
                var tableName = entityType.GetTableName ();
                if (tableName.StartsWith ("AspNet")) {
                    entityType.SetTableName (tableName.Substring (6));
                }
            }
        }

    }

}

Đăng ký AppDbContext và các dịch vụ Identity vào hệ thống

Cập nhật Startup.ConfigureServices

// Đăng ký AppDbContext, sử dụng kết nối đến MS SQL Server
services.AddDbContext<AppDbContext> (options => {
    string connectstring = Configuration.GetConnectionString ("MyBlogContext");
    options.UseSqlServer (connectstring);
});
// Đăng ký các dịch vụ của Identity
services.AddIdentity<AppUser, IdentityRole> ()
    .AddEntityFrameworkStores<AppDbContext> ()
    .AddDefaultTokenProviders ();

// Truy cập IdentityOptions
services.Configure<IdentityOptions> (options => {
    // Thiết lập về Password
    options.Password.RequireDigit = false; // Không bắt phải có số
    options.Password.RequireLowercase = false; // Không bắt phải có chữ thường
    options.Password.RequireNonAlphanumeric = false; // Không bắt ký tự đặc biệt
    options.Password.RequireUppercase = false; // Không bắt buộc chữ in
    options.Password.RequiredLength = 3; // Số ký tự tối thiểu của password
    options.Password.RequiredUniqueChars = 1; // Số ký tự riêng biệt

    // Cấu hình Lockout - khóa user
    options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes (5); // Khóa 5 phút
    options.Lockout.MaxFailedAccessAttempts = 5; // Thất bại 5 lầ thì khóa
    options.Lockout.AllowedForNewUsers = true;

    // Cấu hình về User.
    options.User.AllowedUserNameCharacters = // các ký tự đặt tên user
        "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
    options.User.RequireUniqueEmail = true; // Email là duy nhất

    // Cấu hình đăng nhập.
    options.SignIn.RequireConfirmedEmail = true; // Cấu hình xác thực địa chỉ email (email phải tồn tại)
    options.SignIn.RequireConfirmedPhoneNumber = false; // Xác thực số điện thoại

});

// Cấu hình Cookie
services.ConfigureApplicationCookie (options => {
    // options.Cookie.HttpOnly = true;  
    options.ExpireTimeSpan = TimeSpan.FromMinutes(30);  
    options.LoginPath = $"/login/";                                 // Url đến trang đăng nhập
    options.LogoutPath = $"/logout/";   
    options.AccessDeniedPath = $"/Identity/Account/AccessDenied";   // Trang khi User bị cấm truy cập
});
services.Configure<SecurityStampValidatorOptions>(options =>
{
    // Trên 5 giây truy cập lại sẽ nạp lại thông tin User (Role)
    // SecurityStamp trong bảng User đổi -> nạp lại thông tinn Security
    options.ValidationInterval = TimeSpan.FromSeconds(5); 
});

Thêm vào pipeline của ứng dụng, thêm tại Startup.configure

app.UseAuthentication();   // Phục hồi thông tin đăng nhập (xác thực)
app.UseAuthorization ();   // Phục hồi thông tinn về quyền của User

Phát sinh cơ sở dữ liệu từ AddDbContext

Ở đây dùng kỹ thuật migration trong EntityFramework

Chạy lệnh để tạo Migration và tạo db

dotnet ef migrations add Init
dotnet ef database update

Kết quả đã sinh ra CSDL ban đầu với cấu trúc:

Đến đây đã có thư viện Identity và CSDL đầy đủ, tuy nhiên ta sẽ không sử dụng code giao diện mặc định của Identity để có thể tùy biến. Bạn có thể thực hiện từ đầu theo các hướng dẫn bắt đầu tại: Phát sinh code (scaffold) Identity

Sử dụng lại mã nguồn Identity từ ví dụ trước

Để nhanh chóng có tất cả các trang chức năng đăng nhập, đăng ký, quyên mật khẩu ... , ta sẽ lấy lại toàn bộ kết quả của các ví dụ cũ về Identity của Razor Page đưa vào MVC, copy toàn bộ thư mục /Areas tại Album/Areas vào thư mục Areas của ứng dụng này.

Ở code cũ lớp AppUser ở namespace Album.Models và lớp AppDbContext ở namespace Album.Data trong khi ở ví dụ này thì hai lớp này định nghĩa lại ở namespace mvcblog.Datamvcblog.Models nên hãy dùng tính năng tìm kiếm và thay thế để thay toàn bộ Album.Models, Album.Data thành mvcblog.Models, mvcblog.Data

Đồng thời cũng thay thế /Pages/Shared/_Layout.cshtml bằng /Views/Shared/_Layout.cshtml

Tiếp theo copy mã nguồn ViewComponent - MessagePage (để đúng cấu cấu thư mục) để tạo thông báo khi chuyển hướng, mã nguồn này đã xây dựng tại Tạo ViewComponent - MessagePage thông báo khi chuyển hướng

Tạm thời comment lại dòng using Album.Binder;[ModelBinder(BinderType=typeof(DayMonthYearBinder))] trong Areas/Identity/Pages/Account/Manage/Index.cshtml.cs

Trong file Areas/Admin/Pages/Role/_ViewStart.cshtmlAreas/Admin/Pages/Role/_ViewStart.cshtml sửa thành

@{
    Layout = "/Views/Shared/_Layout.cshtml";
}

Triển khai dịch vụ gửi mail với IEmailSender

Dịch vụ gửi mail để Identity gửi mail trong trường hợp khi đăng ký tài khoản, lấy lại mật khẩu ... Sử dụng IEmailSender dùng gmail để gửi làm theo hướng dẫn Triển khai IEmailSender

Giờ các chức năng của Identity đã hoạt động như đã từng thực hành trên Razor Page, đã có thể đăng ký, đăng nhập ...

Thêm _LoginPartial.cshtml

Xây dựng thêm Partial có tên _LoginPartial.cshtml để chèn vào menu chính các mục chọn đăng nhập, đăng ký ..., xây dựng file này như sau:

Views/Shared/_LoginPartial.cshtml

@using Microsoft.AspNetCore.Identity
@using mvcblog.Models
@using Microsoft.AspNetCore.Mvc.ViewEngines

@inject SignInManager<AppUser> SignInManager
@inject UserManager<AppUser> UserManager
@inject ICompositeViewEngine Engine


<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
    <li class="nav-item">
        <a id="manage" class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">Xin chào @UserManager.GetUserName(User)!</a>
    </li>
    <li class="nav-item">
        <form id="logoutForm" class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Page("/Index", new { area = "" })">
            <button id="logout" type="submit" class="nav-link btn btn-link text-dark">Đăng xuất</button>
        </form>
    </li>
    
    @if (Engine.FindView(ViewContext, "_AdminDropdownMenu", false).Success) {
        @await Html.PartialAsync("_AdminDropdownMenu")
    }
}
else
{
    <li class="nav-item">
        <a class="nav-link text-dark" id="register" asp-area="Identity" asp-page="/Account/Register">Đăng ký</a>
    </li>
    <li class="nav-item">
        <a class="nav-link text-dark" id="login" asp-area="Identity" asp-page="/Account/Login">Đăng nhập</a>
    </li>
}
</ul>

Trong file này để chèn HTML các mục menu để thêm vào thanh menu chính, nó xuất hiện ở bên phải, lưu ý ở file này:

Đoạn code:

@inject ICompositeViewEngine Engine

Để Inject đối tượng ICompositeViewEngine, để có thể kiểm tra một partial có tồn tại hay không trước khi render, trong file này có render _AdminDropdownMenu.cshtml

@if (Engine.FindView(ViewContext, "_AdminDropdownMenu", false).Success) {
    @await Html.PartialAsync("_AdminDropdownMenu")
}

_AdminDropdownMenu.cshtml tạo một Dropdown menu - chung cấp các mục chọn đến chức năng quản lý role trong hệ thống, menu này chỉ xuất hiện khi User thỏa mãn policy: AdminDropdown

policy AdminDropdown được tạo như sau (trong Startup.ConfigureServices)

Xem thêm: chứng thực quyền theo chính sách policy

services.AddAuthorization(options =>
{
    // User thỏa mãn policy khi có roleclaim: permission với giá trị manage.user
    options.AddPolicy("AdminDropdown", policy => {
        policy.RequireClaim("permission", "manage.user");
    });

});

Nội dung _AdminDropdownMenu.cshtml như sau:

@using Microsoft.AspNetCore.Identity
@using mvcblog.Models
@using Microsoft.AspNetCore.Authorization
@inject SignInManager<AppUser> SignInManager

@inject Microsoft.AspNetCore.Authorization.IAuthorizationService authorizationService
@if (SignInManager.IsSignedIn(User) 
    && (await authorizationService.AuthorizeAsync(User, "AdminDropdown")).Succeeded)
{
    <li class="nav-item dropdown">
        <a class="nav-item nav-link dropdown-toggle mr-md-2" href="#"  data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
            Manager
        </a>
        <div class="dropdown-menu dropdown-menu-right" aria-labelledby="bd-versions">
            <a class="dropdown-item" asp-page="/Role/Index" asp-area="Admin">Quản lý role</a>

            <div class="dropdown-divider"></div>
            <a class="dropdown-item" asp-page="/Role/User" asp-area="Admin">Gán role cho User</a>
        </div>
    </li>
}

Trong Views/Shared/_Layout.cshtml thêm vào nội dung để chèn partial _LoginPartial.cshtml: @await Html.PartialAsync("_LoginPartial"), vị trí chèn như sau:

<!DOCTYPE html>
/..
<body>
    <header>
        <nav class="navbar ... ">
            <div class="container">
                / ...
                @await Html.PartialAsync("_LoginPartial")
            </div>
        </nav>
    </header>
    /...
</body>
</html>

Thêm chức năng tạo HTML Paging

Để có chức năng phân trang (ví dụ liệt kê 10 User / 1 trang) thì làm theo hướng dẫn Tạo partial phân trang HTML BootStrap trong ASP.NET truy vấn phân trang LINQ , tạo file Views/Shared/_Paging.cshtml và copy mã nguồn ở link trên vào

Tích hợp multiple-select

Trong mã nguồn có một số chỗ sử dụng thư viện JS multiple-select (như trong file Areas/Admin/Pages/Role/AddUserRole.cshtml), để phần tử HTML Select ở dạng dễ chọn hơn

File nào tích hợp thường có đoạn mã nạp và sử dụng thư viện

<script src="~/lib/multiple-select/multiple-select.min.js"></script>
<link rel="stylesheet" href="~/lib/multiple-select/multiple-select.min.css" />
<script>
      $('#selectrole').multipleSelect({
            selectAll: false,
            keepOpen: false,
            isOpen: false
        });
</script>

Nên hãy đảm bảo có 2 file thư viện là: wwwroot/lib/multiple-select/multiple-select.min.css vả wwwroot/lib/multiple-select/multiple-select.min.js

Để kiểm tra, cần đăng ký User, đăng nhập - tạo role cho user (https://localhost:5001/admin/role/), role này có roleclaim với tên và giá trị: permission: manage.user

Mã nguồn tham khảo ASP_NET_CORE/mvcblog, hoặc tải về bản bài này ex068-identity

Mã nguồn Identity MVC


Đăng ký nhận bài viết mới