八、添加功能、测试和部署

在本章中,我们将介绍如何将注册功能添加到应用中。接下来将创建一个单元测试。这里的目标是帮助您添加一个特性并对其进行测试。最后,我们将把我们的项目升级到 bootstrap4。

我们将继续实施 Rest Buy 服务。这一部分比较关键,因为我们将主要实现逻辑来实现我们的应用。

在本章结束时,您将能够:

  • 添加注册功能
  • 创建单元测试
  • 将我们的项目升级到 bootstrap4
  • 将 Rest Buy 部署到 Azure

添加注册功能

因为我们将开始向应用添加逻辑,所以现在是向项目添加应用层的好时机。正如我们在上一章中所讨论的,我们将应用设计为分层的,并遵循领域驱动的设计标准。

在领域驱动设计中,一种常见的分层方法是洋葱架构。在洋葱架构中,每一层都可以利用内层,但外层必须适应内层。我们尝试从内到外设计我们的应用。这就是我们从实体开始设计应用的原因:

在上图中,我们的核心是一个域模型,在此基础上还有应用服务。紫色的矩形是接口,黑色的箭头表示编译时依赖关系,洋红色的圆圈表示基础设施的外部依赖关系。

应用服务用于处理命令和请求。

There is an on-going discussion on whether entities in the domain model should be accessible outside. Some people prefer using DTOs to pass the data evenly between layers. We will not go with that approach here. Instead, we will ensure the entities are immutable from the outside layers.

登录和注销机制

首先,让我们从我们的登录和注销机制开始。我们需要一个User实体。到目前为止,我们还没有创建一个。让我们创建它。

按照以下步骤开始使用我们的登录和注销机制:

  1. 使用以下代码:

Go to https://goo.gl/cD8tDQ to access the code.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
namespace RestBuy.Entities
{
  public class User : BaseEntity
  {
...
...
  }
}

有趣的是,我们定义了一个散列密码算法。我们还使用一种秘盐。这样,即使我们的密码数据库被破坏,我们的用户密码也不容易恢复(当然,还要结合强大的密码策略)。通过使用 salt,在本例中为secretBytes和用户名,我们实现了两件事:

  1. 通过添加以下代码更新我们的RestBuyContext

Go to https://goo.gl/wWvhiL to access the code.

void ConfigureUser(EntityTypeBuilder <User> builder)
{
  builder.ToTable(userTable);
  builder.HasKey(ci => ci.Id);
  builder.Property(ci => ci.UserName)
  .IsRequired()
  .HasMaxLength(50);
  builder.Property(ci => ci.Password)
  .IsRequired();
}

我们班现在是这样的:

Go to https://goo.gl/CjSv8g to access the code.

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using RestBuy.Entities;
using System;
using System.Collections.Generic;
using System.Text;
namespace RestBuy.Infrastructure.EF
{
  public class RestBuyContext : DbContext
  {
...
...
  }
}
  1. 最后,我们在 package manager 控制台中使用Add-Migration User添加迁移:

Do not forget to change the default project to Infrastructure; otherwise, you will get an error.

  1. 然后,我们最终更新了数据库:
PM> Update-Database
Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[0]
User profile is available. Using 'C:\Users\Onur.Gumus\AppData\Local\ASP.NET\DataProtection-Keys' as key repository and Windows DPAPI to encrypt keys at rest.
Applying migration '20170909141639_User'.
Done.
PM>

创建应用层

对于应用层,我们需要创建另一个项目。

按照以下步骤开始创建应用层:

  1. 创建另一个项目,如下所示:

This project will contain the application service as well as the necessary interfaces required for the external world.

  1. 我们在RestBuy.Application中增加RestBuy的引用,如下所示:

  1. 接下来,我们将开始定义接口。首先,让我们在RestBuy.Application项目的Services/Queries(如果不存在这些文件夹,则创建它们)文件夹中定义一个IQuery<T>接口,如下所示:

Go to https://goo.gl/K9QpWN to access the code.

using RestBuy.Entities;
using System;
using System.Linq.Expressions;
namespace RestBuy.Application.Services.Queries
{
  public interface IQuery<T> where T : BaseEntity
  {
  Expression<Func<T, bool>> Criteria { get; }
  int Take { get; }
...
...
  int Skip { get; }
  }
}
  1. BaseQuery中增加一个默认实现,如图所示:

Go to https://goo.gl/HNHd9k to access the code.

