第 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. S3C2440移植uboot之支持DM9000

      上一节S3C2440移植uboot之支持NANDFLASH操作移植了uboot 支持了NANDFLASH的操作,这一节修改uboot支持DM9000. 目录 通过Makefile把dm9000x编 ...

  2. P1802-DP【橙】

    1.又是一道因为写了异常剪枝而调了好久的题,以后再也不写异常剪枝了,异常情况压根不该出现,所以针对出现的异常情况进行补救的异常剪枝是一种很容易出错的行为,做为两手准备也就罢了,但第一次写成的代码必须能 ...

  3. C语言中的操作符:了解与实践

    ​ 欢迎大家来到贝蒂大讲堂 ​ 养成好习惯,先赞后看哦~ ​ 所属专栏:C语言学习 ​ 贝蒂的主页:Betty's blog 1. 操作符的分类 操作符又叫运算符,它在C语言中起着非常大的作用,以下是 ...

  4. DBA实战面试题(一)

    数据库面试测试题(一) 简述当前主流RDBMS软件有哪些?开源且跨平台的数据库软件有哪些? 参考答案 当前主流的数据库服务器软件有: Oracle . DB2 . SQL SERVER .MySQL ...

  5. Nginx 配置文件备忘单

    Nginx 是用于 Web 服务.反向代理.缓存.负载平衡.媒体流等的开源软件.在这篇文章中,我将提到一些我们经常使用的 Nginx 配置. 监听端口 server { Standard HTTP P ...

  6. 百度网盘(百度云)SVIP超级会员共享账号每日更新(2023.12.26)

    一.百度网盘SVIP超级会员共享账号 可能很多人不懂这个共享账号是什么意思,小编在这里给大家做一下解答. 我们多知道百度网盘很大的用处就是类似U盘,不同的人把文件上传到百度网盘,别人可以直接下载,避免 ...

  7. [转帖]ESXi主机RAID卡_HBA卡_网卡 型号_固件_驱动查询

    https://www.cnblogs.com/vincenshen/p/12332142.html 一.RAID卡/HBA卡 型号_固件_驱动查询 1. 查询所有SCSI设备列表 # esxcfg- ...

  8. [转帖]AF_UNIX 本地通信

    文章目录 一.AF_UNIX 本地通信 1. Linux进程通信机制 2. socket本地域套接字AF_UNIX 3. demo示例 二.AF_INET域与AF_UNIX域socket通信原理对比 ...

  9. parquet极简学习

    parquet极简学习 摘要 parquet的概念: Parquet文件是一种列式存储文件格式,广泛应用于大数据处理框架, 如Apache Hadoop和Apache Spark. 它通过将数据组织成 ...

  10. 人大金仓学习之一_kwr的简单学习

    人大金仓学习之一_kwr的简单学习 摘要 周末在家想着学习一下数据库相关的内容. 网上找了不少资料, 想着直接在本地机器上面进行一下安装与验证 理论上linux上面应该更加简单. windows 上面 ...