譯文,個人原創,轉載請注明出處(C# 6 與 .NET Core 1.0 高級編程 - 41 ASP.NET MVC(下)),不對的地方歡迎指出與交流。
章節出自《Professional C# 6 and .NET Core 1.0》。水平有限,各位閱讀時仔細分辨,唯望莫誤人子弟。
附英文版原文:Professional C# 6 and .NET Core 1.0 - Chapter 41 ASP.NET MVC
C# 6 與 .NET Core 1.0 高級編程 - 41 ASP.NET MVC(上)
C# 6 與 .NET Core 1.0 高級編程 - 41 ASP.NET MVC(中)
-------------------------
最近兩篇譯文來得比較遲,前一陣子忙起來之后忘記了。
由於有點事情,《Professional C# 6 and .NET Core 1.0》第42、43章譯文,是4月中旬之后的事情了。
Enjoy your reading, enjoy your code!
-------------------------
實現操作過濾器
ASP.NET MVC在許多領域是可擴展的。例如,可以實現控制器工廠來搜索和實例化控制器(接口IControllerFactory)。控制器實現 IController 接口。在控制器中查找操作方法可以通過使用IActionInvoker接口來解決。可以使用從ActionMethodSelectorAttribute派生的屬性類來定義允許的HTTP方法。將HTTP請求映射到參數的模型綁定器可以通過實現IModelBinder接口自定義。 “模型綁定器”部分使用FormCollectionModelBinder類型。可以使用實現接口 IViewEngine 的不同視圖引擎。本章使用Razor視圖引擎。還可以通過HTML輔助程序、標記助手和操作過濾器進行自定義。大多數擴展點都超出了本書的范圍,但是操作過濾器是最經常實現或使用的,因此這里將介紹這些過濾器。
在執行操作之前和之后調用操作過濾器。它們被分配給使用屬性的控制器或控制器的動作方法。操作過濾器通過創建從基類ActionFilterAttribute派生的類來實現。這個類可以覆蓋基類成員OnActionExecuting,OnActionExecuted,OnResultExecuting和OnResultExecuted。 OnActionExecuting在調用action方法之前被調用,並且當action方法被完成時調用OnActionExecuted。之后,在返回結果之前,調用OnResultExecuting方法,最后調用OnResultExecuted。
在這些方法中,可以訪問Request對象以檢索調用者的信息。通過Request對象可以根據瀏覽器決定一些操作,可以訪問路由信息,可以動態更改視圖結果等等。以下代碼片段從路由信息訪問變量語言。要將此變量添加到路由,可以如本章前面的“定義路由”部分所述更改路由。通過在路由信息中添加語言變量,如下代碼片段所示可以使用 RouteData.Values 訪問URL提供的值。可以使用檢索到的值更改用戶語言:
public class LanguageAttribute : ActionFilterAttribute { private string _language = null; public override void OnActionExecuting(ActionExecutingContext filterContext) { _language = filterContext.RouteData.Values["language"] == null ? null : filterContext.RouteData.Values["language"].ToString(); //… } public override void OnResultExecuting(ResultExecutingContext filterContext) { } }
注意 第28章“本地化”解釋了全球化和本地化,設置文化和其他區域細節。
如以下代碼段所示,創建的操作過濾器屬性類可以將該屬性應用於控制器。使用該類的屬性,每個action方法都調用屬性類的成員。另外,也可以將屬性應用於操作方法,因此僅當調用操作方法時才調用成員:
[Language] public class HomeController : Controller {
ActionFilterAttribute實現幾個接口:IActionFilter,IAsyncActionFilter,IResultFilter,IAsyncResultFilter,IFilter和 IOrderedFilter。
ASP.NET MVC包括一些預定義的操作過濾器,如 請求 HTTPS 的過濾器,授權調用,處理錯誤或緩存數據。
將在本章后面的“驗證和授權”部分中介紹使用特性Authorize。
創建數據驅動的應用程序
現在你已經閱讀了ASP.NET MVC的所有基礎,是時候來看一個使用ADO.NET實體框架的數據驅動的應用程序。可以看到ASP.NET MVC結合數據訪問提供的功能。
注意 ADO.NET實體框架在第38章“實體框架核心”中有詳細介紹。
示例應用程序 MenuPlanner 用於維護在數據庫中的餐館菜單條目。只有經過身份驗證的帳戶才可以執行數據庫條目的維護。未經身份驗證的用戶則可以瀏覽菜單。
該項目是通過使用 ASP.NET Core 1.0 Web 應用程序模板創建的。身份驗證使用默認選擇的個人用戶帳戶。這個項目模板為ASP.NET MVC和控制器添加了幾個文件夾,包括HomeController和AccountController。它還添加了一些腳本庫。
定義模型
首先在 Models 目錄中定義一個模型。使用ADO.NET實體框架創建模型。 MenuCard類型定義了一些屬性和與菜單列表的關系(代碼文件MenuPlanner/Models/MenuCard.cs):
public class MenuCard { public int Id { get; set; } [MaxLength(50)] public string Name { get; set; } public bool Active { get; set; } public int Order { get; set; } public virtual List<Menu> Menus { get; set; } }
從 MenuCard 引用的菜單類型由Menu類定義(代碼文件MenuPlanner/Models/Menu.cs):
public class Menu { public int Id { get; set; } public string Text { get; set; } public decimal Price { get; set; } public bool Active { get; set; } public int Order { get; set; } public string Type { get; set; } public DateTime Day { get; set; } public int MenuCardId { get; set; } public virtual MenuCard MenuCard { get; set; } }
與數據庫的連接,以及 Menu 和 MenuCard 類型的集合都由 MenuCardsContext 管理。使用ModelBuilder,上下文指定Menu類型的Text屬性不能為null,並且它的最大長度為50(代碼文件MenuPlanner/Models/MenuCardsContext.cs):
public class MenuCardsContext : DbContext { public DbSet<Menu> Menus { get; set; } public DbSet<MenuCard> MenuCards { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Menu>().Property(p => p.Text) .HasMaxLength(50).IsRequired(); base.OnModelCreating(modelBuilder); } }
Web應用程序的啟動代碼定義了用作數據上下文的MenuCardsContext,並從配置文件讀取連接字符串(代碼文件MenuPlanner/Startup.cs):
public IConfiguration Configuration { get; set; } public void ConfigureServices(IServiceCollection services) { // Add Entity Framework services to the services container. services.AddEntityFramework() .AddSqlServer() .AddDbContext<ApplicationDbContext>(options => options.UseSqlServer( Configuration["Data:DefaultConnection:ConnectionString"])) .AddDbContext<MenuCardsContext>(options => options.UseSqlServer( Configuration["Data:MenuCardConnection:ConnectionString"])); // etc. }
配置文件添加 MenuCardConnection 連接字符串。 該連接字符串引用 Visual Studio 2015 附帶的SQL實例 。當然可以改變這個,也可以添加一個到SQL Azure 的連接字符串(代碼文件MenuPlanner/appsettings.json):
{ "Data": { "DefaultConnection": { "ConnectionString":"Server=(localdb)\\mssqllocaldb; Database=aspnet5-MenuPlanner-4d3d9092-b53f-4162-8627-f360ef6b2aa8; Trusted_Connection=True;MultipleActiveResultSets=true" }, "MenuCardConnection": { "ConnectionString":"Server= (localdb)\\mssqllocaldb;Database=MenuCards; Trusted_Connection=True;MultipleActiveResultSets=true" } }, // etc. }
創建數據庫
可以使用Entity Framework命令來創建用於創建數據庫的代碼。命令行提示符中可以使用.NET核心命令行(CLI)和ef命令創建代碼以自動創建數據庫。要使用命令提示符,必須將當前文件夾設置為project.json文件所在的目錄:
>dotnet ef migrations add InitMenuCards --context MenuCardsContext
注意 dotnet工具在第1章“.NET應用程序體系結構”和第17章“Visual Studio 2015”中討論。
因為多個數據上下文( MenuCardsContext 和 ApplicationDbContext )是通過項目定義的,所以需要使用--context選項指定數據上下文。 ef命令在項目結構創建一個Migrations文件夾, InitMenuCards類中使用Up方法創建數據庫表,使用Down方法再次刪除更改(代碼文件MenuPlanner/Migrations/[date] InitMenuCards.cs):
public partial class InitMenuCards : Migration { public override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( name:"MenuCard", columns: table => new { Id = table.Column<int>(nullable: false) .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), Active = table.Column<bool>(nullable: false), Name = table.Column<string>(nullable: true), Order = table.Column<int>(nullable: false) }, constraints: table => { table.PrimaryKey("PK_MenuCard", x => x.Id); }); migrationBuilder.CreateTable( name:"Menu", columns: table => new { Id = table.Column<int>(nullable: false) .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), Active = table.Column<bool>(nullable: false), Day = table.Column<DateTime>(nullable: false), MenuCardId = table.Column<int>(nullable: false), Order = table.Column<int>(nullable: false), Price = table.Column<decimal>(nullable: false), Text = table.Column<string>(nullable: false), Type = table.Column<string>(nullable: true) }, constraints: table => { table.PrimaryKey("PK_Menu", x => x.Id); table.ForeignKey( name:"FK_Menu_MenuCard_MenuCardId", column: x => x.MenuCardId, principalTable:"MenuCard", principalColumn:"Id", onDelete: RefeerentialAction.Cascade); }); } public override void Down(MigrationBuilder migration) { migration.DropTable("Menu"); migration.DropTable("MenuCard"); } }
現在只需要一些代碼來啟動遷移進程,用初始樣本數據填充數據庫。 MenuCardDatabaseInitializer 通過在從 Database 屬性返回的DatabaseFacade對象上調用擴展方法 MigrateAsync 來應用遷移過程。這反過來檢查與連接字符串相關聯的數據庫是否已具有與通過遷移指定的數據庫相同的版本。如果它不具有相同的版本,則調用所需的Up方法以獲得相同的版本。除此之外,創建幾個MenuCard對象將它們存儲在數據庫中(代碼文件MenuPlanner/Models/MenuCardDatabaseInitializer.cs):
using Microsoft.EntityFrameworkCore; using System.Linq; using System.Threading.Tasks; namespace MenuPlanner.Models { public class MenuCardDatabaseInitializer { private static bool _databaseChecked = false; public MenuCardDatabaseInitializer(MenuCardsContext context) { _context = context; } private MenuCardsContext _context; public async Task CreateAndSeedDatabaseAsync() { if (!_databaseChecked) { _databaseChecked = true; await _context.Database.MigrateAsync(); if (_context.MenuCards.Count() == 0) { _context.MenuCards.Add( new MenuCard { Name ="Breakfast", Active = true, Order = 1 }); _context.MenuCards.Add( new MenuCard { Name ="Vegetarian", Active = true, Order = 2 }); _context.MenuCards.Add( new MenuCard { Name ="Steaks", Active = true, Order = 3 }); } await _context.SaveChangesAsync(); } } } }
隨着數據庫和模型到位,可以創建一個服務。
創建服務
在創建服務之前,創建接口IMenuCardsService,該接口定義服務所需的所有方法(代碼文件MenuPlanner/Services/IMenuCardsService.cs):
using MenuPlanner.Models; using System.Collections.Generic; using System.Threading.Tasks; namespace MenuPlanner.Services { public interface IMenuCardsService { Task AddMenuAsync(Menu menu); Task DeleteMenuAsync(int id); Task<Menu> GetMenuByIdAsync(int id); Task<IEnumerable<Menu>> GetMenusAsync(); Task<IEnumerable<MenuCard>> GetMenuCardsAsync(); Task UpdateMenuAsync(Menu menu); } }
服務類MenuCardsService實現了返回菜單和菜單卡的方法,創建、更新和刪除菜單(代碼文件 MenuPlanner/Services/MenuCardsService.cs):
using MenuPlanner.Models; using Microsoft.EntityFrameworkCore using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace MenuPlanner.Services { public class MenuCardsService : IMenuCardsService { private MenuCardsContext _menuCardsContext; public MenuCardsService(MenuCardsContext menuCardsContext) { _menuCardsContext = menuCardsContext; } public async Task<IEnumerable<Menu>> GetMenusAsync() { await EnsureDatabaseCreated(); var menus = _menuCardsContext.Menus.Include(m => m.MenuCard); return await menus.ToArrayAsync(); } public async Task<IEnumerable<MenuCard>> GetMenuCardsAsync() { await EnsureDatabaseCreated(); var menuCards = _menuCardsContext.MenuCards; return await menuCards.ToArrayAsync(); } public async Task<Menu> GetMenuByIdAsync(int id) { return await _menuCardsContext.Menus.SingleOrDefaultAsync( m => m.Id == id); } public async Task AddMenuAsync(Menu menu) { _menuCardsContext.Menus.Add(menu); await _menuCardsContext.SaveChangesAsync(); } public async Task UpdateMenuAsync(Menu menu) { _menuCardsContext.Entry(menu).State = EntityState.Modified; await _menuCardsContext.SaveChangesAsync(); } public async Task DeleteMenuAsync(int id) { Menu menu = _menuCardsContext.Menus.Single(m => m.Id == id); _menuCardsContext.Menus.Remove(menu); await _menuCardsContext.SaveChangesAsync(); } private async Task EnsureDatabaseCreated() { var init = new MenuCardDatabaseInitializer(_menuCardsContext); await init.CreateAndSeedDatabaseAsync(); } } }
要通過依賴注入使服務可用,使用AddScoped方法將服務注冊到服務集合中(代碼文件MenuPlanner/Startup.cs):
public void ConfigureServices(IServiceCollection services) { // etc. services.AddScoped<IMenuCardsService, MenuCardsService>(); // etc. }
創建控制器
ASP.NET MVC提供了構架來創建直接訪問數據庫的控制器。可以通過在解決方案資源管理器中選擇Controllers文件夾來執行此操作,並從上下文菜單中選擇添加->控制器。將打開“添加構架”對話框。從“添加構架”對話框中,可以使用Entity Framework選擇“MVC 6控制器”視圖。單擊添加按鈕將打開添加控制器對話框,如圖41.13所示。該對話框可以選擇 Menu 模型類和實體框架數據上下文MenuCardsContext,配置生成視圖,並給控制器命名。創建具有視圖的控制器以查看生成的代碼,以及視圖。
圖41.13
本書示例不直接使用來自控制器的數據上下文,而是在其間插入服務。這樣做提供了更多的靈活性。可以使用來自不同控制器的服務,同時可以使用來自諸如ASP.NET Web API之類的服務的服務。
注意 ASP.NET Web API在第42章討論。
通過以下示例代碼,ASP.NET MVC控制器通過構造函數注入注入菜單卡服務(代碼文件MenuPlanner/Controllers/MenuAdminController.cs):
public class MenuAdminController : Controller { private readonly IMenuCardsService _service; public MenuAdminController(IMenuCardsService service) { _service = service; } // etc. }
Index方法是當僅使用URL引用控制器而不傳遞操作方法時調用的默認方法。此處,將創建數據庫中的所有 Menu 項,並將其傳遞到 Index 視圖。 Details 方法返回通過從服務找到的菜單的Details視圖。注意錯誤處理。當沒有ID傳遞給Details方法時,使用來自基類的HttpBadRequest方法返回HTTP Bad Request(400錯誤響應)。當在數據庫中找不到菜單ID時,通過HttpNotFound方法返回HTTP Not Found(404錯誤響應):
public async Task<IActionResult> Index() { return View(await _service.GetMenusAsync()); } public async Task<IActionResult> Details(int? id = 0) { if (id == null) { return HttpBadRequest(); } Menu menu = await _service.GetMenuByIdAsync(id.Value); if (menu == null) { return HttpNotFound(); } return View(menu); }
當用戶創建新菜單時,在來自客戶端的HTTP GET請求之后調用第一個Create方法。使用該方法,ViewBag信息將傳遞到視圖。ViewBag包含有關SelectList中的菜單卡的信息。 SelectList允許用戶選擇項目。因為MenuCard集合被傳遞給SelectList,所以用戶可以用新創建的菜單選擇菜單卡。
public async Task<IActionResult> Create() { IEnumerable<MenuCard> cards = await _service.GetMenuCardsAsync(); ViewBag.MenuCardId = new SelectList(cards,"Id","Name"); return View(); }
注意 要使用SelectList類型,必須將NuGet包Microsoft.AspNet.Mvc.ViewFeatures添加到項目。
在用戶填寫表單並將具有新菜單的表單提交給服務器后,第二個Create方法從HTTP POST請求中調用。該方法使用模型綁定將表單數據傳遞到Menu對象,並將Menu對象添加到數據上下文以將新創建的菜單寫入數據庫:
[HttpPost] [ValidateAntiForgeryToken] public async Task<ActionResult> Create( [Bind("Id","MenuCardId","Text","Price","Active","Order","Type","Day")] Menu menu) { if (ModelState.IsValid) { await _service.AddMenuAsync(menu); return RedirectToAction("Index"); } IEnumerable<MenuCard> cards = await _service.GetMenuCardsAsync(); ViewBag.MenuCards = new SelectList(cards,"Id","Name"); return View(menu); }
要編輯菜單卡,需要定義兩個名為Edit的操作方法 - 一個用於GET請求,一個用於POST請求。第一個Edit方法返回單個菜單項,第二個在成功完成模型綁定后調用服務的UpdateMenuAsync方法:
public async Task<IActionResult> Edit(int? id) { if (id == null) { return HttpBadRequest(); } Menu menu = await _service.GetMenuByIdAsync(id.Value); if (menu == null) { return HttpNotFound(); } IEnumerable<MenuCard> cards = await _service.GetMenuCardsAsync(); ViewBag.MenuCards = new SelectList(cards,"Id","Name", menu.MenuCardId); return View(menu); } [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Edit( [Bind("Id","MenuCardId","Text","Price","Order","Type","Day")] Menu menu) { if (ModelState.IsValid) { await _service.UpdateMenuAsync(menu); return RedirectToAction("Index"); } IEnumerable<MenuCard> cards = await _service.GetMenuCardsAsync(); ViewBag.MenuCards = new SelectList(cards,"Id","Name", menu.MenuCardId); return View(menu); }
控制器的最后一部分包括 Delete 方法。因為兩個方法都有相同的參數 - C#中這是不允許的,第二個方法的名稱改為DeleteConfirmed。但是,第二個方法可以從與第一個Delete方法相同的URL鏈接訪問,但第二個方法使用HTTP POST訪問而不是使用ActionName特性的GET訪問。該方法調用服務的DeleteMenuAsync方法:
public async Task<IActionResult> Delete(int? id) { if (id == null) { return HttpBadRequest(); } Menu menu = await _service.GetMenuByIdAsync(id.Value); if (menu == null) { return HttpNotFound(); } return View(menu); } [HttpPost, ActionName("Delete")] [ValidateAntiForgeryToken] public async Task<IActionResult> DeleteConfirmed(int id) { Menu menu = await _service.GetMenuByIdAsync(id); await _service.DeleteMenuAsync(menu.Id); return RedirectToAction("Index"); }
創建視圖
現在是時候創建視圖了。視圖在 Views/MenuAdmin 文件夾中創建。可以通過在解決方案資源管理器中選擇MenuAdmin文件夾來創建視圖,然后從上下文菜單中選擇添加->視圖。打開“添加視圖”對話框,如圖41.14所示。對話框中可以選擇列表、詳細信息、創建、編輯、刪除模板,然后相應地安排HTML元素。使用此對話框選擇的Model類指定了視圖基於的模型。
圖41.14
定義HTML表的 Index 視圖具有作為其模型的菜單集合。對於表的頭元素,帶有標記助手asp-for的HTML元素標簽用於訪問要顯示的屬性名稱。為了顯示條目,使用@foreach迭代菜單集合,並且使用輸入元素的Tag Helper訪問每個屬性值。錨元素的標記助手會為“編輯”、“詳細信息”和“刪除”頁面創建鏈接(代碼文件MenuPlanner/Views/MenuAdmin/Index.cshtml):
@model IList<MenuPlanner.Models.Menu> @{ ViewBag.Title ="Index"; } <h2>@ViewBag.Title</h2> <p> <a asp-action="Create">Create New</a> </p> @if (Model.Count() > 0) { <table> <tr> <th> <label asp-for="@Model[0].MenuCard.Item"></label> </th> <th> <label asp-for="@Model[0].Text"></label> </th> <th> <label asp-for="Model[0].Day"></label> </th> </tr> @foreach (var item in Model) { <tr> <td> <input asp-for="@item.MenuCard.Name" readonly="readonly" disabled="disabled" /> </td> <td> <input asp-for="@item.Text" readonly="readonly" disabled="disabled" /> </td> <td> <input asp-for="@item.Day" asp-format="{0:yyyy-MM-dd}" readonly="readonly" disabled="disabled" /> </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> } </table> }
I在MenuPlanner項目中,MenuAdmin控制器的第二個視圖是 Create視圖。 HTML表單使用asp-action 標簽助手來引用控制器的Create操作方法。 沒有必要使用asp-controller助手來引用控制器,因為action方法與視圖在同一個控制器中。 表單內容使用標簽助手構建標簽和輸入元素。 標簽的asp-for helper返回屬性的名稱,輸入元素的asp-for助手返回值(代碼文件MenuPlanner/Views/MenuAdmin/Create.cshtml):
@model MenuPlanner.Models.Menu @{ ViewBag.Title ="Create"; } <h2>@ViewBag.Title</h2> <form asp-action="Create" method="post"> <div class="form-horizontal"> <h4>Menu</h4> <hr /> <div asp-validation-summary="ValidationSummary.All" style="color:blue" id="FileName_validation_day" class="form-group"> <span style="color:red">Some error occurred</span> </div> <div class="form-group"> <label asp-for="@Model.MenuCardId" class="control-label col-md2"> </label> <div class="col-md-10"> <select asp-for="@(Model.MenuCardId)" asp-items="@((IEnumerable<SelectListItem>)ViewBag.MenuCards)" size="2" class="form-control"> <option value="" selected="selected">Select a menu card</option> </select> </div> </div> <div class="form-group"> <label asp-for="Text" class="control-label col-md-2"></label> <div class="col-md-10"> <input asp-for="Text" /> </div> </div> <div class="form-group"> <label asp-for="Price" class="control-label col-md-2"></label> <div class="col-md-10"> <input asp-for="Price" /> <span asp-validation-for="Price">Price of the menu</span> </div> </div> <div class="form-group"> <label asp-for="Day" class="control-label col-md-2"></label> <div class="col-md-10"> <input asp-for="Day" /> <span asp-validation-for="Day">Date of the menu</span> </div> </div> <div class="form-group"> <div class="col-md-offset-2 col-md-10"> <input type="submit" value="Create" class="btn btn-default" /> </div> </div> </div> </form> <a asp-action="Index">Back</a>
其他視圖的創建方式與此處的視圖類似,因此本書不多講這些視圖。只需從下載的代碼獲取視圖。
現在可以使用應用程序向現有菜單卡添加和編輯菜單。
實現認證和授權
認證和授權是Web應用程序的重要方面。如果某個網站或部分網站不應公開,用戶必須獲得授權。對於用戶的身份驗證,創建ASP.NET Web應用程序時,可以使用不同的選項(請參閱圖41.15:無身份驗證,單個用戶帳戶以及工作和學校帳戶。Windows身份驗證選項不適用於ASP.NET Core 5。)
圖41.15
工作和學校帳戶可以從雲中選擇一個Active Directory進行身份驗證。
單個用戶帳戶可以在SQL Server數據庫中存儲用戶配置文件。用戶可以注冊和登錄,他們還可以使用來自Facebook,Twitter,Google或Microsoft的現有帳戶。
存儲和檢索用戶信息
對於用戶管理,需要將用戶信息添加到商店。 IdentityUser 類(命名空間Microsoft.AspNet.Identity.EntityFramework)定義了一個名稱,並列出了角色、登錄和聲明。用於創建MenuPlanner應用程序的Visual Studio模板創建了一些值得注意的代碼來保存用戶:作為項目一部分的類ApplicationUser來自基類IdentityUser(命名空間Microsoft.AspNet.Identity.EntityFramework)。默認情況下,ApplicationUser為空,但可以從用戶添加所需的信息,並且信息將存儲在數據庫(代碼文件MenuPlanner/Models/IdentityModels.cs)中:
public class ApplicationUser : IdentityUser { }
通過 IdentityDbContext<TUser> 類型與數據庫建立連接。這是一個派生自DbContext的泛型類,因此使用了Entity Framework。 IdentityDbContext<TUser> 類型定義屬性Roles和類型為 IDbSet<TEntity> 的Users。 IDbSet<TEntity> 類型定義了到數據庫表的映射。為了方便起見,創建ApplicationDbContext以將ApplicationUser類型定義為IdentityDbContext類的泛型:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser> { protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); } }
啟動身份系統
數據庫的連接是在啟動代碼中使用依賴注入服務集合注冊的。類似於之前創建的MenuCardsContext,ApplicationDbContext配置來使用配置文件中的連接字符串的SQL Server。身份服務本身使用擴展方法AddIdentity注冊。 AddIdentity方法映射身份服務使用的用戶和角色類的類型。類ApplicationUser是前面提到的從IdentityUser派生的類,IdentityRole是從 IdentityRole<string> 派生的基於字符串的角色類。 AddIdentity方法的重載方法允許使用雙因素身份驗證配置身份系統;電子郵件令牌提供程序;用戶選項,例如要求唯一的電子郵件;或需要用戶名匹配的正則表達式。 AddIdentity返回 IdentityBuilder,允許身份系統的其他配置,例如使用的實體框架上下文(AddEntityFrameworkStores)和令牌提供程序(AddDefaultTokenProviders)。可以添加的其他提供程序包括錯誤、密碼驗證程序、角色管理器、用戶管理器和用戶驗證器(代碼文件MenuPlanner/Startup.cs):
public void ConfigureServices(IServiceCollection services) { services.AddEntityFramework() .AddSqlServer() .AddDbContext<ApplicationDbContext>(options => options.UseSqlServer( Configuration["Data:DefaultConnection:ConnectionString"])) .AddDbContext<MenuCardsContext>(options => options.UseSqlServer( Configuration["Data:MenuCardConnection:ConnectionString"])); services.AddIdentity<ApplicationUser, IdentityRole>() .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders(); services.Configure<FacebookAuthenticationOptions>(options => { options.AppId = Configuration["Authentication:Facebook:AppId"]; options.AppSecret = Configuration["Authentication:Facebook:AppSecret"]; }); services.Configure<MicrosoftAccountAuthenticationOptions>(options => { options.ClientId = Configuration["Authentication:MicrosoftAccount:ClientId"]; options.ClientSecret = Configuration["Authentication:MicrosoftAccount:ClientSecret"]; }); // etc. }
執行用戶注冊
現在讓我們進入用於注冊和登錄用戶的生成代碼。 功能的核心是在 AccountController 類中。 控制器類具有應用的授權特性,它將所有操作方法限制為經過身份驗證的用戶。 構造函數通過依賴注入接收用戶管理器、登錄管理器和數據庫上下文。 電子郵件和SMS發件人用於雙因素身份驗證。 如果不實現作為生成的代碼的一部分的空的AuthMessageSender類,可以刪除IEmailSender和ISmsSender的注入(代碼文件MenuPlanner/Controllers/AccountController.cs):
[Authorize] public class AccountController : Controller { private readonly UserManager<ApplicationUser> _userManager; private readonly SignInManager<ApplicationUser> _signInManager; private readonly IEmailSender _emailSender; private readonly ISmsSender _smsSender; private readonly ApplicationDbContext _applicationDbContext; private static bool _databaseChecked; public AccountController( UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager, IEmailSender emailSender, ISmsSender smsSender, ApplicationDbContext applicationDbContext) { _userManager = userManager; _signInManager = signInManager; _emailSender = emailSender; _smsSender = smsSender; _applicationDbContext = applicationDbContext; }
為了注冊用戶,就要定義RegisterViewModel。 該模型定義用戶在注冊時需要輸入的數據。 中生成的代碼中,此模型只需要電子郵件、密碼和確認密碼(必須與密碼相同)。 如果想從用戶獲取更多信息,可以根據需要添加屬性(代碼文件MenuPlanner/Models/AccountViewModels.cs):
public class RegisterViewModel { [Required] [EmailAddress] [Display(Name ="Email")] public string Email { get; set; } [Required] [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] [DataType(DataType.Password)] [Display(Name ="Password")] public string Password { get; set; } [DataType(DataType.Password)] [Display(Name ="Confirm password")] [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] public string ConfirmPassword { get; set; } }
對於未經身份驗證的用戶,必須進行用戶注冊。這就是為什么 AllowAnonymous 特性應用於AccountController的Register方法。這將覆蓋這些方法的Authorize特性。 Register方法的HTTP POST變量接收RegisterViewModel對象,並通過調用_userManager.CreateAsync方法將ApplicationUser寫入數據庫。用戶成功創建后,通過_signInManager.SignInAsync完成登錄(代碼文件MenuPlanner/Controllers/AccountController.cs):
[HttpGet] [AllowAnonymous] public IActionResult Register() { return View(); } [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task<IActionResult> Register(RegisterViewModel model) { EnsureDatabaseCreated(_applicationDbContext); if (ModelState.IsValid) { var user = new ApplicationUser { UserName = model.Email, Email = model.Email }; var result = await _userManager.CreateAsync(user, model.Password); if (result.Succeeded) { await _signInManager.SignInAsync(user, isPersistent: false); return RedirectToAction(nameof(HomeController.Index),"Home"); } AddErrors(result); } // If we got this far, something failed, redisplay form return View(model); }
現在視圖(代碼文件MenuPlanner/Views/Account/Register.cshtml)只是需要用戶的信息。圖41.16顯示了詢問用戶信息的對話框。
圖41.16
設置用戶登錄
用戶注冊時,在成功注冊完成后立即進行登錄。 LoginViewModel 模型定義了UserName,Password和 RememberMe 屬性 - 用戶通過登錄請求的所有信息。該模型有一些注釋用於HTML Helpers(代碼文件MenuPlanner/Models/AccountViewModels.cs):
public class LoginViewModel { [Required] [EmailAddress] public string Email { get; set; } [Required] [DataType(DataType.Password)] public string Password { get; set; } [Display(Name ="Remember me?")] public bool RememberMe { get; set; } }
要登錄已經注冊的用戶,需要調用AccountController的Login方法。在用戶輸入登錄信息后,登錄管理器通過 PasswordSignInAsync 用於驗證登錄信息。如果登錄成功,則將用戶重定向到原始請求的頁面。如果登錄失敗,則會返回相同的視圖,以便為用戶提供更多正確輸入用戶名和密碼的選項(代碼文件MenuPlanner/Controllers/AccountController.cs):
[HttpGet] [AllowAnonymous] public IActionResult Login(string returnUrl = null) { ViewData["ReturnUrl"] = returnUrl; return View(); } [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null) { EnsureDatabaseCreated(_applicationDbContext); ViewData["ReturnUrl"] = returnUrl; if (ModelState.IsValid) { var result = await _signInManager.PasswordSignInAsync( model.Email, model.Password, model.RememberMe, lockoutOnFailure: false); if (result.Succeeded) { return RedirectToLocal(returnUrl); } if (result.RequiresTwoFactor) { return RedirectToAction(nameof(SendCode), new { ReturnUrl = returnUrl, RememberMe = model.RememberMe }); } if (result.IsLockedOut) { return View("Lockout"); } else { ModelState.AddModelError(string.Empty,"Invalid login attempt."); return View(model); } } return View(model); }
驗證用戶
使用身份驗證基礎架構,通過Authorize 特性注釋控制器或操作方法,可以輕松地要求用戶身份驗證。將該特性應用於類需要該類的每個action方法的角色。如果對不同的操作方法有不同的授權要求,則Authorize 特性也可以應用於操作方法。此特性將驗證調用者是否已經授權(通過檢查授權cookie)。如果請求者尚未授權,則返回401 HTTP狀態代碼,並重定向到登錄操作。
不設置參數應用特性Authorize需要用戶進行身份驗證。要有更多控制權,可以通過分配角色給Roles屬性定義來只有特定用戶角色才能訪問操作方法,如以下代碼段所示:
[Authorize(Roles="Menu Admins")] public class MenuAdminController : Controller {
還可以使用Controller基類的User屬性訪問用戶信息,這允許更多動態的批准或拒絕用戶。例如,根據傳遞的參數值,需要不同的角色。
注意 可以在第24章“安全性”中閱讀有關用戶認證和有關安全性的其他信息的更多信息。
總結
在本章中探討了最新的Web技術來使用ASP.NET MVC 6框架。已經看到了如何提供一個可靠的結構,這是需要正確單元測試的大型應用程序的理想選擇。看到以最少的努力提供高級功能並非難事,以及該框架提供的邏輯結構和功能分離如何使代碼易於理解和易於維護。
下一章繼續討論ASP.NET Core,但討論了ASP.NET Web API形式的服務的通信。