using RestBuy.Entities;
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Text;
namespace RestBuy.Application.Services.Queries
{
  public abstract class BaseQuery<T> : IQuery<T> where T: BaseEntity
...
...
}

We will use this interface to query our repositories. The Take property represents how many items we want to take from the database and Skip is used for setting how many items to skip. These properties are used for the paging of the records. We will also define a lambda expression that denotes the filtering.

  1. 接下来,为RestBuy.Application项目的Repos文件夹中的所有实体定义基础回购(如果不存在,请再次创建):

Go to https://goo.gl/nzouBU to access the code.

using RestBuy.Entities;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using RestBuy.Application.Services.Queries;
namespace RestBuy.Application.Repos
{
  public interface IEntityRepo<T> where T : BaseEntity
  {
    Task<T> GetById(int id, CancellationToken cancellationToken = default);
    Task<List<T>> ListAsync(IQuery<T> query = null, CancellationToken cancellationToken = default);
  }
}

When using default keyword, the IDE will warn you if you want to use the latest version of C#. Comply to it.

我们之所以使用Task,是因为大多数数据库操作都是异步的和可取消的(实际上,大多数 IO 操作都是异步的)。

  1. 然后,我们将IUserRepo接口定义如下:

Go to https://goo.gl/Uykbex to access the code.

using RestBuy.Entities;
using System.Threading;
using System.Threading.Tasks;
namespace RestBuy.Application.Repos
{
  public interface IUserRepo : IEntityRepo<User>
  {
    Task AddAsync(User user, CancellationToken ct = default());
  }
}

我们在这里只定义了一个Add方法,因为目前我们只计划添加用户。根据我们的需要,我们可以修改这个界面。

  1. 最后,我们定义了一个 IUoW 接口,它代表工作单元,如下所示:

Go to https://goo.gl/ePL5Fc to access the code.

using System.Threading;
using System.Threading.Tasks;
namespace RestBuy.Application.Repos
{
  public interface IUoW
  {
    Task SaveChangesAsync(CancellationToken cancellationToken = default);
  }
}

UoW represents a database transaction.

接下来,让我们在基础设施项目中进行实现。

在基础设施项目中执行实现

按照以下步骤在Infrastructure项目中执行:

  1. 我们首先在Infrastructure中添加对Application项目的引用:

  1. 然后我们实现我们的接口,从Infrastructure项目的EF文件夹中的 IUoW 开始:

Go to https://goo.gl/SFBMLt to access the code.

using RestBuy.Application.Repos;
using System.Threading;
using System.Threading.Tasks;
namespace RestBuy.Infrastructure.EF
{
  public class RestBuyUoW : IUoW
  {
...
...
  }
}
  1. 接下来,定义一个BaseRepo,它将是我们所有存储库实现的基类:

Go to https://goo.gl/s7LVBU to access the code.

using Microsoft.EntityFrameworkCore;
using RestBuy.Application.Repos;
using RestBuy.Application.Services.Queries;
using RestBuy.Entities;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace RestBuy.Infrastructure.EF
{
...
  if (query.Skip > 0)
  {
    filterQuery = filterQuery.Skip(query.Skip);
  }
  if (query.Take > 0)
  {
    filterQuery = filterQuery.Take(query.Take);
  }
  if (query.OrderBy != null || query.OrderByDescending != null)
...
}

前面的代码基本上检查查询是否与TakeSkipOrderBy子句相关,并相应地修改查询(取决于定义的查询的这些部分)。

  1. 现在,定义UserRepo

Go to https://goo.gl/9wPekN to access the code.

using RestBuy.Application.Repos;
using RestBuy.Entities;
using System.Threading;
using System.Threading.Tasks;
namespace RestBuy.Infrastructure.EF
{
  public class UserRepo : BaseRepo<User> , IUserRepo
  {
    public UserRepo(RestBuyContext restBuyContext) : base(restBuyContext)
    { }
    public Task AddAsync(User user, CancellationToken ct = default) =>
    this.restBuyContext.Users.AddAsync(user, ct);
  }
}

我们的 EF 文件夹如下所示:

最后,我们需要定义我们的服务接口。我们将从用户注册开始。首先,让我们为注册定义 ViewModel。我们将直接在应用层中定义 ViewModel,因为这将是我们服务接口的输入。通常在洋葱分层架构中,我们尽量不跨层边界传递数据。这就是为什么我们不直接使用实体。这条规则的一个显著例外是持久性。由于持久化层只负责持久化实体,因此直接将实体传递到持久化层并没有什么害处。尽管如此,这仍然是一个激烈争论的话题。

