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》。水平有限,各位阅读时仔细分辨,唯望莫误人子弟。

附英文版原文: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形式的服务的通信。

posted @ 2017-03-24 08:54  沐汐Vicky  阅读(1882)  评论(0编辑  收藏  举报