第 3 章 使用 ASP.NET Core 开发微服务

微服务定义

微服务是一个支持特定业务场景的独立部署单元。它借助语义化版本管理、定义良好的 API 与其他后端服务交互。它的天然特点就是严格遵守单一职责原则。

为什么要用 API 优先

所有团队都一致把公开、文档完备且语义化版本管理的 API 作为稳定的契约予以遵守,那么这种契约也能让各团队自主地掌握其发布节奏。遵循语义化版本规则能让团队在完善 API 的同时,不破坏已有消费方使用的 API。

作为微服务生态系统成功的基石,坚持好 API 优先的这些实践,远比开发服务所用的技术或代码更重要。

以测试优先的方式开发控制器

每一个单元测试方法都包含如下三个部分:

  • 安排(Arrange)完成准备测试的必要配置
  • 执行(Act)执行被测试的代码
  • 断言(Assert)验证测试条件并确定测试是否通过

测试项目:

https://github.com/microservices-aspnetcore/teamservice

特别注意测试项目如何把其他项目引用进来,以及为什么不需要再次声明从主项目继承而来的依赖项。

StatlerWaldorfCorp.TeamService.Tests.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp1.1</TargetFramework>
</PropertyGroup> <ItemGroup>
<ProjectReference Include="../../src/StatlerWaldorfCorp.TeamService/StatlerWaldorfCorp.TeamService.csproj"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0-preview-20170210-02" />
<PackageReference Include="xunit" Version="2.2.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
</ItemGroup> </Project>

首先创建 Team 模型类

Team.cs

using System;
using System.Collections.Generic; namespace StatlerWaldorfCorp.TeamService.Models
{
public class Team { public string Name { get; set; }
public Guid ID { get; set; }
public ICollection<Member> Members { get; set; } public Team()
{
this.Members = new List<Member>();
} public Team(string name) : this()
{
this.Name = name;
} public Team(string name, Guid id) : this(name)
{
this.ID = id;
} public override string ToString() {
return this.Name;
}
}
}

每个团队都需要一系列成员对象

Member.cs

using System;

namespace StatlerWaldorfCorp.TeamService.Models
{
public class Member {
public Guid ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; } public Member() {
} public Member(Guid id) : this() {
this.ID = id;
} public Member(string firstName, string lastName, Guid id) : this(id) {
this.FirstName = firstName;
this.LastName = lastName;
} public override string ToString() {
return this.LastName;
}
}
}

创建第一个失败的测试

TeamsControllerTest.cs

using Xunit;
using System.Collections.Generic;
using StatlerWaldorfCorp.TeamService.Models; namespace StatlerWaldorfCorp.TeamService
{
public class TeamsControllerTest
{
TeamsController controller = new TeamsController(); [Fact]
public void QueryTeamListReturnsCorrectTeams()
{
List<Team> teams = new List<Team>(controller.GetAllTeams());
}
}
}

要查看测试运行失败的结果,请打开一个终端并运行 cd 浏览到对应目录,然后运行以下命令:

$ dotnet restore
$ dotnet test

因为被测试的控制器尚未创建,所以测试项目无法通过。

向主项目添加一个控制器:

TeamsController.cs

using System;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using StatlerWaldorfCorp.TeamService.Models; namespace StatlerWaldorfCorp.TeamService
{
public class TeamsController
{
public TeamsController()
{ } [HttpGet]
public IEnumerable<Team> GetAllTeams()
{
return Enumerable.Empty<Team>();
}
}
}

第一个测试通过后,我们需要添加一个新的、运行失败的断言,检查从响应里获取的团队数目是正确的,由于还没创建模拟对象,先随意选择一个数字。

List<Team> teams = new List<Team>(controller.GetAllTeams());
Assert.Equal(teams.Count, 2);

现在让我们在控制器里硬编码一些随机的逻辑,使测试通过。

只编写恰好能让测试通过的代码,这样的小迭代作为 TDD 规则的一部分,不光是一种 TDD 运作方式,更能直接提高对代码的信心级别,同时也能避免 API 逻辑膨胀。

更新后的 TeamsController 类,支持新的测试