为注册定义 ViewModel

按照以下步骤定义要注册的 ViewModel:

  1. 让我们在RestBuy.Application项目中创建一个ViewModels文件夹,并添加一个名为NewUserViewModel的类:

  1. NewUserViewModel中使用此代码:

Go to https://goo.gl/ATiDPz to access the code.

using RestBuy.Entities;
using System.ComponentModel.DataAnnotations;
namespace RestBuy.Application.ViewModels
{
  public class NewUserViewModel
  {
  [Required, MaxLength(50)]
  public string Username { get; set; }
  [Required, DataType(DataType.Password)]
  public string Password { get; set; }
  [DataType(DataType.Password), Compare
  (nameof(Password))]
  public string ConfirmPassword { get; set; }
    internal User CreateUser() =>
  new User(this.Username, this.Password);
  }
}

Make sure you add System.ComponentModel.DataAnnotations.dll from the Assemblies Framework section of the Add References popup.

注意CreateUser模型。此方法从ViewModel构建User对象。或者,我们可以使用工厂模式。请注意,我们将该方法标记为内部。这是为了我们不希望从其他层调用这些工厂方法。

  1. 创建托管我们服务的Services文件夹并定义IRegistrationService

  1. IRegistrationService内有此代码:

Go to https://goo.gl/wNEq3F to access the code.

using RestBuy.Application.ViewModels;
using System.Threading;
using System.Threading.Tasks;
namespace RestBuy.Application.Services
{
  public interface IRegistrationService
  {
    Task RegisterUser(NewUserViewModel newUserViewModel, CancellationToken token = default);
  }
}
  1. 接下来,将我们的应用项目的引用添加到 web 项目:

最后,我们可以开始定义控制器。首先,让我们定义Accounts控制器。您可以删除已有的HomeController

定义我们的控制器

按照以下步骤定义控制器:

  1. 通过右键单击Controllers文件夹,添加新项目,然后选择 MVC 控制器类,在Controllers文件夹中创建新控制器:

我们的Web项目如下:

  1. 对于 RESTful 实践,当GET请求到达Accounts控制器时,我们将返回一份登记表。使用此代码:
using Microsoft.AspNetCore.Mvc;
namespace RestBuy.Web.Controllers
{
  [Route("[controller]")]
  public class AccountsController : Controller
  {
    [HttpGet]
    public IActionResult RegistrationForm()
    {
    return View();
    }
  }
}
  1. 定义我们的观点:

  1. 将以下代码添加到其中:

Go to https://goo.gl/SvE6Qv to access the code.

@model RestBuy.Application.ViewModels.NewUserViewModel
@{
 ViewBag.Title = "Register";
}
<h1>Register</h1>
<form method="post" >
 <div asp-validation-summary="ModelOnly"></div>
...
...
 <div>
 <input type="submit" value="Register" />
 </div>
</form>

We have not specified any path for posting in our form, because we will post to the same URL as with this page.

现在,如果我们运行我们的应用并尝试从http://localhost:49163/Accounts访问我们的 AccxOuts 页面(您的端口号可能不同),我们将得到以下表单:

显然,表单看起来没有对齐,但这是我们稍后将要解决的问题。

注册成功后,我们还需要一个登录页。

创建注册后登录页

按照以下步骤创建注册后登录页:

  1. 我们可以使用相同的表单页面,但在本例中,我们将在Accounts文件夹中创建SuccesfullyRegistered.cshtml,如下所示:

  1. 使用以下代码:
@{
   ViewBag.Title = "Registration Successful";
}
<h1>You have registered successfully!</h1>
<ul>
  <li><a asp-action="" asp-controller="">Home</a></li>
  <li><a asp-action="" asp-controller="">Login</a></li>
</ul>
  1. 在前面的代码中,我们将在后面填写必要的 action 和 controller 字段。尽管此页面过于简单,但就本演示而言,它暂时是可以接受的。最后一步是修改我们的控制器,如下所示:
using Microsoft.AspNetCore.Mvc;
using RestBuy.Application.Services;
using RestBuy.Application.ViewModels;
using System.Threading;
using System.Threading.Tasks;
namespace RestBuy.Web.Controllers
{
  [Route("[controller]")]
...
...
}

我们已经将我们的IRegistrationService添加到构造函数中。我们的服务将在我们针对一个具体的服务(我们还没有编写)注册后由 ASP.NET 运行时注入。

