C# 6 與 .NET Core 1.0 高級編程 - 41 ASP.NET MVC(下) Professional C# 6 and .NET Core 1.0 - Chapter 41 ASP.N

 譯文,我的原創,轉載請註明出處(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 MVChtml

C# 6 與 .NET Core 1.0 高級編程 - 41 ASP.NET MVC(上)正則表達式

C# 6 與 .NET Core 1.0 高級編程 - 41 ASP.NET MVC(中)redis

-------------------------sql

最近兩篇譯文來得比較遲,前一陣子忙起來以後忘記了。數據庫

因爲有點事情,《Professional C# 6 and .NET Core 1.0》第4二、43章譯文,是4月中旬以後的事情了。編程

Enjoy your reading, enjoy your code!json

-------------------------瀏覽器

實現操做過濾器

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形式的服務的通訊。

相關文章
相關標籤/搜索