using System;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using StatlerWaldorfCorp.TeamService.Models; namespace StatlerWaldorfCorp.TeamService
{
public class TeamsController
{
public TeamsController()
{ } [HttpGet]
public IEnumerable<Team> GetAllTeams()
{
return new Team[] { new Team("One"), new Team("Two") };
}
}
}

接下来关注添加团队方法。

[Fact]
public void CreateTeamAddsTeamToList()
{
TeamsController controller = new TeamsController();
var teams = (IEnumerable<Team>)(await controller.GetAllTeams() as ObjectResult).Value;
List<Team> original = new List<Team>(teams); Team t = new Team("sample");
var result = controller.CreateTeam(t); var newTeamsRaw = (IEnumerable<Team>)(controller.GetAllTeams() as ObjectResult).Value; List<Team> newTeams = new List<Team>(newTeamsRaw);
Assert.Equal(newTeams.Count, original.Count+1);
var sampleTeam = newTeams.FirstOrDefault( target => target.Name == "sample");
Assert.NotNull(sampleTeam);
}

代码略粗糙,测试通过后可以重构测试以及被测试代码。

在真实世界的服务里,不应该在内存中存储数据,因为会违反云原生服务的无状态规则。

接下来创建一个接口表示仓储,并重构控制器来使用它。

ITeamRepository.cs

using System.Collections.Generic;

namespace StatlerWaldorfCorp.TeamService.Persistence
{
public interface ITeamRepository {
IEnumerable<Team> GetTeams();
void AddTeam(Team team);
}
}

在主项目中为这一仓储接口创建基于内存的实现

MemoryTeamRepository.cs

using System.Collections.Generic;

namespace StatlerWaldorfCorp.TeamService.Persistence
{
public class MemoryTeamRepository : ITeamRepository {
protected static ICollection<Team> teams; public MemoryTeamRepository() {
if(teams == null) {
teams = new List<Team>();
}
} public MemoryTeamRepository(ICollection<Team> teams) {
teams = teams;
} public IEnumerable<Team> GetTeams() {
return teams;
} public void AddTeam(Team t)
{
teams.Add(t);
}
}
}

借助 ASP.NET Core 的 DI 系统,我们将通过 Startup 类把仓储添加为 DI 服务

public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddScoped<ITeamRepository, MemoryTeamRepository>();
}

利用这种 DI 服务模型,现在我们可以在控制器里使用构造函数注入,而 ASP.NET Core 则会把仓储实例添加到所有依赖它的控制器里。

修改控制器,通过给构造函数添加一个简单参数就把它注入进来

public class TeamsController : Controller
{
ITeamRepository repository; public TeamsController(ITeamRepository repo)
{
repository = repo;
} ...
}

修改现有的控制器方法,将使用仓储,而不是返回硬编码数据

[HttpGet]
public async virtual Task<IActionResult> GetAllTeams()
{
return this.Ok(repository.GetTeams());
}

可从 GitHub 的 master 分支找到测试集的完整代码

要立即看这些测试的效果,请先编译服务主项目,然后转到 test/StatlerWaldorfCorp.TeamService.Tests 目录,并运行下列命令:

$ dotnet restore
$ dotnet build
$ dotnet test

集成测试

集成测试最困难的部分之一经常位于启动 Web 宿主机制的实例时所需要的技术或代码上,我们在测试中需要借助 Web 宿主机制收发完整的 HTTP 消息。

庆幸的是,这一问题已由 Microsoft.AspNetCore.TestHost.TestServer类解决。

对不同场景进行测试

SimpleIntegrationTests.cs