其次,我们定义了一个Register方法来获取 ViewModel 和取消令牌。取消令牌在这里是完全可选的。ASP.NET 运行时具有智能行为:无论何时从客户端中止 HTTP 请求,它都将触发取消令牌。通常,建议为任何 I/O 呼叫传递取消令牌。在本例中,我们将查询数据库,但如果您发现这种方法过于冗长,可以跳过它。接下来,我们将检查 ViewModel 是否有效。

Remember that we have decorated our view model with few attributes. At the model binding phase, ASP.NET Runtime automatically validates our model and sets the model state accordingly. If our model is valid, then we proceed with the registration by calling the registration service and returning the view.

如果视图无效,我们将显示完全相同的表单页面及其验证错误,这些错误将自动显示。

此时,我们建议您针对控制器编写一个单元测试来验证行为,但我们将跳过该测试,直接实现注册服务。我们的注册服务必须检查数据库中是否存在这样的用户。因此,我们首先为它创建一个查询,它将从IQuery<User>开始实现。

为注册服务创建查询

我们创建查询对象而不是使用 linq,因为这些类型的查询与业务逻辑相关联,将查询包装到对象中允许我们重用它们以及对它们进行单元测试。此外,使用类命名它们可以为我们提供更多关于查询目的的线索,而不是即时查询。

按照以下步骤为注册服务创建查询:

Application项目的Queries文件夹中,我们创建了一个UserExistsQuery类,如下所示:

Go to https://goo.gl/3r92QU to access the code.

using RestBuy.Entities;
using System;
using System.Linq.Expressions;
namespace RestBuy.Application.Services.Queries
{
  class UserExistsQuery : BaseQuery<User>
  {
    public UserExistsQuery(string userName) => 
      this.UserName = userName;
    public string UserName { get; }
    public override Expression<Func<User, bool>>
    Criteria =>
      u => u.UserName == this.UserName;
    public override int Take => 1;
  }
}

基本上,此查询对象搜索具有给定用户名的用户,并通过设置Take = 1尝试获取第一条记录。

Remember, one of the goals of software engineering is to have readable and understandable code. Short code doesn't always mean good code. So whenever necessary, feel free to create new classes and name them properly.

正在验证注册

按照以下步骤验证注册:

  1. 我们在Services/Core文件夹中执行服务实现。我们的服务实施如下:

Go to https://goo.gl/Xxv3kj to access the code.

using RestBuy.Application.ViewModels;
using RestBuy.Application.Repos;
using RestBuy.Application.Services.Queries;
using System.ComponentModel.DataAnnotations;
using System.Threading;
using System.Threading.Tasks;
using RestBuy.Application.Services;
namespace Restbuy.Application.Services.Core
{
  public class RestBuyRegistrationService : IRegistrationService
  {
    readonly IUserRepo userRepo;
    readonly IUoW uow;
...
...
  }
}

在构造器中,我们的工作单元被注入。然后我们履行我们的接口合同。通过使用验证上下文,我们再次验证了视图模型。虽然我们将在控制器中验证视图模型,但应用不(也不应该)知道。

As a security practice, inner layers should not trust upper layers when it comes to data validation.

  1. 因为我们正在从应用层抛出一个验证异常,所以我们的控制器应该处理并显示与此相关的验证消息。我们将我们的注册方法修改如下:

Go to https://goo.gl/KWuyPd to access the code.

[HttpPost]
public async Task<IActionResult> Register(NewUserViewModel newUserViewModel, CancellationToken cancellationToken)
{
  if (ModelState.IsValid)
  {
...
...
  }
  return View(nameof(RegistrationForm));
}

Don't forget to add using System.ComponentModel.DataAnnotations; at the beginning of the file.

如果发生验证错误,我们将在注册页面中显示它。

  1. 然后,我们调用 user exists 查询并获得同名用户。如果用户名已经存在,我们的userList将有一个大于 0 的计数(实际上是 1,因为我们知道我们的查询使用 Take=1)。在这种情况下,我们需要拒绝注册。否则,我们通过从 UoW 获取UserRepo来注册用户,将其添加到用户 repo 中,并通过调用SaveChangesAsync提交更改。

There may be trouble if two requests come with the same username registration request. Although this is a tiny possibility, a malicious user can try to break our data.

我们当前的代码不检查这些并行请求,也不必检查。对于这类请求,我们应该在数据库中设置一个唯一的约束。数据库约束是最后一道防线。因为发生这种情况的几率很小(恶意用户案例除外),所以您的唯一约束将失败,并且您将显示一个错误页面,告诉用户发生了问题,他们应该重试该操作。因此,我们下一步将向Username属性添加我们的唯一索引。

  1. 通过修改RestBuyContextConfigureUser方法,将唯一索引添加到UserName属性中,如下所示:

Go to https://goo.gl/Ncc42w to access the code.

void ConfigureUser(EntityTypeBuilder<User> builder)
{
  builder.ToTable(userTable);
  builder.HasKey(ci => ci.Id);
  builder.Property(ci => ci.UserName)
    .IsRequired()
    .HasMaxLength(50);
  builder.HasIndex(c => c.UserName).IsUnique();
  builder.Property(ci => ci.Password)
    .IsRequired();
}

注意,我们添加了HasIndex方法。

  1. 现在,我们需要通过在 package manager 控制台中执行以下命令来升级迁移和数据库:
Add-Migration AddUsernameIndex
Update-Database 

Make sure you select RestBuy.Infrastructure before running the commands from the default project combobox.

我们的应用项目现在如下所示:

  1. 最后,我们需要创建服务注册。由于我们已经使用了接口及其实现,我们会将这些接口与其实现与内置的依赖注入机制相关联。在 web 项目中,我们将更新Startup.cs中的ConfigureServices方法,如下所示:

Go to https://goo.gl/NJ2zvC to access the code.

public void ConfigureServices(IServiceCollection services)
{
  services.AddEntityFrameworkSqlServer()
  .AddDbContext<RestBuyContext>(options =>
  options.UseSqlServer(Configuration.
  GetConnectionString("DefaultConnection")));
  services.AddScoped<IRegistrationService,
  RestBuyRegistrationService>();
  services.AddScoped<IUoW, RestBuyUoW>();
  services.AddScoped<IUserRepo, UserRepo>();
  services.AddMvc();
}

因此,我们进行了三次注册:IRegistrationServiceDefaultRegistrationServiceIUoWRestBuyUoWIUserRepoUserRepo。我们使用范围方法,这样每个请求只创建一个这些服务的实例。在每次请求结束时,它们将被处理掉。

  1. 接下来,让我们设置连接字符串。我们在Web项目中编辑appsettings.json

Go to https://goo.gl/zTgTcc to access the code.

{
  "ConnectionStrings": 
  {
    "DefaultConnection": "Server=(localdb)\\
    mssqllocaldb;Database=RestBuy;Trusted_
    Connection=True;MultipleActiveResultSets=true"
  },
  "Logging": 
  {
    "IncludeScopes": false,
    "LogLevel": 
    {
      "Default": "Warning"
    }
  }
}

现在我们可以运行我们的应用了。我们应该访问/Accounts页面查看登记表:

注册成功后,我们将重定向到注册成功页面:

如果我们检查我们的数据库,我们可以看到我们的用户已经被插入,如下所示:

创建单元测试

在过去的十年中,单元测试变得很流行。但是很多时候,单元测试的真正目标并没有被很好地理解,可能是由于命名的原因。诚然,单元测试在发现软件中的错误方面非常有用。

编写单元测试非常有用,即使您有一个只包含少量代码的函数。原因是,尽管单元测试是为了寻找 bug,但有一类 bug 是它们最有助于发现的。这就是回归。回归通常是通过在添加新代码时破坏现有功能来实现的。

关于回归,单元测试就像一个熔丝盒。它将确保,如果我们在没有意识到的情况下破坏现有功能,旧的测试将开始失败。如果他们不这样做,程序员会感到舒服,因为他或她知道现有功能不太可能被破坏。尽管单元测试不能保证这种安全性,但如果您不断地添加或更改代码,它将是一项宝贵的资产。许多人将单元测试视为用代码编写的应用规范。不可否认,测试驱动开发最近已经演变为行为驱动开发,开发人员开始将应用的正式规范作为单元测试来编写。

编写单元测试

按照以下步骤为我们的应用编写单元测试:

  1. 让我们创建一个单元测试。我们将其命名为RestBuy.Test,如下图所示:

  1. UnitTest1.cs重命名为EFTest并将appsettings.json从您的 web 项目复制到测试项目中。但将数据库名称更改为RestBuy_Test

Make sure you add references to all other projects from Test. You also need a reference to Microsoft.EntityFramework.Core, and then install Microsoft.Extensions.Configuration from NuGet.