using Xunit;
using System.Collections.Generic;
using StatlerWaldorfCorp.TeamService.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.TestHost;
using System;
using System.Net.Http;
using System.Linq;
using Newtonsoft.Json;
using System.Text; namespace StatlerWaldorfCorp.TeamService.Tests.Integration
{
public class SimpleIntegrationTests
{
private readonly TestServer testServer;
private readonly HttpClient testClient; private readonly Team teamZombie; public SimpleIntegrationTests()
{
testServer = new TestServer(new WebHostBuilder()
.UseStartup<Startup>());
testClient = testServer.CreateClient(); teamZombie = new Team() {
ID = Guid.NewGuid(),
Name = "Zombie"
};
} [Fact]
public async void TestTeamPostAndGet()
{
StringContent stringContent = new StringContent(
JsonConvert.SerializeObject(teamZombie),
UnicodeEncoding.UTF8,
"application/json"); // Act
HttpResponseMessage postResponse = await testClient.PostAsync(
"/teams",
stringContent);
postResponse.EnsureSuccessStatusCode(); var getResponse = await testClient.GetAsync("/teams");
getResponse.EnsureSuccessStatusCode(); string raw = await getResponse.Content.ReadAsStringAsync();
List<Team> teams = JsonConvert.DeserializeObject<List<Team>>(raw);
Assert.Equal(1, teams.Count());
Assert.Equal("Zombie", teams[0].Name);
Assert.Equal(teamZombie.ID, teams[0].ID);
}
}
}

运行团队服务的 Docker 镜像

$ docker run -p 8080:8080 dotnetcoreseservices/teamservice

端口映射之后,就可以用 http://localhost:8080 作为服务的主机名

下面的 curl 命令会向服务的 /teams 资源发送一个 POST 请求

$ curl -H "Content-Type:application/json" \ -X POST -d \ '{"id":"e52baa63-d511-417e-9e54-7aab04286281", \ "name":"Team Zombie"}' \ http://localhost:8080/teams

它返回了一个包含了新创建团队的 JSON 正文

{"name":"Team Zombie","id":"e52baa63-d511-417e-9e54-7aab04286281","members":[]}

注意上面片段的响应部分,members 属性是一个空集合。

为确定服务在多个请求之间能够维持状态(即使目前只是基于内存列表实现),我们可以使用下面的 curl 命令

$ curl http://localhost:8080/teams
[{"name":"Team Zombie","id":"e52baa63-d511-417e-9e54-7aab04286281","members":[]}]

至此,我们已经拥有了一个功能完备的团队服务,每次 Git 提交都将触发自动化测试,将自动部署到 docker hub,并未云计算环境的调度做好准备。

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

如有任何疑问,请与我联系 (MingsonZheng@outlook.com) 。

《ASP.NET Core 微服务实战》-- 读书笔记(第3章)的更多相关文章

  1. ASP.NET Core微服务实战系列

    希望给你3-5分钟的碎片化学习,可能是坐地铁.等公交,积少成多,水滴石穿,码字辛苦,如果你吃了蛋觉得味道不错,希望点个赞,谢谢关注. 前言 这里记录的是个人奋斗和成长的地方,该篇只是一个系列目录和构想 ...

  2. 《ASP.NET Core In Action》读书笔记系列,这是一个手把手的从零开始的教学系列目录

    最近打算系统学习一下asp.net  core ,苦于没有好的中文书藉,只好找来一本英文的 <ASP.NET Core In Action>学习.我和多数人一样,学习英文会明显慢于中文.希 ...

  3. Spring Cloud微服务实战阅读笔记(一) 基础知识

    本文系<Spring Cloud微服务实战>作者:翟永超,一书的阅读笔记. 一:基础知识   1:什么是微服务架构     是一种架构设计风格,主旨是将一个原本独立的系统拆分成多个小型服务 ...

  4. 《ASP.NET Core In Action》读书笔记系列五 ASP.NET Core 解决方案结构解析1

    创建好项目后,解决方案资源管理器窗口里我们看到,增加了不少文件夹及文件,如下图所示: 在解决方案文件夹中,找到项目文件夹,该文件夹又包含五个子文件夹 -Models.Controllers.Views ...

  5. 《ASP.NET Core In Action》读书笔记系列四 创建ASP.NET Core 应用步骤及相应CLI命令

    一般情况下,我们都是从一个模板(template)开始创建应用的(模板:提供构建应用程序所需的基本代码).本节使用 Visual Studio 2017 .ASP.NET Core2.0和 Visua ...

  6. 《ASP.NET Core In Action》读书笔记系列三 ASP.NET Core如何处理请求的?

    在本节中,您将看到ASP.NET Core应用程序如何运行的,从请求URL开始到页面呈现在浏览器中. 为此,您将看到 一个HTTP请求在Web服务器中是如何被处理的.ASP.NET Core如何扩展该 ...

  7. 《ASP.NET Core In Action》读书笔记系列二 ASP.NET Core 能用于什么样的应用,什么时候选择ASP.NET Core

    ASP.NET Core 能用于什么样的应用 ASP.NET Core 可以用作传统的web服务.RESTful服务.远程过程调用(RPC)服务.微服务,这归功于它的跨平台支持和轻量级设计.如下图所示 ...

  8. 《ASP.NET Core In Action》读书笔记系列一 ASP.NET Core 的诞生

    最近打算系统学习一下asp.net  core ,苦于没有好的中文书藉,只好找来一本英文的 <ASP.NET Core In Action>学习.我和多数人一样,学习英文会明显慢于中文.希 ...

  9. 【.NET Core微服务实战-统一身份认证】开篇及目录索引

    简介 ​ 学习.NETCORE也有1年多时间了,发现.NETCORE项目实战系列教程很少,都是介绍开源项目或基础教程,对于那些观望的朋友不能形成很好的学习思路,遇到问题怕无法得到解决而不敢再实际项目中 ...

  10. Spring Cloud 微服务实战笔记

    Spring Cloud 微服务实战笔记 微服务知识 传统开发所有业务逻辑都在一个应用中, 开发,测试,部署随着需求增加会不断为单个项目增加不同业务模块:前端展现也不局限于html视图模板的形式,后端 ...