您的项目应如下所示:

  1. 现在,确保您的appsettings.json文件类似于以下内容:
{
  "ConnectionStrings": 
  {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;
    Database=RestBuy_Test;Trusted_Connection=True;
    MultipleActiveResultSets=true"
  },
  "Logging": 
  {
    "IncludeScopes": false,
    "LogLevel": 
    {
      "Default": "Warning"
    }
  }
}
  1. 右键单击“属性”中的appsettings.json文件,通过为“复制到输出目录”选项选择“如果较新,则复制到输出文件夹”,确保该文件已部署到输出文件夹:
  2. 确保您的EFTest文件也类似于此:

Go to https://goo.gl/NK99He to access the code.

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using RestBuy.Entities;
using RestBuy.Infrastructure.EF;
using System.IO;
using System.Threading.Tasks;
namespace RestBuy.Test
{
...
...
}

这个测试基本上创建数据库,在一个上下文中将产品保存到数据库中,然后在另一个上下文中查询并提交它。

运行单元测试

按照以下步骤运行单元测试:

  1. 现在,要运行测试,请首先构建解决方案,然后从测试菜单打开测试资源管理器,如下所示:

  1. 然后右键单击测试并运行它:

我们的基本模型创建到此结束。

活动:编写用于删除的单元测试

场景

编写一个测试产品删除的单元测试。

完成步骤

  1. 我们可以按如下方式重构测试:

Go to https://goo.gl/EUCb5B to access the code.

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using RestBuy.Entities;
using RestBuy.Infrastructure.EF;
using System.IO;
using System.Threading.Tasks;
namespace RestBuy.Test
{
  [TestClass]
  public class EFTest
  {
...
...
  }
}
  1. 现在要运行测试,请首先构建解决方案,然后从测试菜单打开测试资源管理器,如下所示:

  1. 然后右键单击测试并运行它:

将我们的项目升级到 bootstrap4

在较新版本的 VisualStudio 中,Bower 已被删除。我们将考虑手动将项目升级为引导。如果您使用的是 ASP.NET Core 2.2 或更高版本,则可能不必执行以下步骤,因为这些较新版本附带了 Bootstrap 4。

按照以下步骤启动我们的项目:

  1. 打开_Layout.cshtml。更新脚本引用,如下所示:

Go to https://goo.gl/GtuEk7 to access the code.

<!DOCTYPE html>
<html>
<head>
...
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.3/css/bootstrap.min.css" integrity="sha384-Zug+QiDoJOrZ5t4lssLdxGhVrurbmBWopoEl+
M6BdEfwnCJZtKxi1KgxUyJq13dy" crossorigin="anonymous">
...
</body>
</html>

请注意,我们应该在脚本部分中的引导之前将 popper 添加为 UMD 模块。此外,我们还添加了ValidationScriptsPartial页面。该文件由 Visual Studio 模板生成,并包含必要的客户端验证代码。

  1. ValidationScriptsPartial.cshtml文件中,我们做了以下更改,以便在出现故障时添加相关的引导样式:

Go to https://goo.gl/xud8jd to access the code.

<environment include="Development">
  <script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
  <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
</environment>
...
...
</script>
  1. 接下来,我们使用 Bootstrap 修改注册表:

Go to https://goo.gl/ad7JYA to access the code.

@model Restbuy.Application.ViewModels.NewUserViewModel
@{
ViewBag.Title = "Register";
}
<h1>Register</h1>
<form method="post">
  <div asp-validation-summary="ModelOnly"></div>
  <div class="form-group">
...
...
</form>

以下内容将显示在您的屏幕上:

我们的表单现在在/AccountsURL 中看起来好多了。

  1. 现在让我们点击注册按钮。

这看起来不错,但我们的密码字段看起来是绿色的,好像它是有效的。它是绿色的原因是我们忘记将Required属性添加到ViewModelConfirmPassword字段中。

  1. 更新NewUserViewModel

Go to https://goo.gl/7YN6ke to access the code.

using RestBuy.Entities;
using System.ComponentModel.DataAnnotations;
namespace RestBuy.Application.ViewModels
{
  public class NewUserViewModel
  {
    [Required, MaxLength(50)]
...
...
  }
}
  1. 之后,再次运行我们的页面:

这次我们成功地看到了三个红色方框。注意,即使我们禁用 JavaScript 验证,我们的表单仍然从服务器端受到保护。只是在这种情况下,我们不会看到这些红线,因为它们是由 JavaScript 生成的。还请注意,如果存在相同的用户,则客户端验证无法轻松涵盖发生的错误。相反,我们将使用 ValidationTagHelper

  1. TagHelpers文件夹添加到 Web 项目中,并创建ValidationClassTagHelper类,如下所示:

Go to https://goo.gl/3CBcBe to access the code.

using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.TagHelpers;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using System.Linq;
namespace RestBuy.Web.TagHelpers
{
...
...
}

前面的类只是根据值的有效性设置适当的引导类。

  1. Views文件夹中更改您的ViewImports.chtml文件,如下所示:
@using RestBuy.Web
@using RestBuy.Web.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, RestBuy.Web
  1. 变更RegistrationForm.cshtml,具体如下:

Go to https://goo.gl/TGhPzF to access the code.

@model RestBuy.Application.ViewModels.NewUserViewModel
@{
  ViewBag.Title = "Register";
}
<h1>Register</h1>
...
...
</form>

注意,我们在输入字段中添加了bootrap-validation属性。

  1. 一切都好;现在,我们必须讨论警告。ASP.NET 运行时将清空无效帖子上的密码字段。但如果我们让它们保持原样,它们也会呈现绿色,尽管它们是空的。因此,如果密码字段验证步骤有效,我们希望将其标记为跳过。这是为了让他们永远不会看起来绿色和空虚。为此,我们将控制器方法更改如下:

Go to https://goo.gl/PpWxDY to access the code.

[HttpPost]
public async Task<IActionResult> Register(
NewUserViewModel newUserViewModel,
CancellationToken cancellationToken)
{
  if (ModelState.IsValid)
  {
  try
...
...
  }
  return View(nameof(RegistrationForm));
}

Make sure you add using Microsoft.AspNetCore.Mvc.ModelBinding; at the top of the file.

  1. 现在,如果我们尝试向现有用户注册,我们会得到以下结果:

  1. 最后,我们使用 Bootstrap 在SuccessfullyRegistered页面中成功注册,使用以下代码:

Go to https://goo.gl/C9F1nz to access the code.

@{
  ViewBag.Title = "Registration Successful";
}
<div class="alert alert-success" role="alert">
  <h4 class="alertheading">You have registered successfully!!</h4>
</div>
<ul class="nav">
  <li class="nav-item">
    <a class="nav-link" asp-action="" asp-controller="">Home</a>
  </li>
  <li class="nav-item">
    <a class="nav-link" asp-action="" asp-controller="">Login</a>
  </li>
</ul>

现在,我们的SuccessfullyRegistered页面显示如下:

We should write unit tests to prevent regression. As of now, Bower has been deprecated and using NPM is recommended. We can style our application and even style the validation. We can style our application and even style the validation using Bootstrap.

活动:添加 EULA 协议

场景

您希望添加最终用户许可协议(EULA),以防止用户未经检查就注册。

瞄准

向应用添加最终用户许可协议

完成步骤

  1. 修改NewUserViewModel,如下:

Go to https://goo.gl/nKcvUq to access the code.

using RestBuy.Entities;
using System.ComponentModel.DataAnnotations;
namespace RestBuy.Application.ViewModels
{
...
  public bool TermsAndConditions { get; set; }
  internal User CreateUser() =>
    new User(this.Username, this.Password);
  }
}

注意,我们添加了一个TermsAndConditions验证器和一个只能为 true 的范围验证器。

  1. 然后修改注册页面,如下所示:

Go to https://goo.gl/uUxNau to access the code.

@model RestBuy.Application.ViewModels.NewUserViewModel
@{
  ViewBag.Title = "Register";
}
<h1>Register</h1>
...
...
    <input class="btn btn-primary" type="submit" value="Register" />
  </div>
</form>
  1. _ValidationScriptsPartial文件的底部,添加以下内容:

Go to https://goo.gl/P6GBey to access the code.

<script>
// extend jquery range validator to work for required checkboxes
var defaultRangeValidator = $.validator.methods.range;
$.validator.methods.range = function (value, element, param) 
{
  if (element.type === 'checkbox') 
  {
    // if it's a checkbox return true if it is checked
    return element.checked;
  } 
  else 
  {
    // otherwise run the default validation function
    return defaultRangeValidator.call(this, value, element, param);
  }
}
</script>

现在我们的表格如下所示:

将 RestBuy 部署到 Azure

Microsoft Azure 是 Microsoft 提供的用于构建、部署和管理应用和服务的云计算平台和基础设施。它支持不同的编程语言和服务数组。