随机推荐

  1. 在vue项目中使用momentjs获取今日、昨日、本周、本月、上月、本年、上年等日期,时间比较计算

    https://blog.csdn.net/qq_15058285/article/details/119925056

  2. u-swipe-action 宽度计算的延迟导致组件加载时内部样式错误

    https://toscode.gitee.com/umicro/uView/issues/I1Y50J 左图为电脑显示效果,右图为app显示效果. 原因:u-swipe-action 宽度计算的延迟 ...

  3. 启动vue项目失败,报错Failed at the node-sass@4.14.1 postinstall script.

    https://www.cnblogs.com/xiaodangshan/p/13061618.html

  4. vue双向定位导航效果

    需求:实现双向定位导航效果,点击左侧菜单,右侧滚动到相应的位置.滚动右边,左侧相应菜单高亮. html代码: 1 <ul class="EntTake_main_left" ...

  5. socket TCP DPT 网络编程

    复习: ARP协议: 广播和单播 通过ip地址获得mac地址 机器A发起一个arp请求(只包含A的ip地址) 交换机接收到请求,广播这条消息 所有的机器都会接受到这条请求,只有需要寻找的机器B的ip地 ...

  6. CF1656F Parametric MST 题解

    为了便于解题,先对 \(a\) 数组从小到大进行排序. 首先,根据定义可以得出总价值的表达式: \[\begin{aligned} W&=\sum\limits_{(u,v)\in E}[a_ ...

  7. 针对docker中的mongo容器增加鉴权

    1. 背景 业务方的服务器经安全检查,发现以docker容器启动的mongo未增加鉴权的漏洞,随优化之 2. 配置 mongo以docker compose方式启动,镜像的版本号为4.2.6,dock ...

  8. 03-点亮LED灯

    1.FPGA设计流程 1.设计规划 对项目需求了解,划分子功能模块,子功能模块的输入输出信号及通信关系 2.波形绘制 了解子模块的功能,画出框图,搞清楚如何通过输入信号得到输出信号,进而绘制波形图 3 ...

  9. [转帖]如何不耍流氓的做运维之-SHELL脚本

    https://www.cnblogs.com/luoahong/articles/8504691.html 前言 大家都是文明人,尤其是做运维的,那叫一个斯文啊.怎么能耍流氓呢?赶紧看看,编写SHE ...

  10. [转帖]br 备份时排除某个库

    https://tidb.net/book/tidb-monthly/2023/2023-02/usercase/excluded-a-storeroom-during-br-backup 生产环境中 ...