您可以将应用部署在您网络中具有互联网信息服务IIS的任何服务器上。但这将限制您的应用只能从网络中访问,假设您的服务器只能从 网络中访问(与大多数网络设置一样)。在本节中,我们将在 Microsoft Azure 中部署 ASP.NET Core 应用,以便您的全球用户可以访问您的应用。

注册 Microsoft Azure

为了将您的应用部署到 Azure,您需要在 Azure 上拥有一个帐户。您可以免费创建一个 Azure 帐户,您将有足够的积分在前 30 天内免费部署您的应用(https://azure.microsoft.com/ )。

要注册,请执行以下步骤:

  1. 转到https://azure.microsoft.com/ 。您将在屏幕上看到此页面:
  2. 单击开始免费按钮或免费帐户链接(在页面右上角):
  3. 您将被重定向到下一页。输入您的 Microsoft 帐户凭据,然后单击“登录”按钮。如果您没有 Microsoft 帐户,可以单击页面底部的“立即注册”链接创建一个帐户:

These pages may appear differently than what's shown in the screenshots because of regular Microsoft updates, but the actions you should take are the same.

  1. 登录后,系统将询问您的国家/地区、名字、第二个名字和工作电话的详细信息,如下所示:
  2. 一旦您输入了所有必要的详细信息,就会要求您输入国家代码和电话号码,以便 Azure 可以发短信或打电话给您,验证您是真人而不是机器人。如果您选择文本我的选项,您将获得一个代码到您的手机;您需要在最后一个字段中输入它,然后单击验证代码。
  3. 一旦您通过电话验证,您需要在以下表格中输入您的信用卡信息。您将收到大约 1 美元的账单,并将在五到六个工作日内退还至您的帐户。收集此信息是为了识别用户的身份,除非用户明确选择付费服务,否则不会向用户计费。
  4. 输入信用卡信息并单击“下一步”后,您必须同意订阅协议,作为注册过程的最后一步。
  5. 注册过程完成后,将显示以下屏幕。您还将收到一封确认电子邮件(发送至您在第一步中提供的电子邮件 ID),其中包含订阅详细信息:

Azure 部署的先决条件

要从 Visual Studio 2017 社区版将 ASP.NET Core 应用发布到 Azure,请执行以下步骤:

  1. 启动 Visual Studio 安装程序,如下所示:
  2. 然后您的选择应该如下所示:
  3. 然后单击右下角的“修改”按钮,开始下载。

现在,让我们将 Rest Buy 发布到 Azure。我们应该注意的一点是,我们要运行数据库迁移。通常默认情况下,如果我们在 web 项目中定义了迁移,那么向导会自动询问我们是否也要生成迁移。但是,我们已经在Infrastructure项目中定义了迁移。在这种情况下,我们必须添加一些代码,这样每当我们的应用访问RestBuyContext时,如果有迁移,它就会生成迁移。

将 Rest Buy 部署到 Azure

按照以下步骤将 Rest Buy 部署到 Azure:

  1. 更改我们的RestBuyContext,如下所示:
public class RestBuyContext : DbContext
{
  const string hiloName = "order_hilo";
  const string productTable = "Product";
  const string orderTable = "Order";
  const string orderItemTable = "OrderItem";
  const string userTable = "User";
  static bool initialized;
...
...
}
/// rest of the code remains the same

基本上在代码中,我们添加了一个静态初始化属性,该属性仅在第一次调用构造函数时才被调用。我们不想每次创建上下文时都检查是否应用了迁移;相反,我们希望每个应用启动一次。Database.Migrate()方法确保应用了必要的迁移。一旦我们定义了这个,我们就可以像往常一样继续发布了。

  1. 右键单击解决方案资源管理器中的Web项目,然后选择发布。您将看到以下屏幕:
  2. Services开始,点击加号并填写必要的表格项目,为我们的应用创建一个新的数据库,如下图所示:
  3. 填写上述表单并单击“创建”后,单击“发布”窗格中的“设置”。选择 DefaultConnection 复选框(请注意,此复选框可能需要几秒钟的时间显示),如图所示:
  4. 单击保存,然后单击发布。这样做将发布并运行我们的应用。这是注册屏幕:
  5. 单击 Register 将产生以下结果:

总结

我们已经研究了注册功能的实现。我们为它创建了一个单元测试。我们将项目升级到 Bootstrap4。最后,我们将应用部署到 Microsoft Azure。做得好!我们已经成功地完成了这门课程。