C--和--NET-现代-SAAS-应用构建指南-全-
C# 和 .NET 现代 SAAS 应用构建指南(全)
原文:
zh.annas-archive.org/md5/e4ab3398f5c910a357232871ae58cd41译者:飞龙
前言
我从.NET 的初期就开始从事.NET 应用的开发工作。我认为我在这几年中几乎使用过所有可能的.NET 工具,无论是前端还是后端,无论是好是坏!这包括为处理设施提供培训工具、构建远程监控工具以及开发优化解决方案——所有这些都是在.NET 上完成的!
在这本书中,我结合了我在过去 20 年中参与过的许多项目中的经验,希望为读者提供一个有趣且富有信息性的视角,了解如何使用.NET 构建现代软件即服务(SaaS)应用。
这本书旨在详细阐述并揭开使用.NET 和 Microsoft 技术栈构建 SaaS 应用的世界,并旨在提供构建、测试、部署和维护现代 SaaS 应用所需的技术知识。我们首先介绍我们将使用的技术,并通过一个演示应用来展示它。随着我们继续阅读本书,我们将添加更多的工具和技术,直到我们描述并演示了构建 SaaS 应用所需的所有主要主题。
在阅读这本书的过程中,你将学习到开发现代 SaaS 应用所需的工具、技术和最佳实践,这些应用将给你的用户留下深刻印象并为你的企业提供价值。我们涵盖了整个技术栈,从数据库到用户界面(UI),然后继续到在生产环境中部署和维护应用。
我试图让这本书对每个人都是可访问的。为此,我专门使用了免费和开源的工具,并尝试通过将新主题与核心原则联系起来,尽可能减少假设知识来介绍新主题。在可能的情况下,我用实际示例来阐述理论。
到这本书结束时,你将对所需的理论有全面的理解,并且将通过实际示例探索其中很多内容。你应该已经准备好交付你的第一个真实世界的 SaaS 应用了!
我希望这本书不仅能让你感到教育性和信息性,还能让你觉得有趣。我也非常希望这本书中的学习内容能帮助你事业发展以及在所有未来的努力中!
这本书面向的对象
这本书的目标是面向任何想要构建 SaaS 应用并希望使用 Microsoft 技术栈来构建它的人!然而,SaaS 开发是一个相对高级的话题,这是不争的事实。如果你对 C#和.NET 有扎实的知识,这将大有裨益。同时,对数据库和 Web 开发有实际了解也将非常有用。
尽管本书涵盖了高级主题,但如果你仍然是一个初学者,请不要气馁!在本书中,即使是更高级的主题也是以易于理解的方式介绍和解释的,并且通常以示例为基础,这样每个人都可以学到一些东西——无论技能水平如何!
本书涵盖的内容
第一章**,我们所有人都需要的现代分销模型,简要介绍了 SaaS 的历史,并介绍了本书中将使用的技术。
第二章**,构建一个简单的演示应用程序,深入探讨了本书将关注的技术,并构建了一个我们可以随着学习过程逐步添加的骨架应用程序。
第三章**,什么是多租户,为什么它在 SaaS 应用程序中很重要?,涵盖了多租户,特别关注 SaaS 应用程序。我们在第二章的示例中添加了多租户。
第四章**,构建数据库和为数据丰富的应用程序规划,专注于数据库,特别是使用 SQL Server 和 Entity Framework 进行构建。
第五章**,构建 RESTful API,在数据库层所涵盖的内容基础上,通过添加 API 使数据可以通过 HTTP 进行访问。除了构建示例应用程序外,本章还深入探讨了 RESTful API 背后的理论。
第六章**,SaaS 应用程序的微服务,探讨了微服务和 SaaS,这两者是相辅相成的。本章涵盖了构建微服务应用程序的理论和实践,并将一些学习成果应用于演示应用程序。
第七章**,构建用户界面,解释了为什么 UI 是 SaaS 拼图中非常重要的一部分。本章涵盖了理论,并使用 Blazor 为演示应用程序添加了 UI。
第八章**,身份验证和授权,涵盖了向 SaaS 应用程序添加身份验证和授权的复杂主题。特别提到了在处理多租户和微服务应用程序时遇到的挑战。
第九章**,SaaS 应用程序的测试策略,解释了在构建 SaaS 应用程序时测试的重要性。在本章中,我们探讨了测试的如何和为什么,包括单元测试、集成测试和端到端测试。
第十章**,监控和日志记录,阐述了如果没有这两个既相互独立又相关的概念,在生产应用程序中识别和解决问题可能会非常具有挑战性。本章涵盖了这两个主题,并提供了关于使用哪些工具和技术的一些建议。
第十一章**,频繁发布,尽早发布,介绍了使用持续集成/持续部署(CI/CD)管道以管理方式构建和发布应用程序的过程。
第十二章**,成长之痛 - 规模化运营,讨论了随着 SaaS 应用程序开始增长并获得用户,开发团队必须面对和解决的一系列全新问题。本章涵盖了这些挑战,并提供了如何解决这些问题的指导。
第十三章**,收尾工作,回顾了本书中涵盖的内容,并探讨了读者如何部署他们新获得的 SaaS 知识!
为了充分利用本书
本书旨在尽可能地为任何想要进入构建 SaaS 应用程序领域的人提供便利。尽管如此,SaaS 是一个复杂且高级的主题,因此对以下内容的良好了解将有所帮助:
-
使用 C#进行.NET 应用程序开发
-
SQL Server 和 Entity Framework
-
通用 Web 开发
-
对 Docker 的一些了解可能有用,但不是必需的
然而,我希望任何在软件行业工作或仍在学习技艺的人都能阅读这本书,并提高他们的理解!
以下工具和技术将在本书中广泛使用:
| 本书涵盖的软件/硬件 | 操作系统要求 |
|---|---|
| Visual Studio Code | Windows、macOS 或 Linux |
| Docker Desktop | Windows、macOS 或 Linux |
| .NET v7 | Windows、macOS 或 Linux |
| Entity Framework | Windows、macOS 或 Linux |
| Blazor | Windows、macOS 或 Linux |
| SQL Server | Windows、macOS 或 Linux |
所有初始设置说明都包含在第二章中。在阅读本书的过程中,任何进一步的设置都会根据需要详细说明。
如果您正在使用本书的数字版,我们建议您亲自输入代码或从本书的 GitHub 仓库(下一节中提供链接)获取代码。这样做将帮助您避免与代码的复制和粘贴相关的任何潜在错误。
下载示例代码文件
您可以从 GitHub 下载本书的示例代码文件:github.com/PacktPublishing/Building-Modern-SaaS-Applications-with-C-and-.NET。如果代码有更新,它将在 GitHub 仓库中更新。
我们还提供了来自我们丰富的图书和视频目录中的其他代码包,可在github.com/PacktPublishing/找到。查看它们吧!
下载彩色图像
我们还提供了一份包含本书中使用的截图和图表彩色图像的 PDF 文件。您可以从这里下载:packt.link/IOZxh。
使用的约定
本书中使用了多种文本约定。
文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“Up方法在数据库中创建表,而Down方法删除表。这被转换成 SQL 代码,当发出数据库更新命令时,该代码会被发送到数据库引擎。”
代码块设置如下:
[HttpPut("{id}")]
public async Task<IActionResult> UpdateAsync(int id, UpdateHabitDto request)
{
var habit = await _habitService.UpdateById(id, request);
if (habit == null)
{
return NotFound();
}
return Ok(habit);
}
小贴士或重要注意事项
它看起来像这样。
粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“Sara 点击了列表下方的添加新按钮。”
联系我们
我们始终欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请通过电子邮件发送至 customercare@packtpub.com,并在邮件主题中提及书名。
勘误表:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,如果您能向我们报告,我们将不胜感激。请访问www.packtpub.com/support/errata并填写表格。
盗版:如果您在互联网上以任何形式发现我们作品的非法副本,如果您能向我们提供位置地址或网站名称,我们将不胜感激。请通过电子邮件发送至 copyright@packt.com 并提供材料的链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
分享您的想法
一旦您阅读了《使用 C#和.NET 构建现代 SaaS 应用程序》,我们很乐意听听您的想法!请点击此处直接进入此书的亚马逊评论页面并分享您的反馈。
您的评论对我们和科技社区非常重要,并将帮助我们确保我们提供高质量的内容。
下载此书的免费 PDF 副本
感谢您购买此书!
您喜欢在路上阅读,但又无法携带您的印刷书籍到处走?
您的电子书购买是否与您选择的设备不兼容?
不必担心,现在每本 Packt 书籍都附赠一本无 DRM 的 PDF 版本,无需额外费用。
在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
优惠不会就此停止,您还可以获得独家折扣、时事通讯和每日免费内容的每日电子邮件。
按照以下简单步骤获取好处:
- 扫描下面的二维码或访问以下链接

packt.link/free-ebook/9781804610879
-
提交您的购买证明
-
就这些!我们将直接将您的免费 PDF 和其他福利发送到您的邮箱
第一部分:入门
本节首先介绍了软件即服务(SaaS)的历史,并介绍了我们将在此书中逐步构建的演示应用程序。
本节包含以下章节:
-
第一章,我们都需要的现代分销模式
-
第二章,构建一个简单的演示应用程序
第一章:SaaS – 我们所有人都需要的现代分销模式
本书将带您,读者,了解构建一个软件即服务(SaaS)应用程序的全过程。但在我们动手之前,我们将从 SaaS 的广泛概念介绍开始。应用程序开发的历史并不特别长,但它充满了事件!我们将回顾过去 40 年的软件工程,看看我们是如何到达这个点的,SaaS 作为主导范式出现,并考虑为什么 SaaS 会如此出现。利用 SaaS 对各种规模的企业和团队都有好处。
最后,我们将介绍您作为开发者将学习的工具和技术,使您能够自信地通过构建使用 SaaS 范式的应用程序来解决现实世界的问题。本章将为该主题提供一个介绍。我们不会深入探讨任何特定技能(那将在以后进行!)。相反,我们将为本书的其余部分设定场景,在本书中,我们将学习如何使用 Microsoft 技术堆栈构建、测试和发布现代 SaaS 应用程序。
本章涵盖了以下主要主题:
-
什么是 SaaS?
-
有哪些其他类型的应用程序?
-
一切从何开始?
-
为什么 SaaS 成为大小企业的热门选择?
-
构建 SaaS 应用程序需要哪些工具?
-
将使用哪些技术来构建 SaaS 应用程序?
-
这如何影响开发过程?
-
您可以期待从本书中获得什么
-
如何评估和偿还技术债务
到本章结束时,您将了解 SaaS 的定义。您将了解应用程序开发简短的历史,您将学习我们在本书的后续内容中将要看到的工具和技术!
什么是 SaaS?
SaaS 已经成为向用户交付应用程序的主导范式。但 SaaS 究竟是什么?
你可以说,SaaS 应用程序是通过互联网和浏览器向用户提供的软件应用程序,通常通过每月订阅费来支付。
虽然从技术上讲这是正确的,但这个单句定义忽略了很多复杂性!对用户、供应商,当然还有您——这些应用程序的开发者来说,有很多复杂性!
在本章中,我们将开始构建对 SaaS 的理解,包括构建一个从头开始的 SaaS 应用程序所需的技术、运营和功能知识,我们将使用 Microsoft 技术。我们将扩展之前的定义,直到您——读者,以及即将成为 SaaS 开发者的人——可以自信地通过提供基于 SaaS 的解决方案来解决问题!
让我们从扩展那个定义开始。
SaaS 通常被理解为通过浏览器交付和交互的应用程序。软件不是由用户购买或“拥有”并在他们的电脑上安装的。相反(通常是),用户需要支付每月的会员费,这赋予了用户访问服务的权限。
从用户的角度来看,这种做法的好处是他们可以在任何地方、任何时间、任何设备上访问应用程序。他们不必担心安装应用程序或保持应用程序更新。所有这些只是服务的一部分。
世界上许多最大的科技公司都提供 SaaS 应用,你很可能至少在使用其中之一!
Gmail 是谷歌提供的 SaaS 应用的绝佳例子。虽然对大多数用户来说它是免费的,但对于必须付费使用 G Suite 工具的企业或专业用户来说,则需要支付费用。除了 SaaS 邮件客户端,谷歌还提供日历应用和联系人管理应用,以及用于创建文档、构建电子表格或为演示创建幻灯片的办公工具。
只需支付小额月费,您就可以使用浏览器访问 Shopify 应用程序,当然,它也是一个 SaaS 应用。
所有社交媒体网站也都是 SaaS 应用的例子,尽管对于大多数用户来说它们可能免费使用,但使用这些平台来推广产品的企业却需要付出代价。
除了之前提到的那些拥有巨额、跨国、百亿美元企业的例子,还有许多其他企业正在使用 SaaS 模式提供他们的软件。有软件供应商——从供应商的角度来看,以这种方式提供软件有许多好处。最主要和最明显的好处是他们拥有巨大的市场覆盖范围——一个真正全球的市场,他们可以从中挖掘,并且有多个非常高的收入上限。此外,技术团队只需要支持一个应用程序版本,支持团队也只需要支持一个版本。供应商可以推出更新,所有用户将立即使用最新(并且希望是最好)的应用程序版本。理论上至少,SaaS 在几乎所有情况下都是一个明确的选择。然而……SaaS 很难!
虽然对企业的益处很多,但负责构建应用程序的团队将面临的挑战也很多。从技术人员的角度来看,这正是事情变得有趣的地方!
但在我们深入探讨 SaaS 应用程序的细节之前,让我们首先考虑一下替代方案!
还有其他哪些类型的应用程序?
本书将描述 SaaS,但在讨论 SaaS 是什么以及为什么在许多情况下它是一个好的选择的同时,它将与其他类型应用程序的其他交付机制进行对比。以下几节将讨论一些其他传统的交付机制。
桌面应用程序
这是一种传统的应用程序类型,多年来一直是许多人的主要范式。软件被打包成安装程序,并以某种方式分发给最终用户。分发机制可以是软盘、CD,或者直接从互联网上下载。
该应用程序将所有文件和数据存储在用户的机器上。
通常,这些类型的应用程序会附带一个产品密钥来激活软件。或者,可以安装许可服务器,使公司能够更容易地在多台计算机上许可产品。
本地部署的 Web 应用程序
这种类型的应用程序在很大程度上已被 SaaS 系统完全取代,但曾经有一段时间,开发一个可以随后出售给多个客户并安装在客户组织场所的服务器上的 Web 应用程序是很常见的。
这提供了一些 SaaS 的好处,但同时也带来了桌面交付模型的大量负担。
本地部署的 Web 应用程序的主要好处是购买公司可以完全控制该应用程序。这意味着他们可以根据对更新版本的成本和收益的内部评估来选择是否安装更新。
这种交付机制的另一个重大优势是,存储在数据库中的任何数据或客户端(Web 浏览器)和服务器之间的传输数据都可以完全保留在公司内部网络基础设施中,无需触及更广泛的公共互联网。从理论上讲,这确实允许额外的数据安全。
与上述观点形成对比的是:虽然将数据保留在公司自己的网络中可能看起来更安全,但亚马逊的Amazon Web Services(AWS)或微软的 Azure 等云服务已经投入了大量的资源来确保存储的数据和流动中的数据是安全的——实际上,他们的业务就依赖于这一点。并不能保证公司的内部网络就更加安全。
虽然在购买公司选择的时间点挑选版本和部署更新可能看起来是有益的,但这要求买家雇佣人员来安装和管理应用程序,以及支持他们的用户。
什么是“云”?
SaaS 和通过浏览器交付应用程序的核心是云的概念。在最基本的意义上,云只是别人的电脑……但尽管这个说法准确,但它并没有真正公正地体现其价值。云使得应用程序能够以非常高的可用性运行,并且具有几乎无限且即时的可扩展性。
对于 SaaS 应用程序来说,云服务绝对是至关重要的。在几乎所有情况下,每个组件(数据库、应用程序编程接口(API)和用户界面(UI))都将通过云服务提供商托管。最常用的云服务提供商是 Microsoft Azure、Google Cloud 和 Amazon AWS。
大型云服务提供商本质上只是在提供大量计算能力,并以此收费。这被称为基础设施即服务(IaaS),它是 SaaS 的一个重要支柱。
所有这一切都是从哪里开始的?
虽然这不是一本编程史书,但我认为简要回顾一下过去 60 年左右的应用程序开发历程,看看我们是如何到达现在的位置,是值得的。
我第一次接触专业编程是在 20 世纪 80 年代末,当时我大约 6 岁。我被带到了父亲的办公室,参观了计算机室,里面有一台巨大的 VAX“主机”机器,负责所有思考工作。然后我被带到了办公室,那里的工作人员在他们的桌子上配备了终端(带有绿色的文本,就像在《黑客帝国》中一样),这些终端连接回主机。实际计算机的成本如此之高,以至于只能有一台,每个人只是简单地在自己的桌子上配备了类似这样的哑终端:

图 1.1 – VAX 终端
尽管前面的展示很简单,但我立刻被吸引住了,当我跟随父亲的脚步成为一名应用程序开发者时,这种经历显然与我产生了共鸣,尽管我使用的工具与当时截然不同!
在 20 世纪 80 年代,我们离 SaaS 还非常遥远(尽管“主机”和“终端”配置可能在后来的某些思考中具有指导意义)!
尽管互联网技术自 20 世纪 60 年代以来就存在,但它并没有以当前一代抖音上瘾的青少年能够识别的方式存在。直到 90 年代后期,互联网才仅仅是一个技术上的好奇,而且它远未成为现在交付应用程序的基础。
在 1994 年 8 月,丹尼尔·科恩完成了第一次安全的信用卡交易,这是构建 SaaS 应用程序所需的一个巨大的技术里程碑。他购买了一张 Sting 的 CD——品味这东西真是无法解释!同年 11 月,Netscape Navigator 引入了安全套接字层(SSL)协议,使得任何人都可以在互联网上交易,而不必担心他们的信息被盗。
在此基础上,亚马逊在第二年立即推出了这项新的支付技术,随后不久 eBay 也加入了进来。这是人们愿意在互联网上输入他们的信用卡详细信息并有一定信任度的开始,但在这个时候,这种做法还远未成为主流。(旁白——我在 2001 年 6 月第一次在亚马逊上进行了在线购买,购买的是 DVD 版的《幸福高尔夫球手》;品味这东西真是无法解释!)
第一个真正的 SaaS 应用程序出现得并不慢。Salesforce 发布了被认为是第一个真正的 SaaS 应用程序,其 CRM 平台于 1999 年推出。
然而,说 SaaS 在 1999 年开始并不完全准确。当然——Salesforce 在曲线上领先了数英里,其对 SaaS 应用程序的投资无疑在接下来的几十年里为公司(及其资产负债表)带来了奇迹。但 1999 年,互联网对绝大多数人来说仍然是个好奇的事物。尽管 Salesforce 可以依赖其拥有(相对)快速互联网连接的企业客户,但 SaaS 应用程序的覆盖范围仍然很小。相对较少的家庭拥有互联网,更不用说宽带连接了。绝大多数人永远不会考虑在互联网上使用他们的真实姓名,更不用说他们的信用卡信息了!SaaS 普及的时代还远未到来。
到了 2000 年代初,我开始作为开发者职业生涯,并第一次进入自 80 年代以来的企业编程环境时,事情已经发生了相当大的变化。不再有 VAX 大型机。现在,我们都在自己的桌子上有了非常快的(相比之下)电脑,因此不再需要将计算委托给集中的大型机了。你可以在自己的电脑上直接进行计算。于是,我们就这么做了!在整个 90 年代,成千上万的开发者制作了数百万个“灰色盒子”桌面应用程序,通常使用 VB6 编写,带有(希望)涵盖任何可能情况的古怪安装指南。这并不是应用开发的巅峰,当然也不是企业应用,这在当时构成了绝大多数。
大约在这个时候,互联网开始成熟,成为日常的商业工具,并迅速成为典型家庭中像自来水一样普遍。但即使到了 2005 年中期,"网络应用程序"或 Web 2.0 的概念仍然尚未进入主流。当然——有 PHP 和 ASP,你可以使用这些技术来构建网络交付的应用程序。但这些通常更像是聪明的网站,而不是我们现在所认为的完整的“网络应用程序”。尽管在线支付变得越来越普遍,但真正支付月费以获得“服务”的概念仍然没有。人们的预期仍然是“购买”并因此“拥有”和随后“安装”软件。
当然,在接下来的二十年里,这一切都会改变。
ASP.NET 将“经典”ASP 所开始的东西推向了高潮。Visual Studio 提供了所见即所得的编辑器,使得对于任何曾经使用 VB6 构建现在过时的“灰色盒子”的人来说,工作流程非常熟悉。“企业”几乎立即接受了“网络应用程序”来取代已经变得典型并且通常普遍不受欢迎的灰色盒子应用程序。
这种从企业到通过浏览器提供软件的转变,让一代办公室工作人员了解了 Web 交付软件的好处,不久之后,他们就会开始要求所有应用程序,甚至包括他们个人生活中使用的应用程序,都采用这种方式。
电子邮件很可能是第一个完全基于 Web 的服务,面向个人用户。Gmail 测试版于 2004 年推出,当时免费提供 1 GB 的巨大存储空间……而且还有更多可供付费使用!消费者级软件的“订阅”模式由此诞生。
这些天,SaaS 应用的市场对消费者来说非常庞大,提供了众多任务列表、笔记、日记和电子邮件服务。更不用说娱乐现在也以“服务”的形式提供,许多家庭——如果不是大多数家庭——都通过 Netflix、Spotify 等每月订阅服务来享受。
关于在互联网上输入支付信息没有残留的担忧。几乎没有任何家庭没有宽带互联网服务,即使是发展中国家也经常拥有强大的移动数据网络和许多人的智能手机。现在不再有任何障碍来阻止通过互联网提供基于云的应用程序,以每月订阅的形式。
显然,SaaS 已经吞噬了世界,并成为提供软件的主导方法!SaaS 时代已经到来!
为什么 SaaS 成为大小企业都喜爱的选择?
SaaS 正在成为许多不同行业和不同规模企业的越来越受欢迎的选择。它正在各种商业模式中无处不在。这有几个原因,但大部分原因来自于为用户增加价值以及为供应商增加收入的能力。这是一个双赢的范式。
可以通过互联网和浏览器开发并交付各种不同的应用程序。相同的技术堆栈——因此,相同的发展团队——可以用来交付企业能想到的几乎所有类型的应用程序。这使得它对初创公司和跨国公司都具有几乎相同的吸引力。
使用传统的桌面应用程序模型或本地化 Web 应用,企业需要直接与所有客户保持联系,以便进行发票开具、提供更新等。获取新客户需要销售团队,为新用户部署新实例则需要技术团队。鉴于每个安装都在不同的环境中,很可能还需要一个庞大的支持团队来帮助处理各种不同的安装。
所有这些在 SaaS 中都消失了。唯一的扩展考虑因素是虚拟服务器的可用性,在 Azure 和 AWS 的时代,这些服务器几乎是无限的。
由于 SaaS 为用户和供应商都增加了价值,因此它已成为所有规模企业的明显选择。
建立 SaaS 应用需要哪些工具?
可用于开发 SaaS 应用程序的工具种类繁多。开发 SaaS 应用程序的性质意味着需要特定的工具来构建和测试数据库、API 和前端,以及许多辅助工具,如静态分析、构建管道、源控制和容器化工具等,仅举几例。
本书将专注于微软技术栈,因此将主要使用微软的工具。但我们会使用很多工具——这是构建 SaaS 应用程序的本质。
数据库开发
从技术栈的底层开始,我们将使用 SQL Server Express 进行所有数据库工作。这可以直接安装到您的开发机器上,或者使用 Docker 在容器中运行。本书将详细描述这两种方法,尽管我们通常更倾向于容器化解决方案。
API 开发
API 是一套规则和协议,它指定了两个系统应该如何相互通信。它是让一个系统访问另一个系统功能的一种方式,例如基于 Web 的软件应用程序或服务器。API 允许不同的软件系统相互交互并共享数据和功能。它们通常用于实现不同系统之间的集成,例如移动应用程序和后端服务器,或网站和数据库。API 将使用 C#和.NET 7 进行开发。坚持使用所有微软技术,我们将使用 Visual Studio Community Edition。这是一个免费且功能强大的 IDE,使得开发 C#应用程序变得非常简单。
前端开发
构建 SaaS 应用程序的前端有很多好的选择。选择将主要取决于个人喜好或市场上开发者的可用性。
在撰写本书时,最常用的前端技术是基于 JavaScript 的——通常是 Angular 或 React。然而,未来是 WebAssembly,微软最近发布了使用这项技术的 Blazor,它允许使用熟悉的.NET 语言,如 C#来构建前端。本书将演示使用 Blazor 的前端,但鉴于 JavaScript 框架(目前)更受欢迎,我将确保解释足够通用,以便 Blazor 的学习成果可以应用于任何其他前端框架。
认证和授权
无论使用哪种前端技术,确保认证和授权正确都是至关重要的。我们将在本书的后面章节中专门讨论这一点。我们将使用 OAuth 2.0 协议的实现,并演示如何从 UI 到数据库,再到 UI,确保您的 SaaS 应用程序的安全性!
主机托管
每个 SaaS 应用程序都需要一个存放的地方,通常是在云端。虽然本书的大部分内容将专注于在本地开发者机器上开发,但我们也会探讨如何构建部署管道,并展示您的应用程序“上线”。我们将使用 Azure 门户来托管所有应用程序和数据库。
Docker
开发 SaaS 应用程序真的是“全栈”的典范。我们将使用从数据库到前端的各种工具,不用说还要使用许多不同的测试框架来测试所有这些组件。因此,我们将大量依赖 Docker 来封装所有这些依赖关系并简化开发过程。Docker 是一个真正巨大的主题,本书的范围不包括全面解释 Docker 是什么以及 Docker 做什么。简单来说,Docker 允许各种复杂性封装在一个非常易于使用的容器中。
例如,考虑执行针对 UI 和 API 的几千个单元测试,也许还有几个集成和端到端(E2E)测试。运行这些测试可能涉及许多依赖项,并且通常需要一些时间来配置开发机器以成功执行测试套件。
使用 Docker,可以在 Docker 容器中封装完整的测试套件,通过一个简单的 Docker 命令运行测试。此外,这些测试将在任何运行 Docker 客户端的机器上以相同的方式运行。因此,Docker 化的测试套件在 Mac、Windows、Linux 上以及云服务器上的管道中都能愉快地运行。
简而言之,Docker 封装了复杂性,并简化了与复杂系统的交互。
将使用哪些技术来构建 SaaS 应用程序?
实际上,我们将使用的所有技术都不是在开发任何其他类型的软件应用程序时不会使用的。然而,我将简要提及我在本书中将使用的技术。
测试驱动开发(TDD)
SaaS 的一个巨大好处是应用程序可以非常快速地更新,并且基本上可以一键推出给每个用户。
假设一切按预期工作,这当然很好,但如果代码中存在错误,那就好多了。当然,我们可以构建一套广泛的手动回归测试,并将业务流程构建到发布管道中……但这样做,你将失去 SaaS 的一些所谓优势——能够频繁和早期发布。
要以一定程度的信心快速部署并确保其工作,唯一的方法是构建自动化测试。实际上,构建自动化测试套件的最佳方式是在进行中完成,遵循 TDD(测试驱动开发)方法。
我知道目前 TDD 在业界有一定的争议。在我看来,这是因为错误的 TDD 是一场噩梦,而且错误的 TDD 非常常见。我将介绍多种我认为在开发 SaaS 应用时是优秀支持结构的 TDD。
领域驱动设计 (DDD)
DDD 被定义为一种软件开发方法,其中问题由领域专家指定,而不是由中层管理指定。
DDD 是一种软件开发方法,它专注于理解和建模应用程序的业务域,以提高软件的设计和实现。它强调领域知识在软件开发中的重要性,并鼓励在软件系统的设计和实现中使用领域特定语言。
在 DDD 中,业务域被理解为组织的核心专业领域或关注点,而正在开发的软件被视为支持并增强该领域工作的工具。DDD 的目标是创建与组织的业务需求和目标一致的软件,并准确反映业务域的复杂性和细微差别。
SaaS 产品通常只是简单地提供给任何有网络连接的人,没有专门的销售团队分别接近每一位客户并积极销售产品。因此,产品必须能够自我销售,因此它必须是实用的。为了使这一点成为现实,产品必须满足特定的用户需求并解决特定的问题域。
微服务
SaaS 项目需要具备灵活性,以便产品能够随着市场和客户需求的变化而发展。产品架构必须以允许直接添加新功能,同时对现有功能影响最小的方式进行设计。基于微服务的架构符合这一要求。
多租户
由于每个用户都是同一部署版本的应用程序中的租户,因此用户数据必须在数据存储和检索系统中保持分离。有几种方法可以解决这个问题,这些方法将在本书的后续章节中讨论。
响应式设计
SaaS 应用程序在线运行,并通过浏览器访问。在智能手机和平板电脑的现代时代,无法知道将使用哪种类型的设备来访问您的应用程序。前端真的需要在任何类型的设备上工作,或者至少在无法在任何特定设备上运行时“优雅地失败”。
因此,UI 的设计必须是“响应式的”,这意味着它可以以适合其显示的设备的方式进行渲染。
渐进式网络应用 (PWAs)
“想要有”的东西,但我认为值得考虑。当我们构建 SaaS 应用程序时,我们真的希望用户感觉到他们正在使用一个完整的“应用程序”,而不仅仅是被美化的网站。然而,根据定义,如果没有可用的互联网,网站是无法显示的……基于 PWA 的设计通过允许在互联网可用性较低或没有互联网的情况下运行有限的功能来解决这个问题。
当然,如果没有访问后端,网站上的许多功能将不可用。这是无法避免的,但 PWA 可以让用户体验稍微好一些,因此这是 SaaS 应用程序作者应该了解的重要技术。
我们将演示使用 Blazor 作为前端技术的一个 PWA(渐进式网页应用)。
选择 SaaS 作为所有大小、新旧企业的首选范式的原因:正如我们之前所强调的,大多数人口都在使用某种形式的 SaaS 应用程序,从 Gmail 到 Netflix。如果 SaaS 真的在吞噬世界,那肯定有原因。
无需安装
这是推动企业转向基于 SaaS 的解决方案的最大、最重要的因素。
使用传统的应用程序模型,新客户通常需要联系销售团队,有时还需要联系技术支持团队,以便在本地安装软件并获得适当的许可。
使用 SaaS,新客户可以在几秒钟内发现应用程序并注册账户,而无需与提供应用程序的公司有任何联系。这对开发应用程序的公司来说是一个巨大的节省,因为不需要额外的销售或支持团队来 onboard 新客户。这也防止了从发现到安装的时间滞后,在此期间客户可能会改变主意或发现竞争对手。
基于浏览器的交付
SaaS 应用的用户不仅限于在特定计算机或位于许可证服务器所在特定网络环境中访问应用程序。用户可以从世界各地的任何互联网连接的机器上访问该应用程序。在智能手机和平板电脑的现代时代,SaaS 应用的用户甚至可能不需要电脑就能充分利用提供的服务。
可扩展性
在传统模型中,这是不可能的。说服人们在自己的电脑上安装任何应用程序都很困难。说服公司这样做则更加困难。扩大桌面应用程序的用户群需要专门的销售团队和专门的客户支持团队,首先制作应用程序,然后引导人们完成安装过程。使用 SaaS 应用程序,用户只需在在线表单上注册即可。
由于 SaaS 应用托管在云端(Azure、Google Cloud Platform(GCP)或 AWS),如果需求突然激增,基础设施可以实现近乎瞬间的扩展。没有其他软件交付模式能够面对一夜之间需求增加 10 倍的情况,而供应商不会陷入困境!
可升级性
在“传统”的应用交付方法下,应用升级往往对应用供应商来说变得极其复杂和昂贵。
如果你考虑传统的桌面应用,同一个应用可能被安装在成百上千台机器上,遍布成百上千个不同的企业。没有任何两家企业会运行相同的硬件或操作系统版本,因此不可能让所有人都使用你的软件的同一版本。同样,推出新版本的升级也是完全不可能的。最好的办法是在特定时间停止支持特定版本,并希望所有人都停止使用它(注意——他们不会停止使用)。
对于本地部署的 Web 应用来说,这也是一个类似的问题。虽然安装数量较少,但仍然会有许多不同的版本存在,以满足特定的业务需求。
当你转向 SaaS 模式时,这个问题完全消失。供应商可以完全控制升级,并且可以一键将它们推出给所有客户。
只支持一个版本的 app 对于供应商来说是一项巨大的节省,同时也为开发团队带来了巨大的优势。
快速迭代
SaaS 应用升级优势的一个新兴特性是能够极快地迭代解决方案。关于新特性的反馈可以在非常快的周期内被整合并推送给所有用户。这允许开发者从编写功能到该功能为用户提供价值以及为供应商带来收入之间实现非常快的周转。
考虑一下在传统应用中实现这一价值需要多长时间。代码可能已经在 Git 仓库中存放了好几个月,才被包括在“年度发布”中,然后,用户可能选择不立即升级。
分析
由于所有用户都通过单个 Web 应用进行操作,分析应用是如何被使用的变得极其容易。这可以引导企业做出明智的决策,升级应用中最常用的部分,并推迟对不太常用的部分的工作。结合升级的便利性和快速迭代,这可以为用户提供巨大的价值提升,并应该为供应商带来收入的提升。
全球市场
使用 SaaS,能够随时随地接触到客户对提供产品的企业来说是一个巨大的提升。不存在因为时区问题或销售代表未能回复电子邮件而错失销售的情况。
能够接触到真正全球的受众,使一些公司成为世界上最大的公司——比银行和石油天然气巨头还要大。这种市场准入也允许成千上万的较小公司在真正全球化的环境中蓬勃发展。
灵活的支付模式
对于提供 SaaS 服务的公司来说,有许多不同的支付模式可供选择。这使得它能够吸引大小客户,并从每个客户层或规模中获取最大价值。以下是一些支付模式的类型:
-
分级定价
-
免费增值
-
免费试用期
-
按用户定价
安全性
访问 SaaS 应用程序将通过用户登录进行,这可以保护敏感数据和文件,这些数据和文件被安全地存储在云系统中。这在大多数情况下比在本地服务器上存储数据或桌面应用程序的本地用户机器上存储数据要安全得多。虽然看起来将所有数据保留在本地或本地网络中似乎比通过互联网将数据发送到云服务器上更安全,但这通常并非如此。为通常用于托管 SaaS 应用程序的云服务投入了大量的精力来确保安全,而这种努力通常无法逐个站点复制,无论是保护本地 Web 应用程序还是单独保护桌面安装应用程序上的数据。
这如何影响开发过程?
理论上,构建 SaaS 应用程序的开发过程与其他类型的应用程序相当相似,但在实践中,作为 SaaS 项目开发人员,你必须注意一些细微差别和考虑因素。
经常发布,尽早发布
SaaS 应用程序没有“年度发布周期”。预期功能将被分解成可管理的部分,开发完成后,一旦准备好就立即推出。如果是从更传统的发布周期转变过来,这需要一些思维方式的转变。所有更改都必须是增量式的,通常仅限于应用程序的小部分,并且要尽快准备好发布。
这种将代码交到用户手中的思维方式需要由自动化管道来支持,这些管道可以构建和发布新代码,而无需太多人工干预。
虽然有可能向部分受众推出更新以确保其中没有灾难性的内容,但对于中小型 SaaS 应用程序来说,更典型的情况是一次性将更新推送给所有用户。为了使这成功……
测试,测试,测试
如果代码一次性发布给整个用户群,而且通常没有真正的方法可以回滚更改,你最好希望它能正常工作!
唯一能够建立代码按广告宣传的方式运行的信任的方式是测试它,而在“频繁发布,尽早发布”的心态下,这意味着自动化测试。虽然这并不一定意味着遵循 TDD(测试驱动开发)的心态,但这肯定是有用的。
你最好全栈
好吧——这并不是绝对必要的。我相信大型 SaaS 应用程序是由数据库/后端/前端学科的专家开发的。但当然,对不同的应用程序层次有良好的知识将大有裨益。
SaaS 应用程序以几乎有机的方式通过快速周期和近乎即时的发布“增长”,这意味着至少需要对各个层次有所理解,并且理解数据库中的决策可能如何影响前端是非常宝贵的。
了解你的用户
虽然这可能不是团队中每个开发者的要求,但整个团队理解其产品的使用者、他们为什么使用它以及价值在哪里是绝对必要的。这种知识和理解将来自评估分析,以及“更软”的方法,如焦点小组和与用户的访谈。
这种理解应该通过用户故事流入开发过程。从开发者的角度来看,如果某个特定功能没有在焦点小组中取得良好效果,或者用户访谈表明某个路径是错误的,这可能会导致方向上的突然转变。快速调整方向的能力对整个团队来说都很重要。
从这本书中你可以期待什么
本章已作为 SaaS 是什么、它从何而来、为什么企业和用户喜欢它,以及最后作为开发者,你需要有效地构建 SaaS 应用程序的介绍。
在接下来的章节中,我们将深入探讨所有上述领域,但重点是构建作为开发者构建用户喜爱并希望构建的出色 SaaS 应用程序所需的工具和理解。
为了说明所需的技术点和理解,我们将构建、测试和部署一个全栈 SaaS 应用程序!
我在撰写这本书时,采取了与构建应用程序时相同的心态。我的目标是让这本书成为一个引人入胜、有趣,甚至可能是“令人愉悦”的体验——在这种情况下,就是你!
让我们开始吧!
如何评估和偿还技术债务
技术债务究竟是什么?让我们从定义开始:
技术债务随着项目功能的增加而累积。随着项目某些区域复杂性的增加,该项目的一些其他部分将不再完全适合,并在某个时候必须进行工作。然而,作为开发者的生活现实是,产品必须发货,账单必须支付,因此,并不总是有足够的时间来整理每一个角落,随着时间的推移,技术债务开始 累积 *。
另一个所有项目都会遇到的技术债务来源是在底层框架和技术更新时。更新一个主要版本通常并不简单,遗憾的是,项目滞留在过时版本中——这代表了一种技术债务。
最后一个常见的技术债务来源是当用户习惯改变时。在 iPhone 时代之前,很少有人通过移动设备访问网站。这种情况很快发生了变化,导致许多团队急忙更新他们的网站,以便通过移动设备访问时能够正常工作。
所有技术项目都有一些技术债务——这是无法避免的事实,并且对于所有项目来说,保持对技术债务的控制非常重要。然而,在开发 SaaS 应用程序时,有一些特定的考虑因素必须加以考虑。
对于大多数 SaaS 应用程序,其哲学是尽可能快地将开发工作交给客户。这通常是通过广泛的自动化测试套件,以及构建和发布管道来实现的,以便尽可能快地将代码推送到生产环境。
与传统的发布和交付机制相比,传统的发布和交付机制在发布之间会有一个年度间隔,并且在那一年的某个时间点可能会分配一些时间来偿还技术债务。
在 SaaS 开发中常见的持续开发-发布-重复周期中,保持对技术债务的控制非常重要。
评估和偿还技术债务的第一种也是最重要的方式是允许开发团队每周(或每个冲刺周期)留出一些时间来做“杂事”。这些可能是其他情况下可能会被忽视的小型维护任务,否则它们可能会发展成为严重的技术债务问题。开发团队总是最清楚技术债务在哪里。毕竟,是他们最初创造了它!
静态分析是另一种保持技术债务在控制之下的极其强大的工具。静态分析用于在代码未运行时(当它是静态的!)检查代码的质量,并可以检查是否遵守了标准以及是否正在实施最新的最佳实践。
与静态分析类似,也应该始终执行代码检查,以确保代码格式符合约定的编码标准。
如前所述,过时的包可能成为技术债务的主要来源。虽然保持绝对的前沿可能存在风险,但严重过时通常没有好处。应该定期进行清理,以确保所使用的任何包和框架都足够更新。
最后,应该进行自动性能测试,以确保随着应用的增长和随时间的变化,其性能没有出现退化。
即使严格遵守上述所有要点,项目随着时间的推移仍然会积累技术债务。对此几乎无能为力。但是,在实施上述考虑和缓解措施的情况下,技术债务对项目的影响——以及最终构建项目的公司的盈利能力——可以最小化。
摘要
本章对 SaaS 的概念进行了广泛的介绍。我们回顾了应用开发的历史,并探讨了 SaaS 出现之前盛行的范式。我们考虑了为什么 SaaS 变得如此受欢迎,并分析了其采用的技术、商业和以用户为中心的原因。最后,我们考虑了作为开发者,你需要哪些工具和技术才能成为一个有效的 SaaS 开发者。
希望本章已经为你提供了对 SaaS 的坚实基础理解,我们将从现在开始,随着本书的进展对其进行扩展!
构建 SaaS 应用具有挑战性,但迄今为止,最好的进步方式就是开始构建,并亲自动手!在下一章中,我们将直接介绍本书将使用的工具、工具集和技术,以构建一个 SaaS 应用的演示!
进一步阅读
-
关于当前的商业趋势:
www.datapine.com/blog/saas-trends/ -
关于一些 SaaS 开发的技巧和提示:
www.rswebsols.com/tutorials/software-tutorials/saas-application-development-tips-tricks -
关于扩展 SaaS 应用的更多信息:
medium.com/@mikesparr/things-i-wish-i-knew-when-starting-software-programming-3508aef0b257
第二章:构建一个简单的演示应用
在本章节中,我们将构建一个非常简单的演示应用,这将成为我们在后续章节中构建的 SaaS 应用的基础和框架。本章节的目的是让你熟悉我们将在本书的后续部分以及作为 SaaS 应用构建者职业生涯中利用的所有工具和技术!一旦我们有了这个基础知识和框架应用,开始构建更复杂的功能将会容易得多。
在我们开始构建框架应用之前,我们必须首先发现并安装我们将要使用的工具。然后,我们将构建骨架应用。一旦这个应用开始工作,我们再开始考虑将这个应用扩展成一个真正的 SaaS 应用所需的内容!
本章节将简要介绍许多不同的技术。请不要感到不知所措!我们将从头开始配置一个开发环境,使用 Docker,然后初始化数据库、API 和 UI。
本章节使用了许多不同的技术,其中一些你可能不熟悉。不用担心!本章节中所有内容都写得非常详细,任何在本章节中快速跳过的内容都将在后续章节中详细解释。
开发 SaaS 应用,几乎可以说是定义上就需要广泛的不同技术。在本章节中,我们将快速浏览它们,然后在后续章节中深入我们的知识和理解!
本章节涵盖了以下主要主题:
-
准备工作 - 安装工具
-
构建一个简单的示例应用
-
我们接下来该做什么?
到本章节结束时,你将已经构建、安装和配置了你的开发环境,并且初始化了构建 SaaS 应用所需的所有组件。希望你能在本章节结束时,对如何充分利用这些工具和技术感到启发!
技术要求
本章节的所有代码都可以在github.com/PacktPublishing/Building-Modern-SaaS-Applications-with-C-and-.NET/tree/main/Chapter-2找到。
设置
在本节中,我们将介绍我推荐用于完成本书示例的工具。请注意,实际上并没有专门用于构建 SaaS 应用的特定工具——你可能会习惯的典型开发者工具就可以完成这项工作。然而,本书中我将提到一组特定的工具,因此在本节中我将描述它们,解释我的选择,并介绍如何安装和配置这些工具。
我相信任何流行的工具都完全可以满足需求,所以请不要觉得您必须选择我的选择,您可以自由使用您最舒适的方式!您应该能够将示例适应到您选择的工具中。
在您的机器上,只需安装两个工具即可跟随本书提供的代码:
-
Docker Desktop
-
Visual Studio Code
这些工具相当稳定,使用最新版本可能是最佳选择。为了参考,我正在使用 Docker v20.10.17 和 VS Code v1.71.2。
这些工具被特意选择,因为它们适用于所有操作系统,它们是免费使用的,并且将为我们在代码示例中工作的基础提供一致性。这些是我作为软件开发者在日常工作中发现最有用的两个工具!通常,在构建新的开发者机器时,我会首先安装这两个应用程序。
我将使用 Docker 的一个巧妙技巧来容器化整个开发者设置,这就是为什么这两个工具是您需要在机器上安装的唯一工具,也是我为什么有信心提供的示例可以在任何操作系统上工作。
Visual Studio Code
Visual Studio Code 已经从全功能的 Visual Studio 应用程序的轻量版本转变为所有类型开发工作的绝对工作马。随着大量扩展的可用性,它可以配置为几乎任何编码任务,然后进一步修改以适应个人偏好。
VSCode 已经成为我众多不同项目的首选 IDE,但 SaaS 项目的真正优势在于它可以配置为支持将要构建的所有不同层。您可以添加用于数据库开发、API 开发和 UI 开发的扩展,并构建一个高度定制的开发者环境,该环境专门针对您的项目和您首选的工具。
首先,安装 Visual Studio Code。这可以通过点击此 URL 并遵循您选择的操作系统的说明来完成:code.visualstudio.com/download
当安装了 VSCode 后,您还需要安装几个扩展。
您需要为 VS Code 安装三个扩展:
-
远程容器
-
Remote WSL
-
Docker
所需扩展的标签如下:
ms-vscode-remote.remote-containers
ms-vscode-remote.remote-wsl
ms-azuretools.vscode-docker
当 VSCode 正确配置后,扩展面板应该看起来像这样:

图 2.1 – 配置了扩展的 Visual Studio Code
我们将使用一些针对项目的其他特定扩展,但这些将在便于使用的开发容器中安装——关于这一点稍后会有更多介绍!
Docker
Docker 是开发者工具箱中相对较新的补充,但它很快就成为了构建和部署应用程序不可或缺的工具。在最近几年,Docker 也成为了封装开发机器上所需的所有设置的不可思议的工具。
Docker 是一个容器化工具。其原理来源于货柜。货柜是标准尺寸的,这意味着移动它们以及在内港和船只上装载它们的机械在全球范围内是完全标准化的。当然,货柜里可以装任何东西,从汽车到黄瓜,但移动容器的基础设施是相同的。
这个原则适用于使用 Docker 打包的软件。任何要运行的软件工具都被放入一个 Docker 容器中——类似于一个货柜——然后使用 Docker 基础设施来运行这个容器。
你可以将容器视为一个微型的虚拟机,或者可能是它自己的可执行文件。重要的是要记住,任何被放入 Docker 容器中的代码、应用程序、服务等等,都可以在任何安装了 Docker 的计算机上运行。此外,它将以相同的方式(或不会)运行,无论主机操作系统是 Windows、Mac 还是 Linux。
这种容器概念对于运输运行中的软件来说非常棒。我现在可以拉取几乎任何我能想到的软件实用程序的 Docker 镜像,无论我在什么平台上工作,我都可以执行那个软件。
从软件供应商的角度来看,巨大的好处是,他们可以确信他们的代码在我的机器上能够正确运行,因为它是运行在一个标准平台上的——Docker!
你可能会想知道,当涉及到设置开发者环境时,这为什么很重要。
开发容器是一个相对较新但极其强大的概念,可以将 Docker 在运行应用程序时的所有功能应用到开发应用程序上。现在,可以配置整个开发环境——包括所有相关的依赖项——并将所有这些放入一个容器中!
这可能对更基础的项目来说不是最重要的,但 SaaS 应用程序很少是基础项目。
SaaS 应用程序的本质通常要求在每个开发者机器上安装许多特定项目的工具。更糟糕的是,通常需要特定版本的工具,这往往会使新团队成员的初始设置特别痛苦。这些工具可能包括以下内容:
-
数据库平台
-
一个框架,如.NET——通常有特定的版本要求
-
软件包管理,如 NPM 或 NuGet
-
前端某种类型的 Web 服务器
-
数量庞大的 CLI 工具
-
以及许多其他开发工具
如果你的开发团队支持多个项目——这种情况很常见——这可能会成为一个真正的痛点。
我会尽可能多地使用开发容器来封装依赖项,并在本书的示例中使用这项技术。
请注意,Docker 本身是一个庞大的主题,本书的范围不包括对其进行深入探讨。我将只介绍足够的内容,以便熟悉我使用的技术,并将更深入的探索留给读者自行完成!
开发容器
作为开发者,我们每个人都有自己每天工作中最喜欢的工具集。这将从我们选择的操作系统开始,然后我们将选择 IDE、数据库平台、前端框架以及所有相关的工具。
这么多系统、工具、平台和框架的多样性,对撰写如何构建 SaaS 平台书籍的作者来说是一个挑战...
为了使本书的教程尽可能吸引广泛的开发者,我将使用一个相对较新的概念,称为开发容器。这允许我们配置一个 Docker 容器来完成所有开发工作。这将为你提供一个一致的工作平台,并确保提供的所有示例在实际上任何机器上都能运行。
利用开发容器,我们可以获得一个完全一致的开发平台来工作,这样我们就可以确保本书的所有读者都能够运行提供的示例代码,但启动时需要一些小的配置开销。
什么是开发容器?
继续使用运输容器的类比,开发容器只是围绕你在本书代码工作中使用的各种开发者工具的一个包装。
广义而言,所需的工具和服务如下:
-
数据库平台
-
.NET SDK
-
支持数据库开发的扩展
-
支持 API 开发的扩展
-
支持前端和 Blazor 开发的扩展
所有这些都将被打包进一个开发容器中。
配置 Docker 容器
这个第一个示例的代码可在github.com/PacktPublishing/Building-Modern-SaaS-Applications-with-C-and-.NET/tree/main/Chapter-2找到。你可以克隆仓库或按照这个示例设置进行操作。
如果你正在跟随,那么首先创建一个新的文件夹,这个文件夹将成为你项目的根目录,然后在该文件夹中打开 VSCode。这是一个完全空白的画布,但到本章结束时,我们将拥有一个功能 SaaS 应用的骨架框架。
首先创建以下文件夹和文件结构:

图 2.2 – 预期文件夹结构
在创建文件夹结构后,我们现在可以开始填充这些文件。我们将从 dev-env 文件夹中的 Dockerfile 开始。这将用于配置开发环境,并将包含安装用于构建应用程序的工具的指令。
Dockerfile
打开文件:
docker/dev-env/Dockerfile
然后粘贴以下内容:
# [Choice] .NET version: 7.0, 6.0, 5.0, 3.1, 6.0-bullseye, 5.0-bullseye, 3.1-bullseye, 6.0-focal, 5.0-focal, 3.1-focal
ARG VARIANT="7.0"
FROM mcr.microsoft.com/vscode/devcontainers/dotnet:0-${VARIANT}
# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10
ARG NODE_VERSION="none"
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
# [Optional] Uncomment this section to install additional OS packages.
RUN apt-get update && \
export DEBIAN_FRONTEND=noninteractive && \
apt-get -qy full-upgrade && \
apt-get install -qy curl && \
apt-get -y install --no-install-recommends vim && \
curl -sSL https://get.docker.com/ | sh
RUN dotnet tool install -g dotnet-ef
ENV PATH $PATH:/root/.dotnet/tools
# configure for https
RUN dotnet dev-certs https
这配置了开发环境以促进 .NET 应用程序开发。让我们详细了解一下。第一行确定将使用的 .NET 版本:
ARG VARIANT="7.0"
我们使用的是 7.0,这是撰写本文时的最新版本。
接下来,配置 Node 的版本:
ARG NODE_VERSION="none"
没有安装任何 Node 版本。启用 node 或 npm 通常很有用,但目前在当前情况下并不需要。
下一个命令用于安装您可能想要使用的任何包或工具:
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install --no-install-recommends vim
此命令更新包管理器并安装我们将稍后使用的 Vim CLI 工具。
上述内容都是环境的“标准”配置。接下来,我们将添加一些定制化配置,这将使我们能够使用 Entity Framework:
RUN dotnet tool install -g dotnet-ef
此命令安装我们将用于与数据库交互的 .NET Entity Framework(EF)工具。
最后,我们将 dot net 工具添加到 path 变量中,这样我们就可以从命令行使用它们:
ENV PATH $PATH:/root/.dotnet/tools
Sqlserver.env
此文件仅设置一些环境变量,用于我们即将启动的 SQL Server 实例。复制以下内容:
ACCEPT_EULA="Y"
SA_PASSWORD="Password1"
应该不用说,密码永远不应该在任何环境(除了这个示例)中使用!另外,请注意,将密码提交到版本控制是极其糟糕的做法,在开发生产应用程序时不应这样做。我们将在讨论安全性的第八章**中解决这个问题。
Docker-compose.yaml
这里事情变得有趣。Docker Compose 是一个工具,它允许我们同时使用多个容器。它是一个容器编排工具!
将以下内容粘贴到 docker-compose 文件中:
version: '3.4'
services:
sql_server:
container_name: sqlserver
image: "mcr.microsoft.com/mssql/server:2022-latest"
ports:
- "9876:1433"
volumes:
- habit-db-volume:/var/lib/mssqlql/data/
env_file:
- sqlserver/sqlserver.env
dev-env:
container_name: dev-env
build:
context: ./dev-env
volumes:
- "..:/workspace"
stdin_open: true # docker run -i
tty: true # docker run -t
volumes:
habit-db-volume: null
上述 Docker Compose 命令将我们所做的一切整合在一起。有了这个脚本,我们可以启动一组 Docker 容器,允许我们构建 API、UI,并且与数据库交互——所有这些都不需要在主机机器上直接安装任何东西!
回顾一下:我们已经构建了一个名为 dev-env 的容器,它符合我们的需求。我们还配置了一个包含 SQL Server 2022 实例的容器,该实例将作为我们的数据库平台。
运行容器
这就是所有的 Docker 特定配置。现在我们将运行容器并开始交互。
我们已经构建了一个基于 Docker 的环境——现在是时候启动它了。为此,打开 VSCode,打开一个新的终端,并输入以下内容:
cd .\docker\
docker compose up
您的终端应该看起来像以下这样:

图 2.3 – 预期输出
第一次运行此命令可能需要一些时间,但在后续使用中会显著更快。此命令将启动 docker compose 文件中描述的服务。具体来说,是一个 SQL Server 实例,以及我们使用 .NET 6.0 和 Entity Framework 配置的开发环境。
通过以上步骤,我们已经创建了一个安装了 .NET 和 Entity Framework 的容器。你可以通过尝试以下操作来证实我们确实实现了这一点。在 VSCode 中打开一个新的终端,并输入以下命令以 exec 进入 dev-env 容器:
docker exec -it dev-env /bin/bash
exec 命令允许通过终端与已运行的容器进行交互。
运行前面的命令将在 dev-env Docker 容器中打开一个交互式终端,我们可以在通过前面的 exec 命令打开的控制台中输入以下内容来检查 .NET 和 EF 工具是否正确安装:
dotnet –-version
dotnet-ef --version
前面的命令应该返回以下内容:

图 2.4 – 预期终端输出
接下来,我们可以执行类似的操作以确保 SQL Server 容器正在运行。再次,在 VSCode 中打开一个新的终端,并输入以下内容:
docker exec -it sqlserver /bin/bash
再次,这将在 sqlserver 容器中打开另一个交互式终端。你可以通过输入以下内容来证实这一点:
/opt/mssql-tools/bin/sqlcmd -S localhost -U SA
当提示时,输入 sqlserver.env 文件中的密码(Password1)以进入 SQL Server 命令行界面。你可以对数据库平台进行简单的检查,例如检查版本:

图 2.5 – 检查版本
到目前为止,我们已经完成了所有的 Docker 容器设置,但我们与我们的环境交互的唯一方式是使用命令行,而如今这并不是一个真正可接受的做法!幸运的是,Visual Studio Code 有一个巧妙的技巧!
配置 VSCode 中的 dev 容器
你会记得我们为 VSCode 安装了 Remote Containers 扩展。这将允许我们打开一个 VSCode 实例,该实例利用了我们之前设置的 Docker 容器。这需要一些额外的配置,但一旦设置好,它将在项目的剩余部分“正常工作”!
首先在项目根目录下创建一个名为 dev-env 的文件夹:
.devcontainer
这是 VSCode 将查找以获取配置的地方。
在此文件夹中创建一个名为 devcontainer.json 的文件。你的文件夹结构应该如下所示:

图 2.6 – 文件夹结构
在 devcontainer.json 文件中,粘贴以下内容:
{
"name": "SaaS Book",
"dockerComposeFile": ["../docker/docker-compose.yaml"],
"service": "dev-env",
"workspaceFolder": "/workspace",
"customizations": {
"vscode": {
"extensions": [
"ms-dotnettools.csharp",
"shardulm94.trailing-spaces",
"mikestead.dotenv",
"fernandoescolar.vscode-solution-explorer",
"jmrog.vscode-nuget-package-manager",
"patcx.vscode-nuget-gallery",
"pkief.material-icon-theme",
"ms-mssql.mssql",
"humao.rest-client",
"rangav.vscode-thunder-client",
"formulahendry.dotnet-test-explorer",
"kevin-chatham.aspnetcorerazor-html-css-
class-completion",
"syncfusioninc.blazor-vscode-extensions",
"ms-dotnettools.vscode-dotnet-runtime",
"ms-dotnettools.blazorwasm-companion"
]
}
},
"remoteUser": "root"
}
让我们逐行分析。以下代码行告诉 VSCode 在哪里查找 docker compose 文件。这是我们之前创建的文件,它配置了两个 Docker 容器:
"dockerComposeFile": ["../docker/docker-compose.yaml"],
下一行只是告诉 VSCode,名为 dev-env 的容器是主要容器:
"service": "dev-env",
下一行定义了容器内部的工作文件夹——稍后将有更多介绍:
"workspaceFolder": "/workspace",
以下内容相当巧妙,包括开发容器配置。本节允许我们定义在开始使用开发容器工作时希望可用的扩展。我已经列出三个扩展以供开始,但随着项目的复杂化,这个列表将会增长:
"extensions": [ … ],
这是从微软来的一个特别巧妙的补充,因为这允许 VSCode 配置作为 JSON 格式存在于项目中。此文件已提交到仓库并与代码一起移动,这意味着任何新团队成员拉取此仓库时,他们都将立即拥有一个针对项目完全配置的编辑器。
近年来,我们都熟悉了 基础设施即代码。使用开发容器允许你将 开发者环境作为代码进行配置。这样做使得接纳新团队成员变得极其容易,同时也意味着“好吧,在我的机器上它运行正常”的时代的结束。使用这种技术意味着无论物理硬件、操作系统选择,还是 .NET 或 Node 的具体版本,每个人都在一个完全一致的平台上进行工作。
对于开发 SaaS 应用程序来说,这是一个巨大的胜利,因为新团队成员从零开始的过程既复杂又艰难。
启动环境
这就是配置的结束。我希望我们现在都在一个一致且可预测的平台上进行工作,并且不会在跟随本书中的示例时遇到任何麻烦!
要在 Docker 中启动环境,按 F1 打开命令菜单,并搜索 Remote-Containers: Rebuild and Reopen in Container。
你将在 图 2.7 中看到选项。选择第一个并按 Enter 继续操作。

图 2.7 – 在容器中重建和重新打开
这将关闭当前的 VSCode 实例,并重新打开一个新实例,该实例在之前配置的 dev-env 容器中运行。请注意,第一次这样做可能需要一些时间,但在后续使用中会更快。
你会注意到一些差异!首先,看看左下角,你会看到你正在运行一个名为 SaaS Book 的开发容器:
![]()
如果你点击查看已安装的扩展,你将看到一个单独的面板,显示 VSCode 此实例中安装的扩展与 devcontainers.json 文件中指定的扩展匹配。随着本书的进行,项目逐渐成形,这个列表将显著增长。
图 2.8 展示了一些在容器中安装的扩展:

图 2.8 – 已安装的扩展
如果你打开一个终端,你也会注意到这是一个 Linux 环境。你可以通过运行以下代码来验证这一点:
cat /etc/os-release
你将看到以下输出:

图 2.9 – 输出
这表明我们处于 Debian 环境。请注意,我们正在一个名为 workspace 的文件夹中工作,这是之前在 devcontainer.json 文件中配置的。这是可配置的,根据您的偏好。
为了进一步确信这确实是之前配置的 dev-env 容器,你可以在 VSCode 的终端中再次检查 .NET 和 Entity Framework 的版本:
dotnet –-version
dotnet-ef --version
你将看到以下输出:

图 2.10 – 终端输出
这就是目前所需的配置环境。让我们回顾一下:
-
我们有一个安装了 .NET SDK 的容器,准备好用于构建任何类型的 .NET 应用程序。
-
同一个容器安装了 Entity Framework 命令行工具,这样我们就可以使用 Entity Framework 来构建和与数据库交互。你可能会记得我们在 Dockerfile 中安装了 Entity Framework。
-
我们有一个运行 SQL Server 的独立容器,它托管了我们可以与之交互的数据库。值得注意的是,我们并没有在主机机器上安装 SQL Server 本身。我们通过 Docker 容器访问数据库。你将在 第三章 和 第四章 中看到更多关于这一点的内容。
这现在是一个可以用来构建 SaaS 应用程序的开发者机器的基础,我们没有在您正在工作的机器上直接安装任何这些工具就实现了这一点——所有这些都打包在 Docker 容器中。这个配置作为代码存在,因此它随着仓库移动。任何克隆此仓库的本地开发者都将立即获得所有项目特定的工具和配置。
随着我们在这个项目中的进展,这个设置将变得更加复杂。随着项目的增长,它也会增长。但就目前而言,这已经足够了,所以我们将在这里停止,并开始拼凑一个非常基础的应用程序。
配置 VSCode
在这个早期阶段,由于我们没有真正有应用程序特定的代码,所以不需要很多配置。然而,我们将在这里打下基础。打开一个终端,并输入以下内容:
mkdir .vscode; \
cd .vscode; \
touch launch.json; \
touch settings.json; \
touch tasks.json; \
cd ..;
将以下内容添加到 settings.json 中:
{
"thunder-client.saveToWorkspace": true,
"thunder-client.workspaceRelativePath": ".thunder-client"
}
前面的配置是一些我们将用于 第五章 的 HTTP 测试工具的配置。目前我们不需要在 launch.json 或 tasks.json 中放入任何内容。
这应该是目前所需的全部配置,因此我们可以继续前进。
退出环境
从开发容器环境退出并返回到主机非常简单。只需再次按 F1 并搜索“在本地重新打开文件夹”:

图 2.11 – 返回本地工作区
这将迅速将你带回到主机环境。你将不再看到左下角的指示器,表明你处于开发容器中,终端将再次直接连接到主机机器。
我们在本节中取得了许多成果,并为自己搭建了一个开始使用 Docker 化环境的平台。这可能会感觉比直接在本地机器上编写代码要麻烦一些,但我希望随着我们一起阅读这本书,你将开始看到在项目一开始就花费时间搭建这个环境的价值!
在下一节中,我们将真正开始构建应用程序,并展示前面技术的能力。
构建示例应用程序
在本节中,我们将使用刚刚安装的工具来创建一个 SaaS 应用程序的基础框架。目前,这仅仅是一个可以后续章节中更加完善的基础应用程序。然而,这将让我们熟悉我们将要使用到的所有不同工具。
在我们阅读本书的过程中,我们将构建的示例应用程序是一个跟踪习惯的应用程序 – 这可能是一天中你可能想尝试做的事情,比如学习一门新语言,或者为了跟上出版截止日期而写几页书!我希望这将是你在完成本书后可能能够使用到的东西!
数据库技术
我们将从数据库开始。我们使用 SQL Server 作为数据库平台 – 这是一本基于微软技术的书,所以我们将尽可能坚持使用它们!然而,其他数据库平台也是可用的,并且可以轻松使用。上面提供的基于 Docker 的设置使得实验其他数据库平台变得非常容易。你可以用 Postgres 容器替换 SQL Server 容器,看看一切是否仍然正常工作!
数据库和数据库平台在让开发者加入项目并快速熟悉项目时常常是痛点之一。这在 SaaS 项目中尤为明显,而且使用上述 Docker 化解决方案只能部分缓解。
现在,使用对象关系映射器(ORM)来管理代码与数据库之间的交互非常普遍,我将在本书中遵循这一模式。最初,我将使用 Entity Framework 进行所有交互,但请注意,当我们讨论性能和扩展时,我会涉及到其他在数据库性能至关重要的时刻可以使用的技巧。
我将利用 Entity Framework 的“代码优先”方法来定义数据库,并用一些初始启动数据填充它,同时我会利用迁移来保持数据库的更新。这在后续章节中会非常有用,当我们讨论使用数据库进行测试时,以及当我们查看 CI/CD 和如何更新生产数据库时。
创建数据库
确保你处于开发容器中(检查 VSCode 的左下角)并打开一个新的终端。使用终端创建一个新的.NET 类库,名为GoodHabits.Database,代码如下:
dotnet new classlib --name GoodHabits.Database;
你应该在文件资源管理器中看到文件夹出现,并在终端中看到以下输出:

图 2.12 – 创建数据库
在我们能够使用此项目与数据库交互之前,我们需要添加一些 NuGet 包。所以,再次在终端中输入以下内容:
cd GoodHabits.Database; \
dotnet add package Microsoft.EntityFrameworkCore; \
dotnet add package Microsoft.EntityFrameworkCore.Design; \
dotnet add package Microsoft.EntityFrameworkCore.Analyzers; \
dotnet add package Microsoft.EntityFrameworkCore.Relational; \
dotnet add package Microsoft.EntityFrameworkCore.SqlServer; \
dotnet add package Microsoft.EntityFrameworkCore.Tools; \
touch GoodHabitsDbContext.cs; \
rm Class1.cs; \
touch SeedData.cs; \
mkdir Entities; \
cd Entities; \
touch Habit.cs; \
cd ..;
在前面的步骤中,我们已经指导.NET CLI 添加了所有必要的 NuGet 包,以方便与数据库交互。我们还添加了GoodHabitsDbContext类、SeedData类和Habit类。现在,我们将在这三个文件中添加一些基本的设置,这将为我们后续章节的工作提供一个基础。
在Habits.cs文件中输入以下代码:
namespace GoodHabits.Database.Entities;
public class Habit
{
public int Id { get; set; }
public string Name { get; set; } = default!;
public string Description { get; set; } = default!;
}
上面的代码是一个非常简单的实体类,代表一个用户可能希望嵌入其日常生活中的习惯。
接下来,通过在SeedData.cs文件中添加以下代码添加一些模拟数据:
using GoodHabits.Database.Entities;
using Microsoft.EntityFrameworkCore;
public static class SeedData
{
public static void Seed(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Habit>().HasData(
new Habit { Id = 100, Name = "Learn French",
Description = "Become a francophone" },
new Habit { Id = 101, Name = "Run a marathon",
Description = "Get really fit" },
new Habit { Id = 102, Name = "Write every day",
Description = "Finish your book project" }
);
}
}
现在通过在GoodHabitsDbContext.cs文件中输入以下代码创建一个DbContext:
using GoodHabits.Database.Entities;
using Microsoft.EntityFrameworkCore;
namespace GoodHabits.Database;
public class GoodHabitsDbContext : DbContext
{
public DbSet<Habit>? Habits { get; set; }
protected override void
OnConfiguring(DbContextOptionsBuilder options)
=> options.UseSqlServer("Server=
sqlserver;Database=GoodHabitsDatabase;User
Id=sa;Password=Password1 ;Integrated
Security=false;TrustServerCertificate=true;");
protected override void OnModelCreating(ModelBuilder
modelBuilder) => SeedData.Seed(modelBuilder);
}
这做了几件事情。首先,我们定义了一个DbSet。这映射到数据库中的一个表。
接下来,我们将硬编码数据库连接字符串。当然,硬编码连接字符串是不良的做法,将密码以明文形式放在那里更是双倍的不良做法。我们将在第三章中纠正这些错误,但这对证明我们已经连接到数据库是足够的。
在完成此设置后,我们可以测试一下,看看我们是否可以将这些信息迁移到我们在第二个 Docker 容器中运行的 SQL Server 数据库中。
要做到这一点,让我们首先使用 Entity Framework 创建一个初始迁移。在终端中输入以下内容以生成初始迁移:
dotnet-ef migrations add InitialSetup;
你会在文件资源管理器中看到一个名为Migrations的文件夹,其中InitialSetup迁移被建模为一个类。目前你不必过于担心这个,但花时间看看这个类是值得的。
然后输入以下内容以将迁移部署到 SQL Server 数据库:
dotnet-ef database update;
这将迁移发送到数据库。
到此为止。我们已经使用 Entity Framework 在“代码优先”模式下配置了一个基本的数据库,并将第一个迁移发送到数据库。
我们如何知道这已经成功了?
理解命令已成功执行是一回事,但眼见为实,我们需要深入数据库以确保所有这一切确实按预期工作。
您会注意到,当我们定义要在 dev 容器中安装的扩展时,我们指定了以下扩展应该包含在内:
"ms-mssql.mssql",
这是一个来自微软的 VSCode 扩展,允许我们从 VSCode 直接查询 SQL 服务器数据库。点击扩展,我们将添加一个新的连接,并添加以下信息:
| 参数 | 值 | 备注 |
|---|---|---|
| 主机名 | sqlerver |
这是配置为运行 SQL Server 2022 实例的 Docker 容器的名称 |
| 要连接的数据库 | GoodHabitsDatabase |
这是在DbContext类的连接字符串中定义的 |
| 认证类型 | SQL 登录 |
|
| 用户名 | sa |
|
| 密码 | Password1 |
在sqlserver.env中定义 |
| 保存密码 | 是 |
|
| 显示名称 | GoodHabits |
您可能需要确认一个安全弹出窗口。
正确输入以上信息后,你现在应该可以看到数据库的视图,如下所示:

图 2.13 – 浏览数据库
您会注意到,我们已经在DbContext文件中将Habits表定义为DbSet,并且该表已成功迁移到 SQL Server 数据库。您可以在名为dbo.Habits的表上右键单击,然后点击选择到 1000以查看内容。

图 2.14 – 数据库中存储的数据
再次,您会看到我们在DbContext文件中添加的信息在数据库中是存在且正确的。
API 技术
接下来,我们将转向 API。API 的目的是通过用户方便地存储和检索数据库中的信息,以及进行任何必要的数据处理。
可用于构建 API 的技术有很多种。本书专注于微软技术,因此我们将使用.NET Web API 框架和 C#语言。然而,所描述的工具和技术可以很容易地适应以利用不同的技术。
此外,关于如何最佳地构建 API 有许多不同的想法,没有一种“一刀切”的方法可以适用于所有项目。我决定,我将在这本书的示例中使用 RESTful 范式,但再次强调,如果您的项目使用其他结构,如 GraphQL,所提出的概念和想法应该可以很好地复制。
SaaS 应用程序的本质是,在项目启动时需要做出大量的选择。即使项目是一个书中的演示应用程序,这也依然成立。
创建 HabitService API
确保您处于开发容器环境中,并且位于项目的根目录(workspace 文件夹),然后创建一个新的 webapi 项目,如下所示:
dotnet new webapi --name GoodHabits.HabitService; \
cd GoodHabits.HabitService; \
dotnet add reference ../GoodHabits.Database/GoodHabits.Database.csproj; \
dotnet add package Microsoft.EntityFrameworkCore.Design; \
cd ..;
.NET CLI 在设置 API 方面做了很多工作。我们需要对 launchSettings.json 文件进行更改。打开 Properties 文件夹,并用以下内容替换默认的启动设置:
{
"$schema":
"https://json.schemastore.org/launchsettings.json",
"profiles": {
"HabitService": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5100",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
最重要的是要注意,我们将运行 HabitService 在端口 5100 上。当我们开始查看微服务时,这一点很重要,见 第六章。
默认情况下,Web API 包含了天气 API,您可以查看它以了解这些端点是如何配置的。我们可以通过在终端中输入以下内容来测试:
dotnet run
这将启动在 launchSettings.json 中指定的端口上运行的应用程序 – 在这种情况下是端口 5100。

图 2.15 – 表示成功的控制台输出
您可以通过在浏览器中访问以下 URL 来检查应用程序是否正在运行(记得检查端口号!):127.0.0.1:5100/swagger/index.html
注意,您可能会从浏览器收到一些 HTTPS 警告,如下所示:

图 2.16 – GoodHabits API
用户界面技术
最后,我们需要一个用户界面来显示信息。
就像我对数据库和 API 所指出的那样,有许多不同的 UI 技术,本书中您将学习的底层工具和技术可以应用于其中任何一个。通常,在特定情况下使用最佳技术的是您最熟悉的技术。
这是一本以 Microsoft 为中心的书籍,面向现有的 .NET 开发者,因此我将使用 Blazor 来提供示例。如果您更喜欢 Angular、React、Vue 或其他数百万个 UI 框架中的任何一个,请不要气馁。实际上,将这些示例修改为与您选择的 UI 一起使用将是一个很好的练习,可以帮助您进一步理解本书中将要介绍的概念。
创建 UI
使用 Microsoft 提供的 CLI 工具创建简单的 Blazor UI 是直接的。在终端中输入以下命令以创建 UI:
dotnet new blazorwasm -o GoodHabits.Client;
这遵循了我们创建 HabitServer 项目的相同模式。同样,像 HabitServer 项目一样,我们需要修改 launchSettings.json 中的启动配置。再次,查看 Properties 文件夹,并用以下内容覆盖内容:
{
"profiles": {
"GoodHabitsClient": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:
{url.port}/_framework/debug/ws-
proxy?browser={browserInspectUri}",
"applicationUrl": "http://localhost:5900",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
再次注意客户端正在运行的端口。我们将使用 5900 作为客户端。
配置完成后,您现在可以通过在控制台中输入 dotnet run 来启动客户端。
再次,你会看到 Client 应用正在 launchSettings.json 中指定的端口上运行,应该是端口 5900:

图 2.17 – 表示成功的控制台输出
再次,正如 API 一样,你可以在浏览器中点击此链接查看运行的 Blazor 应用:

图 2.18 – Hello, world!
启动应用程序
目前,我们只有两个项目要运行,即 HabitService API 和 Blazor 客户端。因此,我们可以通过两次输入 dotnet run 来相对容易地启动整个项目。随着我们对这个应用程序的开发,它将变得越来越复杂,以这种方式运行将变得更加困难。因此,我们将创建构建和启动配置来告诉 VSCode 如何启动应用程序。
我们已经在 .vscode 文件夹中创建了这些配置文件。
首先,打开 tasks.json,并复制以下内容:
{
"version": "2.0.0",
"tasks": [
{
"label": "build-client",
"type": "shell",
"command": "dotnet",
"args": [
"build",
"${workspaceFolder}/GoodHabits.Client/
GoodHabits.Client.csproj"
],
"group": {
"kind": "build",
"isDefault": true
}
},
{
"label": "build-habit-service",
"type": "shell",
"command": "dotnet",
"args": [
"build",
"${workspaceFolder}/GoodHabits.HabitService
/GoodHabits.HabitService.csproj"
],
"group": {
"kind": "build",
"isDefault": true
}
},
]
}
你可以在上面的 JSON 中看到定义了两个任务来构建客户端和习惯服务。
接下来,通过添加以下 JSON 配置来修改 launch.json:
{
"version": "0.2.0",
"configurations": [
{
"name": "RunClient",
"type": "blazorwasm",
"request": "launch",
"preLaunchTask": "build-client",
"cwd": "${workspaceFolder}/GoodHabits.Client",
"url": "https://localhost:5900"
},
{
"name": "RunHabitService",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build-habit-service",
"program": "${workspaceFolder}/
GoodHabits.HabitService/bin/Debug/net7.0/
GoodHabits.HabitService.dll",
"args": [],
"cwd":
"${workspaceFolder}/GoodHabits.HabitService",
"stopAtEntry": false,
"console": "integratedTerminal"
},
],
"compounds": [
{
"name": "Run All",
"configurations": [
"RunHabitService",
"RunClient"
]
}
]
}
再次,你可以看到添加了两个配置来运行单个项目。你还可以看到靠近底部有一个设置为运行所有项目的 compounds launch 命令。
你可以通过按 Ctrl + Shift + D 进入 运行和调试 菜单,从下拉菜单中选择 运行 所有,然后按播放按钮来测试它。
你会看到这同时启动了 API 和客户端。当项目数量开始增加时,这将非常方便!
注意,你也可以按 F5 来启动应用程序。
添加解决方案文件
在我们真正开始构建应用程序之前,还有一个小步骤要做,那就是添加解决方案文件。这并不是严格必要的,但在处理 .NET 项目时通常这样做,这将使我们能够通过单个命令轻松构建、清理和测试项目。
要添加解决方案文件,只需导航到项目根目录,并运行以下命令:
dotnet new sln --name GoodHabits; \
dotnet sln add ./GoodHabits.Client/GoodHabits.Client.csproj; \
dotnet sln add ./GoodHabits.HabitService/GoodHabits.HabitService.csproj; \
dotnet sln add ./GoodHabits.Database/GoodHabits.Database.csproj;
这只是添加了一个解决方案文件,并引用了我们创建的三个项目。
那就是最后需要完成的一步设置工作——现在我们可以继续构建应用程序。
我们接下来该做什么?
我们在本章中做了很多工作,但我们还没有真正开始构建应用程序。相反,我们专注于选择我们将使用的工具,并围绕这些工具构建开发环境。我们现在有一个 SaaS 应用的轮廓,我们可以随着本书的进展继续与之合作。
在项目开始时花时间选择正确的工具是任何 SaaS 项目的重要步骤,这个步骤不应被跳过。
在开发任何应用程序时取得成功需要考虑将要使用的技术和框架,以及将要使用的工具。在本章中,我们已经为探索构建 SaaS 应用程序所需的概念做好了充分的准备,以便我们可以亲自动手尝试。
以下章节将开始介绍更多与 SaaS 相关的特定考虑因素,我们将使用这个轮廓应用程序来演示它们。
摘要
在本章中,我们简要地介绍了很多不同的工具、主题和技术。这是开发 SaaS 应用程序的本质——请不要感到不知所措!为了开始,我们安装了我们需要工作的工具,即 Docker Desktop 和 Visual Studio Code。这对于 SaaS 项目来说是一套相当轻量级的工具。但正如你所看到的,我们使用了 Docker 来封装开发环境。我们学习了 dev containers,以及它们如何显著简化复杂项目(如 SaaS 应用程序)的设置,然后我们配置了一个 dev container 并学习了如何在那个环境中工作。
在设置好环境的核心之后,我们使用 Entity Framework 创建了一个数据库,并用一些数据填充它,然后通过 API 使数据可用,最后在 Blazor UI 上展示数据!
通过以上所有内容,我们已经配置了构建功能齐全的 SaaS 应用程序所需的全部单个部分。继续阅读,我们将这样做!
在下一章中,你将学习构建 SaaS 应用程序的核心部分,即多租户。我们将介绍这是什么以及为什么它如此重要,并开始思考如何在我们的示例应用程序中实现它。
进一步阅读
-
Docker 中的 Entity Framework 开发环境:
itnext.io/database-development-in-docker-with-entity-framework-core-95772714626f -
.NET 在 Docker 中使用 Dev Containers 进行开发:
itnext.io/net-development-in-docker-6509d8a5077b -
Blazor 教程 - 构建你的第一个 Blazor 应用程序:
dotnet.microsoft.com/en-us/learn/aspnet/blazor-tutorial/intro -
教程:使用 ASP.NET Core 创建 Web API:
learn.microsoft.com/en-us/aspnet/core/tutorials/first-web-api?view=aspnetcore-7.0&tabs=visual-studio
问题
-
使用 dev containers 的优势是什么?
-
我们是如何能够在不安装 SQL Server 的情况下使用 SQL Server 的?
-
常与 RESTful API 相关联的 HTTP 动词有哪些?
-
使用 Entity Framework 的好处是什么?
在本章中我们涵盖了大量的内容!如果目前对上述问题的答案不是 100%清晰,请不要担心——我们将在接下来的章节中详细展开所有这些内容。
第二部分:构建后端
本节涵盖了所有与后端相关的内容,从数据库开始,逐步扩展到 API 层。本节还介绍了多租户和微服务这两个主题,它们对于开发 SaaS 应用都非常重要。
本节包含以下章节:
-
第三章, 什么是多租户,为什么它在 SaaS 应用中很重要?
-
第四章, 构建数据库和为数据丰富应用规划
-
第五章, 构建 RESTful API
-
第六章, SaaS 应用的微服务
第三章:什么是多租户,为什么它在 SaaS 应用程序中很重要?
多租户已成为现代软件即服务(SaaS)应用程序中的关键架构模式,使提供商能够从他们软件的单个实例为多个客户(租户)提供服务。
本章将深入探讨构建 SaaS 应用程序的这部分非常重要的内容——称为多租户——其中多个租户可以使用部署应用程序的同一实例,同时仍然保持他们的数据私密和隔离。
在多租户系统中,SaaS 应用程序的多个客户可以使用应用程序的同一实例,并且也可以选择使用相同的数据库,但他们的数据是隔离的,这样其他租户就无法看到数据——除非它们被明确共享。这显然引发了许多关于安全和数据隐私的问题。对于正在构建多租户 SaaS 应用程序的公司来说,确保任何客户只能看到自己的数据,永远看不到任何其他人的数据,这一点至关重要!
可以利用许多策略、模式、实用工具和技术来确保单个租户的数据保持隔离,但第一道防线始终是对底层概念有良好的技术理解。当应用程序正在构建时,开发团队理解这一点尤为重要。对于任何希望扩展的 SaaS 应用程序(几乎所有应用程序都会这样做)来说,多租户是必不可少的!
SaaS 多租户应用程序中的数据隐私和安全至关重要。很难想象一个多租户 SaaS 应用程序的实例,它不需要至少保留一些租户和用户的数据保持私密。因此,在构建 SaaS 应用程序时,强大的数据安全和数据隔离是关键考虑因素。
在开始开发新应用程序的周期时,在项目一开始就将强大的安全性构建到应用程序中要比在以后追加上去容易得多!从一开始就正确地实现安全和隔离也要安全得多——一旦不可避免的应用程序蔓延和功能蔓延开始,就相当难以说服自己一切真的像应该的那样被严格锁定。
我们在本书中较早地介绍这个主题,以便在我们构建演示应用程序时,您将对多租户及其对未来设计选择的影响有一个扎实的理解。我强烈建议您在构建 SaaS 应用程序时采取相同的方法,在项目开始时花时间制定多租户策略和将出现的安全考虑。
在本章中,我们将涵盖以下主要内容:
-
理解什么是多租户
-
考虑多租户应用程序的数据存储选项
-
通过应用层理解设计考虑因素
-
讨论安全考虑因素
到本章结束时,您将很好地理解多租户、多租户应用以及在工作于 SaaS 和特别是多租户软件应用时可能出现的具体安全考虑因素。
技术要求
本章的所有代码都可以在github.com/PacktPublishing/Building-Modern-SaaS-Applications-with-C-and-.NET/tree/main/Chapter-3找到。
什么是多租户?
多租户是一种在 SaaS 应用中常用的软件架构模式,其中单个应用实例为多个客户群体或组织(称为租户)提供服务。每个租户共享相同的基础基础设施和资源,如服务器、数据库和应用逻辑,但保持其自己的独立数据和配置。这种方法可以实现资源使用优化、维护流程简化以及运营成本降低。在多租户架构中的主要挑战是确保每个租户的数据隔离、安全和性能,同时提供定制和可扩展性。本质上,多租户使 SaaS 提供商能够通过共享应用实例向多样化的客户提供经济高效、安全且可定制的解决方案。
区分客户、租户和用户
当谈论多租户时,区分相关但不同的概念是否重要:
-
客户: 客户指的是购买或订阅软件产品或服务的组织或个人。在 SaaS 应用的情况下,客户可能被表示为一个或多个租户,每个客户群体都可以访问共享应用实例中的自己的独立环境。术语“客户”通常强调使用软件产品或服务的商业关系和财务方面。
-
租户: 租户代表使用多租户架构中共享的 SaaS 应用实例的独立组织、公司或群体。每个租户都有其自己的独立数据、配置和定制,同时与其他租户共享相同的软件基础设施和资源。多租户系统的一个关键方面是每个租户都可以有多个与其关联的用户,从而在共享的应用实例中实现个人访问和个性化体验。
-
用户:用户是与软件系统或应用程序交互的个体人员,通常具有独特的登录凭证和个性化设置。用户属于特定的租户、组织或组,他们的操作和系统内的访问可以通过基于角色的权限或访问控制进行管理。用户代表实际使用软件的个人,在应用程序内执行各种任务和活动。
让我们考虑一个例子,其中两家大型公司使用一个 SaaS 应用程序。这些公司是客户,为了使这个例子简单,每个公司将在应用程序中有一个单租户。每个公司可能有数百或数千名员工使用该系统。对于这两家公司来说,通常都会希望以某种方式为他们的员工定制应用程序。例如,他们可能希望在用户登录时显示他们的标志。这可以通过在应用程序中设置两个 租户 来实现,每个租户都配置为向属于该公司的用户显示正确的标志。
将有两个级别的数据隔离。单个租户配置(例如标志)将是分开的,并且在该租户内操作的用户必须始终看到正确的标志。将有一个进一步的隔离级别,以保持每个个别用户的隐私数据。
在租户内具有高级权限的用户(管理员用户)可能能够修改同一租户内其他用户的详细信息,但永远不能是任何其他租户。
在我们继续本章内容时,这一点非常重要——在继续之前,请确保您理解用户和租户之间的区别:
-
一个客户可以在一个应用程序中有一个或多个租户,通常是一个,这意味着术语“客户”和“租户”可以互换使用。
-
一个租户下可以有一个或多个用户,通常不止一个,这意味着这两个术语不应互换使用。
多租户的替代方案有哪些?
虽然 SaaS 应用程序在很大程度上等同于多租户应用程序,但从技术上讲,将 SaaS 应用程序作为单租户应用程序提供是可能的。
如果遵循单租户方法,那么每次有新的租户加入时,都会为该特定租户部署一个全新的应用程序堆栈和新的数据库。可能会有一些非常具体且有限的共享服务,例如一个共享登录页面,该页面将客户重定向到他们自己的应用程序实例,但通常,这将是基于每个客户的完全独特和隔离的软件部署。
单租户应用程序通常被认为是最高安全性的方法,并且可以被认为是向单个客户提供应用程序的最可靠方法。然而,为每个客户这样做成本非常高,并且快速扩展这种架构的成本很快就会变得难以管理。
维护、支持、升级和定制此类应用程序所需的资源非常高,这意味着最终用户的成本也很高,通常会将您的客户群限制在企业用户。如果您的最终目标是吸引非企业客户,这种单租户方法很可能不会成功。
这也在一定程度上拉伸了 SaaS 的定义。如果每个客户只是获得一个独特的软件安装,尽管是在云中,那么它就与传统软件交付方式——带有唯一密钥的 CD——非常接近了!
在 SaaS 应用程序中,单租户模式实际上仅适用于非常小的一部分用例。对于本书,我们将不再进一步考虑这一点,除了提到在技术上可以构建一个不使用多租户架构的 SaaS 应用程序!
多租户的优缺点
使用多租户架构的一些优点如下:
-
成本:从资源分配(应用服务器、数据库等)的角度来看,多租户通常是一种非常经济高效的方式来交付应用程序。在考虑持续支持和维护成本时,多租户也往往非常经济高效。一旦应用程序上线,每个额外租户和该租户内每个额外用户的边际成本通常不会对总成本产生太大影响。
-
分层定价:可以为客户提供一系列定价选项,以适应他们及其组织的需求。随着新租户的加入和来自新租户组织的新用户的引入,定价可以线性扩展。
-
易于更新:由于应用程序只有一个实例被多个租户访问,因此只需进行一次更新,就可以让所有租户的用户都升级到最新和最好的版本。请注意,如果更复杂的话,也可以在单租户模式下使用持续部署(CD)管道来配置这一点。
-
可扩展性:使用多租户架构的 SaaS 应用程序通常会随着租户和用户数量的增加而非常容易扩展。假设使用云提供商,这可以在开发团队方面几乎毫不费力地发生。可以将云服务配置为,随着用户基础的不断增长,系统面临更多需求时,自动部署更多资源。
然而,并非全是优点——也有一些缺点也应当考虑:
-
复杂性增加:从软件架构的角度来看,多租户应用程序几乎可以说是构建起来更具挑战性的。在整个应用程序堆栈中存在额外的复杂性,从数据库需要通过租户进行某种形式的分割开始,一直到用户界面,每个用户都必须安全地认证和授权,才能访问系统的特定部分。
-
增加的安全要求:多个租户共享单个应用程序实例的事实需要采取更加彻底的用户安全方法。如果他们的用户私数据通过应用程序泄露给另一个用户,对业务来说通常是一个绝对的灾难场景,如果那个用户属于不同的租户组织,那么情况更是如此。
-
停机是灾难:如果多租户 SaaS 系统因任何原因而停机,那么通常,每个客户都将无法访问应用程序。这显然使得减少停机时间变得绝对关键。
-
嘈杂的邻居:鉴于每个租户共享一个应用程序,因此他们也在共享资源,例如集群的计算时间或应用程序部署的服务器。一个特别计算密集型的用户可能会对系统中的其他每个用户产生连锁反应。
从前面的列表中可以观察到,提供的优势是显著的,如果使用单租户架构,这些优势是无法真正规避的。然而,多租户系统的缺点通常可以通过在系统设计初期投入更多时间来缓解。当然,这样做是有成本的,这在选择应用程序架构和做出系统设计决策时必须仔细权衡。
然而,一般来说,随着租户数量和用户基础的开始增长,SaaS/多租户应用程序的力量得到体现,前期的高成本将会得到多倍的回报!
我希望前面的讨论已经说服了你多租户架构的好处,并鼓励你在项目一开始就考虑这一点。我们现在可以继续讨论多租户设计的具体细节,从最重要的——数据存储!
多租户应用程序的数据存储选项
在任何应用程序中,数据库通常是构建其余应用程序的基础。在选择和设计数据库时所做的决策将对数据访问/API 层产生重大影响,并可能对用户界面产生一定的影响。
此外,应用程序中存储的数据将代表客户的业务信息。通常,这些信息对他们来说极其宝贵,并且通过将其托付给你——作为一个在 SaaS 应用程序上工作的开发者——他们向你展示了显著的信任,相信你能安全地保管这个宝贵的资产。支撑 SaaS 应用程序的数据库应该像银行保险库一样,让客户愿意将他们的数据存入其中!
因此,在数据库设计上做出正确的选择对于整个应用程序的开发至关重要,同样对于应用程序的租户和单个用户也是如此。
虽然本节主要关注数据库,但其作为整个应用程序基础的作用将需要讨论所做出的选择将如何影响其他层和应用程序的其他部分。
关键考虑因素
数据存储采用的方法非常重要,在开始构建之前,有几个关键领域需要考虑。
设计复杂性
通常,在开始构建新的应用程序时,关注设计上的简洁性是一个好主意,只有在需要时才增加复杂性。在应用程序中内置多租户确实会增加应用程序的复杂性,因此考虑复杂性的水平并相应地调整解决方案非常重要。
考虑你预计会有多少租户,并选择一个与之匹配的解决方案。同时也要考虑数据隔离的重要性,并选择一个与之匹配的解决方案。
你还应该记住,一些客户——尤其是大型企业客户——可能期望一定程度的定制化。允许每个客户的定制可能会开始指数级地增加设计的复杂性。虽然通常更喜欢避免任何形式的定制,但这可能并不总是可能的。如果觉得这将是一个要求,那么在设计阶段应该考虑到这种可能性。
支持复杂性
当然,开发应用程序的过程对公司来说成本是高昂的,但考虑应用程序实际运行的成本同样重要。这是应用程序生命周期中预期产生收入的阶段,如果存在巨大的持续支持和维护成本,这是不可能实现的。
持续维护的一个非常重要的方面是监控租户的使用情况。“80-20”原则很可能会适用,你需要知道哪些租户是最有利可图的……以及哪些是最有问题的!
你需要考虑,一些租户可能对可用性的要求比其他租户更高。你能否支持一个要求 99.99%正常运行时间的租户?并且你是否理解提供这一水平服务所涉及的成本和技术权衡?
如果在项目生命周期的早期没有考虑,支持和维护可能会迅速变成一个技术和财务上的噩梦。
规模
没有任何应用程序是带着十亿用户启动的——它们都是从一个人开始的!
在规划 SaaS 应用程序时,你需要至少对短期、中期和长期预期运营的规模有一个大致的了解。有了这个理解,你可以做出适合从启动日到平台成熟的个人用户和租户的明智决策。
在第一天就浪费精力和资源去为十亿用户构建是没有意义的。同样,如果没有适当的计划来服务这么多的用户,那么达到十亿用户的目标也是不可能的。
性能
在多租户系统中,许多用户将共享资源——因此,他们将竞争访问这些资源。这包括属于不同租户的用户,他们通常为不同的组织工作。
你需要考虑典型租户和典型用户将如何使用系统。例如,如果你正在构建一个企业系统并专注于单一时区,你应该预计几乎所有的使用都将在工作时间进行。你需要调整资源以满足这种需求,尽管它们在工作时间之外将是闲置的。
你可能会遇到类似的问题,比如一个在晚上使用量激增的流媒体服务。
注意,这种场景将为在多个时区提供服务的公司提供相当大的优势,其中使用量激增可以被平缓。
在多租户系统中,用户为了争夺共享资源而被称为“嘈杂邻居”问题。这种现象是指一个租户或用户表现出特别重的资源使用,通过这样做,降低了其他用户的系统性能。在构建多租户系统时,这个问题在一定程度上是不可避免的,但有一些方法可以减轻这个问题,例如以下方法:
-
实施节流措施以防止单个租户或用户消耗不成比例的资源。
-
监控租户和用户如何与系统交互,并针对更资源密集型的示例进行调整。例如,它们可以被移动到单独的集群。
-
购买额外的云资源。这是一种相当直接的工具,但它是有效的。
隔离
正如我们之前讨论的,确保与一个租户相关的数据对其他租户以及这些租户中的每个用户都是不可见的,这一点至关重要。单个租户的数据必须被隔离。有几种方法可以实现这一点:
-
可以按一个容器一个租户的方式使用容器。这可以是一个非常有效的系统租户隔离方法,但如果应用程序变成下一个 Facebook,那么就会存在扩展问题。
-
在同一数据库中可以为每个租户使用单独的表。这提供了很好的保证,确保数据不会“泄露”,但再次强调,这不会高效地扩展到数百个租户和数百万用户。
-
基于租户 ID 的隔离,其中数据库中的数据都在一个表中,有一个
tenant_id列。这扩展得非常好,但可能被认为比之前的选择不太安全。
对于这个问题,没有“一刀切”的方法。所需的隔离级别将取决于客户类型和存储的数据类型。这应该在项目开始时仔细考虑,因为项目生命周期后期更改数据隔离的方法可能极具挑战性。
成本
当然,我们希望拥有最安全、性能最佳且易于维护的应用程序,但作为应用程序开发者,我们必须始终在公司构建应用程序所面临的经济现实范围内运作。
即使是像数据安全这样重要的问题,也总是需要做出成本权衡。
如果在项目开始时就理解这些,那么以后就不会有令人不快的惊喜了!
数据库是整个应用程序的基础,关于上述考虑的正确选择至关重要。在这个阶段做出的选择将显著影响应用程序的其余部分。我们将在下一节中考虑这一点。
通过应用程序层的设计考虑
解决多租户问题的方法可能是项目早期需要做出的最重要的决策之一。这可能是项目生命周期后期进行更改时最昂贵和最具技术挑战性的方面之一。针对多租户问题的解决方案规划和设计必须从非常早期阶段开始,并将影响应用程序的所有各个层次。
多租户的核心原因是为了隔离客户存储的数据,使得在另一个租户下用户无法看到这些数据。
关键的决策是要确定数据将被隔离在哪里,这主要影响数据库的设计。这个决策的驱动因素必须与系统的用户相关。为了做出这个决定,了解以下内容至关重要:
-
您的租户将是谁。
-
您的用户将如何使用系统。
-
数据泄露对您的客户可能造成的后果。
-
客户对风险与成本的容忍度。他们更倾向于更昂贵但更安全的解决方案,还是更便宜的解决方案?
-
数据泄露对业务可能造成的后果
社交媒体网站的用户似乎并不过于关心他们的隐私,因此可能会容忍对数据隔离不那么谨慎的方法。此外,社交媒体用户通常不愿意为系统支付任何费用,所以他们非常注重价格,根本不关心数据隐私。事实上,社交网络的业务模式通常是向出价最高者出售用户数据!
企业客户几乎永远不会容忍类似社交媒体的数据处理方法,他们总是更喜欢更安全的制度。一些企业客户甚至完全不会接受多租户系统。例如,军事或政府客户几乎肯定会认为风险太高,并坚持使用单租户解决方案。
如果一个应用程序的客户主要受安全性驱动,特别是严格的数据隔离和绝对保证不会出现任何疏漏,那么最好的解决方案就是不要设计多租户应用程序,因此也不构建 SaaS 应用程序!鉴于这本书是关于开发 SaaS 应用程序的,我们将完全排除这一客户类别。然而,了解这项技术的局限性是很重要的。一个主要受绝对数据安全驱动的客户群不太可能接受通过 SaaS 提供的应用程序。这些用户更有可能需要在其自己的网络内部署本地解决方案,并保留用户组织对数据的完全控制。
既然我们是为那些我们认为会购买 SaaS 应用程序的客户构建的,让我们考虑一下这些客户吧!
每个租户一个数据库
将一个租户的数据与另一个租户的数据完全隔离的最安全方式是将数据存储在完全独立的数据库中。这种方案在维护和更新多个数据库(每个客户一个)方面会产生相当大的开销。这种方法几乎可以保证不会发生数据泄露,但额外的开销是相当大的。使用这种方案的应用程序在扩展方面也会面临挑战,如果选择的数据库平台有按实例许可的费用,可能会导致成本螺旋上升。
规模化问题确实对系统设计可容纳的租户数量设定了一个硬性上限。如果每个租户组织都拥有独特且独立的数据库,微软是无法让他们的 DevOps 平台工作的——因为客户数量太多,这样做根本不可行。
除了安全性之外,这种方案的一个其他好处是,存储在单个数据库中的数据行数将低于共享数据库,因此在数据库层可能会有轻微的性能提升。请注意,这可能在应用层中被抵消。
这种方案只有在客户数量非常少且对安全性极为重视的情况下才会使用。在这种情况下,可能会有人提出构建单租户、本地系统的论点。
共享模式
这是最常见的解决方案,用于大多数日常使用中可能遇到的常用 SaaS 应用程序。甚至一些可能敏感的应用程序,如托管财务和税务信息的系统,也可能使用共享模式。
在这种制度下,使用单个数据库,其中包含来自多个租户的数据表。数据通过数据库中的某种形式的标识来保护,防止共享。
使用这种方案显著提高了最初使用 SaaS 范式所获得的益处。如今,有许多被广泛理解和使用的方案可以在这种方案下确保数据安全。也可能有人提出,只有一个数据库和一个模式需要管理,实际上使得系统更容易得到保护。
每个租户一个模式
这是在上述两种方案之间的折中方案。按照这种方法,使用单个数据库,但相同的模式被部署到数据库中多次,每次针对一个客户。每个模式都是隔离的,并且各个客户的模式之间没有连接或关系。
在某些方面,这是两全其美的方案,而在其他方面,则是最糟糕的。
管理众多模式仍然存在显著额外的开销,并且对可以扩展的个别租户数量仍然有一个硬性上限。尤其是一些对安全性极为重视的客户可能会因此感到不安,认为即使是共享数据库也是一种风险。
每个租户一个表
这与前面提到的“每个租户一个模式”方法非常相似。在这种范式下,系统中的每个租户都会在数据库中添加一个仅与他们相关的表。
之前方法的优势和劣势也适用于此处。
示例
为了说明这些观点,我将构建一个示例,演示前面提到的两种隔离方法。
我们将设想有四个GoodHabits应用程序的租户组织。这些租户如下:
-
AscendTech,一个非常重视安全性和希望拥有私有数据库的组织
-
Bluewave,也是一个重视安全性和希望拥有私有数据库的组织
-
CloudSphere,一个价格敏感的客户,选择更便宜的共享数据库方案
-
DataStream,也将使用共享数据库并与 CloudSphere 共享
通过这四个示例组织,我们可以展示多租户数据库是如何工作的。这个示例不会详尽无遗,但将提供一个坚实的基础理解。
在本节中,我们将基于我们在上一章中创建的GoodHabits数据库和 API 项目进行构建。
添加所需的包
我们只需要为这一章的HabitService项目添加一个 nuget 包。在终端中导航到项目文件夹,并输入以下内容:
dotnet add package Microsoft.AspNetCore.Mvc.NewtonsoftJson;
修改实体
数据库中的许多表将需要一个标识数据所有者(即租户)的列!为了便于此操作,我们可以先创建一个名为IHasTenant的接口。使用以下命令创建文件:
cd GoodHabits.Database; \
mkdir Interfaces; \
cd Interfaces; \
touch IHasTenant.cs; \
cd ..;
然后,将以下代码复制到文件中:
namespace GoodHabits.Database;
public interface IHasTenant
{ public string TenantName { get; set; } }
你会记得我们之前创建了一个名为Habit的实体类,该类定义了数据库的数据结构。这个实体将需要一个TenantName值,因此它应该实现IHasTenant接口。修改我们在第二章中创建的类,使其看起来像这样:
namespace GoodHabits.Database.Entities;
public class Habit : IHasTenant
{
public int Id { get; set; }
public string Name { get; set; } = default!;
public string Description { get; set; } = default!;
public string TenantName { get; set; } = default!;
}
创建租户配置
在实际系统中,租户的配置通常会存储在某个单独的数据库中——例如客户端门户应用程序等。然而,为了演示目的,我们只需使用appsettings.json文件。在Goodhabits.HabitService项目的appsettings.json文件中添加以下内容:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"TenantSettings": {
"DefaultConnectionString": "Data
Source=sqlserver;Initial
Catalog=GoodHabitsDatabase;
User Id=sa;Password=Password1;
MultipleActiveResultSets=True;
TrustServerCertificate=True;",
"Tenants": [
{
"TenantName": "AscendTech",
"ConnectionString": "Data Source=sqlserver;
Initial Catalog=AscendTechGoodHabitsDatabase;
User Id=sa;Password=Password1;
MultipleActiveResultSets=True;
TrustServerCertificate=True;"
},
{
"TenantName": "Bluewave",
"ConnectionString": "Data Source=sqlserver;
Initial Catalog=BluewaveGoodHabitsDatabase;
User Id=sa;Password=Password1;
MultipleActiveResultSets=True;
TrustServerCertificate=True;"
},
{
"TenantName": "CloudSphere"
},
{
"TenantName": "Datastream"
}
]
}
}
上述配置已定义了四个租户:
-
前两个,AscendTech 和 Bluewave,希望有完全隔离的数据库,并且已经指定了仅针对它们的唯一连接字符串
-
后两个,CloudSphere 和 Datastream,没有唯一的连接字符串,因此它们被认为是乐意使用共享数据库的
在GoodHabits.Database项目中,添加一个与之前添加到appsettings.json中的配置结构相匹配的类。该配置将在启动时加载到这个类中。将类命名为TenantSettings,然后粘贴以下内容:
namespace GoodHabits.Database;
public class TenantSettings
{
public string? DefaultConnectionString { get; set; }
public List<Tenant>? Tenants { get; set; }
}
public class Tenant
{
public string? TenantName { get; set; }
public string? ConnectionString { get; set; }
}
创建租户服务
接下来,我们将创建一个服务,它可以向可能感兴趣的应用程序的其他部分提供有关租户的详细信息。
从GoodHabits.Database项目开始,添加一个名为ITenantService的接口,然后添加以下代码:
namespace GoodHabits.Database;
public interface ITenantService
{
public string GetConnectionString();
public Tenant GetTenant();
}
接下来,我们需要实现这个服务。这是在 HabitService 项目中完成的,应该看起来像这样。请务必确认您是将此添加到 HabitService 项目而不是 Database 项目中:
using Microsoft.Extensions.Options;
using GoodHabits.Database;
namespace GoodHabits.HabitService;
public class TenantService : ITenantService
{
private readonly TenantSettings _tenantSettings;
private HttpContext _httpContext;
private Tenant _tenant;
public TenantService(IOptions<TenantSettings>
tenantSettings, IHttpContextAccessor contextAccessor)
{
_tenantSettings = tenantSettings.Value;
_httpContext = contextAccessor.HttpContext!;
if (_httpContext != null)
{
if (_httpContext.Request.Headers.TryGetValue(
"tenant", out var tenantId))
{
SetTenant(tenantId!);
}
else
{
throw new Exception("Invalid Tenant!");
}
}
}
private void SetTenant(string tenantId)
{
_tenant = _tenantSettings!.Tenants.Where(a =>
a.TenantName == tenantId).FirstOrDefault();
if (_tenant == null) throw new Exception("Invalid
Tenant!");
if (string.IsNullOrEmpty(_tenant.ConnectionString))
SetDefaultConnectionStringToCurrentTenant();
}
private void
SetDefaultConnectionStringToCurrentTenant() =>
_tenant.ConnectionString =
_tenantSettings.DefaultConnectionString;
public string GetConnectionString() =>
_tenant?.ConnectionString!;
public Tenant GetTenant() => _tenant;
}
上述代码块的主要功能是拦截传入的 HTTP 请求,检查头部中是否有名为租户的条目,并将该名称与已知的租户进行匹配。
修改 SeedData 和 AppDbContext 类
你会记得在第二章中,我们添加了一些种子数据。由于我们现在要求数据库中必须有租户名称,因此我们必须更新种子数据。复制以下内容,或者只需添加TenantName:
using GoodHabits.Database.Entities;
using Microsoft.EntityFrameworkCore;
public static class SeedData
{
public static void Seed(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Habit>().HasData(
new Habit { Id = 100, Name = "Learn French",
Description = "Become a francophone",
TenantName = "CloudSphere" },
new Habit { Id = 101, Name = "Run a marathon",
Description = "Get really fit",
TenantName = "CloudSphere" },
new Habit { Id = 102, Name = "Write every day",
Description = "Finish your book project",
TenantName = "CloudSphere" }
);
}
}
之前创建的GoodHabitsDbContext类有一个硬编码的单个数据库连接字符串。我们将替换它并使用之前配置中定义的多个数据库连接。
完全用以下内容替换GoodHabitsDbContext类:
using Microsoft.EntityFrameworkCore;
using GoodHabits.Database.Entities;
namespace GoodHabits.Database;
public class GoodHabitsDbContext : DbContext
{
private readonly ITenantService _tenantService;
public GoodHabitsDbContext(DbContextOptions options,
ITenantService service) : base(options) =>
_tenantService = service;
public string TenantName { get => _tenantService
.GetTenant()?.TenantName ?? String.Empty; }
public DbSet<Habit>? Habits { get; set; }
protected override void
OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var tenantConnectionString =
_tenantService.GetConnectionString();
if (!string.IsNullOrEmpty(tenantConnectionString))
{
optionsBuilder.UseSqlServer(_tenantService
.GetConnectionString());
}
}
protected override void OnModelCreating(ModelBuilder
modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Habit>().HasQueryFilter(a =>
a.TenantName == TenantName);
SeedData.Seed(modelBuilder);
}
public override async Task<int>
SaveChangesAsync(CancellationToken cancellationToken =
new CancellationToken())
{
ChangeTracker.Entries<IHasTenant>()
.Where(entry => entry.State == EntityState.Added ||
entry.State == EntityState.Modified)
.ToList()
.ForEach(entry => entry.Entity.TenantName =
TenantName);
return await base.SaveChangesAsync(cancellationToken);
}
}
前面代码的主要变化是现在连接字符串是从我们之前创建的TenantService类中读取的。这更加动态,允许我们在构建应用程序时即时为新的租户创建新的数据库。这也比在源代码中硬编码连接字符串并将其提交到存储库更加安全。
另一个需要注意的重要变化是我们添加了一个在上下文级别的查询过滤器。这确保只有正确的租户可以读取他们的数据,这是一个非常重要的安全考虑。
最后,我们已经重写了SaveChangesAsync方法。这允许我们在这里设置租户名称,而无需在我们的其他实现代码中考虑它。这大大简化了我们的其余代码。
编写服务层
我们现在已经配置了Habit服务以及数据库以多租户方式工作,强制在每个请求中包含租户 ID。这是提供良好安全和租户之间分离的良好开端。
接下来,我们将通过连接数据库和服务,并对 Habit 服务进行一些测试调用,展示如何强制执行租户。
我们将首先编写服务层。在HabitService文件夹中打开一个终端并运行以下脚本:
touch IHabitService.cs; \
touch HabitService.cs;
使用以下内容填充接口:
using GoodHabits.Database.Entities;
namespace GoodHabits.HabitService;
public interface IHabitService
{
Task<Habit> Create(string name, string
description);
Task<Habit> GetById(int id);
Task<IReadOnlyList<Habit>> GetAll();
}
然后,使用以下内容填充该类:
using GoodHabits.Database;
using GoodHabits.Database.Entities;
using Microsoft.EntityFrameworkCore;
namespace GoodHabits.HabitService;
public class HabitService : IHabitService
{
private readonly GoodHabitsDbContext _dbContext;
public HabitService(GoodHabitsDbContext dbContext) =>
_dbContext = dbContext;
public async Task<Habit> Create(string name,
string description)
{
var habit = _dbContext.Habits!.Add(new Habit { Name
= name, Description = description }).Entity;
await _dbContext.SaveChangesAsync();
return habit;
}
public async Task<IReadOnlyList<Habit>> GetAll() =>
await _dbContext.Habits!.ToListAsync();
public async Task<Habit> GetById(int id) => await
_dbContext.Habits.FindAsync(id);
}
此服务只是围绕对数据库的一些调用的一些简单包装。我们可以添加更多功能,但这将有助于展示多租户在实际中的工作方式。
编写控制器
服务创建后,我们现在将添加一个控制器,使其可以通过 HTTP 提供来自服务的数据。
在HabitService文件夹中运行以下脚本以设置所需的文件:
rm WeatherForecast.cs; \
cd Controllers; \
rm WeatherForecastController.cs; \
touch HabitsController.cs; \
cd ..; \
mkdir Dtos; \
cd Dtos; \
touch CreateHabitDto.cs
然后,添加控制器的代码,如下所示:
using GoodHabits.HabitService.Dtos;
using Microsoft.AspNetCore.Mvc;
namespace GoodHabits.HabitService.Controllers;
[ApiController]
[Route("api/[controller]")]
public class HabitsController : ControllerBase
{
private readonly ILogger<HabitsController> _logger;
private readonly IHabitService _habitService;
public HabitsController(
ILogger<HabitsController> logger,
IHabitService goodHabitsService
)
{
_logger = logger;
_habitService = goodHabitsService;
}
[HttpGet("{id}")]
public async Task<IActionResult> GetAsync(int id) =>
Ok(await _habitService.GetById(id));
[HttpGet]
public async Task<IActionResult> GetAsync() => Ok(await
_habitService.GetAll());
[HttpPost]
public async Task<IActionResult>
CreateAsync(CreateHabitDto request) => Ok(await
_habitService.Create(request.Name,
request.Description));
}
此控制器仅提供了两个端点,通过我们之前创建的服务层创建和读取数据库中的习惯。
最后,将以下代码添加到CreateHabitDto文件中:
namespace GoodHabits.HabitService.Dtos;
public class CreateHabitDto {
public string Name { get; set; } = default!;
public string Description { get; set; } = default!;
}
添加服务扩展
现在我们可能正在处理许多数据库实例,我们需要在应用程序启动时添加创建和更新所有数据库的能力。我们将创建一个服务集合的扩展来简化这一过程。
在 HabitService 项目中添加一个名为ServiceCollectionExtensions的类,然后添加以下代码:
using GoodHabits.Database;
using Microsoft.EntityFrameworkCore;
namespace GoodHabits.HabitService;
public static class ServiceCollectionExtensions
{
public static IServiceCollection
AddAndMigrateDatabases(this IServiceCollection
services, IConfiguration config)
{
var options = services.GetOptions
<TenantSettings>(nameof(TenantSettings));
var defaultConnectionString =
options.DefaultConnectionString;
services.AddDbContext<GoodHabitsDbContext>(m =>
m.UseSqlServer(e => e.MigrationsAssembly(
typeof(GoodHabitsDbContext).Assembly.FullName)));
var tenants = options.Tenants;
foreach (var tenant in tenants)
{
string connectionString;
if (string.IsNullOrEmpty(
tenant.ConnectionString))
{
connectionString = defaultConnectionString;
}
else
{
connectionString = tenant.ConnectionString;
}
using var scope = services
.BuildServiceProvider().CreateScope();
var dbContext =
scope.ServiceProvider.GetRequiredService<Good
HabitsDbContext>();
dbContext.Database.SetConnectionString(
connectionString);
if (dbContext.Database.GetMigrations()
.Count() > 0)
{
dbContext.Database.Migrate();
}
}
return services;
}
public static T GetOptions<T>(this IServiceCollection
services, string sectionName) where T : new()
{
using var serviceProvider =
services.BuildServiceProvider();
var configuration =
serviceProvider.GetRequiredService<
IConfiguration>();
var section = configuration.GetSection(
sectionName);
var options = new T();
section.Bind(options);
return options;
}
}
从前面的代码中理解的关键点是数据库连接字符串是基于租户设置的,并且当租户登录到应用程序时,租户的数据库将根据最新的迁移进行更新。
该系统将维护多个数据库的大部分开销从管理员那里移除。所有这些操作都是自动完成的!
应用程序管道
最后,我们需要连接所有这些新服务。这需要在 GoodHabits.HabitService 项目的 Program.cs 文件中完成。在这个文件中,粘贴以下内容:
using GoodHabits.HabitService;
using GoodHabits.Database;
using GoodHabits.HabitService;
using Microsoft.OpenApi.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpContextAccessor();
builder.Services.AddControllers().AddNewtonsoftJson();
builder.Services.AddSwaggerGen(c => c.SwaggerDoc("v1", new OpenApiInfo { Title = "GoodHabits.HabitService", Version = "v1" }));
builder.Services.AddTransient<ITenantService, TenantService>();
builder.Services.AddTransient<IHabitService, HabitService>();
builder.Services.Configure<TenantSettings>(builder.Configuration.GetSection(nameof(TenantSettings)));
builder.Services.AddAndMigrateDatabases(builder.Configuration);
builder.Services.AddEndpointsApiExplorer();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint(
"/swagger/v1/swagger.json", "GoodHabits.HabitService
v1"));
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.MapControllers();
app.Run();
在此代码中,你可以看到我们添加了我们创建的两个新服务。我们还使用了之前创建的服务扩展并配置了 Swagger 端点。
测试
在之前的配置到位后,我们现在可以运行应用程序并查看我们所做的更改如何影响了控制器的操作。
由于我们对数据库进行了一些更改(通过在 Habit 表中添加 TenantName 列),我们需要使用 Entity Framework 创建一个新的迁移。通过导航到数据库项目并运行以下命令来创建迁移:
dotnet-ef migrations add MultiTenant --startup-project ../GoodHabits.HabitService/GoodHabits.HabitService.csproj
使用 dotnet run 启动应用程序并检查 API 是否运行正确。
你可以在 Swagger 中查看 API,但如果尝试访问其中一个端点,你会看到一个无效租户异常。这是预期的,因为我们现在必须为每个请求添加一个头,以标识请求是为哪个租户。

图 3.1 – Swagger 错误
如果一切顺利,API 将会启动,你可以在 Swagger 中查看之前可用的端点!
为了测试 HabitService,我们将使用我们在 第二章 中安装的 ThunderClient 扩展。
打开 ThunderClient 扩展,点击 CloudSphere。

图 3.2 – ThunderClient 请求
上述是一个正确配置的请求,其中已添加租户。点击 发送 将会发出响应,如果一切顺利,你应该看到以下响应:

图 3.3 – ThunderClient 响应
上述内容显示,我们已经成功返回了 CloudSphere 租户的数据。
你现在应该花些时间进行更多调查,并确信我们已经正确地按租户限制了数据访问!
这就是本章实践部分的结束。我们现在将转向考虑我们所做事情的安全影响。
安全性考虑
我认为不用说,多租户显著影响了 SaaS 应用程序的安全格局。
安全性有许多方面,从简单的(或至少标准的)考虑,例如验证用户,到能够抵御非常罕见的事件,例如自然灾害导致关键数据中心或重要互联网骨干网络中断。
在这个语境中,“安全”一词指的是应用程序抵御外界一切可能抛来的挑战的整体能力——并且继续确保租户和用户的数据正确、可访问和私密!这可以包括黑客攻击、攻击、自然灾害、代码错误导致数据泄露,甚至监管问题,如通用数据保护条例(GDPR)。
可能的安全陷阱
每种类型的应用程序的开发者都将面临广泛的安全问题。不幸的是,SaaS 应用程序,尤其是多租户应用程序的现实是,这类应用程序的开发者必须考虑几乎所有可能的安全威胁!
SaaS 应用程序通常是分层的,至少包括数据库、API 和用户界面。每一层都提供了一个攻击向量。SaaS 应用程序也经常托管在云服务上。与本地托管相比,在云中托管通常更安全,但必须考虑许多额外的攻击向量。最重要的是,管理员的用户账户可能会被攻破,并且远程访问管理页面。这在本地解决方案中通常不是一个大问题,因为可能完全没有远程访问。
本节将列出作为 SaaS 应用程序开发者你必须考虑的一些安全考虑因素。
资源访问管理
首先,最明显的是,一个系统中资源的访问权限必须限制只授予那些有权查看这些资源的人。
例如,很明显,一个租户的数据永远不应该对另一个租户的用户可见,除非有故意共享数据的情况。在开发 SaaS 应用程序时,重要的是要理解攻击者会针对用户界面、API 以及数据库进行攻击,以获取数据访问权限。
攻击者还会尝试在数据“在传输中”时拦截数据。当数据从数据库移动到 API,或从 API 移动到用户界面时,它就处于易受攻击的状态。
除了个人用户数据外,很可能应用程序中会有一些只有特定角色的用户才能访问的部分,例如管理员。
任何未能控制资源访问的 SaaS 系统都将很快被攻破,一个因丢失客户数据而声誉受损的应用程序将很快失去客户。在这个领域的失败可能对应用程序和公司的声誉造成灾难性的影响。
这在 SaaS 应用程序中是一个非常重要的考虑因素,尤其是在多租户应用程序中,租户将共享对某些资源(如数据库)的访问权限,而这些资源的访问必须非常谨慎地管理。
数据泄露
与上述类似,数据泄露在声誉方面可能代价高昂,而且它们也可能对拥有应用程序的企业产生巨大的财务影响!
虽然数据泄露在某种程度上是资源访问失败,但它通常是一个更普遍的问题。以下是一些可以考虑的例子:
-
一个用户界面将信用卡详情以纯文本形式从客户端传输到服务器,这允许进行中间人攻击(MitM),从而从所有用户那里收集信用卡信息。如前所述,数据“在传输中”往往容易受到攻击。
-
一个缺少认证属性的 API 端点,因此任何人都可以访问,无论他们是否经过认证。同样,API 端点可能有一个错误的认证属性。这一点非常重要,应该采取措施自动测试 API 是否得到了适当的保护。
-
一个安全性差的数据库允许攻击者访问、下载或删除数据库中的数据。
-
一个配置不当的服务器或虚拟机,可以被恶意用户远程访问。
-
糟糕的密码卫生习惯允许攻击者通过暴力破解、彩虹表或类似的暴力攻击来猜测用户的密码。
近年来,数据泄露可能给运营不安全应用程序的公司带来非常高的财务罚款。2018 年,英国航空公司(BA)的网站泄露了 38 万名客户的信用卡数据——其中一人后来写了这本书!对 BA 的财务成本是 1.83 亿英镑(在撰写本书时为 2.44 亿美元)。对于 BA 来说,最初雇佣一支开发人员和安全专家团队来保护其网站要便宜得多!
在构建 SaaS 应用程序时,特别是具有多租户数据库的应用程序时,需要考虑一个额外的因素。就数据泄露而言,这些类型的应用程序存在一个很大的风险因素,即当两个租户共享存储资源、应用服务器、数据库或类似资源时,一个租户的数据可能会意外地泄露给属于不同租户的用户。正如本章所讨论的,当应用程序处于概念阶段时,需要采取严格的方法来设计多租户解决方案!
数据损坏
在多租户应用程序中,各种用户和租户以之前描述的一种方式相互隔离,但现实情况是,他们经常共享一些(或许多)相同的资源。他们可能共享数据库、应用服务器、用户认证系统等。如果其中一个系统被一个用户破坏,可能会出现一些传染,破坏可能会传播并随后影响另一个用户。在通常使用云资源的现代时代,这种情况很少见,大型云服务提供商应该能够处理这些问题。
针对此问题的主要缓解措施是在隔离单个租户的关注点时非常小心,并使用信誉良好的云服务提供商。
毫无疑问,你应该定期备份你的数据,并检查应用程序是否可以从这些备份中恢复!
漏洞和攻击
虽然上述一些数据泄露可能被视为漏洞或攻击,但数据泄露通常是更“被动”的——就像漏水的水管。漏洞则更为主动——就像有人用斧头砍过的水管!
SaaS 应用程序的运营商可能会面临广泛的攻击,但深入的安全问题超出了本章的范围,实际上也超出了本书的范围。
然而,值得考虑的是共租户攻击的可能性,因为这种攻击是特定于多租户应用程序的。虽然外部攻击者通常可以访问应用程序,假设至少登录页面可以通过公共互联网访问,那么付费用户将定义上能够访问系统中的更多部分;因此,他们将处于更好的位置发起攻击。这种攻击可能针对基础设施或其他用户。
配置
SaaS 应用程序增加了复杂性,而随着这种复杂性,配置开销也会随之而来。
例如,配置错误可能会意外泄露生产密码或对数据库或其他资源的访问权限。
很容易忽略配置参数,但这可能是一个代价高昂的错误。一切应尽可能以最安全的方式存储,尽可能多地使用密钥保管库和密钥管理器。
在检查源代码仓库中的配置文件时也应格外小心,因为不幸的是,这是泄露密码等信息的常见方式。
存储
每个 SaaS 应用程序都将以某种形式存储数据,而这些数据构成了一个攻击向量,可以利用它来损害应用程序。
数据保留和删除
与上述类似,SaaS 应用程序将保留敏感数据。确保这些数据的安全性至关重要。在数据保留方面,另一个关键考虑因素是,在许多司法管辖区,个人拥有“被遗忘的权利”,即与该个人相关的所有数据都必须匿名化。
在 SaaS 应用程序的背景下,这可能非常具有挑战性,原因如下:
-
数据库通常是多租户的。在不孤立其他记录的情况下,完全清除与单个租户相关的所有信息有多容易?
-
记录很可能会在所有层——用户界面、API 和数据库——上执行。你有多确定那些日志中没有敏感的客户数据?
-
备份显然是系统安全的重要组成部分,但如果你需要在任何时候进行恢复,你如何确保之前删除的数据现在没有被恢复?
所有上述内容都需要对数据保留和删除进行细致入微的处理,以确保你的客户知道他们的数据是安全的。
规章制度
在构建现代 SaaS 应用程序时,数据安全的一个重要部分是理解你必须遵守的监管框架。互联网为你的应用程序提供了真正全球的覆盖范围,但这种全球覆盖范围意味着你可能必须考虑大量的监管框架,违反这些框架的后果通常是天文数字般的罚款和处罚。
欧洲联盟的 GDPR 法律规定了组织必须如何使用个人数据,违反这些规定的处罚可能非常严厉——1000 万欧元,或公司全年全球营业额的 2%。这类法律在许多司法管辖区变得越来越普遍。
在这方面,你应该问自己的关键问题是:
-
哪些司法管辖区管理将存储在此应用程序中的数据?
-
你的应用程序是否遵守这些要求?
-
与你合作的合作伙伴和第三方是否遵守这些规定?
多租户应用程序的良好安全实践
我希望前面提到的潜在恐怖故事没有让你感到沮丧!在本节中,我将列出一些应该考虑的缓解措施。
治理、风险和合规性(GRC)协议
GRC 是一套业务出于多种原因(如满足目标、维护监管合规性或充分保护 SaaS 应用程序)将实施的流程和程序!以下是一些可能的例子:
-
详细说明应用程序升级应如何执行以及预发布检查清单。
-
管理和控制对客户私有数据或 IP 访问的流程。
-
对哪些员工有权访问生产数据库的限制。请注意,理想情况下,没有人应该有权限!
资产和资源的隔离
在实际可能的情况下,资源应该相互隔离。显然,应用层需要访问数据库;否则,它如何访问数据?然而,将数据库和应用运行在不同的服务器或虚拟机上是一种良好的实践。请注意,如果做得不正确,隔离可能会带来性能问题——这是一个微妙的平衡!
对于多租户应用程序,应尽可能将单个用户和租户的数据与其他用户和租户隔离。理想情况下,所有用户数据都应该加密,以便只有数据所有者才能理解其含义。这样,即使发生数据泄露,也无法从数据泄露中推断出有价值的信息。
审计
这是对所有应用程序的良好实践,而不仅仅是 SaaS/多租户。应咨询独立第三方对所有 IT 系统和租户数据进行安全性和合规性审计。
除了检查数据是否物理安全外,审计员还应确认是否遵循行业标准,以及是否遵守任何监管框架。
使用数据泄露防护(DLP)软件
现在有许多商业数据泄露防护(DLP)解决方案可供选择。值得考虑将这个复杂且极其重要的任务委托给专业提供商。
仔细管理访问权限
SaaS/多租户应用程序特别容易受到由于资源分配不当而引发的安全问题的影响。从公司角度来看,这是正确的——开发者不应有权访问生产数据库。从用户的角度来看,这也是正确的。
应根据身份(谁是个人用户?)和角色(个人属于哪些用户类别?)来管理用户访问权限。
对协作工具非常小心
通常,SaaS 应用程序会允许用户选择与同一租户内的其他选定用户共享某些资源,而较少与不同租户的用户共享。这可以是一个极其有效的 SaaS 应用程序的补充,并且确实使用多租户解决方案的一个巨大优势。然而,这也引入了一个安全风险,即应用程序本身可能会以编程方式泄露数据。在构建 SaaS 应用程序中的协作元素时,应该采取重大的额外谨慎,因为这些将是恶意行为者首先试图利用以访问其他用户数据的地方。
在任何应用程序中,良好的安全性都是困难的,尤其是在处理多租户应用程序时。一如既往,在项目开始时就实施良好的实践要比后来添加它们容易得多!
摘要
如我们所见,多租户是一个庞大且复杂的话题,但它是理解构建 SaaS 应用程序时绝对核心的。
关于如何隔离数据库所做的决策将对应用程序的其余部分产生最大的影响,但考虑多个租户的存在如何影响 API 和用户界面层也非常重要。
多租户引入了相当多的额外安全考虑。这些应该在开发过程一开始就考虑,并始终牢记在心。
说到这里,实施多租户解决方案的额外挑战提供了巨大的潜在优势!应用程序的覆盖范围可以真正实现全球化,其可扩展性是任何其他范式都无法比拟的。考虑一下世界上最大的科技公司——谷歌、Meta、Netflix 等等。这些公司中的每一个都接受了 SaaS 和多租户的概念,而且他们这么做是有充分理由的!
在下一章中,我们将基于我们对多租户的了解,学习如何构建数据库并为数据丰富的应用程序进行规划。
进一步阅读
-
单租户与多租户:SaaS 架构:
www.clickittech.com/aws/single-tenant-multi-tenant/ -
多租户策略:
www.linkedin.com/pulse/effective-multi-tenancy-strategies-saas-applications-kulkarni/ -
构建可扩展的多租户 SaaS 解决方案的战略:
aws.amazon.com/blogs/apn/in-depth-strategies-for-building-a-scalable-multi-tenant-saas-solution-with-amazon-redshift/ -
实现多租户 SaaS 应用:
developers.redhat.com/articles/2022/05/09/approaches-implementing-multi-tenancy-saas-applications -
多租户如何影响嵌入式分析:
yurbi.com/blog/what-is-multi-tenancy-security-and-how-does-it-impact-embedded-analytics/ -
DLP 定义:
digitalguardian.com/blog/what-data-loss-prevention-dlp-definition-data-loss-prevention
第四章:构建数据库和规划数据丰富的应用
在前面的章节中,我们建立了一个基础 SaaS 应用,该应用包含一个简单的数据库和单个表。该数据库连接到 API,我们展示了使用 SQL Server、.NET 和 Entity Framework 实现的 secure multi-tenancy。
在本章中,我们将更深入地探讨数据库层及其与 Entity Framework 的交互。作为整个应用的基础,数据库层面的设计选择将影响堆栈中的每一层。我们将探讨如何构建和设计适用于数据密集型 SaaS 应用的健壮数据库。您将掌握各种技能,例如规范化、索引、性能优化,以及测试和维护数据库的技术。
一旦部署,数据库通常代表系统中最具挑战性的部分,需要保持最新状态。数据库本质上是状态化的,在更新过程中防止数据丢失或损坏至关重要。除了学习数据库设计和构建,我们还将探讨维护和更新数据库的几种策略,重点介绍 Entity Framework 提供的工具。
本章中获得的技能对于创建可扩展和可靠的 SaaS 应用至关重要。通过掌握本章讨论的方法,您将能够设计出针对性能、可扩展性和可维护性优化的数据库,从而促进 SaaS 应用的开发和维护。
本章涵盖了以下主要主题:
-
数据在 SaaS 应用中的重要性
-
使用 SQL Server 和 Entity Framework 构建数据库
-
测试数据库和数据丰富的应用
-
在生产环境中工作,保持数据库更新,并确保数据安全
数据和数据库是 SaaS 应用的基础。让我们首先考虑它们的重要性。
技术要求
所有本章的代码都可以在github.com/PacktPublishing/Building-Modern-SaaS-Applications-with-C-and-.NET/tree/main/Chapter-4找到。
数据在 SaaS 应用中的重要性
在 SaaS 应用中,数据库是应用的基础。数据库(或更准确地说,其中的数据)是驱动应用并位于用户主要价值所在的地方。没有数据的 SaaS 应用只是一个空壳!
在构建 SaaS 应用程序时,一个关键的考虑因素是应用程序将存储的数据类型以及其使用方式。应用程序是否会存储大量结构化数据,例如客户记录或交易历史?或者它是否会存储非结构化数据,例如支撑社交媒体数据流的数据?数据类型将对应用程序的设计和架构产生重大影响。
另一个重要的考虑因素是数据如何被访问和操作。数据是否会被大量用户同时访问,或者只是由少数用户一次访问?数据是否会被频繁更新,或者主要是静态的?这些因素将影响数据库技术选择和数据模型设计。
在 SaaS 应用程序中,考虑数据如何在不同的租户之间共享也很重要。正如我们在上一章中讨论的,每个租户的数据必须保持独立和安全,同时仍然允许高效地访问和操作与当前租户相关的数据。这需要仔细规划和设计数据模型和数据库模式。
还需要考虑可扩展性。虽然 SaaS 应用程序可能一开始用户基础较小,因此读写次数相对较低,但随着用户基础的快速增长,这可能会迅速改变!设计数据模型和模式时,重要的是要确保应用程序可以扩展。同样,通过互联网传输的数据量也必须得到管理。带宽不是免费的,也不是无限的,在数据密集型应用程序中,这可能会成为一种负担。
数据的重要性不仅限于技术考虑。数据在用户体验中扮演着至关重要的角色。数据的表现方式、组织方式和访问方式可以显著影响应用程序的可用性。当然,这取决于用户界面、底层数据的结构和查询的便捷性和速度,这些都会在前端被最终用户注意到。
对于构建应用程序并托管数据库的公司来说,数据可以是一个关键的收入来源。一些 SaaS 应用程序通过向寻求针对特定市场细分市场的企业销售有关消费者人口统计和购买习惯的信息来货币化其数据。这些数据通常由机器学习算法收集和分类,从而可以深入了解数据的用户和创建者。有了这些宝贵的信息,企业可以创建有针对性的营销活动,并改进其产品和服务,更好地满足客户的需求。
数据对于众多原因来说都很重要,因此维护数据安全和合规性是构建成功的 SaaS 应用程序的一个关键方面。确保敏感数据,如财务或个人信息,得到安全保护并符合相关法规是 SaaS 提供商的责任。为了实现这一点,SaaS 提供商可能需要实施各种安全措施,如加密和访问控制,以保护数据。
数据和数据库是 SaaS 应用程序中绝对关键的部分,对于 SaaS 开发人员来说,了解如何设计、构建和维护数据丰富的应用程序非常重要。
构建数据库
在本节中,我们将重点介绍使用 SQL Server 和 Entity Framework 设计和构建您的 SaaS 应用程序的数据库。我们将涵盖一系列主题,包括选择合适的数据库技术、使用 Entity Framework 设计高效且可扩展的数据模型,以及使用 SQL Server 实施数据库安全性和合规性措施。在本节结束时,您将深入了解如何为您的 SaaS 应用程序构建一个强大且可靠的数据库。
数据库类型
因为这是一本专注于利用 Microsoft 堆栈的书籍,所以我会重点关注 SQL Server 作为底层数据库平台,并使用 Entity Framework 与数据库交互。SQL Server 是一种 关系数据库,它以表的形式存储数据,其中行代表单个记录,列代表数据属性。这是一种非常结构化的数据存储方式,数据的“形状”必须在设计时预先知道并构建到应用程序中。
虽然我们将重点关注 SQL Server 以及因此关系数据,但简要考虑替代方案是有价值的,这些替代方案超出了本书的范围。如果您有特定的用例可能需要除关系数据之外的数据,以下的一些替代方案可能值得进一步研究:
-
文档数据库:文档数据库是一种以文档形式存储数据的数据库类型。在这个上下文中,文档是由键值对组成的数据,旨在可扩展和灵活。在文档数据库中,数据的结构或形状在数据库设计时不会被设定,这使得它成为存储和查询具有不同结构的大量数据的良好选择。
-
图数据库:这些数据库以节点(表示数据实体)和边(表示节点之间的关系)的形式存储数据。它们通常用于需要表示数据实体之间复杂关系的应用程序中,例如社交网络或推荐引擎。
-
键值存储:这些数据库以键值对的形式存储数据,其中键用于标识数据,值是数据本身。它们通常用于简单的数据存储和检索场景,其中数据不需要查询或索引。这与文档数据库类似,但更有限,因为它仅真正适用于简单的用例。
-
内存数据库:这些数据库将数据存储在内存中,而不是磁盘上。它们通常用于需要快速读写数据访问的应用程序,例如在线游戏或金融应用程序。请注意,内存数据库也可以用于帮助测试数据库。这两个用例是分开的,不应混淆。
-
时间序列数据库:这些数据库专门设计用于存储和查询带时间戳的数据,如传感器读数或金融交易。它们通常用于需要分析随时间变化数据的程序。
在本章中,我们将专注于 SQL Server,这是一个关系型数据库。我们将使用微软专门为此目的开发的工具与数据库进行交互——即实体框架(Entity Framework)。
什么是 ACID?
当与数据库(尤其是关系型数据库)一起工作时,你经常会遇到ACID这个缩写。这指的是数据库事务的四个属性——即原子性、一致性、隔离性和持久性:
-
原子性:事务被视为一个单一、不可分割的工作单元,要么所有操作都完成,要么一个都不完成
-
一致性:事务将数据库从一个有效状态转换到另一个有效状态,保持数据库的不变性和约束
-
隔离性:事务的并发执行会导致一个系统状态,这个状态就像事务按某种顺序串行执行一样
-
持久性:一旦事务被提交,其对数据库的更改将持久存在并能够生存未来的系统故障
这些属性是一组由数据库管理系统保证的属性,以确保数据库中存储数据的可靠性和一致性。ACID 最常与关系型数据库管理系统(RDBMS)相关联,例如 Oracle、MySQL、PostgreSQL 和 Microsoft SQL Server。然而,一些较新的数据库,如 NoSQL 数据库和新 SQL 数据库,也可能提供 ACID 保证,尽管它们可能具有不同的一致性和持久性级别。ACID 支持的水平取决于特定的数据库技术以及它的实现和配置。
ACID 通常与关系型数据库中的事务相关联,而在 NoSQL 或文档数据库中则较少见。在本章中,实际上在整个书中,我们将专注于 SQL Server,这是一个提供 ACID 事务支持的关系型数据库。
实体框架(Entity Framework)
Entity Framework 是一个 对象关系映射(ORM)工具,它允许开发者使用 .NET 对象与数据库进行交互。它通过消除编写 SQL 查询和手动将数据映射到对象的需求来简化访问和操作数据的过程。Entity Framework 对于熟悉 .NET 并希望简化数据访问和操作任务的开发者来说非常适合,这使得它成为本以 .NET 为重点的书籍中学习的绝佳选择!
虽然我们会专注于 SQL Server,但使用 Entity Framework 的一大好处是它能够生成数据库无关的代码,这使得开发者能够在不显著重写代码的情况下更改数据库平台或支持多个数据库平台。当处理多租户 SaaS 应用程序时,这一特性尤其有趣,因为某些客户可能要求使用特定的数据库平台。
Entity Framework 包含了与编写与数据库交互的代码相关的许多复杂性。例如,懒加载、更改跟踪以及数据模式和架构更改的自动迁移等概念都是开箱即用的。
与许多其他 ORM 不同,Entity Framework 支持与数据库交互的几种不同方法,包括传统的数据库优先方法、代码优先方法和模型优先方法。这为开发者提供了选择最适合他们需求的方法的灵活性。
Entity Framework 是一个强大的工具,可以通过简化数据访问和操作任务来大大提高经验丰富的 .NET 开发者的生产力,并且在处理高度依赖于数据平台的项目(如 SaaS 应用程序)时,它被高度推荐。
本章的范围不包括所有可能的 Entity Framework 使用方式,因此我将专注于其中一种——代码优先。
使用 Entity Framework 的代码优先
使用 Entity Framework 的代码优先是一种开发方法,它允许开发者使用 C# 代码创建他们的 .NET 应用程序的数据模型,而不是使用 SQL Server Management Studio 等用户界面来设计数据库,或者通过编写和维护 SQL 脚本来实现。这种方法对于喜欢使用代码并且希望对应用程序的数据模型有更多控制的开发者来说特别有用。使用代码优先,开发者可以使用代码中的类和属性来定义他们的数据模型,而 Entity Framework 将处理底层数据库的创建和管理。这种方法允许开发者专注于应用程序的数据模型和业务逻辑,而无需担心数据库的实现细节。
我们在第二章中看到了这种方法。当数据库配置时,我们没有写一行 SQL 代码——我们编写了一个名为 GoodHabits.cs 的 C# 类,并在 C# 代码中定义了数据结构。然后我们使用了两个 Entity Framework CLI 命令来更新数据库。在第三章中,我们修改了这个文件以支持多租户。
使用 Entity Framework 的存储过程
传统上,在数据库设计时使用存储过程是非常常见的。虽然这仍然是一种非常有效且实用的数据库开发方法,但它越来越被视为最佳实践,即使用 ORM(对象关系映射)如 Entity Framework 来访问和操作数据库中的数据,而不是使用存储过程。这有几个原因。
Entity Framework 允许你在代码中与对象和实体一起工作,而不是必须编写原始的 SQL 查询。这为你提供了一个更高层次的抽象,可以使开发应用程序变得更加容易。采用这种方法,你可以以熟悉面向对象的方式构建数据库,这可以使推理和维护变得更加容易。Entity Framework 能够从 C# 代码中解析对象之间的关系,并创建数据库关系。如果你在存储过程中创建逻辑并将其建模,Entity Framework 就会失去对这种逻辑的视线。
使用 Entity Framework 的另一个巨大好处是,许多数据库平台都支持即插即用。然而,存储过程中的逻辑通常无法在不同数据库平台之间迁移,并且必须分别在每个平台上单独构建和维护。
最后,Entity Framework 提供了多种可用的测试工具。使用存储过程和触发器将需要特定的测试基础设施和技术,这可能会使得测试应用程序变得更加困难,因为逻辑被分散在代码和数据库之间。
在某些情况下,使用存储过程或触发器可能是有益的。以下是一些例子:
-
当处理非常大的数据集或在性能至关重要的场合时,通过存储过程直接在数据库上执行逻辑可能是有利的。
-
在数据安全是关注点的情况下,存储过程可以帮助通过限制对数据库可以运行的查询类型来防止对数据的未授权访问。
-
在你想要将应用程序的数据访问层从底层数据库模式抽象出来的情况下,使用存储过程可以帮助解耦这两个部分。这在数据库模式可能频繁变化的情况下尤其有用。
在决定是否在数据库中使用存储过程时,理解你的具体用例非常重要。对于演示应用程序,我们将继续使用 Entity Framework 来操作和访问数据库。
规范化
数据库规范化是将数据库组织成减少冗余并提高数据完整性的过程。它涉及将数据库划分为更小、更专注的表,这些表通过关系相互关联。规范化的目标是消除冗余并确保每条数据只存储在数据库中的一个地方。规范化是数据库设计过程中的一个重要步骤,可以极大地提高数据库的性能和可靠性。
Entity Framework 支持以多种方式支持数据库的规范化过程。其中主要的方式是通过在数据库中创建和修改表以及关系。这允许开发者以减少冗余并提高数据完整性的方式来结构化他们的数据——这是规范化的关键目标。Entity Framework 还包括对数据变更自动迁移的支持。这意味着当开发者对他们的数据模型进行更改时,这些更改会自动反映在底层数据库中。这在规范化数据库时尤其有用,因为它允许开发者轻松地更新他们的数据结构,而无需在表之间手动迁移数据。
此外,Entity Framework 的 LINQ 查询语法允许开发者轻松地从规范化数据库中检索和操作数据。它支持广泛的操作,包括过滤、排序和聚合,使得处理来自多个表的数据变得容易。最后,Entity Framework 对预加载和懒加载的支持允许开发者通过仅在需要时加载数据来优化应用程序的性能,而不是一次性加载所有数据。这在规范化不良的数据库中可能更困难。总的来说,Entity Framework 提供了一系列工具和功能,以帮助开发者规范化他们的数据库,并提高他们应用程序的性能和可靠性。
有几种范式可以用来衡量数据库中规范化的程度。前三个(称为 1NF、2NF 和 3NF)用于解决数据库中的冗余问题,并且在大多数情况下通常被认为是良好的实践。
超过前三个,额外的范式是为了解决特定类型的问题而设计的;然而,这些范式使用较少,并且被认为超出了本章的范围。
值得注意的是,实现更高的范式并不总是构成更好的数据库设计。通常,围绕特定的用例设计一个高效且性能良好的数据库比盲目遵循规范化规则要好。尽管如此,实现 3NF 通常是一个良好的起点,从这里可以进一步进行规范化,或者实际上进行反规范化。
让我们用一个例子来说明这一点。让我们考虑向我们在第二章中开发的GoodHabit示例中添加一个用户表。
要达到第一范式(1NF),数据库中的所有属性都必须是原子的。这意味着数据库中的每一列都应该包含一个单一值。我们可以设计一个看起来像这样的用户表:

图 4.1 – 未良好归一化的数据
前面的表格显示了未良好归一化的数据。名称列包含两份数据(姓名和姓氏),这些信息可能单独使用时很有用。习惯列包含以逗号分隔的数据列表。这可以通过以下方式改进:

图 4.2 – 第一范式(1NF)中的数据
前面的表格显示了 1NF 中的数据。每个属性只包含一个值。现在数据库中为每个习惯都有一个行,但用户出现了多次。如果 Dave 决定他更愿意被称为 David,我们就必须更新多个地方的数据。
要将此数据移动到第二范式,我们需要将数据拆分为两个表——一个用于用户,一个用于习惯。我们还需要一个第三张表来将用户与将要工作的习惯联系起来:

图 4.3 – 第二范式(2NF)中的数据
这样就更好了,我们可以开始看到这些数据可以非常整洁地进行查询和更新。尽管如此,我们还可以进行一项进一步的改进。习惯表有一个频率列,它是间接依赖于ID列的。这被称为传递依赖。为了将此数据移动到第三范式,我们必须通过添加一个频率表来打破这种传递依赖:

图 4.4 – 第三范式(3NF)中的数据
在这个阶段,第三范式已经足够了,我们不会进一步扩展。你可以看到所有数据都被分离到单独的表中,并通过外键约束与用户习惯表相连接。这允许高效地查询和更新数据库中的数据。
索引和性能优化
索引数据库意味着创建一个单独的数据结构,用于提高某些类型查询的性能。索引通常在表中的特定列上创建,允许数据库快速定位匹配给定条件的行。例如,如果你有一个包含大量客户记录的大表,并且你经常通过姓氏搜索客户,你可以在last_name列上创建一个索引来提高这些搜索的性能。索引可以显著提高某些类型查询的性能,但它们在存储空间和维护方面也有成本。因此,仔细考虑哪些列应该被索引,并权衡索引的好处与成本是很重要的。
为了使用 Entity Framework 进行索引,开发者可以使用各种工具和方法。进行索引的一种方法是使用 Entity Framework Fluent API,它允许开发者通过代码在其实体上定义索引。要使用 Fluent API 创建索引,开发者可以使用HasIndex方法并指定应包含在索引中的属性。另一个选项是使用 Entity Framework Designer,这是一个可视化工具,允许开发者通过图形界面设计他们的数据模型。设计器包括通过在实体上右键单击并选择添加索引来定义实体索引的能力。最后,开发者可以通过向迁移文件中添加适当的代码来使用数据库迁移在他们的实体上创建索引。
使用 Entity Framework 配置索引非常简单!
如果我们考虑我们在第二章中开发的GoodHabit表,我们使用了以下 C#代码来定义表的结构:
public class GoodHabit
{
public int Id { get; set; }
public string Name { get; set; } = default!;
}
我们可以通过以下方式使用属性装饰类来向Name列添加索引:
[Index(nameof(Name))]
public class GoodHabit
{
public int Id { get; set; }
public string Name { get; set; } = default!;
}
这将指导数据库平台为Name列创建一个索引。我们可以以同样的方式为ID列创建索引。你可以通过以下方式设置属性来创建一个组合索引:
[Index(nameof(Name), nameof(Id))]
如果你需要设置索引的排序顺序,你可以使用以下方法之一:
[Index(nameof(Name), nameof(Id)), AllDescending = true]
[Index(nameof(Name), nameof(Id)), IsDescending = new[] { false, true }]
如果你想要给你的索引命名,你可以使用以下方法:
[Index(nameof(Name), Name = "Index_Name")]
Entity Framework 提供了大量的灵活性,本书的范围不包括所有内容。
你可以在learn.microsoft.com/en-us/ef/core/modeling/indexes?tabs=data-annotations了解更多信息。
现在,我们将为示例应用程序构建数据库,同时考虑到我们刚刚学到的内容。
使用 Entity Framework 代码首先设计数据库模式
在我们开始设计数据库之前,让我们先停下来思考一下我们的需求是什么:
-
数据库将存储用户信息以识别单个用户
-
用户可以向数据库添加习惯以跟踪他们的进度并实现他们的目标
-
用户可以为每个习惯记录进度并设置提醒来执行它
-
用户可以为他们的习惯设置目标,例如跑一定数量的英里或存一定数量的钱
-
数据库可以通过跟踪和管理用户习惯来支持用户实现目标并提高他们的整体福祉
下面的图示显示了代表我们刚刚配置的数据库的图表。这显示了表、列以及表之间的关系:

图 4.5 – 建议的数据库模式
前面的图示显示了一个可以满足先前定义要求的模式。
创建实体类
由于我们使用的是 Entity Framework 代码优先,我们将通过编写 C# 代码来构建这个数据库。使用以下内容创建实体类:
cd GoodHabits.Database/Entities; \
touch Goal.cs; \
touch Progress.cs; \
touch Reminder.cs; \
touch User.cs;
我们已经有了 Habit 实体,但我们将使用一些额外的属性来更新它。将以下代码复制到 Habit.cs 中:
using Microsoft.EntityFrameworkCore;
namespace GoodHabits.Database.Entities;
[Index(nameof(Name), nameof(Id))]
public class Habit : IHasTenant
{
public int Id { get; set; }
public string Name { get; set; } = default!;
public string Description { get; set; } = default!;
public int UserId { get; set; }
public virtual ICollection<Progress> ProgressUpdates {
get; set; } = default!;
public virtual ICollection<Reminder> Reminders { get;
set; } = default!;
public virtual Goal Goal { get; set; } = default!;
public Duration Duration { get; set; }
public string TenantName { get; set; } = default!;
}
public enum Duration { DayLong, WeekLong, MonthLong }
接下来,Goal.cs 应该看起来像这样:
using Microsoft.EntityFrameworkCore;
namespace GoodHabits.Database.Entities;
[Index(nameof(Id))]
public class Goal
{
public int Id { get; set; }
public int HabitId { get; set; }
public virtual Habit Habit { get; set; } = default!;
}
Progress.cs 应该看起来像这样:
using Microsoft.EntityFrameworkCore;
namespace GoodHabits.Database.Entities;
[Index(nameof(Id))]
public class Progress
{
public int Id { get; set; }
public DateTime Date { get; set; }
public int HabitId { get; set; }
public virtual Habit Habit { get; set; } = default!;
}
Reminder.cs 应该看起来像这样:
using Microsoft.EntityFrameworkCore;
namespace GoodHabits.Database.Entities;
[Index(nameof(Id))]
public class Reminder
{
public int Id { get; set; }
public Frequency Frequency { get; set; }
public int HabitId { get; set; }
public virtual Habit Habit { get; set; } = default!;
}
public enum Frequency { Daily, Weekly, Monthly }
最后,User.cs 应该看起来像这样:
using Microsoft.EntityFrameworkCore;
namespace GoodHabits.Database.Entities;
[Index(nameof(Id))]
public class User
{
public int Id { get; set; }
public string FirstName { get; set; } = default!;
public string LastName { get; set; } = default!;
public string Email { get; set; } = default!;
}
将以下代码行添加到 GoodHabitsDbContext 类中:
public DbSet<User>? Users { get; set; }
public DbSet<Progress>? Progress { get; set; }
public DbSet<Reminder>? Reminders { get; set; }
public DbSet<Goal>? Goals { get; set; }
您可以通过在控制台中运行以下命令来创建迁移:
dotnet-ef migrations add AdditionalEntities --startup-project ../GoodHabits.HabitService/GoodHabits.HabitService.csproj
当 HabitService 运行时,迁移将自动应用到数据库中,因此只需运行习惯服务即可将更改推送到数据库。
使用服务器资源管理器查看数据库,我们可以看到模式已成功应用!

图 4.6 – 应用到数据库的模式
前面的图示显示,模式已成功迁移到数据库中。
测试数据丰富的应用程序
测试 SaaS 应用程序的数据库层是开发过程中的一个重要部分。数据库是任何应用程序的关键组件,因为它存储和管理应用程序所依赖的数据。确保数据库正常工作对于应用程序的整体稳定性和性能至关重要。
在测试应用程序的数据库层时,您可能会遇到几个挑战。一个挑战是确保数据库模式正确,并且数据被正确存储和检索。另一个挑战是确保数据库已适当优化以获得性能,尤其是如果您处理大量数据时。
您可以使用多种不同的技术来测试应用程序的数据库层。一种常见的技术是使用单元测试来验证单个数据库功能是否正常工作。另一种技术是使用集成测试来确保数据库与应用程序的其余部分一起正确工作。您还可能想使用性能测试来确保数据库能够处理大量数据而不会出现任何问题。
单元测试是一种自动化测试类型,用于验证单个代码单元的行为,例如单个函数或方法。与单元测试不同,集成测试关注于测试应用程序的不同部分作为一个系统如何协同工作。集成测试用于确保应用程序的不同组件能够正确地相互通信和交互。
升级数据库
使用 Entity Framework 升级数据库涉及对数据库架构和数据进行更改,以反映应用程序中的更改。这可能包括添加新表或列、修改现有表或列,以及将数据从一种格式迁移到另一种格式。
当使用 Entity Framework 升级数据库时,你可以采取几种方法。一种方法是用DbMigrations类自动生成和执行必要的 SQL 命令来更新数据库架构。这可能是方便的,因为它允许你使用高级 API 对数据库进行更改,而不是必须编写原始 SQL 命令。然而,它可能不如其他方法灵活,因为它依赖于 Entity Framework 生成 SQL 命令,而这可能并不总是产生最佳结果。这是我们本章将采用的方法。
值得注意的是,有一个流行的替代方法,即使用 Entity Framework 的DbContext类手动执行 SQL 命令来更新数据库。这可以更加灵活,因为你可以完全控制执行的 SQL 命令。然而,这也可能更加耗时和容易出错,因为你必须自己编写和调试 SQL 命令。
最后,当然,你可以使用任何首选方法独立于 Entity Framework 更新数据库,这很可能涉及执行 SQL 脚本。
我认为通常最好使用内置的迁移工具,并让 Entity Framework 为我们做繁重的工作。
我们在实践中看到了这个过程两次——当我们创建第二章中的初始迁移时,以及当我们刚刚更新架构时。
如果你查看“数据库”项目中的“迁移”文件夹,你应该会看到以下文件:

图 4.7 – 迁移文件夹
在前面的图中,我们可以看到包含我们第一次尝试的.._InitialSetup.cs文件,包含我们在第三章中做出的修改的.._MultiTenant.cs文件,以及我们在这章中添加的AdditionalEntities.cs文件。
如果你还记得,在第二章中,我们添加了以下初始迁移:
dotnet-ef migrations add Initial
然而,在创建第二个迁移时,我们使用了以下方法:
dotnet-ef migrations add MultiTenant --startup-project ../GoodHabits.Api/GoodHabits.Api.csproj
这是因为我们在第三章中引入多租户所做的工增加了构造函数的参数,这些参数在 API 项目中定义。将迁移工具指向 API 项目允许它通过HabitService项目创建迁移。
如果你查看这些生成的类中的任何一个,你会看到两个名为Up和Down的方法。这些方法允许迁移被添加到数据库中,或者回滚。
应用迁移
由于我们在第三章中配置了数据库的多租户模式,因此我们不需要手动更新每个数据库。然而,如果你发现你需要手动更新数据库,你可以使用以下方法:
dotnet-ef database update --startup-project ../GoodHabits.Api/GoodHabits.HabitService.csproj
发出此命令将指示 Entity Framework 在Migrations文件夹中查找,并将找到的迁移与数据库中现有的迁移进行比较。如果有任何尚未应用到数据库的额外迁移,Entity Framework 将应用这些迁移。让我们更详细地看看我们创建的第一个迁移,名为initial:
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
#pragma warning disable CA1814 // Prefer jagged arrays over
// multidimensional
namespace GoodHabits.Database.Migrations
{
/// <inheritdoc />
public partial class InitialSetup : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder
migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Habits",
columns: table => new
{
Id = table.Column<int>(type: "int",
nullable: false)
.Annotation("SqlServer:Identity",
"1, 1"),
Name = table.Column<string>(type:
"nvarchar(max)", nullable: false),
Description =
table.Column<string>(type:
"nvarchar(max)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Habits",
x => x.Id);
});
migrationBuilder.InsertData(
table: "Habits",
columns: new[] { "Id", "Description",
"Name" },
values: new object[,]
{
{ 100, "Become a francophone",
"Learn French" },
{ 101, "Get really fit",
"Run a marathon" },
{ 102, "Finish your book project",
"Write every day" }
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder
migrationBuilder)
{
migrationBuilder.DropTable(
name: "Habits");
}
}
}
此代码由迁移工具自动生成,但如果有必要,可以手动调整此处的代码。我们在这里可以看到两个方法。一个叫做Up,另一个叫做Down。
Up方法在数据库中创建表,而Down方法删除表。这被转换为 SQL 代码,当发出database update命令时,将向数据库引擎发出。
摘要
我们在本章中涵盖了大量的内容!
我们了解到数据在 SaaS 应用程序中非常重要。这不仅从技术角度来看是如此,而且从用户的角度以及构建应用程序的组织角度来看也是如此。
然后,我们转向使用 Entity Framework 的技术实现,展示了如何使用 C#在代码中构建数据库,然后自动生成迁移并更新数据库。
我们还讨论了测试策略和在生产中维护数据库。
在下一章中,我们将构建 API 层并开始与之前创建的数据结构进行交互。应用程序将开始成形!
进一步阅读
-
什么是 Code-First?:
www.entityframeworktutorial.net/code-first/what-is-code-first.aspx -
使用 Entity Framework 进行测试:
www.michalbialecki.com/en/2020/11/28/unit-tests-in-entity-framework-core-5/ -
数据驱动文化:
www.smartkarrot.com/resources/blog/data-driven-culture/ -
数据库规范化:
www.sqlshack.com/what-is-database-normalization-in-sql-server/
问题
-
商业如何货币化 SaaS 应用程序中存在的数据?
-
数据货币化有哪些伦理考量?
-
外键关系在 Entity Framework 中是如何表示的?
-
单元测试和集成测试之间有什么区别?
-
如果我想在实时数据库中回滚迁移,我如何确保不丢失任何数据?
第五章:构建 RESTful API
在当今以在线为中心的数字景观中,应用程序编程接口(APIs)在软件即服务(SaaS)应用程序的开发中变得无处不在。它们允许不同的系统和应用程序相互通信并共享数据。在众多类型的 API 中,表征状态转移(REST)API 已成为最广泛使用和接受的标准,这也是本章我们将关注的重点。
本章将向您介绍构建 RESTful API 的基础以及指导其设计的核心原则。您将了解 RESTful API 的关键组件,例如资源、表示和主要的 HTTP 动词(GET、POST、PUT、PATCH和DELETE)。
此外,您还将了解各种版本 RESTful API 的策略,例如 URL 版本控制、自定义头版本控制、媒体类型版本控制和弃用及停用。
本章涵盖的主要主题如下:
-
什么是 RESTful API?
-
将 API 操作与 HTTP 动词匹配
-
使用 REST 设计得更好
-
公共 API 版本控制
-
测试 API
到本章结束时,您将对构建 RESTful API 的关键原则和策略有扎实的理解,并且将准备好设计、开发和测试它们。
技术要求
本章中所有代码均可在github.com/PacktPublishing/Building-Modern-SaaS-Applications-with-C-and-.NET/tree/main/Chapter-5找到。
什么是 RESTful API?
REST 是由 Roy Fielding 在 2000 年于加州大学欧文分校的博士论文中提出的。在他的论文中,Fielding 定义了构成 RESTful 系统基础的架构约束,并描述了如何使用 REST 来构建可扩展和灵活的 Web 服务。他在论文中概述的概念自那时起已被广泛采用,并作为构建许多现代 Web API 的基础。
RESTful API 是一种基于 Web 的接口,允许不同软件系统之间的通信。它们利用 REST 架构定义的标准约束和原则,在客户端和服务器之间交换数据。资源通过唯一的 URL 进行标识,对这些资源的行为由使用的 HTTP 方法定义。RESTful API 通常用于构建可扩展和灵活的 Web 服务,可以以不同的格式返回数据,如 JSON 或 XML。它们为不同的软件系统在互联网上交互和交换数据提供了一种简单灵活的方式。
让我们来分解一下 REST 这个缩写的含义!
表示指的是 RESTful API 中每个资源都由一个唯一的标识符(如 URL)表示,并且可以用多种格式表示,例如 JSON 或 XML。资源的表示是其当前状态的快照,客户端可以使用它来操作资源。
你可以将资源视为一个对象,例如系统中用户描述。用户通常会有一个唯一的 ID,用于引用该用户。在 REST 系统中,ID 为 123 的用户资源可以通过以下 URL 表示:
https://www.a-system.com/api/v1/users/123
用户可以通过使用此 URL 检索、修改或删除。该 URL代表在消费PAI的任何外部系统上的用户。
GET、POST、PUT和DELETE。
如果你向前面的虚拟 URL 发出GET请求,你会收到该 URL 所表示对象的状态。
传输指的是将资源的表示从服务器传输到客户端,反之亦然。传输通常通过 HTTP 协议执行,并基于无状态和统一资源标识的原则。在 RESTful API 中,状态的传输用于在服务器上创建、读取、更新和删除资源。
RESTful API 不一定要通过 HTTP 进行通信,尽管它们通常是这样。它们可以使用任何其他可能的通信协议,例如远程过程调用(RPCs)。然而,绝大多数 RESTful API 使用 HTTP 作为选择的通信机制,这是我们本章将考虑的全部内容。如果你有使用替代通信协议的用例,那么我希望本章中的信息在更广泛的意义上是有用的!
在我们深入探讨构建 RESTful API 的细节之前,有一些一般性的观点需要考虑,这将有助于我们理解随后将出现的某些更复杂的概念。
幂等性
在 RESTful API 的上下文中,幂等性是 API 端点的一个属性,它允许多个相同的请求产生与单个请求相同的效果。这意味着,无论相同的请求被发出多少次,最终结果都应该是相同的。
幂等性请求无论执行多少次,都会从服务器产生相同的响应。这个属性在向同一端点发出多个请求时非常有用,尤其是在处理网络连接问题或其他类型的故障时,可以减少错误和冲突的可能性。
考虑到幂等的 HTTP 方法最常见的是GET、PUT、DELETE和某些类型的POST请求。另一方面,非幂等的方法,如未指定幂等语义的POST,如果多次重复执行,可能会产生意外的副作用。
这意味着你可以根据需要多次从 URL 获取资源,并且每次的响应都将相同。GET 请求是幂等的。
安全性
在 RESTful API 的上下文中,“安全”操作是指保证不会修改服务器状态或产生任何副作用的操作。安全操作是只读的,并且不会更改服务器上的任何资源。
最常见的安全操作示例是 GET 请求,它从服务器检索数据而不更改它。其他安全操作可能包括 OPTIONS、HEAD 以及一些专门设计为仅检索数据而不更改服务器状态的 POST 请求类型。
安全操作与“不安全”操作(如 PUT、POST 和 DELETE)相对,这些操作会修改服务器状态并可能产生副作用。这些操作被认为是“不安全”的,因为如果执行不当,可能会产生意外的后果,例如删除重要数据或以意想不到的方式更改资源。
HTTP 状态码
HTTP 状态码是服务器在响应客户端请求时返回的三位数。这些代码指示请求的结果,并提供有关请求资源状态的信息。
HTTP 状态码有很多,我只会简要地引用我认为对构建 RESTful API 最适用的那一组。不必担心记住所有这些!正如你将看到的,当我们开始构建示例时,哪些应该使用以及何时使用是非常直观的!而且记住,像这样的信息永远只在 Google 搜索的一步之遥!
每个状态码是一个三位数。第一位数字给出了状态码的类别。有五种类别代码,每种都有特定的含义和目的。具体如下:
-
100 继续进行。 -
200 OK和201 已创建。 -
301 永久移动和302 找到。 -
400 错误请求和401 未授权。 -
500 内部服务器错误和503 服务不可用。
这些类别中有许多状态码。其中一些常见且适用于 RESTful API 的状态码如下:
-
200 OK: 请求成功,并返回了请求的资源。这个状态通常在成功的GET、PUT或PATCH请求中返回。 -
201 已创建: 请求成功,并且因此创建了一个新的资源。这个状态码通常作为成功POST请求的结果返回。 -
204 无内容: 请求成功,但没有返回任何资源。这个状态通常在成功的DELETE请求中返回。 -
400 错误请求: 请求格式不正确或无效。在 RESTful API 中,请求通常以 JSON 格式。这意味着对象不符合 API 的预期。 -
401 未授权: 请求需要身份验证,但客户端没有提供有效的凭据。 -
403 禁止访问: 客户端没有访问请求资源的权限。 -
404 Not Found:请求的资源找不到。请求正在寻找一个不存在的资源。 -
405 Method Not Allowed:请求方法(如GET、POST和DELETE)在服务器上不允许对请求的资源进行操作。 -
500 Internal Server Error:服务器上发生了未指定的错误。这个状态码是一个“通配符”错误,用于让用户知道服务器上出了问题——例如,后端代码中可能发生了异常。 -
503 Service Unavailable:服务器目前无法处理请求,可能是由于维护或流量过高。
有许多其他 HTTP 状态码可以使用,每个都有其特定的含义和目的。我们将在本章后面的示例中利用这些代码。
处理错误
当涉及到处理 RESTful API 上的错误代码时,实施一个清晰且一致的方法非常重要。HTTP 状态码是这个方法的一个基本部分,通过正确使用状态码,API 的客户端能够理解发生了什么问题,并会有一个关于为什么发生问题的想法。
除了正确使用状态码外,提供清晰且信息丰富的错误消息也有帮助。这些消息应以易于理解的方式解释发生了什么问题,并在可能的情况下,提供解决问题的指导。
这将帮助 API 的用户,但 API 的开发者当出现错误时也应得到通知,以便他们可以采取措施解决问题或防止再次发生。由于开发者无法监控每个 API 交互,这通常是通过日志记录来完成的。
日志记录是指捕获和记录有关 API 行为和性能的信息,并将这些信息持久化存储在数据存储中,以便以后可以搜索以识别问题和排除故障。日志记录是任何 API 运营基础设施的重要组成部分,因为它记录了系统上发生的事情。
本章将重点介绍 API 实现,但我们没有忘记日志记录和监控——我们将在第九章中详细介绍这两者!
JSON 数据格式化
虽然 RESTful API 不必使用 JavaScript 对象表示法(JSON)格式,但它是一个非常流行的选择,并将成为本书中使用的格式标准。JSON 是一种轻量级的数据交换格式,易于人类阅读和编写,同时也易于机器解析和生成。它是完全语言无关的,但它确实使用了与 C 家族语言(C、C++、C#、Java 等)熟悉的约定。
以下是一些以 JSON 格式表示的信息示例:
{
"name": "Roger Waters",
"age": 79,
"isBassist": true,
"numbers": [90, 80, 85, 95],
"address": {
"street": "123 Main St",
"city": "A Town",
}
}
JSON 数据采用键值对的形式,其中每个键都是字符串,每个值可以是字符串、数字、布尔值、null、数组或另一个 JSON 对象。嵌套 JSON 对象的能力允许以这种方式表示复杂类型。
键始终是字符串,因此它们被双引号包围。如果值是字符串,则被引号包围;如果是数组,则被方括号包围;如果是对象,则被花括号包围。所有这些都在前面的代码片段中展示。
我们已经确定 JSON 编码的数据将通过 HTTP 发送和接收。接下来,我们将探讨这些数据是如何传输的,查看最常用的 HTTP 动词,并描述如何以及何时使用它们。
将 API 操作与 HTTP 动词匹配
在语言中,动词是一个“动作”词。它描述了一个动作、状态或事件。在英语中,动词的例子包括“run”(跑)、“think”(思考)、“sing”(唱歌)和“do”(做),以及成千上万的更多!
HTTP 动词描述了您可以通过 HTTP “执行”的事情!使用了五个主要动词——GET、POST、PUT、PATCH 和 DELETE。每个动词都有不同的用途,尽管每个动词的确切用途并没有严格定义,而且不小心使用错误的动词并不少见。在本节中,我们将介绍五个常用 HTTP 动词的用法,并给出它们在我们演示应用程序中使用的示例。
GET
GET HTTP 动词用于从服务器检索资源。当客户端向服务器发送 GET 请求时,服务器通过返回请求的资源来响应客户端。资源可以是任何类型的数据,例如网页、图像或文件。GET 动词是使用最广泛的 HTTP 动词,被认为是安全且幂等的,这意味着它可以多次调用而不会产生任何副作用。它还可以缓存,这意味着响应可以存储在缓存中并重复使用以提高性能。GET 动词应仅用于检索信息,绝不应用于在服务器上做出更改。
当涉及到 RESTful API 时,GET 动词用于从服务器检索资源或资源集合的表示。资源可以通过 GET 请求识别,也可以包括查询参数,这些参数可以用于过滤结果或指定返回数据的格式。服务器通过以 HTTP 响应的形式返回资源的表示以及适当的 HTTP 状态码来响应 GET 请求。GET 请求最常见的状态码是 200 OK,表示请求成功且资源已返回。
POST
POST HTTP 动词用于将实体提交给由 URI 标识的资源进行处理。通常情况下,POST 请求用于创建新的资源或(有时)更新现有的资源。POST 动词不是幂等的,这意味着它的效果可能因调用次数的不同而不同。它也不是安全的,意味着它可以修改服务器上的资源。
当客户端向 RESTful API 发送 POST 请求时,通常情况下,服务器会使用请求体中提供的数据创建新的资源,并返回一个包含状态码的响应,指示请求的结果。对于成功的 POST 请求,最常见的状态码是 201 Created,表示新资源已成功创建。新创建资源的 URI 通常包含在响应头中,以便客户端应用程序可以立即检索和处理新创建的资源。POST 请求中的数据可以是任何格式,如 JSON 或 XML,但通常使用 JSON 格式。
DELETE
DELETE HTTP 动词用于从服务器中删除资源。DELETE 请求用于从服务器中删除指定的资源。DELETE 动词是幂等的,这意味着它可以多次调用而不会产生任何副作用。它也不是安全的,意味着它可以修改服务器上的资源。
当客户端向 RESTful API 发送 DELETE 请求时,服务器会删除指定的资源,并返回一个包含状态码的响应,指示请求的结果。对于成功的 DELETE 请求,最常见的状态码是 204 No Content,表示资源已成功删除。客户端通常不会在响应体中收到任何内容,只有状态码。DELETE 请求通常需要在请求中指定资源的 URI,以便服务器能够识别要删除的资源。
值得注意的是,一些 RESTful API 可能不允许 DELETE 请求,如果收到 DELETE 请求,将返回 405 Method Not Allowed 状态码。
PUT
PUT HTTP 动词用于更新现有资源或(如果不存在)创建新资源。PUT 请求用于提交要更新或创建的资源表示。资源的表示包含在请求体中,通常以 JSON 或 XML 等格式编码。PUT 动词是幂等的,这意味着它可以多次调用而不会产生任何副作用。它也不是安全的,意味着它可以修改服务器上的资源。
当客户端向 RESTful API 发送 PUT 请求时,服务器使用请求体中提供的数据更新指定的资源,并返回一个包含状态码的响应,指示请求的结果。成功的 PUT 请求最常见的状态码是 200 OK,表示资源已成功更新。如果创建了新的资源,将返回 201 Created 状态码。更新资源的 URI 通常包含在响应头中。
值得注意的是,PUT 请求可能要求客户端在请求体中发送资源的完整表示,包括所有属性,即使只有少数属性需要更新。这可能会使 PUT 请求在带宽使用方面效率低下,可能更好的选择是使用 PATCH 动词。
PATCH
PATCH HTTP 动词用于在服务器上部分更新现有的资源。PATCH 请求用于提交要应用于指定资源的更改集,而不是替换整个资源。更改集通常以 JSON 或 XML 等格式编码,并包含在请求体中。PATCH 动词是幂等的,这意味着它可以多次调用而不会产生任何副作用。它也不是安全的,这意味着它可以修改服务器上的资源。
在 RESTful API 的上下文中,PATCH 动词通常用于在服务器上部分更新现有的资源。当客户端向 RESTful API 发送 PATCH 请求时,服务器将请求体中提供的更改应用于指定的资源,并返回一个包含状态码的响应,指示请求的结果。成功的 PATCH 请求最常见的状态码是 200 OK,表示资源已成功更新。更新资源的 URI 通常包含在响应头中。PATCH 请求中的数据可以是任何格式,例如 JSON 或 XML。
值得注意的是,PATCH 请求要求客户端发送要应用于资源的特定更改集,而不是资源的完整表示。这使得 PATCH 请求在部分更新方面比 PUT 请求更轻量级和高效。
数据传输对象
在与 API 一起工作时,另一个需要理解的重要概念是数据传输对象(DTOs)。DTO 是一种常用的设计模式,用于在层或系统之间传输数据。在 RESTful API 的情况下,这通常是用于将后端(API)的数据传输到前端用户界面(UI)。DTO 的一般目的是将数据的结构与其使用的底层系统解耦,从而提供更大的灵活性和更易于维护。它们还提供了一种标准化的数据处理方式,使得系统中的不同组件之间的通信和交换信息变得更加容易。
DTOs 在 RESTful API 中特别有用,因为它们提供了一个在客户端和服务器之间发送和接收请求时表示数据的标准方式。在设计 RESTful API 时,使用 DTOs 允许 API 定义交换数据的结构,而无需将 API 的实现与数据的结构紧密耦合。这种解耦使得 API 更容易进化,并且可以在不影响 API 客户端的情况下对底层数据模型进行更改。此外,使用 DTOs 可以使 API 修改返回的数据以更好地满足客户端的需求,减少通过网络传输的数据量,并提高性能。此外,DTOs 还可以用来验证客户端和服务器之间传递的数据,确保只接受和处理有效数据。
在本章中,我们将看到的第一个 DTO 集将非常类似于我们在 第三章 和 第四章 中为数据库定义的实体类型,并且它们将与我们可能希望对数据库执行的操作相关。例如,以下实体类型代表了数据库中的 Habit:
[Index(nameof(Id), nameof(UserId))]
public class GoodHabit : IHasTenant
{
public int Id { get; set; }
public string Name { get; set; } = default!;
public int UserId { get; set; }
public virtual User User { get; set; } = default!;
public virtual ICollection<Progress> ProgressUpdates {
get; set; } = default!;
public virtual ICollection<Reminder> Reminders { get;
set; } = default!;
public virtual Goal Goal { get; set; } = default!;
public Duration Duration { get; set; }
public string TenantName { get; set; } = default!;
}
假设我们想要创建一个简单的 Habit,它只包含一个已填充的 Name 属性,并且与某个 User 相关联。我们可以发送以下 DTO:
public class CreateHabitDto {
public string Name { get; set; }
public int UserId { get; set; }
}
这可以被后端用来在数据库中创建一个简单的 GoodHabit 对象。
如果我们只想检索具有名称和 ID 属性的 GoodHabit 对象,我们可以使用一个看起来像这样的 DTO:
public class GetGoodHabitDto {
public int Id { get; set; }
public string Name { get; set; }
}
如果我们需要比仅仅名称和 ID 更多的信息,我们可以进一步定义另一个看起来像这样的 DTO:
public class GetGoodHabitDetailDto {
public int Id { get; set; }
public string Name { get; set; }
public string UserName { get; set; }
public string GoalName { get; set; }
public string Duration { get; set; }
}
您可以看到我们如何从一个具有大量数据库特定信息的实体类型开始,并且我们可以根据不同的用例选择性地以不同的方式建模这些数据。
我们将在本章的后面通过示例来说明这一点!
使用 REST 设计得更好
使用 RESTful API 进行良好的设计对于创建易于使用和理解的 API 至关重要。REST 的一个关键原则是它基于资源及其表示的使用。因此,设计 API 的资源和它们的表示方式,使其有意义且一致是至关重要的。
在设计资源时,使用有意义的和一致的 URI 非常重要。资源应该以易于理解的方式命名,并且 URI 应该以逻辑和层次结构的方式组织 – 例如,/users/1/orders/2 比起 /users?id=1&orderid=2 更有意义。
资源的表现形式应该是一个易于解析的格式,例如 JSON 或 XML。同时,使用 Content-Type 和 Accept 头部来指定请求和响应的格式也很重要。这允许客户端指定其首选的格式,服务器提供适当的表示。
良好的 RESTful API 设计的另一个重要方面是无状态通信。这意味着客户端和服务器在请求之间不应保持任何状态。这可以通过使用 HTTP 头和 cookie 来实现。这允许更高的可扩展性和灵活性,因为服务器不需要为每个客户端维护任何状态。
安全性也是 RESTful API 设计的一个重要方面。使用安全的通信协议,如 HTTPS,以及实现身份验证和授权机制来保护 API 的资源是很重要的。
除了上述要点外,良好的 RESTful API 设计还包括错误处理、缓存、版本控制和文档。良好的文档对于开发者理解如何使用 API 是必不可少的。当发生错误时,向客户端提供清晰且信息丰富的错误消息也很重要。在前一章中,我们建立了一个数据库。在本章中,我们将现在添加一个 API 来与我们所构建的数据库交互。我们将使用一个非常典型的 SaaS 应用程序层结构,看起来像这样:

图 5.1 – 层次结构
在前面的图中,我们可以看到我们之前章节中提到的数据库和对象关系映射(ORM)。在本章中,我们将学习关于 API 的内容,因此我们将构建一个服务层来与数据库交互,以及一个控制器层来处理与 UI 或任何其他客户端的通信。
服务层是软件设计中的一个架构模式,它充当应用程序的 UI 和底层数据存储之间的中介。服务层的主要目的是封装和抽象应用程序的业务逻辑,促进关注点的分离,并使代码的维护和修改更容易。它还允许更好的单元测试,因为服务层可以在与应用程序的其他部分隔离的情况下进行测试。此外,它可以通过允许 UI 和数据存储组件独立演变来提高应用程序的可扩展性。
在 RESTful API 中,控制器负责处理来自客户端的 HTTP 请求并返回适当的 HTTP 响应。它们充当客户端和应用程序业务逻辑之间的中介,使用适当的服务层执行任何必要的操作。
控制器负责将 URL 映射到应用程序中的特定操作,例如从数据库检索数据、创建新资源或更新现有资源。它们解析传入的请求以确定所需操作,然后使用适当的服务层执行该操作并生成响应。
一个示例 API 设计
你可能会记得,在 第三章 中,我们开始构建 HabitService API,并添加了一些端点。我们将从 第三章 中我们停止的地方开始,但我们将向控制器添加更多功能!
我们已经添加的三个端点是以下这些:
一个 GET 端点,根据传入的 ID 获取单个习惯:
public async Task<IActionResult> GetAsync(int id) => Ok(await _habitService.GetById(id));
另一个 GET 端点,用于返回所有习惯:
public async Task<IActionResult> GetAsync() => Ok(await _habitService.GetAll());
最后,一个用于在数据库中创建新习惯的 POST 端点:
public async Task<IActionResult> CreateAsync(CreateHabitDto request) => Ok(await _habitService.Create(request.Name, request.Description));
在本节中,我们将为本章讨论的五个主要 HTTP 动词中的每一个添加一个端点。我们已经有 GET 和 POST,所以我们将添加 PUT、PATCH 和 DELETE。
DTOs
但是,在我们编写端点之前,我们首先会添加 DTOs。我们已经在 第三章 中添加了 CreateHabitDto。从根目录运行以下脚本,或者手动添加文件:
cd GoodHabits.HabitService/Dtos; \
touch HabitDetailDto.cs; \
touch HabitDto.cs; \
touch UpdateHabitDto.cs; \
cd ..;
将以下内容复制到 HabitDetailDto 类中:
namespace GoodHabits.HabitService.Dtos;
public class HabitDetailDto {
public int Id { get; set; }
public string Name { get; set; } = default!;
public string UserName { get; set; } = default!;
public string GoalName { get; set; } = default!;
public string Duration { get; set; } = default!;
}
然后将以下内容添加到 HabitDto 类中:
namespace GoodHabits.HabitService.Dtos;
public class HabitDto
{
public int Id { get; set; } = default!;
public string Name { get; set; } = default!;
public string Description { get; set; } = default!;
}
最后,将以下内容添加到 UpdateHabitDto 类中:
namespace GoodHabits.HabitService.Dtos;
public class UpdateHabitDto
{
public string Name { get; set; } = default!;
public string Description { get; set; } = default!;
}
对于 DTOs,这就足够了。当我们开始构建端点时,我们将使用这些。
AutoMapper
我们现在需要考虑一个工具,我们将使用它来在数据库类型和 DTOs 之间进行转换。这个工具就是 AutoMapper。
AutoMapper 是一个开源库,它允许你建立配置以将一个对象转换为另一个对象。这在将不同类型的对象(如数据库实体和 DTOs)之间进行转换时尤其有用。尽管 DTO 和实体可能具有相似的结构,但它们的实现可能不同。这个库通过减少在不同类型之间转换所需的重复和样板映射代码量,有助于保持你的代码库整洁和可维护,使你的应用程序更高效,更容易修改或添加新功能。
AutoMapper 被设计成简化将一种类型的对象映射到另一种类型的过程,并提供了一个简单、流畅的 API 来定义映射。AutoMapper 的一些关键特性包括:
-
支持对象层次结构的展开和折叠
-
支持在不同数据类型和自定义类型转换器之间进行转换
-
支持高级配置选项,例如映射到和从接口以及继承层次结构
-
支持在映射过程中使用自定义逻辑和约定
-
支持使用 语言集成查询(LINQ)表达式来定义映射
AutoMapper 可以通过减少在不同类型之间转换所需的重复、样板映射代码量,帮助你保持代码库整洁和可维护。这可以使你的应用程序更高效,并使添加新功能或修改现有功能变得更加容易。
要开始使用 AutoMapper,请在 API 项目中使用以下命令安装工具:
dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection
如前代码片段所示,更新了包后,我们可以开始为创建的数据库类型创建映射。
您会记得我们在数据库项目中添加了一个名为“Habit”的实体类型,在 第三章 中,我们还在 第四章 中为该对象添加了多个附加属性。如果您运行 HabitService 并使用 Thunder Client 打击返回所有习惯的端点,您将看到返回的数据包括所有这些附加属性。
这个习惯类代表了一个数据库实体。它非常具体于数据库,并且非常适合代表特定用例中良好习惯的概念。但是,它不适合将数据传输到 UI。
我们更希望数据以 DTO 的形式发送到 UI,就像我们之前创建的那样。
-
没有必要包括进度更新或提醒的集合。包含这些信息可能会大大增加应用程序所需的带宽。
-
TenentName属性对用户没有用处,因为他们已经知道他们属于哪个租户!
我们创建的 DTO 看起来是这样的:
namespace GoodHabits.HabitService.Dtos;
public class HabitDto
{
public int Id { get; set; } = default!;
public string Name { get; set; } = default!;
public string Description { get; set; } = default!;
}
在这个例子中,我们直接从实体类型中获取习惯的 ID、名称和描述,但更复杂的转换也是可能的。
虽然我们可以简单地手动复制属性,但这可能会很快变得非常乏味,所以我们将使用 AutoMapper 自动完成这项工作!
首先,进入 Program.cs 类并添加 AutoMapper 服务:
builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
接下来,打开 HabitsController 类,并在 using 语句中添加以下内容:using AutoMapper。
然后,在类定义中添加以下内容:
private readonly IMapper _mapper;
接下来,添加 using 语句:
using AutoMapper;
接下来,修改构造函数以接受映射器,如下所示:
public HabitsController(
ILogger<HabitsController> logger,
IHabitService goodHabitsService,
IMapper mapper
)
{
_logger = logger;
_habitService = goodHabitsService;
_mapper = mapper;
}
最后,修改控制器中现有的两个 GET 端点以使用 AutoMapper,如下所示:
[HttpGet("{id}")]
public async Task<IActionResult> GetAsync(int id) =>
Ok(_mapper.Map<HabitDto>(await
_habitService.GetById(id)));
[HttpGet]
public async Task<IActionResult> GetAsync() =>
Ok(_mapper.Map<ICollection<HabitDto>>(await
_habitService.GetAll()));
在之前,控制器只是简单地从数据库返回实体对象,现在该对象正在自动映射到一个 DTO,该 DTO 由控制器返回。但这确实需要一点配置。
最后一步是告诉 AutoMapper 如何在两种类型之间进行转换。
在 HabitService 项目中添加一个名为 Mappers 的文件夹和一个名为 HabitMapper.cs 的类。您可以使用以下脚本:
mkdir Mappers; \
cd Mappers; \
touch HabitMapper.cs; \
cd ..;
在这个类中,添加以下内容:
using AutoMapper;
using GoodHabits.HabitService.Dtos;
using GoodHabits.Database.Entities;
namespace GoodHabits.HabitService.Mappers;
public class HabitMapper : Profile
{
public HabitMapper()
{
CreateMap<Habit, HabitDto>();
}
}
CreateMap 方法指示 AutoMapper 在两种类型之间进行映射。
现在,您可以使用 Thunder Client 打击端点以获取习惯,您应该看到如下内容:

图 5.2 – 成功的响应
本节展示了我们如何自动在数据库类型和数据传输类型之间进行转换。这是 API 拼图中的一个非常关键的组成部分,理解如何操作 AutoMapper 将帮助你编写更好的代码,并减少 API 和任何连接的客户端之间传输的数据量。
修改服务
在我们可以在 API 上构建更新和删除习惯的额外端点之前,我们需要在服务类中添加一些功能。我们已经在第三章中创建了服务类和接口,但我们将在这里扩展其功能。
将以下内容添加到接口中:
using GoodHabits.Database.Entities;
using GoodHabits.HabitService.Dtos;
namespace GoodHabits.HabitService;
public interface IHabitService
{
Task<Habit> Create(string name, string description);
Task<Habit> GetById(int id);
Task<IReadOnlyList<Habit>> GetAll();
Task DeleteById(int id);
Task<Habit?> UpdateById(int id, UpdateHabitDto request);
}
实现前面接口的HabitService类需要添加两个方法来删除和更新存储在数据库中的习惯。将以下两个方法添加到HabitService类中:
public async Task DeleteById(int id)
{
var habit = await _dbContext.Habits!.FindAsync(id)
?? throw new ArgumentException("User not found");
_dbContext.Habits.Remove(habit);
await _dbContext.SaveChangesAsync();
}
public async Task<Habit?> UpdateById(int id,
UpdateHabitDto request)
{
var habit = await _dbContext.Habits!.FindAsync(id);
if (habit == null) return null;
habit.Name = request.Name;
habit.Description = request.Description;
await _dbContext.SaveChangesAsync();
return habit;
}
你还需要在服务类中添加一个using语句:
using GoodHabits.HabitService.Dtos;
服务层中所需的所有内容都已添加。
这已经有很多配置了,但我们现在准备好构建控制器类。
添加到控制器
我们已经通过添加 DTOs、配置 AutoMapper 和构建服务层完成了大部分繁重的工作。我们需要在控制器中添加三个额外的端点。让我们从DELETE端点开始:
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteAsync(int id)
{
await _habitService.DeleteById(id);
return NoContent();
}
这非常直接。它使用服务方法来删除数据库中的条目,然后返回NoContent——这是删除方法的最佳实践。
接下来,添加使用PUT动词更新对象的端点:
[HttpPut("{id}")]
public async Task<IActionResult> UpdateAsync(int id,
UpdateHabitDto request)
{
var habit = await _habitService.UpdateById(id,
request);
if (habit == null)
{
return NotFound();
}
return Ok(habit);
}
这里有一些错误处理,如果客户端尝试更新一个不存在的条目,它将返回404。
最后,添加使用PATCH动词更新对象的端点:
[HttpPatch("{id}")]
public async Task<IActionResult> UpdateAsync(int id,
[FromBody] JsonPatchDocument<UpdateHabitDto> patch)
{
var habit = await _goodHabitsService.GetById(id);
if (habit == null) return NotFound();
var updateHabitDto = new UpdateHabitDto { Name =
habit.Name, Description = habit.Description };
try
{
patch.ApplyTo(updateHabitDto, ModelState);
if (!TryValidateModel(updateHabitDto)) return
ValidationProblem(ModelState);
await _goodHabitsService.UpdateById(id,
updateHabitDto);
return NoContent();
}
catch (JsonPatchException ex)
{
return BadRequest(new { error = ex.Message });
}
}
这部分内容稍微复杂一些,因为它使用了JsonPatchDocument来修改对象。你还需要添加两个using语句:
using Microsoft.AspNetCore.JsonPatch;
using Microsoft.AspNetCore.JsonPatch.Exceptions;
到目前为止,我们只需要做这些。现在我们有一个很好的例子,展示了五种最常见的 HTTP 动词。在我们继续之前,我们应该测试这些是否都正常工作。我们将使用 Thunder Client 进行测试。
测试
为了测试我们刚刚添加的端点,我们需要一个测试客户端。遵循使用Visual Studio Code(VS Code)的主题,我们将向代码中添加一个扩展,以便我们可以在一个地方完成所有操作。我们已经提到了这个工具几次,但我们将在本节中对其进行详细探讨。
你将在扩展工具栏上看到 Thunder Client 图标:

图 5.3 – Thunder Client 图标
使用 Thunder Client,你可以直接从 VS Code 中调用你的 API 并检查其是否按预期运行。我们现在就做这个。通过在 VS Code 的终端中启动 API,导航到 API 项目,并输入以下内容来启动 API:
dotnet run
这将构建项目并启动 API。现在我们可以开始添加测试了!
添加一个 GET 请求
现在,完成以下步骤:
-
点击 Thunder Client 图标(如果你在左侧菜单中看不到它,退出并重新启动* Docker 环境)。
-
点击
GoodHabits。 -
在新创建的集合上右键点击,点击
GET习惯(不要点击在新请求按钮下面的部分;右键点击集合)。
你的集合应该看起来像这样:

图 5.4 – Thunder Client 集合
- 点击带有
CloudSphere值的tenant键(你会记得从第三章中,我们需要为多租户指定租户)。
当你完成时,它应该看起来像这样:

图 5.5 – 配置的请求
前面的截图显示了一个正确配置的GET请求,它应该返回数据库中的所有习惯。
最后,点击GET请求并测试端点。你会看到以下内容:

图 5.6 – 返回的习惯
我们为了达到这个阶段投入了相当多的工作!我们正在展示数据库项目中SeedData文件的数据,这些数据是从我们的HabitsService返回的。我们很快将构建一个 UI 来展示这些信息。
添加一个 POST 请求
重复前面的步骤,构建一个POST用户请求。在这种情况下,我们需要在正文中以 JSON 格式指定习惯详情:

图 5.7 – 配置的 POST 请求
你可以看到指定的 JSON 与CreateHabitDto类匹配。
不要忘记在头部设置租户并将请求类型更改为POST!点击发送将确认习惯已被创建。
到目前为止,我们已经测试了get all端点和POST端点。添加另一个GET来测试get-by-id端点也是一个有用的练习!
添加一个 DELETE 请求
我们可能想要从数据库中删除一个习惯,因此我们已经向服务和控制器中添加了所需的方法。我们可以以同样的方式再次测试:

图 5.8 – 配置的 DELETE 请求
前面的截图显示在正文中不需要任何内容。但不要忘记添加租户头部!
添加一个 PUT 请求
测试我们添加的PUT端点是相当直接的。配置一个像这样的PUT请求:

图 5.9 – 配置的 PUT 请求
前面的图示显示了如何配置一个PUT请求。这将更改id=103的习惯的名称和描述。如果你在过程中对数据进行过更改,可能需要更改 URL 中的 ID。你可以通过再次点击get-by-id端点来检查是否已进行了更改。
添加一个 PATCH 请求
测试 PATCH 端点稍微有些棘手。您会记得,我们在控制器中构建的 PATCH 端点期望一个 JsonPatchDocument 对象,因此这就是我们必须提供的。一个 Patch 文档可能看起来像这样:
[
{
"op": "replace",
"path": "/Name",
"value": "A New Name"
}
]
上述代码使用 replace 操作符来更改 Name 变量的值。我们可以这样设置请求:

图 5.10 – 配置的 PATCH 请求
配置此设置并点击发送将更新具有 id=103 的习惯。再次提醒,您可能需要更改 URL 中的 ID。
您会注意到,在成功修补后,服务将返回 204 No Content 响应。这是预期的行为,并且被认为是成功 PATCH 响应的最佳实践。
这将是讨论更多关于 HTTP 状态码的好时机!
使用正确的 HTTP 状态码
在本章前面,我们讨论了 HTTP 状态码,并查看了一些可能对基本示例有用的状态码。
我们应该添加并测试的两个是以下内容:
-
如果请求获取一个不存在的用户,正确的状态码应该是
404未找到 -
如果成功请求创建一个新用户,正确的状态码应该是
201 Created
由于我们已经将服务和控制器分离,我们不需要更改任何服务逻辑来促进这一点。控制器负责分配 HTTP 状态码。虽然这里的例子相当直接,但我希望您能看出以这种方式分离逻辑在事情变得复杂时非常有好处。
我们将从修改 GET 方法开始,该方法接受一个 id 参数,如下所示:
[HttpGet("{id}")]
public async Task<IActionResult> GetAsync(int id)
{
var habit = await _habitService.GetById(id);
if (habit == null) return NotFound();
return Ok(_mapper.Map<HabitDto>(await
_habitService.GetById(id)));
}
我们只是添加了一个检查,看看用户对象是否为 null,如果是,则返回 NotFound(),这将返回 404 状态码。
您可以在 Thunder Client 中通过请求一个您知道在数据库中不存在的用户 ID 来测试这一点:

图 5.11 – 未找到习惯
在前面的屏幕截图中,我们演示了请求一个不存在的用户现在会导致 404 HTTP 状态码。
接下来,让我们修复创建新用户的 HTTP 状态码。将 Create 端点修改如下:
[HttpPost]
public async Task<IActionResult>
CreateAsync(CreateHabitDto request)
{
var habit = await _habitService
.Create(request.Name, request.Description);
var habitDto = _mapper.Map<HabitDto>(habit);
return CreatedAtAction("Get", "Habits", new { id =
habitDto.Id }, habitDto);
}
我们已经将返回值从 Ok() 更改为 CreatedAtAction(…)。这将返回 201 – Created 以及新创建的资源位置给用户。
如果您回到 Thunder Client 并创建另一个用户,您将看到以下内容:

图 5.12 – 使用 201 状态码创建
点击 Headers 选项卡将给出新创建资源的位置。这对希望立即与新资源交互的 API 消费者非常有用。
如果你查看HabitsController中的PUT和PATCH端点,你会看到我们根据处理响应时发生的情况返回几个不同的状态码。这是一个很好的练习,你可以通过这些来确信你已经理解了为什么选择了每一个。
如果你查看PATCH端点,你会看到它执行以下操作:
-
它会检查提供的 ID 是否有效,如果不是,则返回
404 –Not Found。 -
它会检查更新的模型是否有效,如果不是,则返回一个验证问题(
400Bad Request的一个子集)。 -
如果更新有任何其他问题,它返回
400Bad Request。 -
如果没有问题,它返回
204No Content。
使用No Content HTTP 状态码(204)来表示服务器已成功处理请求,并且没有响应体需要返回。在PATCH请求的情况下,No Content状态码用于表示服务器已成功处理对资源的更新,但在响应中未返回任何内容。其理念是客户端已经知道更新后的资源看起来是什么样子,因此没有必要在响应中返回更新后的资源信息。客户端可以简单地假设更新已成功,并且资源已按请求更新。
公共 API 的版本控制
版本控制公共 RESTful API 是创建和维护多个 API 版本的过程,以适应更改和新功能。这确保了现有客户端不会受到对 API 所做的更改的影响,并且新客户端可以利用新功能。
版本控制是 API 开发和维护的关键方面,因为它使得 API 随着时间的推移而演变,同时保持与现有客户端的兼容性。这在 API 被多个客户端使用的情况下尤为重要,因为破坏性更改会影响这些客户端的功能。通过版本控制,可以同时存在多个 API 版本,客户端可以选择升级到最新版本或继续使用满足其需求的早期版本。这样,版本控制为 API 随着时间的推移而演变和改进提供了必要的灵活性,而不会破坏现有集成的稳定性。
有几种策略可以用于版本控制 RESTful API,每种策略都有其自身的优缺点:
-
/v1/users或/v2/users。这种方法易于实现和理解,但随着版本数量的增加,可能难以维护和扩展。 -
X-API-Version。这种方法允许更大的灵活性,因为 URI 不需要改变,但它可能更复杂来实现,并且可能不被所有客户端支持。 -
application/vnd.example.v1+json或application/vnd.example.v2+json。这种方法提供了更大的灵活性,因为 URI 和头信息不需要更改,但实现起来可能更复杂,并且可能不被所有客户端支持。 -
弃用和停用:这种策略涉及将 API 的旧版本标记为弃用,并最终将其停用。这种方法允许逐步过渡,并为客户端提供时间在旧版本被移除之前更新他们的代码。
值得注意的是,最合适的版本控制策略将取决于 API 及其客户端的具体需求。向 API 的客户端传达版本控制策略和旧版本弃用的时间表,以最小化中断并允许他们相应地规划,这是非常重要的。
为 API 进行版本控制最常见的方式是将版本号包含在 API 端点的 URL 中。例如,API 端点的 URL 可能看起来像这样:
https://api.example.com/v1/resources
这种方法允许不同版本的 API 共存,并且通过简单地更改 URL 中的版本号,就可以轻松地管理 API 随时间的变化。这也允许客户端选择他们希望在应用程序中使用的 API 版本,并有助于防止 API 的破坏性更改影响现有的客户端应用程序。
如果创建了前面示例的第二版本,它可以在以下链接中找到:
https://api.example.com/v2/resources
这个好处是,两个版本可以同时存在,并且那些仍然期望v1版本的客户端用户可以无缝地继续工作。当然,支持多个版本可能很困难,理想情况下,这应该是一个过渡状态,目的是在某个时候弃用v1版本。
示例代码展示如何对 API 进行版本控制
在本章的前面部分,我们构建了一个控制器来管理用户,并向其添加了多个端点。尽管如此,我们还没有为 API 添加任何版本控制;请注意,我们使用 Thunder Client 测试的 URL 没有与它们关联的版本,如下所示:
http://localhost:5100/api/habits
让我们改变一下!
首先打开控制台,并将版本控制包添加到HabitService项目中:
dotnet add package Microsoft.AspNetCore.Mvc.Versioning
将using语句添加到Program.cs文件中:
using Microsoft.AspNetCore.Mvc.Versioning;
接下来,将以下内容复制到Program.cs文件中:
builder.Services.AddApiVersioning(opt =>
{
opt.DefaultApiVersion = new
Microsoft.AspNetCore.Mvc.ApiVersion(1,0);
opt.AssumeDefaultVersionWhenUnspecified = true;
opt.ReportApiVersions = true;
opt.ApiVersionReader = ApiVersionReader.Combine(new
UrlSegmentApiVersionReader(),
new HeaderApiVersionReader("x-api-version"),
new MediaTypeApiVersionReader("x-api-version"));
});
让我们详细回顾一下前面的代码:
-
第一个标志设置默认的 API 版本。这允许客户端在无需指定版本的情况下与 API 一起工作。
-
第二个标志指示应用程序在没有指定任何内容时使用默认版本。这是一个防御性编程的例子——你的用户会感谢你的!
-
第三个标志是返回选项——这会在响应头中返回可用的版本,以便调用客户端可以看到该方法有可用的选项。
-
最后,
ApiVersionReader使客户端能够选择是否将版本放在 URL 中或请求头中。再次强调,给 API 的消费者选择是很好的。
现在,我们需要更新HabitsController以支持多个版本。
为了说明这一点,我们将只使用一个简单的GET端点。但您可以将相同的逻辑应用于任何端点。
将HabitsController类的属性更改为以下内容:
[ApiController]
[Route("api/[controller]")]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("1.0")]
让我们通过向控制器添加一个端点并将其映射到version 1.0来证明我们所做的工作,如下所示:
[MapToApiVersion("1.0")]
[HttpGet("version")]
public virtual async Task<IActionResult> GetVersion()
{
return Ok("Response from version 1.0");
}
我们已将此方法标记为虚拟,以便我们可以在后续版本中覆盖它。
创建一个名为HabitsControllerv2.cs的文件,并将其添加以下内容:
using Microsoft.AspNetCore.Mvc;
namespace GoodHabits.HabitService.Controllers.v2;
[ApiController]
[Route("api/[controller]")]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("2.0")]
public class HabitsController : ControllerBase
{
[MapToApiVersion("2.0")]
[HttpGet("version")]
public virtual IActionResult GetVersion()
{
return Ok("Response from version 2.0");
}
}
注意,这将version端点映射到v2 API。您可以在 Thunder Client 中按常规方式测试它,您将看到更改 URL 中提供的版本将更改您收到的响应。
此外,请注意,我们已两次指定Route属性——一次包含版本,一次不包含。这允许在Program.cs中指定的默认版本生效。
在 Thunder Client 中运行三个测试——一个没有版本,一个有v1,一个有v2:
-
版本 1:
http://localhost:5100/api/v1/Habits/version -
版本 2:
http://localhost:5100/api/v2/Habits/version
您将看到第一个返回v1,因为这是默认版本,您将看到其他两个按预期执行。
您还应该注意,我们在 Thunder Client 中之前设置的所有请求继续按预期运行。从 API 消费者的角度来看,这是非常好的。我们刚刚引入了版本控制并添加了v2,而没有破坏任何现有功能!
测试 API
在本章中,我们广泛地展示了如何使用 Thunder Client 测试您的 API。测试 API(以及一般测试)是一个巨大的主题,可以成为一本单独的书的主题。如果您感兴趣,我在以下部分提供了一些进一步阅读的指南!
以下列表提供了一些测试类型示例,以确保您的 API 正常运行。单元测试涉及测试 API 的各个组件以确保它们按预期工作。这通常使用单元测试框架,如 NUnit,并且可以自动化:
-
功能测试涉及端到端测试 API 以确保所有组件都正确协同工作。这可以手动完成或使用自动化测试工具,如 Selenium 或 TestComplete。
-
集成测试涉及在与其他系统(如数据库或其他 API)结合的情况下测试 API。这可以通过使用集成测试框架,如 Cucumber 或 FitNesse 来完成。
-
性能测试包括测试一个 API 以确保其能够处理预期的负载并表现最佳。
-
安全测试包括测试一个 API 以确保其安全且不受常见安全威胁的影响,例如 SQL 注入或跨站脚本攻击。这可以通过使用安全测试工具,如 Nessus 或 OWASP ZAP 来实现。
-
可用性测试包括测试一个 API 以确保其易于使用和理解。这可以手动进行或使用可用性测试工具,如 UserTesting 或 Crazy Egg。
-
Postman 是测试 RESTful API 的流行工具。它允许开发者轻松创建、发送和分析 HTTP 请求。它具有用户友好的界面,支持各种功能,如请求和响应验证、环境变量和自动化测试。它还允许我们测试端到端场景,并且可以与其他工具如 Jenkins 集成。
值得注意的是,测试 RESTful API 是一个持续的过程,应该在开发过程中始终进行,而不仅仅是最后。这将有助于确保 API 按预期工作,并且任何问题都能迅速识别和解决。
在本章中,我们展示了如何在 VS Code 中使用 Thunder Client 测试 API。这是一个非常有用的工具,其优点是定义的测试被保存在仓库中,并与代码进行比对。
摘要
本章我们涵盖了很多内容!希望它没有让你感到不知所措!我们从 REST 的定义开始,然后介绍了 HTTP 状态码和 HTTP 动词,以提供一些关于 REST API 底层基础知识的背景。
然后,我们查看了一个示例,介绍了五个最重要的 HTTP 动词(GET、POST、DELETE、PUT和PATCH),并展示了我们如何在 VS Code 中直接使用 Thunder Client 构建和测试它们!
我们还研究了AutoMapper以及如何简化对象转换以从实体类型创建 DTO。
最后,我们通过一个示例演示了如何对 API 进行版本控制,并探讨了额外的测试技术。
在下一章中,我们将考虑微服务,并探讨如何将此应用程序拆分为多个较小的微服务!
进一步阅读
-
使用 HTTP 方法进行 RESTful 服务:
www.restapitutorial.com/lessons/httpmethods.html -
HATEOAS 及其在 RESTful API 中的必要性
www.geeksforgeeks.org/hateoas-and-why-its-needed-in-restful-api/ -
测试一个 API:
learning.postman.com/docs/designing-and-developing-your-api/testing-an-api/ -
如何在 ASP.NET Core Web API 中使用 API 版本控制并将其与 .NET 6 集成:
blog.christian-schou.dk/how-to-use-api-versioning-in-net-core-web-api/
问题
-
使用
PATCH而不是PUT的优势是什么? -
使用 AutoMapper 的好处是什么?
-
当创建新资源时应该使用哪个 HTTP 状态码?
-
HTTP 状态码的
500范围表示什么? -
REST 代表什么?
-
DTO 的用途是什么?
第六章:SaaS 应用程序的微服务
微服务是一种强大的架构模式,在现代软件开发中越来越受欢迎。微服务是将大型应用程序组织成更小、更易于管理和独立的子服务的方式,这些子服务可以使用定义良好的接口相互通信。随着公司寻求构建更具可扩展性和健壮性的软件系统,这种方法在近年来越来越受欢迎。随着现代应用程序复杂性的增加,传统的单体软件构建方法已不再足够。微服务提供了一种将复杂应用程序分解为更小、更易于管理、可独立开发、部署和维护的组件的方法。
实施微服务需要改变我们思考构建和部署软件的方式。使用微服务,我们摆脱了在单个应用程序中构建一切的单体方法,转而构建一系列较小的、独立的子服务,这些子服务可以独立部署和扩展。这种方法的转变使我们能够构建更灵活和有弹性的系统,因为我们能够独立更新、测试和部署单个服务。同时,我们需要确保我们的微服务能够高效地相互通信,并且我们有机制来处理分布式系统中的错误和故障。
本章将探讨构建微服务时的重要考虑因素,并提供指南,帮助您在软件开发项目中开始实施微服务。
本章将涵盖以下主要主题:
-
微服务及其用途
-
构建高性能和安全的微服务的最佳实践
-
如何使用我们学到的技能来处理微服务的 RESTful API
-
构建微服务时的常见陷阱以及如何避免它们
-
一些实用建议
技术要求
本章中所有代码均可在github.com/PacktPublishing/Building-Modern-SaaS-Applications-with-C-and-.NET/tree/main/Chapter-6找到。
什么是微服务以及为什么使用它们?
微服务是一种软件架构风格,它将应用程序结构化为一系列松散耦合的服务。与所有功能都打包在单个代码库中的传统单体架构不同,微服务将功能分解为更小、更独立的子服务,这些子服务可以独立开发、部署和扩展。
微服务架构最早在 2000 年代初被引入,近年来由于对更快开发周期、灵活扩展和改进应用程序弹性的需求增加而受到欢迎。微服务被视为快速向客户提供价值并具有更高敏捷性的方式,这使得它们非常适合寻求快速迭代和创新的组织。
微服务也为组织带来了多项好处。通过将应用程序分解为更小、更专注的服务,组织可以提升其开发流程的速度和效率。团队可以并行工作在更小、独立的服务上,减少延迟的风险,并确保每个服务都能以高质量交付。此外,微服务可以独立部署和扩展,使组织能够更快地响应需求变化并优化其应用程序的性能。
在学习微服务时,理解松散耦合和紧密耦合的底层概念非常重要。
松散耦合与紧密耦合的区别
在紧密耦合的系统中,组件之间相互依赖性很强,彼此之间关系紧密。这意味着一个组件的变化可以对系统中的其他组件产生重大且通常是破坏性的影响。紧密耦合的系统可能会迅速变得非常复杂且难以维护。系统某一部分的更改可能导致其他地方出现意外的后果。如果自动化测试有限,这可能会非常难以检测,并可能导致生产代码中的错误。
相比之下,松散耦合的系统具有独立组件,彼此之间的依赖性最小。这允许组件独立开发、部署和维护,对系统中的其他组件影响很小或没有影响。松散耦合使团队能够更高效、更敏捷地工作,因为对单个组件的更改对系统其他部分的影响很小或没有影响。虽然松散耦合听起来像是一个明显的选择,但在实践中,设计一个松散耦合的系统需要大量的额外工作。
微服务架构基于松散耦合的原则,即应用程序被分解为更小、更专注的服务,这些服务可以独立开发、部署和扩展。这使组织能够更快地响应需求变化,优化其应用程序的性能,并提高其开发流程的速度和效率。
SaaS 应用程序通常相当复杂。项目上通常会有一支相当大的团队在合作,每个团队都有自己的专业领域。通过将应用程序分解成更小、更专注的服务,团队可以并行工作,降低延迟风险并提高交付速度。这有助于组织更快地响应需求变化并向客户更快地交付价值。
微服务的另一个优点是更好的可伸缩性。依赖基于云的基础设施,每个服务都可以独立部署和扩展,使组织能够更快地响应需求变化并优化其应用程序的性能。这对于经常经历使用模式波动的 SaaS 应用程序尤为重要。当使用量高时,可以通过提供更多资源来提高应用程序的性能。当使用量低时,可以关闭一些资源,帮助组织管理其云计算成本并优化资源的使用。
微服务还应设计为具有高度弹性,这意味着即使一个服务失败,对整个系统的影响也最小。这使得微服务非常适合 SaaS 应用程序,因为停机时间可能会对客户满意度产生重大影响,并相应地影响提供应用程序的公司的收入。通过将应用程序分解成更小、更专注的服务,组织可以降低在系统更改时意外后果的风险,使维护和修改更容易,并降低停机风险。
微服务是有志于开发 SaaS 应用程序的组织的一个有价值的工具。它们提供了更高的敏捷性、更好的可伸缩性、改进的弹性、更简单的维护和更好的成本管理,使它们成为寻求快速迭代和创新的组织的理想选择。
Docker
使用GoodHabits服务,我们一直在使用 Docker 运行一个包含开发环境和 SQL 服务器数据库的devcontainer。这只是 Docker 可以使用的多种方式之一。在其核心,Docker 是一个可以在容器中运行进程的工具。你可以将容器想象成一个非常轻量级的虚拟机,通常运行 Linux 发行版。
Docker 经常成为构建微服务时非常重要的工具。一个微服务应用程序通常会有许多不同的组件,这些组件必须运行才能使整个系统运行。这可能包括使用不同的编程语言运行代码,以及针对多个不同的数据库平台运行。试图在开发机器上可靠地运行所有内容,并在多个云环境中运行,很快就会变成一场噩梦!
Docker 提供了一种高效且可靠的方式来在一系列网络容器中运行您的微服务。通过容器化您的微服务,您可以隔离它们,使它们更容易构建和运行。
此外,Docker 允许您轻松打包您的微服务和它们的依赖项,使您能够将服务部署到不同的环境中,包括在开发环境中运行。这有助于确保您的微服务能够在不同的系统上保持一致的工作,这对于微服务构建来说是至关重要的,尽管具有挑战性。
虽然 Docker 对于构建微服务并非绝对必要,但它作为最佳实践被高度推荐,以提高微服务部署的效率和可靠性。我们将在下一节中详细讨论一些更多最佳实践。
构建微服务的最佳实践
在构建微服务时,考虑最佳实践以确保您从构建微服务应用程序所需的额外工作中获得好处是非常重要的。系统应该是可扩展的、可维护的和有弹性的——这是额外努力的回报!以下是一些最重要的最佳实践或“原则”,您需要记住。
失败设计
微服务应该“设计为失败”。如果它们会失败(所有软件都会失败!),它们应该优雅地失败,内置冗余以确保即使一个服务失败,系统也能继续运行。
在设计失败时,一个重要的步骤是向您的系统添加冗余。这可以通过让每个服务的多个实例并行运行来实现,这样如果其中一个失败,其他实例可以继续运行。请注意,这将产生一些额外的成本,例如云托管成本。负载均衡有助于在服务的多个实例之间均匀分配负载,降低单个实例过载的风险,并在一个实例失败时帮助将负载重定向到另一个实例。
断路器在设计失败时也是另一个有用的工具。这些可以用来自动检测和隔离失败的服务,防止它们影响系统的其余部分。这使得即使一个服务失败,整体系统也有更大的可能性继续运行。
在接受“失败设计”原则时,幂等性也是至关重要的。这涉及到确保每个服务都是幂等的,意味着它可以多次执行并得到相同的结果。这允许您在服务失败时重试请求,从而降低数据丢失或不一致结果的风险。您会记得,我们在上一章学习某些 HTTP 动词时遇到了这个话题。这里的原理是相同的。
应该使用健康检查定期测试每个服务并确定其是否正常运行。然后,可以使用这些信息在原始实例失败时自动将请求重定向到该服务的其他实例(依赖于冗余原则)。这些健康检查应该自动运行或在定义的计划上运行,并在出现任何问题时立即通知团队。
专注于解耦
微服务应该是(按定义)松散耦合的,服务之间依赖性最小。这允许服务独立地进行开发、部署和修改,从而降低意外后果的风险。
解耦是一个核心原则,也是构建基于微服务系统的关键方面。解耦指的是在不同服务之间分离关注点,使它们能够独立运行并降低级联故障的风险。
必须有明确的服务边界——每个服务都应该有特定的责任和目的,不与其他服务重叠。这样做将有助于确保每个服务可以独立开发和部署,减少服务之间的相互依赖。
异步通信是解耦服务的重要方面。而不是直接通信,消息可以在服务之间发送并在稍后处理。这允许每个服务独立运行,降低阻塞和级联故障的风险。
在解耦服务时实施某种形式的版本控制非常重要。每个服务都应该有一个版本号,这可以允许系统中共存多个服务版本。这允许对服务进行更改,而不会影响其他服务,减少服务之间的相互依赖。我们已经在上一章中讨论了版本控制。
使用事件驱动架构是解耦服务的重要部分。事件可以在其他服务中触发动作,减少直接通信的需求。这允许每个服务独立运行,减少服务之间的相互依赖。这在基于事件的消息队列促进系统内各种服务之间的通信时经常看到。
最后,服务发现是解耦服务的有用工具。如果你考虑一个系统,该系统可能包含 20 个不同的松散耦合服务,这些服务以 Docker 容器的星系形式托管在云中,并且可能有一些服务的多个版本,跟踪它们所有运行的地点可能会变得非常具有挑战性。使用某种形式的服务发现允许系统自动检测并连接到其他服务,减少硬编码连接的需求。
接受“关注解耦”原则有助于构建一个健壮的基于微服务的系统。通过定义清晰的服务边界,使用异步通信,实现版本控制,使用事件驱动架构,以及考虑服务发现,你可以确保你的系统是可扩展的、灵活的并且具有弹性。
接受自动化
自动化对于微服务的有效运行至关重要,因为它有助于确保服务之间的一致性和可靠性。应尽可能使用自动化来提高测试(使用自动化测试套件)、部署(CI/CD)和扩展(可能使用 Terraform)。
自动化是所有类型软件开发中的一个非常重要的原则,但在构建利用微服务架构的 SaaS 应用程序时,这一点尤为重要。自动化部署和测试流程有助于减少人工工作量并加快将新功能交付给用户的速度。自动化的部署和测试流程确保服务的一致部署,并在开发周期早期发现任何问题。这有助于减少停机时间并提高系统的整体效率。
实施 CI/CD有助于确保代码更改自动构建、测试和部署。这有助于减少测试新功能所涉及的人工工作量,并尽可能快地将它们交付给用户。CI/CD 还有助于确保代码更改的一致部署,并在开发周期早期发现任何问题。使用管道自动构建、测试和部署微服务将使项目管理在项目开始增长时变得更加容易!
自动化监控和日志记录也是有用的。自动化监控和日志记录有助于早期发现问题并减少停机时间。自动化的监控和日志记录流程确保系统持续被监控,并且任何问题都能被早期发现,减少人工工作量并提高系统的整体效率。
在生产环境中,SaaS 应用程序可能会经历系统各部分需求量的快速波动。自动化可以促进自动扩展,以确保系统在无需人工干预的情况下处理增加的流量。自动扩展流程确保系统可以根据流量进行扩展或缩减,减少人工工作量并提高系统的整体效率。
接受“拥抱自动化”原则有助于构建一个健壮且高效的基于微服务的系统。自动化部署和测试流程、实施 CI/CD、自动化监控和日志记录以及自动化扩展流程有助于简化流程、减少人工工作量并提高系统的效率。
使用基于合同的开发
微服务应该有定义良好的合同,这些合同定义了服务之间的接口。这允许服务独立演进,同时仍确保兼容性。在这个背景下,“合同”指的是一个协议,它指定了服务之间的交互,包括每个服务的输入和输出细节、通信协议和数据格式。这个合同可以用各种形式表示,如 API 规范、消息格式或文档,并且应由所有参与构建和维护服务的团队达成一致。
基于合同的开发要求在服务之间定义清晰的合同。这些合同应定义每个服务的输入和输出,并确保服务按预期运行。这有助于降低破坏性变更的风险,并提高系统的整体稳定性。
就像应用的其他许多部分一样,测试非常重要。合同测试确保服务之间的合同得到测试并遵守,降低破坏性变更的风险,并提高系统的整体稳定性。
“使用基于合同的开发”原则有助于构建一个健壮且稳定的基于微服务的系统。在服务之间定义清晰的合同、测试合同、实施合同测试和自动化合同测试,有助于确保服务按预期运行并遵守定义的接口,降低破坏性变更的风险,并提高系统的整体稳定性。
主动监控和记录
微服务系统生成大量数据,因此实施一个健壮的监控和日志记录策略至关重要。这将帮助您检测和诊断问题——希望在此之前,这些问题不会对您的用户产生影响!
系统应持续自动监控,监控范围包括服务的整体健康状况、整个系统和每个微服务的响应时间以及资源利用率。
除了监控解决方案之外,还应该有一个日志记录机制。日志记录有助于跟踪系统的活动、检测问题以及排除任何故障。这种日志记录应包括记录请求、响应时间和任何错误消息。
使用集中式日志记录和监控有助于减少人工工作量并提高系统的效率。集中式日志记录和监控确保日志和监控数据存储在单一位置,这使得检测问题和故障排除变得更加容易。
如果系统从未报告任何问题,进行这种监控和记录就没有意义了!自动警报流程确保任何问题都能及早发现,并且适当的团队会得到通知,从而减少人工工作量并提高系统的整体效率。
监控、日志记录和警报有助于确保系统稳健高效。虽然它们确实会在开发过程中增加一点额外的工作量,这在系统运行顺畅时并不明显,但当事情不可避免地出错时,这些投入的时间是非常值得的!
实施安全
这一点虽然不言而喻,但微服务应该是安全的,应具备适当的身份验证、授权和加密协议。同时,拥有涵盖整个系统(包括网络、基础设施和自身服务)的安全策略也非常重要。
微服务的本质使得安全可能具有挑战性。经常会运行多个容器,运行各种不同的软件,每个软件都有自己的攻击向量。
接受“实施安全”原则是构建基于微服务系统的基本方面。安全有助于保护敏感信息,降低安全漏洞的风险,并确保数据的机密性和完整性。我们必须遵循一些步骤来接受这一原则。
实施身份验证和授权是接受“实施安全”原则的第一步。身份验证和授权有助于确保只有授权用户才能访问敏感信息,降低安全漏洞的风险。身份验证和授权过程应该是稳健和安全的,以确保数据的机密性和完整性,并且必须涵盖系统的每个部分。
加密敏感数据是接受“实施安全”原则的关键方面。加密有助于保护敏感信息,降低安全漏洞的风险,并确保数据的机密性和完整性。加密应应用于所有敏感数据,包括静态数据和传输中的数据。
由于 SaaS/微服务系统中有许多动态部分,因此应在网络层面实施安全措施,以涵盖系统的每个部分。网络层面的安全措施应包括防火墙、入侵检测和预防系统以及网络分段。
在任何应用程序中,安全都极其重要。SaaS 应用程序通常具有复杂的安全需求。从项目一开始就接受“实施安全”原则有助于构建一个安全可靠的基于微服务的 SaaS 应用程序。实施身份验证和授权、加密敏感数据以及在网络层面实施安全措施有助于降低安全漏洞的风险并确保数据的机密性和完整性。
关注可扩展性
微服务应该设计成可扩展的,无论是水平扩展(通过添加更多实例)还是垂直扩展(通过向现有实例添加更多资源)。这将使您能够快速响应需求的变化,并确保系统在重负载下仍能良好运行。
拥抱“关注可扩展性”原则是构建基于微服务的 SaaS 应用程序的另一个重要方面。可扩展性有助于确保系统可以处理增加的流量,减少停机时间,并提高系统的整体性能。
设计可扩展性是拥抱“关注可扩展性”原则的第一步。可扩展的设计有助于确保系统可以处理增加的流量,减少停机时间,并提高系统的整体性能。设计应考虑预期的流量和资源利用率,并应包括在需要时增加资源的措施。
如我们之前讨论的,容器化通过打包服务和它们的依赖项,有助于提高系统的可扩展性,使得按需部署和扩展服务变得更加容易。
要从容器化应用程序中获得最大利益,您应包括负载均衡器。负载均衡有助于在可用资源之间均匀分配流量,减少停机时间并提高系统的整体性能。
通过容器和负载均衡器,可以实现应用程序的自动化扩展。实施自动扩展有助于确保系统可以处理增加的流量,减少停机时间,并提高系统的整体性能。自动扩展会根据流量和资源利用率自动增加或减少所需的资源。
设计可扩展性、拥抱容器化、实施负载均衡和实施自动扩展有助于确保系统可以处理增加的流量,减少停机时间,并提高系统的整体性能。
分离的数据存储
大多数应用程序都有一个数据库,存储与该应用程序相关的所有信息。虽然这种方法也可以在微服务应用程序中采用,但您还可以按服务实现数据库。这种方法有其优缺点,并且您不一定需要为每个服务使用单独的数据存储。是否使用每个服务的单独数据存储或共享数据存储取决于您系统的需求和限制。
为每个服务拥有单独的数据存储可以提供如下好处:
-
提升可扩展性:每个服务都可以独立扩展其数据存储,从而实现更好的资源利用并降低资源争用的可能性
-
提升弹性:即使数据存储中的一个出现问题时,每个服务仍可以继续运行
-
提高数据隔离性:每个服务对其数据有完全的控制权,这使得维护数据一致性和完整性更容易
然而,独立的数据存储也可能带来挑战:
-
增加的操作复杂性:管理多个数据存储可能比管理单个数据存储更复杂
-
增加延迟:服务之间为了访问不同数据存储中的数据而进行的通信可能会引入延迟
-
增加的数据重复:相同的数据可能需要在多个数据存储中存储,这会增加存储成本和数据不一致的风险
另一方面,使用共享数据存储可以简化架构并减少操作复杂性,但它也可能对可扩展性、弹性和数据隔离引入限制。
最终,选择独立数据存储和共享数据存储取决于你系统的具体需求和限制,并且应该基于对涉及到的权衡的仔细评估来做出决定。
设计微服务应用程序是困难的。有很多事情需要考虑,以确保你能实现这种方法的益处!这些最佳实践将帮助你构建可扩展、可维护和有弹性的微服务。通过遵循它们,你可以确保你的基于微服务的系统在性能、效率和可靠性方面得到优化!
混合微服务和 RESTful API
当构建基于微服务的架构时,REST 通常用作不同服务之间的通信协议。REST,或表征状态转移,是一种常用且广泛采用的 Web 服务架构,为客户端和服务器之间提供了标准化的通信方式。微服务和 REST 是自然匹配的,因为 REST 为微服务之间的通信和数据交换提供了必要的通信基础设施。我们之前讨论了基于契约的开发;REST API 的表面可以看作是服务之间通信的契约。
在基于微服务的系统中使用 REST 的一个关键优点是,它为服务之间提供了清晰和标准的通信方式。REST 定义了一套规则,用于服务如何交换数据,包括使用 HTTP 方法(如 GET、POST 和 DELETE)以及使用 HTTP 状态码来指示成功或失败。这使得开发者更容易构建和维护微服务,因为他们知道在与其他服务通信时可以期待什么。
在基于微服务的系统中使用 REST 的另一个优点是,它为服务之间提供了一种可扩展且灵活的通信方式。REST 是平台无关的,通常通过 HTTP 进行通信,这意味着它可以与各种编程语言和技术一起使用,使其成为构建微服务的理想选择。
最后,在基于微服务的系统中使用 REST 提供了一种安全的服务间通信方式。REST 使用标准的 Web 安全措施,如 SSL/TLS 加密,这有助于保护传输中的数据,以及 HTTP 身份验证,这有助于确保只有授权客户端可以访问数据。
微服务和 REST 是天生匹配的,使用 REST 作为微服务之间的通信协议提供了一种清晰、可扩展且安全的服务间通信和交换数据的方式。通过使用 REST,开发者可以自信地构建和维护基于微服务的系统,因为他们知道他们有一个可靠且广泛采用的通信基础设施。
将单个 REST API 拆分为微服务
当你想到一个“典型”的 RESTful API 时,你可能会想到一个包含多个控制器、每个控制器包含几个相关方法或端点的系统。一个企业系统拥有单个单体API 并拥有数十个控制器和数百个端点并不罕见。将它们拆分为基于契约的微服务系统并不容易。没有一种正确的方法来处理这个问题,这更像是一门艺术而不是科学。
这里有一些可以将单体 REST API 拆分为微服务的方法:
-
功能驱动: 这种方法涉及根据它们提供的功能将单体 API 分解为更小的服务。例如,可以创建一个服务来处理用户身份验证,另一个服务来处理产品管理。这种方法使得管理和维护服务变得更加容易,因为每个服务都专注于特定的任务。
-
数据驱动: 在这种方法中,单体 API 被分解为基于它们管理的数据的服务。例如,可以创建一个服务来管理客户信息,另一个服务来管理订单信息。当不同数据集有不同的数据访问模式、安全要求或性能要求时,这种方法很有用。
-
领域驱动: 这种方法涉及根据它所代表的领域将单体 API 分解为服务。例如,可以创建一个服务来管理客户信息,另一个服务来管理产品信息。当存在可以分解为更小、更易管理的部分的复杂业务领域时,这种方法很有用。
-
微前端: 这种方法涉及将单体 API 分解为微服务,并使用微前端架构将服务组合成一个单一的用户界面。这种方法提供了一种独立扩展前端和后端的方式,同时仍然提供无缝的用户体验。
无论使用哪种方法,在确定将单体 API 拆分为微服务的最佳方式时,考虑 API 的复杂性、API 不同部分之间的依赖关系以及开发团队的技能和资源都是非常重要的。此外,根据需要持续评估和重构微服务,以确保它们继续满足应用程序和业务的需求。
讨论 REST 和微服务时,经常被问到的问题是,“API 中的每个控制器是否应该是自己的微服务?”
答案并不直接,取决于您系统的具体要求以及每个控制器的规模和复杂性。一般来说,每个微服务应代表一个单一、自包含的业务能力,如果多个控制器共同提供单一业务功能,它们可以是一个微服务的一部分。
如果控制器紧密耦合且无法分离,将它们放在单个微服务中可能是有意义的。另一方面,如果每个控制器具有独立的企业逻辑和数据存储,并且可以独立部署和扩展,那么每个控制器在其自己的微服务中可能更适合。
关键在于确定需要执行的业务功能,并将系统分解成一系列自包含的微服务,这些微服务可以独立开发、部署和扩展。当不确定时,最好从较小的微服务开始,如果需要的话再进行整合。这有助于简化测试和调试,以及更快的开发和部署周期。
API 中的每个控制器不一定是自己的微服务,但决策应基于您系统的具体要求以及每个控制器的规模和复杂性。
当结合微服务和 REST 时,有几个重要主题需要涵盖,以构建一个健壮且可扩展的系统,这在构建基于微服务的系统中起着至关重要的作用:
-
设计 RESTful API:RESTful API 应设计为可扩展、灵活且易于消费。
-
API 文档:API 文档应清晰、简洁且易于理解,并提供如何消费 API 的明确说明。
-
API 版本控制:API 版本控制有助于确保系统可以进化,而不会破坏现有的集成。
-
API 安全:应实施 API 安全以防止未经授权的访问、数据盗窃和其他安全风险。
-
错误处理:应实施错误处理以确保系统能够一致和可预测地处理和响应错误。
-
数据一致性:数据一致性是构建基于微服务系统的关键方面。应在微服务之间保持数据一致性,以确保系统按预期运行。
当结合微服务和 REST 时,重要的是要专注于设计 RESTful API,提供清晰的 API 文档,实现 API 版本控制,确保 API 的安全性,处理错误,并维护数据一致性。这些主题有助于构建一个健壮且可扩展的系统,能够处理增加的流量并提供更好的用户体验。
常见陷阱及其避免方法
构建 SaaS 应用很难。构建微服务应用很难。将两者结合起来真的很难,而且有几个常见的陷阱你应该避免!
避免的第一个、最常见且最重要的陷阱是过早地构建微服务。通常情况下,从单体开始,并在需要时逐渐将应用的小部分拆分为小型、自包含的服务,而不是提前这样做,会更容易。
因此,关于微服务,我能给出的最好建议可能就是不要使用它们……除非你真的需要!但是,鉴于这是一本关于微服务的章节,以下是一些如果你决定走这条路应该避免的常见陷阱!
-
过度复杂化:在构建微服务时最常见的陷阱之一就是过度复杂化架构。这可能导致额外的错误、增加的维护成本和更长的开发时间。
-
缺乏沟通和协调:在构建微服务时,确保团队之间有有效的沟通和协调是很重要的。没有这一点,可能会出现延误和误解,这可能导致整体架构出现问题,这不可避免地会表现为系统用户体验的下降。
-
数据不一致:在使用微服务时,确保数据在所有服务中保持一致是很重要的。否则,可能会导致数据完整性和准确性问题。
-
部署复杂性增加:微服务可能会增加部署复杂性,因为每个服务都必须单独部署和管理。
-
调试复杂性:调试复杂的微服务架构可能比调试单体架构更困难。
下面是如何在构建微服务时避免常见陷阱的方法:
-
过度复杂的微服务架构:可以通过保持架构简单、专注于单一职责原则,并为每个微服务定义清晰的边界来避免。同样重要的是要优先考虑需要构建的微服务,并确保它们与业务目标一致。
-
团队间缺乏沟通和协作:可以通过营造协作文化,在团队之间建立清晰的沟通渠道来缓解。定期召开团队会议也很重要,以确保每个人都处于同一页面上。
-
低估数据管理的复杂性:为了避免这一点,重要的是要为每个微服务合理规划数据管理策略。这包括定义数据源、数据所有权和数据一致性。使用数据管理解决方案,如数据管理平台或事件溯源,也可以有所帮助。
-
未能充分监控微服务:为了避免这个陷阱,重要的是要有一个稳固的监控策略。这包括为每个微服务设置日志记录和监控,并在出现问题时设置警报机制。
-
缺乏安全考虑:为了避免这一点,重要的是要有一个稳固的安全策略。这包括在微服务开发的每个阶段(包括架构、设计、开发和部署)考虑安全。定期审查安全策略并根据需要做出更改也同样重要。
一些实用建议
微服务是一个庞大且复杂的话题——而不是在这里尝试展示一个完整的微服务应用程序的演示,我将提供一些基于我们一直在构建的演示应用程序的实用建议。具体的实现留给你来完成!
微服务架构示例
值得重申的是,在规划新的应用程序时,在许多情况下,最佳做法是从一个单体的应用程序开始,并在需要时将应用程序的部分划分为微服务。
对于这个示例,我将假设 Good Habits 应用程序已经发展到需要开始考虑将其划分为微服务的地步。我认为一种有用的分割方式可能如下:

图 6.1 – 建议的微服务架构
让我们更详细地讨论一下这个图中的组件。
用户界面和客户端
现在,一个应用程序既有网络客户端应用程序又有移动应用程序——有时,还有桌面应用程序,这种情况非常普遍。所有这些客户端都将与同一个后端和同一个微服务进行通信。通常,会构建一个单一的 网关 API 来管理各种微服务之间的所有外部通信。
API 网关
虽然有可能让用户界面应用程序直接与微服务进行通信,但管理这一点可能会变得极其复杂。图中只显示了三个微服务,但实际上可能有 20 个或更多。考虑一下在 20 个或更多的微服务以及三种或更多类型的客户端 UI 之间协调通信的附加复杂性——这种情况在视觉上变得更加困难,在实践中也更难以管理!
在微服务架构中使用 API 网关提供了几个好处。首先,API 网关充当客户端的单一点入口,使得请求管理、身份验证和授权更加容易。它还允许将不同的微服务组合成一个统一的 API,这可以简化客户端与系统的交互。API 网关还可以提供负载均衡和故障转移功能,这对于高可用性系统非常重要。另一个重要好处是能够强制执行安全性和流量策略,以及监控和记录请求。通过使用 API 网关,开发者可以更容易地管理和演进微服务架构,同时保持高水平的安全性和性能。
消息代理
消息代理用于促进各种后端微服务之间的通信。这在后端执行的功能与 API 网关在前端执行的功能非常相似。它解开了服务之间的所有通信。虽然我们图中只有三个服务,但我们应记住,现实世界的系统可能拥有更多服务,并且服务间的通信可能会迅速变得极其复杂和低效。
在微服务架构中使用消息代理提供了许多好处。其中一个主要优势是它允许服务以异步方式相互通信,解耦发送者与接收者。这可以提高可靠性和可伸缩性,因为服务可以以自己的速度处理消息,不会被其他服务的性能所阻塞。消息代理还可以作为服务之间的缓冲区,这在某个服务暂时不可用时尤其有用。它可以通过持久化消息直到它们可以被交付到适当的服务来帮助避免消息丢失。消息代理还可以提供一种集中式的方式来监控和管理服务之间的消息流,这使得跟踪和调试问题更加容易。最后,通过将通信关注点从业务逻辑中分离出来,服务可以更容易地进行测试和独立部署。
使用消息代理(以及 API 网关)遵循了我们之前在本章中讨论的许多微服务良好设计原则。
在.NET 微服务应用程序中,常用的几个消息代理包括以下:
-
RabbitMQ:一个支持多种消息协议的开源消息代理,包括 AMQP、MQTT 和 STOMP
-
Apache Kafka:一个优化的分布式流平台,适用于处理高容量和高速度的数据流
-
Azure Service Bus:由 Microsoft Azure 提供的一个完全托管的 messaging 服务,支持传统消息模式和 pub/sub 场景
-
AWS Simple Queue Service (SQS): 亚马逊网络服务提供的一项完全托管的消息队列服务,用于解耦和扩展微服务、分布式系统和无服务器应用程序
-
NServiceBus: 一个.NET 消息框架,提供了一种统一的编程模型,用于使用各种消息模式构建分布式系统
所有这些工具都提供可靠的消息传递、可扩展性和容错性,并且可以帮助简化分布式系统中微服务之间的通信。
服务
以举例来说,我已经概述了三种服务:
-
一个用户服务,将处理与用户相关的一切。这包括身份验证、密码管理和保持个人信息更新。
-
一个习惯服务,处理与用户试图跟踪的习惯相关的一切。
-
一个内容服务。在这个更高级的 Good Habits 应用版本中,我假设将能够以社交媒体风格的内容流查看你朋友的进度。
请注意,我选择将数据存储也分开到单独的数据库中。这使我们能够对每个数据存储进行稍微不同的处理。我还决定为用户和习惯服务使用关系型数据库,而为内容服务使用文档(NoSQL)数据库。这是微服务的一个超级功能——可以根据单个微服务的用例使用不同类型的数据存储。
用户服务
我特别将其分开,因为存储用户私人数据应该非常认真对待。这些数据可能包括银行信息(如果服务有付费层)和可能包括个人信息。很容易理解存储在用户服务中的数据可能需要比习惯服务中的数据更安全地处理。
我们还应该考虑许多司法管辖区中的用户有权被遗忘。通过将所有个人数据集中在一个地方,我们使这一点更容易实现。
用户服务可能不会面临特别高的需求。预计用户不会经常更新他们的详细信息,因此可以为这个服务分配较少的资源。
习惯服务
在 Good Habits 应用程序中,预计习惯服务将在应用程序中承担大部分繁重的工作,因此将分配额外的资源给它。这个服务还应设计得易于扩展,以便可以将更多的时间投入到性能上,也许对用户服务的关注会少于对安全性的关注。(当然,安全性仍然很重要!!)
该服务中的数据类型将高度相关,因此关系型存储是最合适的。
内容服务
如果我们设想一个更高级的 Good Habits 应用程序版本,我们可能已经扩展到拥有类似社交网络的功能集,这允许用户看到他们朋友的成绩并与他们一起庆祝他们的成功。
这种类型的服务通常使用文档存储或类似 GraphQL 的东西来建模。关系型存储不合适。使用微服务架构允许我们选择最合适的数据存储类型!
播放器中的所有信息都将由用户选择以供公开,因此在此服务中数据安全不太重要。我们可以确信在此服务中没有私有数据,因为用户数据存储对此服务不可访问。
总体架构
图 6.1 中的架构展示了我们可以使用微服务来拆分应用程序的一种方式。但做这件事有许多可能的方法,这更多的是艺术而非科学。
最重要的是要遵循用户的需求,并考虑到你在拆分单体应用时所做的选择将如何影响用户。
实际示例
虽然在本章范围内提供微服务项目的完整示例超出了范围,但我们可以在GoodHabits项目中构建一些原则,以巩固我们对先前建议的理解。
为了说明这一点,我们可以做以下操作:
-
添加一个非常基础的
UserService,以展示我们如何与多个微服务交互。 -
添加一个 API 网关,作为所有客户端与系统交互时的单一入口点。
UserService
运行以下脚本以添加用户服务的项目和文件:
dotnet new webapi -n GoodHabits.UserService; \
cd GoodHabits.UserService; \
rm ./WeatherForecast.cs; \
rm ./Controllers/WeatherForecastController.cs; \
touch Controllers/UsersController.cs; \
dotnet add package Microsoft.AspNetCore.Mvc.Versioning; \
dotnet add reference ../GoodHabits.Database/GoodHabits.Database.csproj; \
cd ..; \
dotnet sln add ./GoodHabits.UserService/GoodHabits.UserService.csproj;
接下来,我们将配置如何启动用户微服务。设置launchSettings.json文件,使其看起来像这样:
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"UserService": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5200",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
最后,将以下代码添加到控制器中:
using GoodHabits.Database.Entities;
using Microsoft.AspNetCore.Mvc;
namespace GoodHabits.UserService.Controllers;
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private readonly Ilogger<UsersController> _logger;
public UsersController(
Ilogger<UsersController> logger
)
{
_logger = logger;
}
[HttpGet()]
public async Task<IactionResult> GetAsync()
{
return Ok(new List<User>()
{
new User() { Id = 111, FirstName = "Roger",
LastName = "Waters", Email = "rw@pf.com"},
new User() { Id = 222, FirstName = "Dave",
LastName = "Gilmore", Email = "dg@pf.com"},
new User() { Id = 333, FirstName = "Nick",
LastName = "Mason", Email = "nm@pf.com"}
});
}
}
这就是设置一个非常简单的用户服务所需的所有内容。你可以独立启动它,并查看它与 Swagger 如何协同工作。提供的功能非常基础,尝试构建这个服务更多一些将是一项极好的练习。
API 网关
如前所述,API 网关为使用应用程序的客户端提供了一个进入应用程序的单一点。他们需要做的只是与网关通信,从而隐藏了微服务实现的复杂性。
我们将使用一个名为 Ocelot 的包,它提供了我们需要的绝大多数功能。要开始,执行以下脚本以设置ApiGateway项目:
dotnet new webapi -n GoodHabits.ApiGateway; \
cd GoodHabits.ApiGateway; \
rm ./WeatherForecast.cs; \
rm ./Controllers/WeatherForecastController.cs; \
dotnet add package Ocelot; \
dotnet add package Ocelot.Cache.CacheManager; \
dotnet sln add ./GoodHabits.ApiGateway/GoodHabits.ApiGateway.csproj; \
touch ocelot.json;
正如我们对UserService所做的那样,我们需要修改launchsettings.json文件来配置 API 网关的启动方式。设置文件如下:
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"ApiGateway": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:5300",
"dotnetRunMessages": true
}
}
}
接下来,使Program.cs文件看起来像这样:
using Ocelot.Cache.CacheManager;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Configuration.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true);
builder.Services.AddOcelot(builder.Configuration)
.AddCacheManager(x =>
{
x.WithDictionaryHandle();
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
app.UseCors(policy =>
policy.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod()
);
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
await app.UseOcelot();
app.Run();
在这里,你可以看到Ocelot包的关键行。
最后,通过向Ocelot.json添加以下配置来配置Ocelot:
{
"GlobalConfiguration": {
"BaseUrl": "http://localhost:5900"
},
"Routes": [
{
"UpstreamPathTemplate": "/gateway/habits",
"UpstreamHttpMethod": [ "Get", "Post" ],
"DownstreamPathTemplate": "/api/habits",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 5100
}
],
"RateLimitOptions": {
"EnableRateLimiting": true,
"Period": "10s",
"PeriodTimespan": 10,
"Limit": 3
}
},
{
"UpstreamPathTemplate": "/gateway/habits/{id}",
"UpstreamHttpMethod": [ "Get", "Delete", "Put",
"Patch" ],
"DownstreamPathTemplate": "/api/habits/{id}",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 5100
}
],
"RateLimitOptions": {
"EnableRateLimiting": true,
"Period": "10s",
"PeriodTimespan": 10,
"Limit": 1
}
},
{
"UpstreamPathTemplate": "/gateway/users",
"UpstreamHttpMethod": [ "Get" ],
"DownstreamPathTemplate": "/api/users",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 5200
}
]
}
]
}
如果你查看配置文件,你会看到我们只是在网关中的一个 URL 和我们创建的两个微服务(HabitService 和 UserService)中的另一个 URL 之间进行映射。这看起来可能是一个不必要的复杂性,但如果你考虑到可能还会添加更多微服务到整个应用程序中,那么提供一个单一的入口点是有意义的。
运行组合应用程序
要运行完整的应用程序,我们需要分别启动四个项目(HabitService、UserService、APIGateway 和 Client)。这可能会变得具有挑战性,因此我们将设置任务和启动配置来帮助我们管理。
在 .vscode 文件夹中,将以下代码添加到 tasks.json:
{
"label": "build-user-service",
"type": "shell",
"command": "dotnet",
"args": [
"build",
"${workspaceFolder}/GoodHabits.UserService/
GoodHabits.UserService.csproj"
],
"group": {
"kind": "build",
"isDefault": true
}
},
{
"label": "build-api-gateway",
"type": "shell",
"command": "dotnet",
"args": [
"build",
"${workspaceFolder}/GoodHabits.ApiGateway/
GoodHabits.ApiGateway.csproj"
],
"group": {
"kind": "build",
"isDefault": true
}
}
在同一文件夹中,将以下代码添加到 launch.json:
{
"name": "RunUserService",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build-user-service",
"program": "${workspaceFolder}/
GoodHabits.UserService/bin/Debug/net7.0/
GoodHabits.UserService.dll",
"args": [],
"cwd": "${workspaceFolder}/
GoodHabits.UserService",
"stopAtEntry": false,
"console": "integratedTerminal"
},
{
"name": "RunApiGateway",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build-api-gateway",
"program": "${workspaceFolder}/
GoodHabits.ApiGateway/bin/Debug/net7.0/
GoodHabits.ApiGateway.dll",
"args": [],
"cwd": "${workspaceFolder}/
GoodHabits.ApiGateway",
"stopAtEntry": false,
"console": "integratedTerminal"
}
还在 launch.json 中添加以下复合任务:
"compounds": [
{
"name": "Run Server",
"configurations": [
"RunHabitService",
"RunUserService",
"RunApiGateway"
]
},
{
"name": "Run All",
"configurations": [
"RunHabitService",
"RunClient",
"RunUserService",
"RunApiGateway"
]
}
]
上述配置将允许通过按 F5 键或使用构建和运行菜单来启动 VSCode 的所有四个项目。
在此阶段,我们还可以对演示应用程序做更多的事情。以下是一些建议:
-
将用户服务构建得更加完善,使其包含更多对于真实应用程序所必需的功能
-
向
Ocelot配置中添加额外的路由 -
添加一个消息队列(提示:尝试 RabbitMQ)
我希望我们已经成功地展示了本章的大部分关键学习内容,并为您提供了进一步构建的基础。
摘要
微服务是一个庞大且复杂的话题,远远超出了在 SaaS 书籍的一章中可以解决的问题!在本章中,我们简要介绍了微服务,包括它们是什么以及为什么很重要。我们讨论了使用微服务的优点,包括提高可伸缩性、容错性和灵活性。我们还讨论了实现微服务架构的挑战和陷阱,例如复杂性增加。
接下来,我们探讨了设计微服务的常见模式,包括服务发现、API 网关和消息代理。我们还研究了容器和容器编排系统(如 Docker)在部署和管理微服务中的作用。最后,我们提供了一系列关于如何使用 C#、.NET 和各种支持工具实现微服务架构的指南。虽然这仅仅是微服务世界的一小部分,但我们希望这有助于为您进一步探索这个重要话题打下基础。
在下一章中,我们将学习如何使用 Blazor 构建用户界面。我们将把这个 UI 与我们在前几章中构建的 Good Habits 后端接口起来!
进一步阅读
要了解更多关于本章所涵盖的主题,请查看以下资源:
-
如何构建 .NET Core 微服务:
www.altkomsoftware.com/blog/microservices-service-discovery-eureka/ -
创建一个简单的数据驱动 CRUD 微服务:
learn.microsoft.com/en-us/dotnet/architecture/microservices/multi-container-microservice-net-applications/data-driven-crud-microservice -
8 种保护微服务架构安全的方法:
www.okta.com/resources/whitepaper/8-ways-to-secure-your-microservices-architecture/ -
按照以下 6 个关键步骤在生产环境中部署微服务:
www.techtarget.com/searchitoperations/tip/Follow-these-6-steps-to-deploy-microservices-in-production -
使用 .NET 的微服务:
dotnet.microsoft.com/en-us/apps/aspnet/microservices
问题
回答以下问题以测试你对本章知识的掌握:
-
单体架构和微服务架构之间的区别是什么?
-
微服务架构中 API 网关的目的是什么?
-
消息代理如何促进微服务之间的通信?
-
使用微服务架构如何影响数据库设计和管理?
-
在实施微服务时,需要考虑哪些常见的挑战或陷阱?
第三部分:构建前端
在上一节学习了后端之后,我们将继续学习前端。在本节中,我们将使用 Blazor 构建一个简单的 用户界面(UI),并将其与上一节的后端连接起来。除了实践技能外,本节还将介绍许多关于前端开发以及构建优秀用户界面的理论。
本节包含以下章节:
-
第七章, 构建用户界面
-
第八章, 身份验证和授权
第七章:构建用户界面
在本章中,我们将探讨如何使用 Blazor 构建基于 Web 的前端,Blazor 是一个客户端 Web 框架,它与我们之前章节中构建的.NET Web API 后端进行交互。Blazor 是一个强大且灵活的框架,它允许我们编写在浏览器中运行的 C#代码,使我们能够使用单个代码库创建丰富和交互式的 Web 应用程序。
我们将首先探讨在构建用户界面之前你必须理解的重要技术。这些任务包括生成用户画像、构建用户旅程、线框图和创建票据。
在我们完成背景工作之后,我们将开始修改 Docker 化的开发环境,以方便前端开发并创建一个新的 Blazor 项目。我们还将探索 Blazor 应用程序的基本结构,包括组件、页面和布局。
接下来,我们将介绍如何连接到.NET Web API 后端。我们将展示如何从 Web API 中获取数据,使用客户端代码调用 API 端点并检索数据,然后在用户界面中显示它。
接下来,我们将深入了解设计用户界面的细节,包括创建布局、设计组件以及添加必要的控件和元素,以创建一个响应式且视觉上吸引人的用户界面。
到本章结束时,你将全面了解如何构建一个连接到.NET Web API 后端的 Blazor 前端,以及如何创建一个响应式且可扩展的 Web 应用程序,为你的客户提供无缝的用户体验。
本章涵盖的主要主题如下:
-
技术栈的一般介绍
-
如何确保你的应用程序满足客户需求
-
一些使用 Blazor 构建 UI 的实用示例
-
如何与后端交互
技术要求
本章的所有代码都可以在github.com/PacktPublishing/Building-Modern-SaaS-Applications-with-C-and-.NET/tree/main/Chapter-7找到。
技术栈介绍
构建 SaaS 应用程序的前端有许多可用的选项。Angular 和 React 是流行的基于 JavaScript 的框架,它们被广泛使用,并且是构建 SaaS 应用程序前端的良好选择。然而,鉴于这是一本以.NET 为重点的书籍,我们将坚持使用.NET 技术并使用 Blazor。
Blazor 是由微软开发的一个现代 Web 应用程序框架,它允许开发者使用 C#和.NET 而不是 JavaScript 来构建客户端 Web 应用程序。Blazor 最初于 2018 年作为一个实验性项目推出,后来在 2019 年作为.NET Core 3.0 的一部分发布。Blazor 的主要目标是使开发者能够完全使用 C#和.NET 编写全栈 Web 应用程序,从而提供一个更加熟悉和高效的开发体验。
Blazor 设计用于解决开发和管理复杂、数据驱动的 Web 应用程序的问题,这些应用程序需要大量的客户端交互性,例如 SaaS 应用程序。使用 Blazor,开发者可以编写在浏览器中运行的代码,使用 WebAssembly,使他们能够创建丰富的交互式用户界面,而无需依赖 JavaScript。Blazor 还提供了一系列内置的功能和工具,如路由、数据绑定和表单验证,这些可以帮助开发者更快、更轻松地构建复杂的 Web 应用程序。
由于其易用性、灵活性和生产力,Blazor 近年来变得流行。通过允许开发者在使用客户端时使用 C# 和 .NET,Blazor 提供了一种更一致、更熟悉的开发体验,降低了学习曲线,并使开发者能够更加高效。此外,Blazor 与 .NET 的紧密集成提供了一个无缝的开发体验,包括内置的调试、测试和部署工具。
Blazor 特别适合构建 SaaS 应用程序,因为它提供了一个可扩展且可靠的开发平台,能够处理大量用户和数据。Blazor 与 .NET 网络 API 后端的接口能力使其成为创建完整端到端解决方案的强大工具,具有强大的后端和响应式的前端。此外,Blazor 内置的用于处理用户输入、数据验证和安全的特性和工具使其成为构建复杂和安全 SaaS 应用程序的理想选择。
什么是 WebAssembly?
WebAssembly 是一种低级二进制格式,它使得代码能够在网页浏览器中执行。它是一个可移植的、基于栈的虚拟机,与 JavaScript 并行运行,为在浏览器中执行代码提供了一种更快、更高效的方式。WebAssembly 设计用于与 HTML、CSS 和 JavaScript 协同工作,允许开发者使用包括 C、C++、Rust 和 AssemblyScript 在内的多种编程语言编写 Web 应用程序。
WebAssembly 与 JavaScript 不同,因为它是一种编译型语言,而 JavaScript 是一种解释型语言。这意味着在 WebAssembly 中编写的代码可以预先编译,使其在浏览器中加载和执行更快。此外,WebAssembly 提供了一个更安全、更沙箱化的执行环境,这有助于防止安全漏洞并提高 Web 应用程序的可靠性。
WebAssembly 作为构建 Web 应用程序的方式越来越受欢迎,尤其是在游戏、图像和视频处理以及科学模拟等性能密集型任务中。随着 Blazor 等工具和框架的日益流行,WebAssembly 在构建使用 JavaScript 以外的语言编写的客户端 Web 应用程序的背景下也被更频繁地使用。总的来说,WebAssembly 是一项令人兴奋且强大的技术,它改变了我们构建 Web 应用程序的方式,并使 Web 上的创新进入了一个新的时代。
开发环境
到目前为止,我们所有的工 作都是在 Visual Studio Code 中完成的。我们添加了扩展,使我们能够操作.NET 项目、托管和交互数据库,以及执行 API 上的测试。
我选择 Visual Studio Code 作为本书的示例,因为它免费、跨平台,并且允许我们做各种事情,而无需安装大量工具。此外,我使用了 dev 容器来尝试确保无论你在什么电脑上运行,一切都将“正常工作”。
我认为这真的很酷,而且在将演示应用程序组合到书中时也非常实用。我将继续在这一章中构建 UI 时采用这种理念,但在此处需要承认的是,“主要”的 Visual Studio 应用程序的开发环境优于 Blazor。我希望这将在不久的将来改变,并且这一章能够经受住时间的考验。
对于我这里提供的简单示例,Visual Studio Code 是足够的。如果你正在构建一个更复杂的现实世界项目,那么你可能需要投资 Visual Studio。
UX 考虑因素——了解你的客户
用户体验(UX)在现代应用程序开发中至关重要。用户如何与你的 SaaS 应用程序互动至关重要,因为糟糕的 UX 可能导致用户感到沮丧和放弃。因此,在设计 UI 时,考虑用户体验应该是过程中的最重要部分。
UX 的核心是设计对用户来说有用、易用且令人愉悦的数字产品、服务和系统。在当今这个人们每天都要与技术互动的数字时代,UX 在创造成功的产品和服务方面变得越来越重要。良好的 UX 可以提升用户的满意度、参与度和忠诚度,而糟糕的 UX 则可能导致用户感到沮丧、困惑和放弃。UX 设计涉及理解用户的需求、目标和行为,并利用这些知识来指导 UI 设计和交互。它包括视觉设计、交互设计、信息架构和用户研究等多个学科。随着数字产品和服务的日益重要,UX 将继续成为其成功的关键因素。
用户体验很重要,但它也是一个复杂且多面的领域,可能难以掌握。虽然有一些科学方法和原则可以应用于用户体验设计,例如用户研究、可用性测试和数据分析,但创建出色的用户体验的过程通常需要大量的艺术性和创造性。与一些其他领域不同,用户体验设计中没有绝对的对或错,对某个用户或产品有效的方法可能对另一个用户或产品无效。用户体验设计师必须平衡广泛的考虑因素,包括用户需求、业务目标、技术限制和视觉美学等。这需要结合分析技能、设计技能和直觉来创建既有效又美观的用户体验。简而言之,虽然有一些科学方法和原则可以帮助指导用户体验设计,但在创建出色的用户体验中仍然涉及大量的艺术性。
在 SaaS 应用的背景下,用户体验(UX)甚至比其他类型的软件更重要。SaaS 应用通常是基于订阅的,这意味着用户需要持续支付以获取软件的访问权限。这意味着如果他们对所获得的服务不满意,他们可以随时切换到竞争对手。换句话说,SaaS 公司始终在与用户保持关系上处于一场持续的战斗中,而糟糕的用户体验可能是用户留下或离开的决定性因素。此外,SaaS 应用通常复杂且功能丰富,具有广泛的选择和设置。这可能会使得用户体验设计变得更加具有挑战性,因为用户需要能够快速且轻松地找到他们需要的东西。因此,设计出色的用户体验对于 SaaS 应用的成功至关重要,因为它可以帮助提高用户满意度,减少流失率,并最终推动业务的成功。
在本节中,我将提供一些在构建 SaaS 应用时可能有用的通用指南。
用户画像
用户画像是一系列虚构的角色,代表可能与应用程序互动的不同类型的用户。它们通过调查、访谈和其他研究形式收集关于真实用户的信息来开发。用户体验设计师使用这些信息创建一组代表目标用户不同需求、行为和动机的用户画像。用户画像在用户体验设计过程中非常重要,因为它们有助于对将使用应用程序的人有一个清晰的理解。通过了解不同角色的特征,设计师可以做出明智的决定,关于如何构建用户界面,包含哪些功能,以及如何优先考虑用户体验的不同方面。例如,如果目标受众包括一些技术熟练的个人和一些技术不太熟练的个人,设计师可能需要创建一个既直观又易于导航的用户界面。通过创建用户画像,用户体验设计师可以确保应用程序的设计围绕用户的需要和期望,这最终可能导致更好的用户体验和更高的用户满意度。
这里是一个可能的虚构技术熟练用户“萨拉”的例子:
萨拉是一个技术熟练的用户,使用多个设备,并且对技术感到舒适。她对技术趋势和新应用有很好的理解,并喜欢探索应用程序中的新功能和设置。萨拉更喜欢使用最新的技术和应用,可能对在应用程序中使用快捷键和其他高级用户功能感兴趣。她能够舒适地处理问题并独立寻找解决方案,可能对加载时间慢或其他技术问题有较低的容忍度。如果一个应用程序不符合她的期望,她可能会更倾向于放弃它。总的来说,萨拉是一个对技术感到舒适并且对她使用的应用程序有高期望的用户,应用程序的设计应该反映她的需求和期望 。
为用户画像创建一个头像对设计师和开发者来说很有用,因为它有助于产生同理心,并更好地理解用户的需求、目标和行为。我们为萨拉创建以下头像!

图 7.1 – 萨拉的头像
用户旅程映射
用户旅程映射是用户在应用程序中完成任务所采取步骤的视觉表示,从初始接触点直至任务完成。用户旅程映射很重要,因为它有助于识别用户体验中的痛点、挫败感区域以及改进机会。通过绘制用户旅程,设计师可以清楚地了解用户如何与应用程序互动以及他们可能遇到的问题。然后,这些信息可以用来完善设计,使用户体验更加流畅和直观。用户旅程映射与用户画像相关,因为它有助于更详细地了解不同画像如何与应用程序互动。通过为不同画像绘制用户旅程,设计师可以确定不同类型用户之间的用户体验差异,并做出针对每个画像需求的设计决策。最终,用户旅程映射是创建以用户为中心的设计的有价值工具,以满足所有用户的需求。
这里是 Sara,一个技术熟练的GoodHabit数据库用户的用户旅程示例:
-
Sara 导航到
GoodHabits应用程序并登录。 -
应用程序在屏幕左侧显示 Sara 现有习惯的列表,并在右侧显示一个空的添加/编辑表单。
-
Sara 点击习惯列表下的添加新内容按钮。
-
应用程序使用默认值填充表单,并将按钮文本更改为保存新内容。
-
Sara 填写表格并点击保存新内容按钮。
-
应用程序验证表单数据,创建新习惯,并将其与 Sara 的用户账户关联。
-
应用程序将新习惯添加到屏幕左侧的列表中,并自动选中该习惯。
-
应用程序在屏幕右侧更新表格,显示新习惯的数据,并将按钮文本更改为保存更改。
-
如果需要,Sara 可以对习惯进行进一步修改,然后点击保存更改按钮。
-
应用程序验证表单数据,并使用新数据更新习惯。
-
应用程序在屏幕左侧的列表中突出显示更新的习惯,以表明它已被修改。
-
Sara 确认新习惯已成功添加或更新,并且正确显示在列表中。
将这些用户旅程写出来对于设计团队和开发团队来说非常有用,可以帮助他们更好地理解用户将如何与系统互动。对这些互动的良好理解将使用户体验得到改善,这通常会导致 SaaS 应用程序产生更多收入!
不应假设使用您的 SaaS 应用程序的每个人都具有相同的能力水平,并且考虑可访问性也非常重要。
可访问性
可访问性是指以确保残疾人士可以使用的方式设计和制作数字产品和内容。这包括视力、听力、肢体和认知障碍的人士,以及其他人士。可访问性很重要,因为它有助于确保所有用户,无论其能力如何,都能访问和使用数字产品和内容。这不仅有利于残疾人士,而且具有更广泛的社会和经济利益,因为它有助于创建一个更加包容的社会。
在为可访问性设计时,以下是一些提示和需要考虑的事项:
-
为图像提供替代文本,以便视力障碍用户能够理解图像的内容
-
确保文本和背景之间的颜色对比度足够,以便视力障碍用户能够轻松阅读文本
-
为视频和音频内容提供字幕或文本记录,以便听力障碍用户能够理解内容
-
使用语义 HTML 以确保辅助技术可以准确解析和解释网页的内容
-
确保应用程序仅使用键盘导航即可操作,以便肢体障碍用户能够轻松使用应用程序
-
提供清晰简洁的说明和反馈,以便认知障碍用户能够有效理解和使用应用程序
创建一些用户画像来代表将从这些可访问性考虑中受益的用户可能是有帮助的,为特定用户构建一些用户旅程。
为可访问性进行设计是任何数字产品或内容的重要考虑因素,有助于创建一个更加包容和可访问的社会。
视觉吸引力的设计
虽然用户体验(UX)总是在某种程度上融合了艺术和科学,但创建视觉吸引力的设计更多地依赖于艺术创造力。在构建应用程序时,很容易忽视这一点,而专注于更技术性的方面。然而,具有视觉吸引力的设计是用户体验的重要方面,并且可以对用户如何看待和交互数字产品产生影响。一个精心设计的用户界面可以使用户更容易理解和导航应用程序,并创造一种对建立强大品牌至关重要的信任感和信誉感。视觉吸引力的设计应该是美观的、引人入胜的,同时也要实用和易用。这意味着使用一致的颜色方案、字体和布局,这些对眼睛友好,并提供清晰的视觉层次。这也意味着使用适当的图像和图形来增强内容并支持整体用户体验。最终,视觉吸引力的设计应该是直观的、易于使用的、引人入胜的,有助于为所有用户创造积极的用户体验。
导航和信息架构
用户体验设计应努力使应用程序的导航对用户简单直观。这意味着设计清晰一致的下拉菜单结构,并为每个菜单项提供有用的标签和描述。导航应从应用程序中的任何页面都容易访问,并使用户能够快速轻松地在应用程序的不同区域之间移动。
在 SaaS 应用程序中,用户通常试图完成特定的任务或目标,他们需要能够轻松地找到完成任务所需的内容和功能。有效的导航可以帮助用户快速轻松地在应用程序的不同区域之间移动,而不会迷路或困惑。
信息架构是将应用程序内的内容以逻辑和有意义的方式组织和结构化的过程。它包括将相关内容分组在一起,创建信息层次结构,并建立不同内容之间的清晰关系。一个设计良好的信息架构可以更容易地帮助用户找到所需的信息,并且还可以为应用程序内的内容提供上下文和意义。在设计信息架构时,重要的是要考虑用户的需要和目标,以及正在呈现的内容,以便创建一个清晰、直观和有效的结构。
信息架构对于 SaaS 应用程序非常重要,因为它可以帮助确保应用程序内的内容以直观和有意义的方式组织。这可以帮助用户更好地理解和参与内容,同时也可以使他们更容易找到所需的信息。通过设计清晰有效的信息架构,SaaS 应用程序设计师可以创建一个既实用又愉悦的用户体验,有助于建立品牌忠诚度和客户满意度。
响应式设计
响应式设计是一种旨在创建能够适应和响应不同屏幕尺寸和设备类型的数字产品的设计方法。随着越来越多的用户在包括台式机、笔记本电脑、平板电脑和智能手机在内的各种设备上访问网站和应用,这种方法变得越来越流行。响应式设计使得网站或应用程序的布局、内容和功能针对每种设备类型进行了优化,使用户无论使用哪种设备都能获得一致无缝的体验。
为了实现响应式设计,用户体验设计师通常使用灵活布局、流动图像和媒体查询的组合,这使得设计能够适应不同的屏幕尺寸和分辨率。这意味着设计的元素,如导航、内容和图像,将根据屏幕大小进行调整和重新定位,以提供最佳的 UX 体验。
响应式设计有助于创建积极的用户体验,因为它确保用户可以在任何设备、任何时间访问和使用数字产品。随着越来越多的用户在移动设备上访问网页和应用,响应式设计已成为用户体验设计师的关键考虑因素,对于创建成功且易于访问的数字产品至关重要。
当考虑 SaaS 应用程序时,这一点变得更加重要,因为应用程序的客户通常需要能够在任何时间、任何设备上访问应用程序。
反馈和用户测试
反馈和用户测试对于创建成功的用户体验至关重要,因为它们允许设计师从真实用户那里收集关于其设计的可用性、功能和有效性的见解和信息。这些反馈可用于识别设计中的有效区域以及需要改进的区域,帮助设计师完善和优化用户体验。
为了收集反馈和用户测试数据,用户体验设计师使用各种技术,包括调查、访谈、可用性测试和用户分析。调查和访谈可以帮助设计师收集关于用户体验的定性反馈,包括喜好、不喜欢和痛点。另一方面,可用性测试涉及观察用户如何与设计互动,提供了关于用户如何使用应用程序以及可能导致困惑或挫败感的可能设计区域的宝贵见解。用户分析也可以用来收集关于用户行为的数据,例如用户多久访问一次某些功能,或者他们在用户旅程中的哪个环节放弃。
一旦收集到反馈和用户测试数据,设计师可以使用它们来指导他们的设计决策,根据从真实用户那里收集到的见解进行更改和优化。通过将反馈和用户测试纳入设计过程,用户体验设计师可以创建更以用户为中心且有效的用户体验,从而提高参与度、满意度和客户忠诚度。
构建简单的用户界面
你可能还记得,在第二章中,我们创建了一个 Blazor 应用程序。现在,我们将开始构建这个应用程序。
在本章的早期,我们想象了一个名为 Sara 的用户角色,并描述了 Sara 向她的数据库添加新习惯的用户旅程。在本节中,我们将构建这个用户旅程!
规划用户界面
构建良好的用户界面是一个复杂的过程,涉及周密的规划和深思熟虑的执行。在开始创建用户界面之前,花时间规划应用程序的布局、设计和功能至关重要。这项规划应涵盖从用户体验到用户访谈、线框图和原型设计的一切。
规划 UI 的第一步是定义用户角色和用户体验。理解用户的需求和应用程序的目标对于创建既可用又吸引人的界面至关重要。实现这一目标的一种方法是通过进行用户访谈,这有助于识别痛点并找到改进的机会。然后,可以将这些反馈用于塑造 UI 的设计和功能。
一旦定义了用户角色和用户体验,就到了开始考虑 UI 布局的时候了。线框图和原型设计是可视化界面布局和设计的有用技术。这些技术允许设计师尝试不同的想法,并确保应用程序直观且易于导航。线框图是界面的基本草图,有助于建立整体布局,而原型是交互式模拟,允许用户与 UI 交互并提供反馈。
除了线框图和原型设计,考虑应用程序的技术方面也很重要。这包括选择合适的技术栈,例如适当的框架和工具。例如,Blazor 是一个流行的框架,用于使用 C# 和 .NET 创建 UI,并且是构建 SaaS 应用程序的一个优秀选择。
总体而言,规划 UI 是创建简单而有效的 SaaS 应用程序界面的关键步骤。它涉及考虑用户体验、进行用户访谈、线框图和原型设计,以及选择合适的技术栈。经过仔细规划,可以创建一个既功能性强又美观的 UI,这最终有助于提高用户参与度和满意度。
为了我们能够取得进展,让我们假设我已经完成了所有前面的工作,并决定以下内容代表 Good Habits SaaS 应用程序的理想 UI。

图 7.2 – Good Habits 应用程序的模拟图
上述截图代表一个非常基本的 UI,允许用户查看他们的习惯列表,添加新的习惯,以及编辑或删除现有习惯。这基本上只是一个简单的 创建、读取、更新、删除(CRUD)实现,可能会让用户体验设计师感到沮丧,但这对这个演示来说已经足够了!
配置环境
由于我们将坚持使用 Visual Studio Code 来构建 UI,我们将添加一些扩展来使我们的工作更加轻松。一如既往,我们将通过修改 devcontainer.json 并添加到扩展数组中来实现这一点。添加以下扩展:
"kevin-chatham.aspnetcorerazor-html-css-class-completion",
"syncfusioninc.blazor-vscode-extensions",
"ms-dotnettools.vscode-dotnet-runtime",
"ms-dotnettools.blazorwasm-companion"
这些扩展将使我们在构建这个 UI 时的工作变得更加轻松!
您需要退出并重新进入开发容器环境,以便应用更改。
编写工单
在软件开发中,“工单”是一个术语,用来描述项目上需要完成的工作单元。工单通常被创建来跟踪错误、功能请求以及开发团队需要完成的任何其他任务。它们为项目经理和开发者提供了一种跟踪需要做什么、谁负责完成一项任务以及工作状态的方法。工单可以是简单的错误报告或功能请求,也可以非常详细,包括需求、设计和其他文档。使用工单有助于保持开发过程的有序和高效,确保团队中的每个人都朝着相同的目标努力。
Gherkin 是一种常用于编写软件开发项目规格说明的语言,它也可以在创建工单时使用。它旨在让技术和非技术利益相关者都易于理解,并有助于确保在需要做什么方面,每个人都处于同一页面上。Gherkin 规格说明采用了一种易于阅读和理解的特定格式,并且可以用来创建确保软件满足已指定要求的自动化测试。通过使用 Gherkin 编写工单,开发者可以确保他们所做的工作直接与项目的需求相关联,并且团队中的每个人都理解需要做什么以及为什么这么做。这有助于减少混淆和误解,从而提高开发过程的效率和效果。
Gherkin 是一种纯文本、领域特定的语言,通常用于协助编写软件应用的自动化验收测试。它旨在让利益相关者、开发者和测试人员都能易于阅读和理解。Gherkin 提供了一种简单的语法,以结构化和组织的方式定义应用程序的行为,使用一组关键词和短语。
使用 Gherkin 编写 UI 工单的一个关键好处是,它有助于确保开发团队之间的清晰性和一致性。通过使用标准化的格式来描述所需的 UI 行为,参与开发过程的所有人都可以理解期望的内容以及如何实现它。Gherkin 还提供了一种通用语言,可以用于不同背景和专长的团队成员之间的沟通。
使用 Gherkin 为 UI 工单带来的另一个优点是,它促进了以用户为中心的软件开发方法。通过关注期望的用户行为和体验,Gherkin 鼓励开发者构建直观、易于使用且满足最终用户需求的软件。这种方法可以导致更有效的测试和更好的整体用户体验。
Gherkin 是一个强大的工具,可以用来编写 UI 票据,有助于在软件开发中促进清晰性、一致性和以用户为中心的设计。通过提供一种共同的语言和结构化的格式来描述行为,Gherkin 可以帮助确保所有参与开发过程的人都能理解预期的内容和如何实现它。此外,通过启用自动化测试,Gherkin 可以帮助早期发现问题并确保最终产品的质量。
这里是一个可以用来描述我们之前为萨拉定义的用户旅程的票据示例。我们将使用这个票据来指导我们开发 UI:
Feature: Add a new habit to the list
Scenario: User adds a new habit to the list
Given Sara is on the Good Habits page
And there is a button labeled "Add new"
When Sara clicks the "Add new" button
Then a form for adding a new habit should appear
And the form should have input fields for the habit name, description, and category
And the form should have a "Save" button
And the form should have a "Cancel" button
Scenario: User saves a new habit to the list
Given Sara has filled out the form for adding a new habit
When she clicks the "Save" button
Then the new habit should be added to the list
And the list should show the habit name, description, and category
And the new habit should be displayed at the top of the list
And the new habit should be highlighted to indicate it has been added successfully
And the form for adding a new habit should disappear
上述票据为我们提供了开发 UI 的路线图。结合我们之前创建的线框图,构建这个简单的 UI 应该很容易!
但在我们开始构建之前,让我们快速了解一下我们将要使用的技术!
什么是 Blazor?
Blazor 是一个使用 C# 和 .NET 构建交互式 Web UI 的现代 Web 框架。它允许开发者使用与桌面和移动应用程序以及 RESTful API 相同的熟悉语言和工具来编写 Web 应用程序。Blazor 支持两种构建 Web 应用程序的模式——Blazor WebAssembly 和 Blazor Server。
Blazor WebAssembly 是一种客户端模型,允许开发者完全使用 C# 构建 Web 应用程序,并在任何现代 Web 浏览器中运行,无需任何服务器端代码。应用程序直接在浏览器中加载和执行,与服务器之间的任何通信都使用标准 Web 技术,如 HTTP 和 WebSockets——通常使用我们在前几章中使用的 WebAPI 后端。Blazor WebAssembly 应用程序可以在线和离线运行,这使得它们适合构建渐进式 Web 应用程序。
Blazor Server 是一种服务器端模型,允许开发者使用与客户端 Blazor 相似的编程模型来构建 Web 应用程序,但服务器运行应用程序代码并在浏览器中渲染 UI。在这个模型中,应用程序代码在服务器上运行,UI 以 HTML 和 JavaScript 的形式流式传输到客户端。这使得 Blazor Server 应用程序能够拥有与客户端应用程序相同的丰富交互功能,同时具有对用户体验的更高控制度。
Blazor 的两种模型都为开发者提供了构建 Web 应用程序强大且现代的方法,开发者可以根据项目的具体需求在客户端和服务器端之间进行选择。Blazor 使用 C# 和 .NET,这使得它成为熟悉这些技术的开发者极具吸引力的选择,而能够在 Web 和其他类型的应用程序之间共享代码可以提高效率和生产力。此外,Blazor 对 Razor 语法和与 Visual Studio 及其他开发工具的集成支持,使得开发者能够在熟悉且舒适的环境中工作。
对于这个示例,我们将使用 Blazor WebAssembly (Wasm)。当使用具有 RESTful API 后端的 SaaS 应用程序构建时,这是一个不错的选择,原因如下:
-
Wasm 完全在浏览器中运行,这意味着用户不需要等待服务器响应来加载应用程序。这可以导致加载时间更快,用户体验更好,并且减少服务器负载。
-
由于应用程序完全在浏览器中运行,它可以离线使用。这对于那些在移动中且没有可靠互联网连接的用户特别有用。
-
Wasm 允许更有效地处理复杂计算和图形密集型应用程序,这对于可能需要大量数据处理或高级图形的 SaaS 应用程序尤其相关。
-
通过将更多处理任务卸载到客户端,基于 Wasm 的 SaaS 应用程序可以减少服务器和客户端之间需要传输的数据量。这可能导致带宽需求减少,加载时间更快,并减少服务器负载。
-
Wasm 可以为 SaaS 应用程序提供更好的安全性,因为它在沙盒环境中运行,这使得恶意行为者更难利用应用程序中的漏洞。
-
由于 Wasm 是平台无关的,使用 Wasm 构建的 SaaS 应用程序可以在各种设备上运行,包括桌面、笔记本电脑、平板电脑和智能手机。这可以帮助增加应用程序的覆盖范围,并使其对用户更加易于访问。
设置 Blazor 项目
我们已经在第二章中设置了一个 Blazor 项目,但我会在这里回顾一下步骤。打开控制台并输入以下内容:
dotnet new blazorwasm -o GoodHabits.Client
cd GoodHabits.Client
太好了!您现在可以在控制台中输入dotnet run来启动客户端。您将在浏览器中看到以下内容:

图 7.3 – “Hello, world!” Blazor 应用程序
我们将从这个模板开始,并使其看起来更接近原型。最初,我们将使用虚拟数据,但我们将连接前端到 API – 到本章结束时,我们将有一个完整的栈 SaaS 应用程序!
构建用户界面
首先,在客户端应用程序的Program.cs中需要少量配置。使类看起来如下所示:
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using GoodHabits.Client;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
var apiBaseAddress = "http://localhost:5300/gateway/";
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(apiBaseAddress) });
await builder.Build().RunAsync();
注意,前面的基本地址连接到我们在第六章中配置的 API 网关。
在完成这个小配置后,我们可以通过添加一个新页面来开始功能,该页面将显示我们在前几章中构建的数据库中的信息。我们还将修改导航菜单,将其链接到这个新页面,并删除示例页面。
在Pages文件夹中,您可以删除Counter.razor和FetchData.razor文件,并添加一个名为Habits.razor的新文件。您的Pages文件夹应如下所示:

图 7.4 – 页面文件夹
配置好上述内容后,我们可以在Habits.razor页面上添加一些基本设置。打开文件并复制以下内容:
@page "/goodhabits"
@inject HttpClient httpClient
<div class="row">
<div class="col-md-4">
<h3>Good Habits</h3>
</div>
<div class="col-md-8">
<h3>Habit Details</h3>
</div>
</div>
@code {
private List<Habit> GoodHabits { get; set; } = new
List<Habit>()
{
new Habit { Name = "Drink water", Description =
"Drink at least 8 glasses of water every day." },
new Habit { Name = "Exercise", Description = "Do at
least 30 minutes of exercise every day." },
new Habit { Name = "Meditation", Description =
"Meditate for at least 10 minutes every day." },
};
private class Habit
{
public int Id { get; set; }
public string Name { get; set; } = default!;
public string Description { get; set; } = default!;
}
}
上述代码将为我们提供一个可以开始构建的概要。然而,在我们能够在应用中看到这些内容之前,我们需要修改导航菜单以链接到这个新页面。
打开Shared文件夹并定位到NavMenu.razor文件。将导航菜单代码更改为以下内容。注意,href属性与我们在Habits.razor页面上的@page属性中设置的内容相匹配:
<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href=""
Match="NavLinkMatch.All">
<span class="oi oi-home" aria-
hidden="true"></span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="goodhabits">
<span class="oi oi-plus" aria-
hidden="true"></span> Good Habits
</NavLink>
</div>
</nav>
</div>
配置菜单后,我们可以使用dotnet run运行应用程序并查看我们所做的更改。

图 7.5 – 开始构建 GoodHabits 页面
在前面的屏幕截图中,我们可以看到我们已经成功添加了导航并勾勒出了 GoodHabits 页面!让我们添加一些更多功能。
我们将首先修改代码块中的代码,以便我们可以查看、添加、编辑和删除习惯。我们将很快将这些函数绑定到 UI 控件,以便我们可以操作列表中的习惯。
在@code块中,首先添加一些类级别变量:
private Habit? SelectedHabit { get; set; }
private Habit? EditingHabit { get; set; }
private Habit? AddingHabit { get; set; }
private bool IsEditing { get; set; } = false;
这将允许我们从列表中选择一个习惯,编辑该习惯,并存储一个状态变量,告诉视图我们正在编辑。
接下来,添加一个允许我们选择一个习惯的函数:
private void ShowDetails(Habit habit)
{ SelectedHabit = habit;
IsEditing = false;
}
添加一个函数,允许我们将具有一些默认属性的新习惯添加到列表中:
private void AddHabit()
{
Habit newHabit = new Habit()
{
Name = "New Habit",
Description = "Enter a description here"
};
GoodHabits.Add(newHabit);
SelectedHabit = newHabit;
}
添加一个函数,允许我们编辑一个习惯:
private void EditHabit(Habit habit)
{
SelectedHabit = habit;
ShowEditForm();
}
添加一个函数,允许我们从列表中删除一个习惯:
private void DeleteHabit(Habit habit)
{
GoodHabits.Remove(habit);
if (SelectedHabit == habit)
{
SelectedHabit = null;
}
}
添加一个函数,允许我们从列表中编辑一个习惯:
private void ShowEditForm()
{
IsEditing = true;
EditingHabit = new Habit() {
Id = SelectedHabit!.Id,
Name = SelectedHabit!.Name,
Description = SelectedHabit!.Description};
}
添加一个函数,允许我们保存我们正在编辑的习惯:
private void SaveHabit()
{
GoodHabits.Add(EditingHabit!);
GoodHabits.Remove(SelectedHabit!);
IsEditing = false;
SelectedHabit = null;
}
最后,添加一个函数,允许我们在改变主意时取消我们所做的任何编辑:
private void CancelEdit()
{
IsEditing = false;
EditingHabit = null;
SelectedHabit = null;
}
这应该允许我们操作列表中的习惯!现在,我们将添加 UI 并将元素绑定到我们刚刚创建的函数!
我们首先将习惯列表添加到左侧面板。直接在 HTML 中的<h3>Good Habits</h3>行下方复制以下 HTML:
<ul class="list-group">
@foreach (var habit in GoodHabits)
{
<li class="list-group-item d-flex justify-content-
between align-items-center">
<span @onclick="() =>
ShowDetails(habit)">@habit.Name</span>
<div>
<i class="oi oi-eye mr-2 text-primary"
@onclick="() => ShowDetails(habit)"></i>
<i class="oi oi-pencil mr-2 text-primary"
@onclick="() => EditHabit(habit)"></i>
<i class="oi oi-trash text-danger"
@onclick="() => DeleteHabit(habit)"></i>
</div>
</li>
}
</ul>
<button class="btn btn-primary mt-3" @onclick="AddHabit">Add Habit</button>
您可以看到,我们遍历了代码块中包含的虚拟习惯列表,将这些习惯绑定到我们添加到@code块中的函数。我们已添加按钮来添加、编辑和删除列表中的习惯。
最后,我们需要添加 HTML 以在屏幕右侧显示或编辑习惯。直接在<h3>Habit Details</h3>元素下方添加以下 HTML:
@if (SelectedHabit != null)
{
<div class="card">
<div class="card-body">
@if (!IsEditing)
{
<h5 class="card-
title">@SelectedHabit.Name</h5>
<p class="card-
text">@SelectedHabit.Description</p>
<button class="btn btn-danger mt-3"
@onclick="() => DeleteHabit
(SelectedHabit)">Delete Habit</button>
<button class="btn btn-primary mt-3"
@onclick="() => ShowEditForm()">Edit
Habit</button>
}
else
{
<form>
<div class="form-group">
<label for="edit-habit-
name">Name</label>
<input type="text" class="form-
control" id="edit-habit-name"
placeholder="Enter habit name"
@bind-value="EditingHabit.Name"
/>
</div>
<div class="form-group">
<label for="edit-habit-
description">Description</label>
<textarea class="form-control"
id="edit-habit-description"
rows="3" @bind=
"EditingHabit.Description">
</textarea>
</div>
<button type="submit" class="btn btn-
primary mt-3" @onclick="() =>
SaveHabit()">Save</button>
<button type="button" class="btn btn-
secondary mt-3" @onclick="() =>
CancelEdit()">Cancel</button>
</form>
}
</div>
</div>
}
这里有很多事情在进行。我们添加了两个表单,一个在IsEditing属性设置为true时显示,另一个在它不是时显示,这样我们就可以查看或编辑习惯。
理解正在发生的事情的最好方法是启动项目并查看我们创建的内容!
在控制台中输入dotnet run,然后在浏览器中访问网站。然后,导航到 GoodHabits 页面,你应该看到以下内容:

图 7.6 – 好习惯 UI
如前一个截图所示,我们有一个习惯列表,点击任何这些习惯的名称将弹出“查看”表单。
UI 应该相当直观,如果不是非常漂亮!我认为玩这个 UI 并查看我们添加到 Razor 文件中的各种功能和元素是一个非常有用的练习。
到目前为止,我们有一个功能性的 UI,但还没有连接到后端。现在让我们着手解决这个问题!
将 UI 连接到后端
之前,我们构建了 UI 并添加了添加、删除、查看和更新习惯的功能。然而,目前,我们的 UI 还没有连接到我们构建的 API。现在让我们解决这个问题!
处理 CORS 问题
CORS代表跨源资源共享。这是一种允许网页向服务网页的不同域名发送请求的机制。CORS 是一个安全功能,有助于防止未经授权访问 Web 服务器。当网页尝试进行跨源请求时,它所请求的服务器必须以允许请求通过的特定头信息响应。如果服务器没有发送这些头信息,浏览器将阻止请求。
在开发模式下,我们需要配置 API 网关项目以接受来自任何来源的连接。请注意,这代表了一个安全风险,不应在生产环境中复制。
在API Gateway项目中打开Program.cs文件(不是在 Blazor 项目中 – 确保你是在正确的 Program.cs 文件中)。定位到开发设置:
if (app.Environment.IsDevelopment())
{…}
现在,确保包含以下代码:
app.UseCors(policy =>
policy.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod()
);
这将确保 API 将接受请求,但再次强调,这仅适用于开发环境,不适用于生产环境!
逻辑,包括调用 API
我们将修改之前构建的代码以连接到 API。如果我们做得好,并且正确地将 UI 元素与逻辑分离,我们只需修改代码就可以进行此更改。让我们看看我们做得怎么样!
首先添加一个指向 API URL 的字符串常量:
private const string ServiceEndpoint = "http://localhost:5300/gateway/habits";
确保正确设置端口号。此外,请注意,硬编码 URL 并不是一个好的实践,但在这个演示中足够了。
接下来,注释掉或删除我们添加的三个虚拟习惯。从现在起,我们将获取我们存储在数据库中的习惯:
private List<Habit> GoodHabits { get; set; } = new List<Habit>();
我们需要添加一个钩子来从数据库获取习惯。我们将利用初始化钩子并添加以下方法:
protected override async Task OnInitializedAsync()
{
httpClient.DefaultRequestHeaders.Add("tenant",
"CloudSphere");
GoodHabits = await
httpClient.GetFromJsonAsync<List<Habit>>($"
{ServiceEndpoint}");
}
这使用httpClient来调用 API 端点,该端点将返回存储在数据库中的习惯列表。
在我们可以添加与添加和编辑端点交互的调用之前,我们需要创建一些类。添加两个私有类,如下所示:
private class CreateHabit
{
public string Name { get; set; } = default!;
public string Description { get; set; } = default!;
public int UserId { get; set; }
}
private class UpdateHabit
{
public string Name { get; set; } = default!;
public string Description { get; set; } = default!;
}
接下来,修改AddHabit方法以与 API 交互:
private async Task AddHabit()
{
var newHabit = new CreateHabit()
{
Name = "New Habit",
Description = "Enter a description here",
UserId = 101
};
var response = await httpClient
.PostAsJsonAsync(ServiceEndpoint, newHabit);
var habit = await response
.Content.ReadFromJsonAsync<Habit>();
// Add the new habit to the list of habits
GoodHabits.Add(habit!);
SelectedHabit = habit;
}
在DeleteHabit方法中添加一行:
private void DeleteHabit(Habit habit)
{
httpClient.DeleteAsync($"{ServiceEndpoint}/
{habit.Id}");
GoodHabits.Remove(habit);
if (SelectedHabit == habit)
{
SelectedHabit = null;
}
}
最后,修改SaveHabit方法以包含所需的交互:
private void SaveHabit()
{
httpClient.PutAsJsonAsync($"{ServiceEndpoint}/
{EditingHabit!.Id}",
EditingHabit);
GoodHabits.Add(EditingHabit!);
GoodHabits.Remove(SelectedHabit!);
IsEditing = false;
SelectedHabit = null;
}
完成了!仅仅通过一些小的改动,我们现在已经连接到了 API 和数据库。现在我们已经创建了一个完全功能 SaaS 应用程序的轮廓!
为了证明这一点,进入运行和调试菜单并执行我们在第六章中创建的运行所有复合任务。这将启动微服务项目、API 网关和客户端项目。然后,在浏览器中导航到客户端。你应该能在屏幕上看到你的习惯数据库内容!我们离完成我们的 SaaS 应用程序又近了一步!

图 7.7 – 好习惯 UI,连接到网关 API
构建可用的 UI
我们在这里构建的示例是结合 SaaS 后端和 UI 的基本技术的良好技术演示,但我认为可以安全地说,它缺少一点风格。如今,普通消费者对 SaaS 应用程序 UI 的期望非常高——基本上,客户将要求与原生应用程序非常相似但运行在浏览器中的体验。
为了使 UI 看起来更加现代和响应,我们通常会使用现成的响应式 UI 框架。除了期望 UI 表现得像桌面应用程序一样,通常还期望 UI 能够在平板电脑和手机上运行。最后,并非每个人都有相同的技能,而且有一些标准方法来确保您的 UI 对不同能力的人都是可访问的。在本节中,我们将探讨所有这些内容,从响应式 UI 框架开始。
响应式 UI 框架
响应式 UI 框架是一组预构建的组件、样式和脚本,旨在帮助开发者轻松创建响应式和自适应的 Web 应用程序。这些框架为开发者提供了一套工具,可用于构建能够自动调整到不同设备和屏幕大小的 UI,确保在各种平台上提供一致且愉悦的 UX。
使用响应式 UI 框架可以通过提供现成的组件和一致的设计系统来显著简化开发过程。这使得开发者可以更多地关注应用程序的功能性,而不是从头开始构建响应式布局的复杂性。此外,这些框架通常遵循已建立的设计原则和最佳实践,确保最终产品不仅视觉上吸引人,而且易于访问和用户友好。
Bootstrap 是由 Twitter 开发的一个开源且广泛使用的 CSS 框架。它通过提供一套全面的可重用组件,如按钮、表单和导航元素,简化了创建响应式和移动优先网站的过程。Bootstrap 还包括一个基于 弹性盒布局模块(Flexbox)的响应式网格系统,这使得创建适应不同屏幕尺寸的流畅布局变得容易。凭借其广泛的文档和庞大的社区,Bootstrap 仍然是开发者中的热门选择。
由 ZURB 创建的 Foundation 是另一个流行的响应式前端框架,它专注于提供强大的灵活基础来构建自定义网络应用程序。它提供各种预构建组件、一个响应式网格系统和模块化架构,使开发者能够仅使用他们项目所需的组件。Foundation 以其性能优化和与广泛设备的兼容性而闻名,使其成为需要高级定制和性能的复杂项目的合适选择。
Material-UI 是一个基于 React 的流行 UI 框架,它实现了谷歌的 Material Design 指南。Material-UI 为网络应用程序提供了统一且现代的外观和感觉,确保 UI 既有视觉吸引力又易于导航。它包括一组预构建的组件、一个响应式布局系统以及一个主题系统,允许轻松定制。通过遵循 Material Design 原则,Material-UI 帮助开发者创建符合既定可用性标准的 UI。
虽然 Flexbox 是一个强大的 CSS 布局模块,它简化了网页中灵活和响应式布局的设计,但它并不是一个完整的响应式 UI 框架。相反,它是一个非常有价值的工具,可以与其他框架结合使用来创建自适应布局。许多响应式 UI 框架,如 Bootstrap 和 Foundation,将 Flexbox 作为其网格系统的一部分,利用其功能来为组件创建多用途和流畅的布局。
肯定推荐选择其中之一与你的项目集成。将其中一个框架应用于我们之前创建的演示 UI 将是一个极好的学习练习!
响应式设计技术
简单地安装之前描述的任何响应式框架还远远不够。你需要掌握一些技术,才能充分利用它们。
在本节中,我们将快速探讨基本的响应式设计技术,包括流体网格、灵活图像和媒体查询。通过理解和应用这些技术与响应式 UI 框架相结合,你可以构建可用的 UI,在各种平台上提供一致且愉快的体验。
流体网格是响应式设计的核心,它通过允许根据屏幕尺寸动态调整的灵活布局来实现。而不是使用像素这样的固定宽度单位,流体网格使用百分比这样的相对单位来定义元素的宽度。这确保了布局会随着视口的变化自动缩放和重新排列。例如,当使用 Bootstrap 时,您可以通过利用其预定义的网格类,如.container、.row和.col,来创建流体网格。这些类使您能够定义一个响应式网格结构,该结构可以适应不同的屏幕尺寸。
弹性图像确保媒体内容(如图像和视频)也能根据不同的屏幕尺寸进行缩放和适应。通过将图像的最大宽度属性设置为 100%,图像将自动缩放到适合其包含元素的宽度,防止其溢出并破坏布局。当使用 Foundation 或 Material-UI 等框架时,您可以使用它们内置的类或组件来处理图像缩放,确保您的媒体内容在各种设备上保持响应式。
媒体查询是一个强大的 CSS 功能,它允许您根据用户设备的特征(如屏幕尺寸、分辨率或方向)应用不同的样式。通过使用媒体查询,您可以在布局和样式发生变化的断点定义,确保您的 UI 在不同屏幕尺寸下保持可用性和视觉吸引力。大多数响应式 UI 框架,如 Bootstrap 和 Material-UI,都提供了预定义的媒体查询和断点,您可以使用或自定义以满足您的特定需求。
通过结合这些核心响应式设计技术与响应式 UI 框架提供的功能,您可以创建既美观又高度功能性强、适应各种设备和屏幕尺寸的 Web 应用程序。这最终有助于构建可用的 UI,从而提升整体 UX,满足多样化的用户需求和偏好。
为不同屏幕尺寸设计
现在,预测用户将选择哪种设备来访问您的 SaaS 应用程序是不可能的。因此,在设计响应式 Web 应用程序时,考虑不同的屏幕尺寸和分辨率至关重要。这样做可以确保您的 UI 在不同设备上看起来和运行良好,提供一致且愉悦的 UX。
当为不同屏幕尺寸设计时,遵循一些关键指南和最佳实践是至关重要的:
-
采用移动优先的方法可能非常有帮助。首先为较小的屏幕(如智能手机)设计布局和 UI,然后逐步增强对较大屏幕的设计。这种方法确保了您的应用程序在小设备上保持功能性和视觉吸引力,同时利用较大屏幕的额外显示空间。
-
如前所述,响应式 UI 框架,如 Bootstrap、Foundation 和 Material-UI,提供了预构建的组件、网格系统和预定义的媒体查询,这使得为各种屏幕尺寸创建自适应布局变得更加容易。利用这些框架可以显著简化开发过程,并确保你的 UI 在不同设备上保持一致和功能。记住要应用最佳实践并使用良好的响应式设计技术!
-
总是在多种设备和屏幕尺寸上进行测试。在开发过程中,使用各种设备和屏幕尺寸测试你的应用,以识别潜在问题并确保一致的 UX。你可以使用设备模拟器、浏览器开发者工具或物理设备来测试应用的响应性并进行必要的调整。
-
优化你的应用性能以适应不同设备,因为较慢的加载时间和低效的资源使用可以显著影响用户体验,尤其是在移动设备上。考虑诸如图像优化、代码最小化和懒加载等因素,以改善应用在不同屏幕尺寸下的性能。
通过遵循这些指南和最佳实践,你可以创建响应式网络应用,这些应用在各种设备和屏幕尺寸上提供一致且愉悦的用户体验。利用响应式 UI 框架和响应式设计技术将确保你的 UI 能够无缝适应,满足用户多样化的需求和偏好。
易于访问性
创建易于访问和包容性的网络应用是负责任和富有同理心的设计的一个基本方面。通过考虑不同能力用户的需要,你确保了你的 SaaS 应用为每个人提供平等访问和机会,从而营造一个更加包容的在线环境。
在你的网络应用中拥抱易于访问性和包容性非常重要,原因有几个。这样做可以扩大受众范围。通过使你的应用易于访问,你可以满足更广泛的受众,包括可能在与你的应用互动时遇到障碍的不同能力用户。
通常来说,设计得易于访问的用户界面将导致整体体验更加出色。易于访问的设计原则通常会使所有用户都能获得更好的可用性,因为它们促进了清晰直观的界面,这些界面易于导航和理解。
最后,这是正确的事情。我们应该努力使互联网,以及世界,成为一个更加包容的地方。如果我们能通过花时间使我们的应用易于访问来做出非常微小的贡献,那么我们就应该这样做!
要创建更可访问的网络应用程序,遵循既定的可访问性标准和指南非常重要,例如网络内容可访问性指南(WCAG)和美国残疾人法案(ADA)可访问性设计标准。这些指南提供了一个框架,以确保您的应用程序对有不同能力用户是可用的和可访问的。
有多种工具和技术可用于解决常见的可访问性挑战,并改善不同能力用户的用户体验:
-
屏幕阅读器:这些辅助技术将文本和其他屏幕内容转换为语音或盲文,帮助视力受损用户访问和导航网络应用程序。确保您的应用程序内容在语义上结构良好,正确使用标题、地标和图像的替代文本,以支持屏幕阅读器用户。
-
键盘导航:一些用户可能完全依赖键盘来导航网络应用程序。确保您的应用程序通过提供可见的焦点指示器、逻辑的标签顺序和可键盘访问的交互元素来支持键盘导航。
-
颜色对比度:视力受损或色盲的用户可能难以感知对比度低的内容。确保您的应用程序的色彩方案和设计元素遵循 WCAG 规定的推荐对比度比率。
-
可访问的表单:认知或运动障碍的用户可能难以处理复杂的表单和输入字段。简化表单,提供清晰的标签,并使用适当的输入类型,以便所有用户都能轻松与您的应用程序交互。
-
可访问的富互联网应用(ARIA):这组属性有助于增强动态内容和高级用户界面控件的可访问性。使用 ARIA 属性提供有关您应用程序结构和功能的信息,确保辅助技术可以正确解释和与之交互。
通过考虑不同能力用户的需求并实施这些工具和技术,您可以创建更可访问、包容和用户友好的网络应用程序。这不仅有利于您的用户,也有助于您应用程序的整体成功。
摘要
在本章中,我们讨论了为 SaaS 应用程序设计和构建用户界面的大量内容。我们讨论了用户体验的重要性以及如何为用户画像设计、规划用户旅程以及创建视觉吸引力和响应式设计。我们还讨论了 UI 测试的重要性以及如何使用 Blazor 构建简单的 UI。
本章的一个重要收获是用户体验(UX)在 SaaS 应用程序开发中的重要性。一个设计良好且直观的 UI 可以在用户采用、保留和满意度方面产生重大差异。通过规划用户角色和用户旅程,我们可以确保我们构建的界面符合目标受众的需求和期望。
另一个重要收获是使用现代 UI 框架(如 Blazor)的价值。通过使用 Blazor,我们可以利用.NET 的强大功能和灵活性来构建丰富、交互式且响应迅速的 UI,这些 UI 可以有效地与后端 API 通信。Blazor 允许我们使用 C#和.NET 技能构建在浏览器中运行的 Web 应用程序,使用 WebAssembly 在客户端执行.NET 代码。
我们还介绍了一些构建 UI 的关键最佳实践,包括设计可访问性、使用响应式设计、优化性能和加载时间,以及为用户提供反馈和测试。这些都是设计良好且用户友好的 UI 的基本要素。
在本章的第二部分,我们深入探讨了如何将 Blazor UI 连接到后端 API。我们讨论了如何配置 Blazor 客户端以与 API 通信,如何定义数据模型,以及如何检索和更新数据。我们还讨论了错误处理、测试和调试的重要性,确保我们的应用程序健壮且可靠。
通过遵循本章中概述的步骤,你将深入了解如何为 SaaS 应用程序设计和构建 UI,以及如何将界面连接到后端 API。你将具备构建直观、视觉吸引力强且响应迅速的 UI 所需的工具和知识,同时确保应用程序性能和可靠性。
总之,UI 是任何 SaaS 应用程序的关键组件,设计和构建一个优秀的 UI 需要技术能力和创造力的结合。通过遵循本章中涵盖的最佳实践和指南,你将朝着构建直观、吸引人且有效的 UI 迈进,这有助于你实现业务目标。
在下一章中,我们将讨论身份验证和授权,具体参考这将如何影响多租户、微服务 SaaS 应用程序!
进一步阅读
-
使用 Blazor 构建美丽的 Web 应用程序:
dotnet.microsoft.com/en-us/apps/aspnet/web-apps/blazor -
VS Code 和 Blazor WASM:
dev.to/sacantrell/vs-code-and-blazor-wasm-debug-with-hot-reload-5317 -
从 ASP.NET Core Blazor 调用 Web API:
learn.microsoft.com/en-us/aspnet/core/blazor/call-web-api?view=aspnetcore-7.0&pivots=server -
Blazor WebAssembly HttpClient:
code-maze.com/blazor-webassembly-httpclient/
问题
-
什么是 Blazor,以及它是如何让我们使用 C# 和 .NET 创建 Web 应用的?
-
我们如何自动从 Blazor 应用生成一个连接到 .NET Web API 的客户端?
-
我们如何在 Blazor 应用中消费来自 .NET Web API 的数据,以及处理错误和异常的一些最佳实践是什么?
-
用户画像是什么?
-
为什么在开始构建 UI 之前映射用户旅程很重要?
第八章:身份验证和授权
身份验证和授权是软件开发中的基本概念,在软件即服务(SaaS)应用的背景下尤为重要。在 SaaS 环境中,所有用户数据通常都存储在远程位置,其安全性仅与身份验证和授权机制相当。这些机制有助于确保用户可以以安全和受控的方式与应用交互,并且敏感数据和资源可以免受未经授权的访问。
在本章中,我们将探讨实现身份验证和授权的关键概念和最佳实践,重点关注在 SaaS 应用中实现这些功能。当然,我们将重点关注 Microsoft 技术栈,但我们所讨论的原则应该适用于大多数现代的 Web 开发选项。我们将从讨论身份验证和授权之间的区别以及这些机制如何协同工作以提供用户及其数据的安全环境开始。
接下来,我们将探讨在 SaaS 应用中实现身份验证和授权的一些技术考虑因素,并考虑这些应用开发者面临的一些具体挑战。特别是,我们将考虑多租户(如在第3 章中讨论的)和微服务架构(如在第6 章中讨论的)如何影响安全格局。
在一个应用的生命周期中,用户会来来去去,有时他们会在应用中改变他们的角色。我们将探讨如何管理不断变化且希望增长的用户基础。
最后,我们将通过一个实际示例来工作,在这个示例中,我们将使用本章中介绍的技术,将身份验证和授权添加到我们的演示应用中,构建一个健壮的安全模型,该模型可以扩展到用于实际应用。
到本章结束时,你将清楚地理解在 SaaS 应用中实现身份验证和授权的基本概念和最佳实践。你还将更深入地理解正确实现这些机制的重要性,以及这样做如何有助于保护宝贵的数据和资源,以及如何在用户之间建立信任和信心!
本章涉及的主要主题如下:
-
身份验证和授权概述
-
由核心 SaaS 概念(如多租户和微服务)引发的问题
-
如何管理用户、角色和权限
-
在我们的演示应用中添加身份验证和授权
技术要求
本章中所有代码都可以在github.com/PacktPublishing/Building-Modern-SaaS-Applications-with-C-and-.NET/tree/main/Chapter-8找到。
认证和授权是什么
在深入实施细节之前,让我们花一点时间通过一个现实世界的类比来理解认证和授权的基本概念。想象一下,你的应用程序就像一个安全的大楼,而应用程序中的各种资源或操作由大楼内的房间表示。为了确保大楼及其内容的安保,房间的访问通过两步过程进行控制:认证和授权。
认证是验证试图进入大楼的人或实体的身份的过程,就像在入口处向保安出示身份证一样。在应用程序的上下文中,认证涉及确认用户是他们所声称的人,通常是通过使用用户名和密码。这是确保你的应用程序安全的第一步。
一旦用户的身份得到验证并被允许进入大楼,下一步就是确定他们在大楼内可以做什么。这就是授权发挥作用的地方。授权是根据经过验证的用户权限,授予或拒绝访问特定资源或操作的过程,就像在身份验证后获得的访问卡或钥匙一样。这些权限通常通过角色或声明分配,可以像你的应用程序所需的那样简单或复杂。
这种需要 ID 才能进入大楼,进入后对大楼的某些部分使用钥匙卡访问的想法,是认证和授权的一个非常有用的类比,你应该在我们深入探讨这些概念时牢记在心!
认证
我们将首先深入探讨认证的各个方面,包括不同的形式和方法,它们在.NET 中的实现以及最佳实践。通过理解认证的细微差别以及如何正确实施它,你可以在应用程序的其余部分成形时,为保护你的应用程序及其用户建立一个坚实的基础。
虽然我们通常认为用户名和密码是认证的方式,但还有几种方法可以接近这个问题。用户名和密码的替代方案包括基于令牌的认证、多因素认证(MFA)和单点登录(SSO)。我们将探讨如何实现这些认证方法,重点关注这些方法在基于.NET 的应用程序中的工作方式。
我们还将涵盖一些其他重要主题,例如安全地存储密码和机密信息,以及实施强密码策略和账户锁定策略的最佳实践。
认证形式
在应用程序安全的世界里,有几种认证形式来验证希望使用应用程序的人的身份。每种方法都有其自身的优点和局限性。
最常见的身份验证形式是简单的用户名和密码系统,这是我们所有人都熟悉的!这种方法依赖于用户保持其密码的机密性,并选择强大、复杂的密码以降低未经授权访问的风险,这在安全系统中可能是一个相当显著的缺陷!
使用多因素认证(MFA)可以帮助减轻这一问题。多因素认证要求用户提供两种或多种身份验证方式以验证其身份,这可以大大提高系统的整体安全性。
在企业环境中,组织通常使用单点登录(SSO)。这允许用户使用一组凭证访问多个相关应用程序或服务。这个优点是组织对安全设置有更多的控制。例如,他们可以坚持使用特定复杂性的密码或强制执行多因素认证。
在 .NET 中实现身份验证
在本小节中,我们将探讨如何在 .NET 中使用 ASP.NET Core Identity 实现各种身份验证方法,并与外部身份验证提供者集成。我们将讨论这些方法的配置和定制,以符合您应用程序的需求。
在本章的后面部分,我们将使用这些技术为我们的演示应用程序添加身份验证。
ASP.NET Core Identity
ASP.NET Core Identity 是一个灵活且可扩展的框架,它提供了一种安全的方式来管理用户身份验证和授权。它包括密码散列、双因素认证以及支持外部身份验证提供者等功能。要开始使用 ASP.NET Core Identity,您需要安装必要的 NuGet 包,并按照以下步骤配置您的应用程序:
-
使用以下命令安装所需的 NuGet 包:
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore dotnet add package Microsoft.AspNetCore.Identity.UI -
更新您的应用程序的
DbContext以继承自IdentityDbContext,它包括存储用户信息的必要Identity表。 -
在
Startup类的ConfigureServices方法中注册Identity服务,通过添加services.AddIdentity和services.AddAuthentication。 -
通过在
Startup类的Configure方法中添加app.UseAuthentication和app.UseAuthorization来配置Identity和身份验证的中间件。 -
修改您的视图和控制器以包含必要的身份验证功能,例如登录、注册和注销操作。
当我们在本章后面添加身份验证到演示应用程序时,您将看到前面的步骤是如何发挥作用的。
与外部身份验证提供者集成
为了增强您应用程序的用户体验和安全,您可能希望与外部身份验证提供者集成,例如 OAuth 2.0 和 OpenID Connect,以及社交登录,如 Microsoft、Google、Facebook 或 Twitter。
OAuth 2.0 是一个授权框架,它使您的应用程序能够获取对外部服务上用户账户的有限访问权限,而 OpenID Connect (OIDC) 是建立在 OAuth 2.0 之上的一个认证层,它提供了一种安全的方式来认证用户并获取他们的基本配置文件信息。
在您的 .NET 应用程序中实现 OAuth 2.0 和 OIDC,您可以使用 Microsoft.AspNetCore.Authentication.OpenIdConnect 包。此包包括用于处理 OIDC 认证流程的中介件,例如获取授权代码、将其交换为访问令牌以及验证令牌。
这样做超出了演示应用程序的范围,但尝试自己添加它可能是一个有用的练习!
ASP.NET Core Identity 也支持与流行的社交登录提供者(如 Google、Facebook 和 Twitter)的集成。要在您的应用程序中实现社交登录,请按照以下步骤操作:
-
将您的应用程序注册到所需的社交登录提供者,以获取客户端 ID 和客户端秘密。
-
安装社交登录提供者的相应 NuGet 包,例如
Microsoft.AspNetCore.Authentication.Google、Microsoft.AspNetCore.Authentication.Facebook或Microsoft.AspNetCore.Authentication.Twitter。 -
通过在
Startup类的ConfigureServices方法中添加services.AddAuthentication().Add[ProviderName]并传递之前获得的客户端 ID 和客户端秘密来配置社交登录提供者。 -
更新您的登录视图,以包含每个社交登录提供者的按钮或链接。
您应用程序的每个用户都是不同的,并且对登录您的应用程序有不同的偏好。通过在 .NET 中实现各种认证方法并与外部提供者集成,您可以为您的 SaaS 应用程序创建一个安全且用户友好的认证体验。
安全存储密码和秘密
保护敏感信息,如用户密码和应用秘密,对于维护您的 SaaS 应用程序的安全性和完整性至关重要。在本节中,我们将讨论在您的 .NET 应用程序中安全存储密码和秘密的技术。
密码哈希和加盐
当在数据库中存储用户的密码或任何其他地方时,将密码以“明文”形式存储始终是一个巨大的错误,这将危害您应用程序的安全性。相反,密码应在存储到数据库之前进行哈希和加盐处理。
明文是指将密码以用户输入的形式存储。所以如果密码是‘Passw0rd1’,那么这个字符串就是该密码的明文表示。哈希是一种单向函数,它将密码转换为一个固定长度的字符字符串,而加盐则是在哈希之前向密码中添加一个随机值(称为“盐”),以防止使用预计算表进行的攻击。
ASP.NET Core Identity 自动处理密码散列和加盐,这是通过在 Startup 类的 ConfigureServices 方法中设置 IdentityOptions.Password 来实现的。
利用 .NET 内置的身份工具提供了显著的优势。开发自定义身份提供者可能既具有挑战性又容易出错。利用经过良好建立和实战检验的解决方案始终是首选的方法!
安全管理 API 密钥和其他秘密
除了用户的密码外,您的应用程序还可能依赖于敏感信息,如 API 密钥、连接字符串或加密密钥。同样,将这些秘密以纯文本形式存储或在源代码中硬编码是一种错误,可能会使您的应用程序面临安全风险,应不惜一切代价避免!
与内置的 .NET Core Identity 服务应被使用的方式类似,应使用现有的、经过实战检验的工具和技术来管理应用程序的秘密。以下是一些您应该采取的最佳实践!
-
在您的应用程序中,
IConfiguration接口是一个很好的方法,可以将开发秘密与生产环境的秘密分开。 -
环境变量:将秘密存储在环境变量中有助于将它们与应用程序代码分开,并允许轻松进行配置更改。在生产环境中,请考虑使用集中式配置管理解决方案来安全地管理环境变量和秘密。
-
使用
Microsoft.Extensions.Configuration.AzureKeyVault包,并在您的Startup类中进行配置。
通过安全地存储密码和应用程序秘密,您有助于保护您的应用程序及其数据免受未经授权的访问和潜在的安全漏洞。采用这些最佳实践将确保敏感信息在您的基于 .NET 的 SaaS 应用程序中保持机密和安全。
身份验证最佳实践
实施一个安全有效的身份验证过程对于您 SaaS 应用程序的整体安全性至关重要。通过遵循最佳实践,您可以提升用户体验,提高安全性,并最小化未经授权访问的风险。
强制执行强密码策略
为了防止弱密码或容易猜测的密码,请在您的应用程序中强制执行强密码策略。ASP.NET Core Identity 允许您配置密码要求,例如最小长度、复杂性和字符类型。请考虑以下关于强密码策略的指南:
-
密码的最小长度至少为 12 个字符;越长越好。过短的密码很容易受到暴力破解攻击。
-
强制使用字符类型的组合,包括大写和小写字母、数字和特殊字符。增加可选择的字符数量会使密码更难猜测。
-
禁止容易猜测的密码或常见模式,例如“password123”或“qwerty”。
-
不要要求定期更改密码。过去,要求用户频繁更改密码被视为良好的做法,但这种情况已经不再适用,因为频繁的更改可能导致密码强度降低,因为用户难以记住不断变化的密码。
-
鼓励使用多因素认证(MFA)。MFA 通过要求除密码之外的其他验证方法(如一次性代码、硬件令牌或生物识别数据)来增加额外的安全层。
监控和审计身份验证事件
监控和审计身份验证事件可以帮助您识别可疑活动、检测未经授权的访问尝试,并为您的 SaaS 应用程序维护一个安全的环境。ASP.NET Core Identity 提供了内置的身份验证事件日志记录支持,应始终使用它而不是编写自己的实现。
考虑实施以下监控和审计实践:
-
记录所有身份验证事件,包括成功的登录、失败的登录尝试、密码更改和账户锁定。
-
定期审查身份验证日志以识别异常模式,例如来自同一 IP 地址的多次失败登录尝试或异常登录时间。此过程可以自动化。
-
实施对关键身份验证事件(如重复的失败登录尝试或对敏感资源的未经授权访问)的实时监控和警报。
-
确保日志安全存储并保留足够长的时间以支持事件响应和法医分析。
实施账户锁定策略
账户锁定策略可以帮助防止暴力攻击,其中攻击者反复尝试猜测用户的密码。ASP.NET Core Identity 支持账户锁定功能,允许您在指定数量的失败登录尝试后锁定用户的账户。
在实施账户锁定策略时考虑以下指南:
-
在锁定账户之前设置合理的失败登录尝试次数阈值,例如 3-5 次尝试。
-
确定适当的锁定持续时间,在安全问题和用户体验之间取得平衡。这可以从几分钟到几小时不等,具体取决于您应用程序的需求。
-
实施用户解锁账户的机制,例如通过联系支持、重置密码或使用二级身份验证因素。
-
监控账户锁定事件以识别潜在的暴力攻击或其他安全威胁。
在开发过程中,团队可以在一定程度上选择他们想要严格遵循的最佳实践。这在大多数情况下是可以的,但在身份验证方面却截然不同。应始终遵循公认的最好实践,并且始终首选开箱即用的实现,而不是内部工具。通过在开发过程开始时牢记这些最佳实践,我们可以确保我们的 SaaS 应用程序尽可能安全!
授权
我们已经详细介绍了身份验证;现在,是时候转向授权了。授权涉及确定已验证用户在您的应用程序中可以访问哪些操作和资源。
我们将首先讨论授权的核心概念,如基于角色的访问控制(RBAC)、基于声明的访问控制(CBAC)和基于属性的访问控制(ABAC)。接下来,我们将探讨使用 ASP.NET Core 授权策略、角色和声明管理以及自定义授权中间件和过滤器在.NET 中实现授权。
最后,我们将讨论授权的最佳实践,包括最小权限原则(POLP)、职责分离(SoD)以及定期审计和监控访问控制。
理解授权概念
让我们从查看授权的核心概念开始,这些概念涉及确定用户在应用程序中可以访问哪些操作和资源。通过理解这些概念,您可以为您 SaaS 应用程序创建一个安全且高效的访问控制系统。
基于角色的访问控制(RBAC)
RBAC 是一种授权方法,它将用户分配到特定的角色,从而授予他们执行某些操作或访问特定资源的权限。RBAC 通过允许您在角色级别而不是直接分配给单个用户来管理权限,从而简化了访问控制的管理。
SaaS 应用中角色的示例可能包括“管理员”、“经理”和“用户”,每个角色对应用程序资源和功能具有不同的访问级别。
RBAC 通常用于管理具有相似职责的用户组权限,这使得根据预定义的角色更容易授予和撤销对资源的访问权限。
基于声明的访问控制(CBAC)
CBAC 是一种关注声明的授权替代方法,声明是关于用户的信息片段,例如他们的姓名、角色或其他属性。在 CBAC 中,权限是基于用户的声明而不是他们的角色授予的。
此方法允许进行更细粒度的访问控制,并且与 RBAC 相比,可以提供更灵活和动态的授权系统。声明可以由您的应用程序或外部身份验证提供者(如社交登录或企业身份系统,例如 Azure Active Directory(Azure AD))颁发。
当您需要更细粒度和动态控制用户访问时,基于声明的访问控制是首选。
基于属性的访问控制(ABAC)
ABAC 是一种更高级的授权方法,它评估与用户、资源、操作和环境相关联的一组属性,以确定是否授予访问权限。ABAC 允许基于丰富的属性集进行上下文感知的访问控制决策,并可以支持复杂的访问控制策略。
在 ABAC 系统中,规则或策略使用策略语言(如 eXtensible Access Control Markup Language(XACML))定义。然后,这些规则由 策略决策点(PDP)评估,以确定是否授予或拒绝访问。
当您需要一个高度细粒度和上下文感知的授权系统,考虑多个属性(如用户特征、资源属性和环境因素)以做出访问控制决策时,ABAC(基于属性的访问控制)是首选。
在 .NET 中实现授权
之后,我们将在我们的演示应用程序中构建授权。首先,我们将讨论如何使用 ASP.NET Core 授权策略、ASP.NET Core Identity 的角色和声明管理以及自定义授权中间件和过滤器在 .NET 中实现各种授权概念。
ASP.NET Core 授权策略
ASP.NET Core 提供了一个强大且灵活的授权框架,允许您根据角色、声明或自定义逻辑定义和执行访问控制策略。要在您的 .NET 应用程序中实现授权策略,请按照以下步骤操作:
-
在
Startup类的ConfigureServices方法中定义授权策略,通过添加services.AddAuthorization并使用AddPolicy方法配置策略选项。您可以根据角色、声明或自定义规则指定要求。 -
使用指定策略名称的
[Authorize]属性将授权策略应用于您的控制器或操作方法。此属性确保只有满足策略要求的用户才能访问受保护的资源。 -
如有必要,创建自定义授权处理程序和需求以实现复杂的授权逻辑或与外部系统集成。在
Startup类的ConfigureServices方法中注册您的自定义处理程序。
使用 ASP.NET Core Identity 进行角色和声明管理
ASP.NET Core Identity 提供了对角色和声明的内置支持,使得在您的应用程序中实现 RBAC(基于角色的访问控制)和 CBAC(基于声明的访问控制)变得容易。要使用 ASP.NET Core Identity 中的角色和声明,请按照以下步骤操作:
-
通过将
DbContext更新为继承自具有如IdentityRole之类的角色类型的IdentityDbContext来在您的应用程序中启用角色管理。 -
将角色管理服务添加到您的
Startup类的ConfigureServices方法中,通过调用services.AddIdentity并使用AddRoles方法来实现。 -
在您的应用程序中使用
RoleManager和UserManager类来创建、更新和删除角色,将角色分配给用户,并管理与用户关联的声明。 -
使用前一小节中讨论的
[Authorize]属性和基于角色或策略的授权来保护您的应用程序资源。
自定义授权中间件和过滤器
在某些情况下,您可能需要实现超出角色、声明和策略的定制授权逻辑。ASP.NET Core 允许您创建自定义中间件和过滤器以执行额外的授权检查或在全局级别强制执行访问控制。
要创建自定义中间件,定义一个新的类实现IMiddleware接口,并在InvokeAsync方法中执行您的授权检查。通过在Startup类的Configure方法中调用app.UseMiddleware来注册您的自定义中间件。
要创建自定义授权过滤器,定义一个新的类实现IAuthorizationFilter或IAsyncAuthorizationFilter接口,并在OnAuthorization或OnAuthorizationAsync方法中执行您的授权检查。通过将自定义过滤器添加到Startup类的ConfigureServices方法中的services.AddControllers或services.AddMvc选项来全局注册您的自定义过滤器。
与外部授权服务集成
在某些场景中,您可能希望将您的.NET 应用程序与外部授权服务集成,例如 Azure AD、Azure AD B2C 或 OAuth 2.0 资源服务器,以管理用户的访问控制。在本小节中,我们将讨论如何将应用程序与这些服务集成。
Azure AD 和 Azure AD B2C
Azure AD 是微软提供的一个基于云的身份和访问管理(IAM)服务。Azure AD 允许您集中管理用户、组和应用程序的访问控制。Azure AD B2C 是一个相关的服务,提供以消费者为中心的身份管理,允许您为应用程序用户实现单点登录和多因素认证。
要将您的.NET 应用程序与 Azure AD 或 Azure AD B2C 集成,请按照以下步骤操作:
-
在 Azure 门户中注册您的应用程序,并配置应用程序以使用 Azure AD 或 Azure AD B2C 进行身份验证和授权。
-
在您的.NET 应用程序中,添加
Microsoft.Identity.Web包,并在Startup类的ConfigureServices方法中通过调用services.AddAuthentication和services.AddMicrosoftIdentityWebApp来配置身份验证服务。 -
使用前几节中讨论的
[Authorize]属性、策略或自定义授权逻辑来保护您的应用程序资源。
OAuth 2.0 作用域和资源服务器
OAuth 2.0 是一种行业标准授权协议,允许您代表用户授予第三方应用程序访问其资源,而无需共享其凭据。在 OAuth 2.0 的上下文中,您的.NET 应用程序可能充当资源服务器,它托管受保护资源并需要有效的访问令牌进行授权。
要将您的.NET 应用程序与 OAuth 2.0 授权服务器集成,请按照以下步骤操作:
-
在授权服务器中注册您的应用程序,并配置它使用 OAuth 2.0 进行身份验证和授权。
-
在您的.NET 应用程序中,添加适当的 OAuth 2.0 或 OpenID Connect 中间件包,例如
Microsoft.AspNetCore.Authentication.OAuth或Microsoft.AspNetCore.Authentication.OpenIdConnect,并在Startup类的ConfigureServices方法中配置身份验证服务。 -
根据 OAuth 2.0 作用域或声明,通过实现如前几节所述的自定义授权逻辑来定义和执行访问控制策略。
通过将您的.NET 应用程序与外部授权服务集成,您可以利用集中的 IAM、SSO、MFA 和其他高级安全功能来保护您的应用程序资源并提供无缝的用户体验。
授权最佳实践
为了确保您的 SaaS 应用程序有一个安全高效的访问控制系统,遵循授权最佳实践至关重要。在本小节中,我们将讨论在实现应用程序中的授权时需要牢记的一些最重要的最佳实践。
最小权限原则
POLP 是一个基本的安全概念,规定用户应获得执行其任务所需的最小访问级别。通过遵守这一原则,您可以最小化未经授权的访问或用户账户受损可能造成的潜在损害。要实现 POLP,请确保您执行以下操作:
-
将用户分配到最少的权限角色或创建具有所需最小权限的自定义角色。
-
定期审查和更新用户权限,以确保它们与其当前的责任保持一致。
-
当需要进一步限制对敏感资源的访问时,使用声明或属性实现细粒度访问控制。
职责分离
SoD 是另一个重要的安全概念,涉及将关键任务和责任分配给多个用户或角色,以防止任何单个用户拥有过度的访问或控制。要在您的应用程序中实现 SoD,请确保您执行以下操作:
-
为不同的任务和责任定义不同的角色,并根据用户的职能分配用户到这些角色。
-
实施检查和平衡措施,例如对关键操作要求多个批准或使用不同的角色进行数据录入和数据验证。
-
定期审计和监控用户活动,以确保保持最小权限原则(SoD),并识别任何潜在的违规或冲突。
定期审计和监控访问控制
持续监控和审计您的访问控制系统可以帮助您识别潜在的安全风险,确保用户权限是最新的,并检测未经授权的访问或滥用。为了实施定期的访问控制审计和监控,请考虑以下实践:
-
记录所有授权事件,如角色或权限更改、访问尝试和政策评估的详细日志。
-
定期审查这些日志,以识别异常模式或潜在的安全风险,例如权限过大的用户、未经授权的访问尝试或政策违规。
-
实施对关键授权事件或异常的实时监控和警报,并迅速调查和解决任何识别出的问题。
通过遵循这些授权最佳实践,您可以为您 SaaS 应用程序创建一个安全且高效的访问控制系统,保护您的宝贵资源和数据,同时确保无缝的用户体验。
身份验证和授权的协同效应
在一个强大且安全的 SaaS 应用程序中,身份验证和授权携手合作,以保护您的资源和数据。虽然身份验证验证用户的身份,确认他们就是他们所声称的人,但授权确定已验证用户可以访问哪些操作和资源。通过有效结合这两个概念,您可以为您的应用程序创建一个强大且全面的访问控制系统。
在.NET 应用程序中有效地集成身份验证和授权涉及使用 ASP.NET Core Identity、OAuth 2.0 和 Azure AD 等技术。这些技术为用户提供无缝体验,同时确保适当的访问控制。通过遵循身份验证和授权的最佳实践,您可以最小化潜在的安全风险,并保持 SaaS 应用程序的完整性。
一个实施良好的访问控制系统,将身份验证和授权协同结合,不仅为您的 SaaS 应用程序提供一个安全的环境,而且有助于创建无缝且高效的用户体验,从而有助于应用程序的整体成功。
安全身份验证和授权的重要性
在 SaaS 应用程序开发的快速发展的世界中,从一开始就优先考虑安全和隐私至关重要。通过在身份验证和授权方面投入时间和资源,您不仅保护了应用程序和用户,还为未来的增长和适应性奠定了坚实的基础。
强调适当的认证和授权的一个关键原因是安全漏洞的潜在影响。数据泄露、未经授权的访问和网络攻击可能对企业和最终用户造成严重后果。数据泄露相关的财务成本、品牌声誉的损害和客户信任的丧失可能是毁灭性的。通过实施强大的安全措施,您可以显著降低此类事件及其相关责任的风险。
另一个重要因素是遵守数据保护和隐私法规,例如欧洲的通用数据保护条例(GDPR)和美国的加州消费者隐私法案(CCPA)。这些法规要求企业实施适当的安全措施来保护用户数据和隐私。忽视这些措施可能导致巨额罚款和法律后果。适当的认证和授权机制对于展示您对数据保护的承诺以及遵守这些法规至关重要。
然而,实施有效的认证和授权可能具有挑战性,尤其是在可能涉及多租户、微服务和分布式系统的复杂 SaaS 环境中。随着您扩展应用程序,您需要确保安全措施继续提供高水平保护并适应不断变化的需求。
其中一些挑战包括在各个服务中管理用户身份和访问控制、安全地存储和传输敏感数据,以及保持租户之间的隔离。此外,您还需要保持对最新的安全最佳实践和新兴威胁的更新,以确保您的应用程序在面对新的漏洞和攻击向量时保持安全。
在一开始就投资于强大的认证和授权对于您 SaaS 应用程序的安全性、隐私和成功至关重要。通过这样做,您将保护您的用户,遵守法规,并为未来的增长奠定坚实的基础。虽然这可能是一项具有挑战性的任务,但花时间做对的事情将带来长远的好处,确保客户持续的安全和信任!
认证和授权是庞大且复杂的主题,即使在简单的应用程序中也是如此。然而,在使用 SaaS 的情况下,我们处于困难模式——我们还需要考虑如何确保多租户和微服务应用程序的安全性。我们将在下一节中探讨这些细微差别。
多租户和微服务
在本节中,我们将探讨在多租户和基于微服务的 SaaS 应用中实现身份验证和授权的独特挑战和考虑因素。多租户需要特别注意确保适当的租户识别和隔离,以及管理特定于租户的角色和权限。另一方面,微服务架构本身也带来了一系列挑战,例如集中式身份验证、API 网关访问控制和安全的服务间通信。
多租户考虑因素
作为多租户应用的开发者,我们必须考虑一些特定的因素。
租户识别和隔离
在多租户 SaaS 应用中,正确识别和隔离租户是确保数据安全和隐私的关键方面。租户识别是确定用户在与您的应用交互时属于哪个租户的过程。租户隔离确保数据资源在租户之间安全分离,防止未经授权的访问或数据泄露。
如您从第三章中记得的那样,有几种方法可以用于租户识别,包括使用子域名、URL 路径或自定义头信息。选择的方法应该是连贯且易于管理的。无论您选择哪种方法,验证每个请求中的租户标识符都非常重要,以确保用户只能访问属于其租户的数据和资源。
租户隔离可以在不同的级别实现,例如数据库、应用或基础设施级别。例如,您可以为每个租户使用单独的数据库或模式,确保数据在物理上分离。或者,您可以使用具有行级安全性的共享数据库,在数据访问层强制执行租户隔离。在应用级别,您可以实施租户感知的中间件或过滤器,在每个请求中强制执行租户隔离。
当您设计多租户 SaaS 应用时,请考虑这些方法在复杂性、可扩展性和可维护性方面的权衡。通过有效地实现租户识别和隔离,您可以构建一个安全且合规的 SaaS 应用,保护租户的数据和资源。
特定于租户的角色和权限
SaaS 应用通常是多租户的,因此了解如何管理特定于租户的角色和权限对于确保用户在其租户内拥有适当的资源访问级别和功能至关重要。这不仅有助于维护数据安全,还为每个租户提供定制的用户体验,因为不同的租户可能需要不同的角色和权限集。
管理特定租户的角色和权限的一种方法是通过包括租户标识符来扩展现有的角色和权限模型。这样,当您为用户分配角色和权限时,可以将它们与特定的租户关联起来。这确保了用户只能在租户的上下文中执行操作和访问资源。
在实施特定租户的角色和权限时,请考虑以下最佳实践:
-
定义一个清晰且灵活的角色层次结构,以满足不同租户的需求。这可能包括所有租户共享的常见角色,以及针对某些特定租户的定制角色。
-
根据最小权限原则(POLP)分配角色和权限,确保用户只有执行其任务所必需的访问权限。
-
为租户管理员实现一个用户友好的界面,以便在他们的租户内管理角色和权限。这允许租户对用户访问级别有更多的控制,并简化了管理过程。
-
定期审查和更新特定租户的角色和权限,以确保它们准确反映每个租户的需求和应用程序的功能。
租户的加入和终止
除了用户来来去去,租户也会加入应用程序,(遗憾的是)也会离开。租户的加入和终止是多租户 SaaS 应用程序管理中的重要流程。正确管理这些流程有助于确保租户获得顺畅和高效的经验,同时保持安全和合规性。
新租户的加入始于租户注册,在此过程中,您收集有关新租户的基本信息,例如他们的组织名称、联系信息和可能需要的任何自定义配置选项。接下来,为租户设置必要的资源,如数据库、模式或命名空间,并应用任何特定租户的配置。在初始设置之后,创建用户帐户,包括租户管理员,并分配适当的角色和权限。
除了基本设置之外,考虑应用任何针对租户特定需求的品牌、集成或定制。最后,提供文档、教程或其他支持材料,以帮助租户的用户开始使用您的应用程序。
当终止租户时,遵循一个定义良好的流程对于确保干净和安全的停用至关重要。首先,为租户提供以标准格式导出其数据的能力,确保他们保留对其信息的访问。一旦租户的数据已导出,继续进行资源清理,例如删除租户的资源,如数据库、模式或命名空间,以及任何相关数据。
此外,还应停用用户账户并撤销与租户相关的任何访问令牌或 API 密钥。这一步骤有助于防止租户离场后未经授权访问您的应用程序或系统。最后,记录离场流程并保留已退役租户的记录,以供审计和合规性目的使用。
租户上线和离场关注于管理 SaaS 应用程序中租户的整个生命周期,包括创建和删除特定于租户的资源、配置和定制,而用户配置和去配置主要涉及单个用户账户管理,例如在现有租户的上下文中创建、更新和删除用户账户。
这应该为您解决在确保多租户应用程序安全方面涉及的具体挑战提供了良好的起点。在下一节中,我们将考虑我们已引入的其他附加复杂性——微服务。
微服务架构考虑事项
如我们在前一章中讨论的,微服务架构提供了显著的优势,但它们也带来了一些额外的复杂性。在本节中,我们将讨论一些来自与微服务一起工作的额外复杂性。
集中式身份验证和授权
在基于微服务的 SaaS 应用程序中,实现集中式身份验证和授权对于一致且高效地管理多个服务之间的访问控制非常重要。集中这些流程确保每个服务遵守统一的安全策略,并降低配置错误或不一致导致漏洞的风险。
在微服务架构中集中身份验证和授权的一种常见方法是通过使用 IAM 服务,例如 Azure AD 或 Identity Server。IAM 服务充当用户身份验证的唯一真实来源,并为跨所有服务提供统一的方式来管理角色、权限和访问令牌。
当用户尝试访问受保护资源时,请求首先发送到 IAM 服务进行身份验证。如果用户身份验证成功,IAM 服务将生成一个访问令牌,该令牌通常包括用户的身份、角色和权限。然后,此访问令牌将与后续请求一起传递到其他服务,允许每个服务根据令牌的内容授权用户。
要在您的微服务架构中实现集中式身份验证和授权,请考虑以下最佳实践:
-
使用支持行业标准协议(如 OAuth 2.0 和 OpenID Connect)的成熟 IAM 服务或框架,以促进互操作性和简化集成。
-
使用加密(如 HTTPS 或双向传输层 安全(mTLS))来确保服务与 IAM 服务之间的安全通信。
-
在每个服务中实施令牌验证和缓存机制,以最小化性能开销并保护免受令牌篡改或重放攻击。
-
定期审查和更新 IAM 服务中定义的角色和权限,以确保它们准确反映每个服务的功能和使用要求。
请注意,实施此类系统超出了演示应用程序的范围,但对于任何希望继续深化对该主题理解的读者来说,将是一个很好的项目。
API 网关和访问控制
在基于微服务的 SaaS 应用程序中,API 网关在管理和保护对服务访问方面发挥着至关重要的作用。API 网关充当所有客户端请求的单一点入口,提供统一的访问控制层,简化了在微服务中管理安全性的过程。
通过在 API 网关上集中访问控制,您可以在所有服务中强制执行一致的认证和授权策略,而无需在每个单独的服务中重复逻辑。这降低了服务的复杂性,因为它们可以专注于实现其特定的功能,而不是直接处理访问控制。
当客户端发送请求以访问受保护资源时,API 网关拦截请求并执行必要的身份验证和授权检查。这可能包括验证访问令牌、验证用户角色和权限,以及应用速率限制或其他安全措施。如果请求符合所需标准,API 网关将请求转发到适当的服务。否则,请求将被拒绝,并向客户端返回错误消息。
为了有效地在 API 网关上实施访问控制,请考虑以下最佳实践:
-
选择支持您的身份验证和授权要求的 API 网关解决方案,例如 Ocelot、Kong 或 Azure API Management。确保解决方案与您选择的 IAM 服务兼容,并且可以高效地处理令牌验证和权限检查。
-
配置 API 网关,以确保在所有服务中一致地执行访问控制策略,包括验证访问令牌、检查用户角色和权限,以及应用速率限制或其他安全措施。
-
使用加密,如 HTTPS 或 mTLS,在 API 网关和您的服务之间建立安全的通信,以防止数据泄露和中间人攻击。
-
在 API 网关级别监控和记录访问尝试,以了解潜在的安全威胁,并帮助进行审计和合规性检查。
通过在 API 网关上实施访问控制,您可以增强基于微服务的 SaaS 应用程序的安全性,同时简化管理并确保所有服务的一致访问控制策略。
服务间通信和认证
在基于微服务的 SaaS 应用程序中,确保服务间的安全通信对于维护系统的机密性、完整性和可用性至关重要。服务间认证有助于确保只有授权的服务才能相互通信,从而保护您的应用程序免受未经授权的访问或潜在的安全威胁。
为了实现安全的服务间通信和认证,您可以根据应用程序的要求和架构利用各种技术和协议。以下是一些常见的方法:
-
mTLS:在 mTLS 中,客户端和服务器服务在 TLS 握手过程中都提供 TLS 证书,允许每个服务验证另一个服务的身份。这种方法提供了强大的身份验证、加密和数据完整性,使其成为在微服务架构中保护服务间通信的流行选择。
-
基于令牌的认证:在这种方法中,服务使用访问令牌,例如JSON Web Tokens(JWTs),在与其他服务通信时进行身份验证。访问令牌通常包含有关服务身份的信息,并可能包括额外的声明,例如角色或权限。为了验证令牌,接收服务验证令牌的签名,并检查声明是否符合其访问控制策略。
-
API 密钥:API 密钥是唯一标识符,可用于在服务向其他服务发出请求时进行身份验证。API 密钥通常是预共享的秘密,这意味着它们必须安全地分发给每个服务,并保密以防止未经授权的访问。为了验证请求,接收服务将提供的 API 密钥与有效密钥列表进行比对,如果匹配,则授予访问权限。
在实施服务间通信和认证时,请考虑以下最佳实践:
-
选择一种符合您的安全要求且与现有基础设施和服务兼容的认证方法。
-
使用传输层安全,如 HTTPS 或 mTLS,加密服务间的通信,以保护传输中的数据。
-
实施令牌或 API 密钥验证和缓存机制,以最小化性能开销并保护免受令牌篡改或重放攻击。
-
定期轮换和吊销令牌、证书或 API 密钥,以限制其潜在暴露并降低未经授权访问的风险。
通过实施安全的服务间通信和认证,您可以保护基于微服务的 SaaS 应用程序免受未经授权的访问和潜在的安全威胁,确保系统的机密性、完整性和可用性。
管理用户、角色和权限
在 SaaS 应用程序中,高效且安全地管理用户访问至关重要。用户配置和取消配置是控制资源访问和确保只有授权用户拥有必要权限的基本过程。让我们详细探讨这些过程!
用户配置和取消配置
用户配置是创建、更新和管理系统或应用程序中用户账户及其访问权限的过程。这个过程通常包括创建具有唯一标识符的用户账户,例如用户名或电子邮件地址。一旦账户创建完成,就会根据用户在组织中的职责分配角色或权限。此外,实施密码策略,如最小长度、复杂性和过期期限,确保用户账户保持安全。
自动配置对于大型组织或与外部身份提供者(例如 Azure AD 或 OAuth2)集成尤其有益。通过自动化配置过程,您可以减少用户账户创建和角色分配中的手动错误,改善新用户的入职体验,简化跨多个服务或应用程序的用户访问管理,并通过确保只有授权用户才能访问特定资源来增强安全性。
用户取消配置是在不再需要用户访问权限时撤销用户访问权限的过程,例如当员工离职或更改角色时。这个过程通常包括禁用或删除用户账户,并撤销任何分配的角色或权限。在某些情况下,还可能需要归档或转移任何相关数据。记录取消配置过程对于审计和合规至关重要。
及时且准确的取消配置对于维护安全和最小化未经授权访问的风险至关重要。通过实施系统化的取消配置过程,您可以防止前任员工或承包商访问敏感数据或资源,减少因孤儿账户或非活动账户导致的潜在安全漏洞,简化用户访问管理,并确保只有当前员工拥有适当的权限。此外,彻底的取消配置过程有助于您遵守要求及时移除用户访问权限的数据保护和隐私法规。
对用户配置和取消配置采取谨慎的方法可以确保在整个用户与 SaaS 应用程序交互的生命周期中,安全得到维护,访问权限得到准确管理,并遵守数据保护和隐私法规。
这是一个重要的话题,应该在应用程序的生命周期早期就达成一致并实施相关流程。通过实施稳健的流程来处理这两项任务,您可以增强安全性、维护合规性,并简化应用程序生态系统中访问管理。
角色管理和分配
在 SaaS 应用程序中,管理和分配角色是访问控制的关键方面。角色定义了一组权限,这些权限决定了用户可以在应用程序中执行的操作。通过有效地管理角色并将它们分配给用户,您可以实现更高的安全性并保持职责的明确分离。
角色管理包括创建和维护一组代表您应用程序中不同访问级别或职责的角色。这些角色应设计为反映用户需要执行的各种任务和功能。例如,您可能有“管理员”、“经理”、“编辑”和“查看者”等角色,每个角色都有独特的权限集。角色管理还包括根据需要更新角色,例如当应用程序添加新功能或现有权限需要调整时。
角色分配是将用户与特定角色关联的过程。通过为用户分配角色,您可以确保每个用户都有执行其工作职责所需的适当访问级别,而不会授予他们不必要的权限。角色分配可以手动进行,通过自动化流程进行,或者通过集成外部身份提供者,如 Azure AD 或 OAuth2。
为了优化角色管理和分配,请考虑以下最佳实践:
-
根据最小权限原则(POLP)定义角色,这意味着授予用户完成任务所需的最小权限。
-
定期审查和更新角色,以确保它们准确反映您应用程序的当前需求。
-
实施一致的流程进行角色分配,例如使用模板或自动化,以最大限度地减少人为错误并简化访问管理。
-
监控角色分配和访问日志,以识别任何差异或潜在的安全风险。
通过有效地管理角色并将它们分配给用户,您可以在您的 SaaS 应用程序中实现更安全、更井然有序的访问控制系统。这不仅增强了安全性,而且促进了职责的明确分离,并有助于符合数据保护和隐私法规。
权限管理和细粒度访问控制
权限管理涉及定义用户可以在您的应用程序中访问的一组操作或资源。然后,这些权限可以分配给角色,或者在某些情况下直接分配给用户。细粒度访问控制通过允许您为广泛场景创建高度详细和具体的权限,超越了仅定义一组操作或资源。
细粒度访问控制提供了几个好处,包括增强的安全性、提高效率和更容易的合规性。通过仅向用户提供必要的权限,您可以最小化未经授权的访问或可能危害您应用程序安全性的操作的可能性。通过更精确的访问控制,用户可以快速找到并交互所需的资源,同时避免不必要的杂乱和干扰。例如,市场营销经理可能只需要访问与其活动相关的客户数据,从而避免被无关数据淹没。如果我们回顾我们安全建筑物的例子,我们可以想象建筑物的区域被非常清楚地标示或可能是用颜色编码的,这使得谁可以进入建筑物的哪个部分非常清晰和明显!
在您的 SaaS 应用程序中实现细粒度访问控制,重要的是要识别和定义用户可能需要访问您应用程序的具体操作和资源,考虑到不同的角色和职责。创建一个逻辑上组织权限的权限层次结构,使其更容易管理和维护访问控制。根据最小权限原则(POLP)分配权限给角色或用户,确保用户拥有执行其任务所需的最小访问权限。例如,客户支持代表可能只需要访问客户记录和基本账户信息,而经理可能需要访问更敏感的财务数据。
定期审查和更新权限,以确保它们准确反映您应用程序当前的需求和功能。监控和审计权限分配和访问日志,以检测差异或潜在的安全风险。
摘要
在本章中,我们探讨了 SaaS 应用程序中身份验证和授权的基本概念。身份验证是通过使用凭证(如用户名和密码)来验证用户身份的过程。另一方面,授权是确定用户在应用程序中授权执行哪些操作的过程,通常使用访问控制列表(ACLs)或基于角色的访问控制(RBAC)系统。
我们讨论了身份验证和授权是如何紧密相关并且协同工作,为用户提供一个安全的环境与应用程序交互。在 SaaS 应用程序中,数据泄露的后果可能是严重的,正确处理身份验证和授权对于防止数据泄露和保护敏感数据至关重要。
我们还讨论了在多租户应用程序中实施强大的身份验证和授权机制的重要性,在多租户应用程序中,每个租户的数据和资源必须受到保护,防止其他租户或外部实体未经授权的访问。在 SaaS 应用程序中实施身份验证和授权的技术考虑因素包括使用微服务架构、实施隔离技术以及实施自动化测试和监控。
我们探讨了在 SaaS 应用程序中实施身份验证和授权的一些业务考虑因素。这包括明确定义租户边界和责任、制定清晰的定价模型以及提供全面的入职(和离职)流程。
最后,我们已经完成了一个实践示例,为我们的演示应用程序添加了身份验证和授权功能!
通过解决技术和业务考虑因素,SaaS 应用程序可以提供一个安全、可靠且可扩展的平台,满足应用开发者和租户的需求。实施强大的身份验证和授权机制可以帮助防止数据泄露并保护敏感数据,而提供清晰透明的定价模型和全面的入职流程可以帮助将应用程序确立为值得信赖的有价值服务的提供商。
在下一章中,我们将学习关于测试的内容。测试是一个非常重要的主题,尤其是在处理 SaaS 应用程序时。我们将介绍跨应用程序堆栈的测试策略。
进一步阅读
-
使用 WebAPI 和 ASP.NET Core Identity 在客户端 Blazor 中进行身份验证:
chrissainty.com/securing-your-blazor-apps-authentication-with-clientside-blazor-using-webapi-aspnet-core-identity/ -
Blazor WebAssembly - 用户注册和登录示例与教程:
jasonwatmore.com/post/2020/11/09/blazor-webassembly-user-registration-and-login-example-tutorial -
ASP.NET Core 上的身份介绍:
learn.microsoft.com/en-us/aspnet/core/security/authentication/identity?view=aspnetcore-7.0&tabs=visual-studio&viewFallbackFrom=aspnetcore-2.2 -
如何选择主密码:
medium.com/edgefund/choosing-a-master-password-5d585b2ba568
问题
-
在 SaaS 应用程序中,身份验证和授权之间的区别是什么,为什么两者都很重要?
-
在 SaaS 应用程序中实施身份验证和授权时,有哪些技术考虑因素,以及这些因素如何帮助预防数据泄露?
-
为什么在多租户应用程序中实施强大的身份验证和授权机制尤为重要,以及不这样做可能带来哪些风险?
-
在 SaaS 应用程序中实施身份验证和授权时,有哪些关键的商业考虑因素,以及这些因素如何帮助将应用程序确立为值得信赖的有价值服务的提供者?
-
在 SaaS 应用程序中数据泄露可能带来哪些潜在后果,以及如何通过实施强大的身份验证和授权机制来减轻这些风险?
-
如何利用自动化来增强 SaaS 应用程序的安全性,以及这样做有哪些好处?
第四部分:部署和维护应用程序
本节重点关注应用程序构建完成后的操作,以及如何在用户基数开始增长时保持其在生产中的平稳运行。除了涵盖测试,本节还涉及监控和日志、持续集成/持续部署(CI/CD),并提供有关如何在用户基数开始增长时扩展您的 SaaS 应用程序的建议。
本节包含以下章节:
-
第九章,SaaS 应用程序的测试策略
-
第十章,监控与日志
-
第十一章,频繁发布,尽早发布
-
第十二章,成长之痛 – 规模化运营
第九章:SaaS 应用程序的测试策略
测试在软件行业中无处不在,但往往花费时间进行测试的原因被忽略了。在本章中,我们将讨论各种测试技术时,我们将强调每种测试方法背后的理由。通过理解实施这些测试实践不仅是如何,而且为什么要这样做,你将更好地做出关于你的测试策略的明智决策,并确保你的软件即服务(SaaS)应用程序的长期成功。
在本章中,我们将探讨测试在 SaaS 应用程序的开发和维护中扮演的重要角色。我们将结合理论和实际案例,全面了解各种测试方法和它们的益处。到本章结束时,你应该对测试策略有一个坚实的基础,这将帮助你确保你的 SaaS 应用程序的可靠性、功能性和整体质量。
我们将从一般测试开始。这包括查看测试金字塔,一个说明不同类型测试(单元测试、集成测试和端到端测试(E2E))及其在开发过程中各自角色的概念。这将给你一个清晰的思路,了解各种测试方法及其在确保你的应用程序按预期工作并满足用户需求中的重要性。
接下来,我们将深入研究测试驱动开发(TDD),这是一种强调在编写实际代码之前编写测试的开发方法。TDD 近年来因其众多益处而受到欢迎,如提高代码质量、更快的开发周期和更易于维护。我们将讨论 TDD 背后的原则,并提供如何在你的项目中应用这些原则的示例。
我们将更详细地介绍测试金字塔上显示的三个广泛测试类别,并探讨如何将这些技术应用于 SaaS 应用程序。
在本章中,我们将介绍在 Microsoft 生态系统中常用的测试工具和框架。了解这些工具将使你能够为你的特定测试需求选择最合适的工具,并帮助你为你的 SaaS 应用程序制定更稳健的测试策略。
测试是一个庞大的主题,本章将仅提供一个主题概述。然而,到本章结束时,你应该对如何进行 SaaS 应用程序的测试有一个全面的理解,并对可用的各种工具和技术有一个了解。
本章涵盖的主要主题如下:
-
适用于 SaaS 应用程序的特定测试策略
-
测试驱动开发(TDD)
-
端到端金字塔 – 单元测试、集成测试和端到端测试
技术要求
本章的所有代码都可以在github.com/PacktPublishing/Building-Modern-SaaS-Applications-with-C-and-.NET/tree/main/Chapter-9找到。
SaaS 应用程序的测试策略
测试是软件开发过程中的一个基本方面。它有助于确保应用程序的质量、可靠性和功能。结构良好的测试和测试策略允许开发者尽早在开发过程中识别和修复问题,从而防止可能后来出现的昂贵且耗时的错误。
除了确认软件尽可能没有错误之外,测试还提供了一种验证软件是否满足其要求并在各种场景中按预期执行的方法。通过将测试实践融入开发过程的每一步,开发者可以创建更稳健、更易于维护的应用程序,从而提高用户满意度并增加对软件的信任,这最终提高了项目成功的可能性。
测试对 SaaS 应用程序的重要性
不充分的测试对任何软件应用程序都可能产生严重后果,包括增加开发成本、延迟发布、用户体验不佳和声誉损害。当测试不足时,问题和缺陷更容易被忽视,导致部署后出现问题的可能性更高。这可能导致耗时且昂贵的修复,以及损害用户信任和满意度。
如果测试过程不足,那么你的用户就会变成你的质量保证(QA)团队。通常情况下,用户并不喜欢这种做法!
在开发任何软件应用程序时,确保在用户获得应用程序之前完成测试和 QA 是非常重要的。对于 SaaS 应用程序来说,这一点尤为重要。这些应用程序通常同时服务于多个客户,任何错误都会同时影响所有用户。更糟糕的是,一个用户实例上的错误可能导致整个网站的故障。停机或功能问题可能会对用户满意度产生重大影响,导致客户流失和声誉损害。
SaaS 应用程序通常需要频繁更新和功能添加,以保持竞争力并满足客户不断变化的需求。一个稳健的测试策略允许开发者有信心地发布新功能和更新,而不会影响应用程序的稳定性或引入未预见的问题。最后,SaaS 应用程序通常涉及各种组件、服务和 API 之间的复杂交互,因此彻底测试这些交互对于确保无缝操作和数据完整性至关重要。
我们将首先探讨一些测试应用程序的最佳实践。
测试最佳实践
测试可能是一项具有挑战性的任务,但正确执行的好处是众多的,包括提高代码质量、增强对应用程序功能自信,以及降低缺陷进入生产的风险。通过遵循最佳实践,你可以创建一个更健壮和可靠的测试过程,这不仅能够及早发现问题,还能指导软件的设计和开发。在本节中,我们将提供一系列的提示和技术,以帮助您最大限度地提高测试工作的效率,确保您能够交付满足用户需求的优质软件。
-
编写可测试的代码:如果你使代码易于测试,那么测试过程将会变得……简单!遵循 SOLID 原则,使用依赖注入,创建模块化和解耦的组件,并保持你的类小而封装良好。这通常是好的建议,但在测试过程中它会产生巨大的影响。
-
尽早测试,多测试:在开发过程中越早开始测试,过程就会越简单。通过测试实现 100%的代码覆盖率并不是真正必要的,但实现高覆盖率通常会导致更好的代码,并减少回归。很少有代码库会因为测试过多而受到影响,但有很多代码库会因为测试过少而受到影响。
-
保持测试隔离:每个(单元)测试应该只测试系统的一个部分,并且不应依赖于其他任何测试的结果。集成测试和(E2E)测试可能需要多个系统部分,但它们应该只测试单个集成点或用户交互。
-
保持测试简单和专注:每个测试都应该尽可能短和简洁。测试应该易于理解,易于维护。
-
ShouldCorrectlyAddUpTheNumbers()是一个很好的测试名称,确保数字能够正确相加! -
避免测试实现细节:专注于测试代码的行为和功能,而不是其内部实现。尝试测试函数的输入集,以生成特定的输出。例如,如果你正在测试一个计算两个数字之和的函数,你应该专注于确保该函数对于各种输入组合返回正确的结果,而不是检查函数内部是如何执行计算的。通过这样做,你可以确保即使实现发生变化,只要函数的预期行为保持一致,你的测试仍然相关且有用。
-
培养测试文化:在您的团队和组织中培养测试文化非常重要,因为它强调了测试在交付高质量软件中的重要性,并鼓励每个人都对产品的整体质量负责。强大的测试文化创造了一个环境,其中开发者、测试人员和其他利益相关者积极合作,在整个开发过程中识别、预防和修复缺陷。
下一个最佳实践是使用 TDD。这值得一个单独的小节!
测试驱动开发 (TDD)
TDD 是一种软件开发方法,乍一看可能似乎不符合直觉,因为它强调在编写实际代码之前编写测试。然而,这种方法有几个优点,并帮助开发者创建更健壮、可靠和易于维护的软件。
TDD 的核心思想是为特定的功能或功能创建一个失败的测试,然后实现必要的代码以使测试通过。通过先编写测试,开发者被迫明确定义代码的预期结果和需求,这反过来又导致更好的整体设计和结构。这个过程也有助于开发者尽早在开发周期中捕捉到任何问题,最大限度地减少引入错误或意外行为的可能性。
一旦编写了测试并实现了代码以通过测试,开发者通常会重构代码以改进其结构、可读性或性能。在重构过程中,现有的测试作为安全网,确保对代码所做的任何更改都不会破坏其功能。这个编写测试、实现代码并根据需要重构的周期会一直重复,直到达到预期的功能。这被称为红绿重构周期。

图 9.1 -– 红绿重构周期
在项目中采用 TDD 可以带来几个好处。首先,它促进了一种更自律的编码方法,因为开发者必须在实施之前考虑需求和预期结果。其次,TDD 简化了调试和维护,因为全面的测试套件可以快速定位问题并确保更改不会引入新的问题。最后,TDD 鼓励团队成员之间更好的协作,因为测试作为代码功能及其预期行为的明确文档。
TDD 类型
TDD 提供了在编写代码之前编写测试的一般方法,但也有一些 TDD 的子类型或变体,强调特定的方面或技术。以下列出了一些这些子类型:
-
行为驱动开发(BDD):BDD 是 TDD 的扩展,它从最终用户或利益相关者的角度关注软件的行为。BDD 鼓励使用共享语言和规范格式(例如,Gherkin)来描述软件的预期行为,使其以人类可读和易于理解的方式呈现。这种共享理解有助于推动 TDD 测试的创建,促进开发人员、测试人员和业务利益相关者之间的更好协作。
-
验收测试驱动开发(ATDD):ATDD 是 TDD 的另一种变体,它侧重于在开始实现功能之前定义和验证验收标准。在 ATDD 中,开发人员、测试人员和业务利益相关者协作创建验收测试,以定义从用户角度期望的系统行为。然后,这些测试被用来指导开发过程,确保生成的软件符合定义的验收标准。
-
数据驱动开发(DDD):不要与领域驱动设计混淆,在 TDD 的上下文中,数据驱动开发是一种侧重于使用数据来指导测试创建和开发过程的方法。开发人员根据一系列输入数据和预期结果创建测试用例,确保代码可以处理各种场景和边缘情况。这种方法在处理复杂算法或数据处理任务时特别有用。
-
示例规范(SBE):SBE 是一种协作的 TDD 方法,涉及根据现实世界示例创建可执行的规范。开发人员、测试人员和业务利益相关者共同努力,确定关键示例,这些示例说明了系统的期望行为。然后,这些示例被用来创建指导开发过程的测试,确保生成的软件符合商定的预期。
这些 TDD 的子类型提供了不同的视角和技术,用于处理测试驱动开发。
对 TDD 的批评
虽然 TDD 已经获得了流行并有许多支持者,但它也因各种原因而面临批评。TDD 的一些常见批评包括以下内容:
-
过度强调测试:批评者认为,TDD 可能导致过度关注编写测试,而牺牲了其他重要的开发任务,如架构和设计。这种对测试的过度强调可能导致开发人员在编写测试上花费太多时间,而在开发过程的其它方面投入不足。
-
不完整的测试覆盖率:TDD 不能保证完整的测试覆盖率,因为开发人员在编写测试时可能无法预见到所有可能的场景或边缘情况。这可能导致一种虚假的安全感,并可能导致软件中存在未检测到的错误。
-
缓慢的开发过程:在编写代码之前编写测试可能会减慢开发过程,尤其是对于刚开始接触 TDD 的开发者来说。在编写和维护测试上花费的额外时间可能会被视为一种额外的成本,从而降低了整体的开发速度。
-
关注单元测试:TDD 往往会导致过分关注单元测试,而忽视了其他测试技术,如集成测试或端到端测试。虽然单元测试很有价值,但它们不能捕捉到所有类型的问题或验证整个系统的行为,这可能导致遗漏的 bug 或集成问题。
-
过度设计:TDD 可能会鼓励过度设计,因为开发者可能会倾向于编写满足测试的代码,而不是专注于问题的最简单和最有效的解决方案。这可能导致不必要的复杂代码,使得代码更难维护和理解。
-
学习曲线:TDD 有一个学习曲线,对于刚开始采用这种方法的开发者来说,可能会发现适应开发过程具有挑战性。他们可能会在编写有效的测试、组织代码和遵循红-绿-重构周期方面遇到困难,这可能导致挫败感和生产力的下降。
尽管存在这些批评,许多开发者和团队发现 TDD 是一种有价值的方法,可以提高代码质量、可维护性和整体软件可靠性。TDD 成功的关键在于理解其局限性,并调整方法以适应项目的具体需求和限制。本书作者认为,如果做得正确,TDD 是软件开发过程中一个极其宝贵的部分。
测试技术
在软件测试的世界里,采用了各种技术来创建有效且可维护的测试。这些技术有助于确保你的测试专注于代码的正确方面,使得更容易识别和解决潜在的问题。采用适当的测试技术可以导致更可靠的软件、更快的开发周期和减少的维护工作。通过理解和应用这些技术,你可以创建出不仅高效而且对整个团队来说更容易理解和维护的测试。
模拟
模拟是测试中用来用模拟版本替换真实对象或服务的技术,这些模拟版本被称为模拟。模拟的主要目的是将待测试的代码与其依赖项隔离开来,使你能够在不依赖外部因素的情况下单独测试各个组件。模拟帮助你控制依赖项的行为,并验证你的代码是否正确地与它们交互。
模拟的常见用例包括模拟外部服务的行为,例如 API、数据库或第三方库,这些服务可能在测试环境中不可靠、速度慢或难以设置。通过使用模拟,你可以专注于测试自己的代码逻辑,而无需担心这些外部依赖的行为。
对于.NET,有几个流行的模拟库,如 Moq,它简化了在测试中创建和管理模拟对象的过程。Moq 允许你创建接口或抽象类的模拟,并使用流畅的 API 定义其行为。
存根
存根是测试中使用的另一种技术,其中你创建轻量级对象,称为存根,为特定的方法调用返回预定的响应。存根通常用于仅用于检索数据且不需要任何复杂逻辑或行为的对象。存根的主要目的是提供可预测和一致性的测试数据,使你能够专注于测试消耗数据的代码。
这里有一个简单的存根示例:
public class CustomerControllerb : ICustomerController
{
public Customer GetCustomerById(int id)
{
return new Customer { Id = id, Name = "Dave
Gilmore" };
}
}
在前面的代码片段中,创建了一个具有一些预定义属性的Customer存根。
伪造
伪造是类或接口的简化或部分实现,用于测试目的。它们通常实现与真实对象相同的接口,但为测试提供了一个受控的环境。伪造可以是手动编写的或使用测试库生成。当需要模拟依赖项的行为而不需要完整实现时,它们可以用作模拟和存根的轻量级替代品。
模拟、存根和伪造在概念上非常相似,根据所进行的测试的详细情况,它们可以在一定程度上互换使用。
测试金字塔 – 单元、集成和端到端测试
测试金字塔是一个概念,它说明了软件项目中测试类型的最优分布。它提供了单元、集成和端到端测试之间关系的视觉表示,突出了它们的相对重要性和执行速度。参考以下图表以更好地理解测试金字塔的结构:

图 9.2 – 测试金字塔
在金字塔的底部,我们有单元测试。这些测试数量最多,专注于验证单个组件或函数在隔离状态下的正确性。单元测试执行速度快,这使得开发人员能够在开发过程中频繁运行它们。
在金字塔的中间部分,我们找到了集成测试。与单元测试相比,集成测试的数量较少,但它们在验证应用程序中不同组件和服务之间的交互方面发挥着至关重要的作用。集成测试的运行时间比单元测试长,因为它们通常涉及更复杂的场景和依赖关系。
在金字塔的顶端,我们有端到端(E2E)测试。这些测试数量最少,但确保应用程序的整体功能性和用户体验至关重要。端到端测试通过从开始到结束与应用程序交互来模拟真实用户场景,通常通过浏览器自动化完成。因此,与单元测试和集成测试相比,它们的执行速度较慢。
测试金字塔强调拥有平衡的测试策略的重要性,包括大量的快速单元测试、少量的集成测试以及少数精心选择的端到端测试。通过理解每种测试类型及其相对执行速度,您可以为您的大规模应用程序(SaaS)创建一个高效且有效的测试策略。
单元测试
单元测试是对软件应用程序的各个单元或组件进行独立测试的过程。单元测试的主要目标是验证每段代码的正确性和可靠性,确保其按预期工作。通过独立测试每个组件,开发者可以在开发过程的早期阶段识别并修复问题。
提高代码质量是单元测试的主要好处之一。它鼓励开发者编写结构良好且模块化的代码,从而产生更易于维护和更少错误的程序。单元测试还有助于加快开发速度,因为它可以早期捕捉到问题,从而最小化调试和修复问题所花费的时间。此外,单元测试还作为宝贵的文档,提供了对每个组件预期行为和功能的见解。
使用 SOLID 原则编写可测试的代码
为了有效地利用单元测试,编写可测试的代码至关重要。可测试的代码是模块化的,每个组件都有明确的职责,这使得隔离和测试单个单元变得更容易。确保您的代码可测试的一种方法是通过遵循 SOLID 原则,这是一套旨在促进软件开发中可维护性、灵活性和可测试性的设计指南。SOLID 原则包括以下内容:
-
单一职责原则(SRP):每个类或模块应该有一个单一职责或变化的原因,确保组件具有专注的目的,并且不太可能受到系统其他部分变化的影响。
-
开闭原则(OCP):软件实体应该是可扩展的,但应该是封闭的,这意味着在添加新功能时不应更改现有代码,从而降低引入错误的风险。
-
Liskov 替换原则(LSP):子类型应该是其基类型的可替换的,确保派生类保持其基类的行为,并且不会引入意外的副作用。
-
接口隔离原则(ISP):客户端不应被迫依赖它们不使用的接口。通过创建小型、专注的接口,开发者可以避免不必要的依赖并提高模块化。
-
依赖倒置原则(DIP):高层模块不应依赖于低层模块,而应依赖于抽象。这个原则鼓励使用接口和抽象类来解耦组件,使得它们在隔离状态下更容易测试。
遵循 SOLID 原则可以帮助开发者创建更容易测试和维护的代码,从而提高应用程序的整体质量。
带有单元测试的 TDD
如前所述,TDD 是一种强调在编写实际代码之前编写测试的开发方法。单元测试在 TDD 中扮演着至关重要的角色,因为它们允许开发者验证单个组件的正确性并驱动新功能的实现。
在测试驱动开发(TDD)中,开发者首先为特定功能编写一个失败的单元测试。测试应明确定义代码的期望结果和需求。接下来,开发者编写必要的最少代码以使测试通过。这个过程确保每段代码都是为明确的目的编写的,并且其功能得到了彻底的测试。
一旦测试通过,开发者可以重构代码以改进其结构、可读性或性能,同时确保测试仍然通过。这个编写测试、实现代码、然后根据需要重构的周期会一直重复,直到达到期望的功能。通过使用带有单元测试的 TDD,开发者可以创建更可靠、可维护和健壮的软件应用程序。
单元测试的挑战和局限性
虽然单元测试在三个测试方法中概念上可能最简单,但它仍然有其自身的挑战和局限性。虽然单元测试通常比集成测试更快、更可靠,但它们受限于被测试代码的范围。单元测试专注于独立组件,因此它们无法检测到组件之间交互产生的问题。这意味着通过单元测试并不能保证系统在集成后能正确运行。单元测试的另一个挑战是编写可测试的代码,这需要遵循最佳实践,如 SOLID 原则和依赖注入。正确模拟和存根依赖项也可能是一个挑战,因为这可能需要深入理解依赖项的行为,以创建准确的测试替身。最后,如果单元测试与代码的实现细节耦合得太紧,它们可能会变得脆弱,这使得在不破坏测试的情况下重构代码变得困难。
集成测试
集成测试是软件开发过程中一个至关重要的部分,专注于验证应用中各种组件或模块之间的正确交互。随着软件系统变得更加复杂,确保这些相互关联的部分无缝协作的重要性变得更加关键。在本节中,我们将讨论集成测试的必要方面,包括测试 API 端点和与数据库协同工作。通过理解和实施有效的集成测试策略,开发者可以构建更可靠和健壮的软件应用。
集成测试是什么以及为什么它很重要
集成测试是验证软件应用中各种组件或模块正确协作的过程。与侧重于测试单个组件的单元测试不同,集成测试旨在确保组件在相互集成时按预期工作。这在复杂系统中尤为重要,因为组件间的交互可能导致意外的问题或故障。
集成测试的重要性在于它帮助开发者识别和修复由组件间交互引起的问题。这些问题在单元测试期间可能并不明显,因为只有在各个组件组合在一起时才会变得明显。通过执行集成测试,开发者可以确保软件作为一个整体正确且可靠地运行,从而提供更好的用户体验。
测试 API 端点
API 端点是现代软件应用的关键部分,因为它们促进了不同组件或服务之间的通信。API 端点的集成测试涉及验证 API 返回预期的结果,并在系统中的其他组件调用时表现正确。
要测试 API 端点,开发者通常使用 Postman、Insomnia 或自定义测试脚本等工具,向 API 发送 HTTP 请求并验证响应。这些测试可以验证 API 的各个方面,例如以下内容:
-
响应状态码:这意味着确保 API 在不同场景下返回预期的状态码(例如,200 OK,404 Not Found)
-
响应数据:这意味着验证 API 以预期的格式返回正确的数据(例如,JSON,XML)
-
错误处理:这意味着检查 API 能够优雅地处理错误,并返回有意义的错误信息
-
性能和可靠性:这意味着在不同负载下测试 API 的性能,并确保其满足所需的性能标准
与数据库的集成测试
数据库在许多软件应用程序中扮演着核心角色,因为它们存储和管理系统使用的数据。与数据库的集成测试包括验证应用程序是否正确地与数据库交互,并确保数据按预期进行读取、写入、更新和删除。
值得注意的是,测试数据库可能会具有挑战性,并且通常会被跳过,转而进行更健壮的应用与数据库交互的测试。然而,尝试尽可能多地测试应用程序仍然是一种良好的实践,因此,如果你决定走这条路,以下是一些提示。
要进行与数据库的集成测试,开发者可以使用各种技术,例如以下这些:
-
使用测试数据:开发者可以创建代表不同场景的测试数据集,例如典型用户数据、边缘情况或无效数据。这些数据集可以用来测试应用程序与数据库的交互,并验证数据是否被正确处理。
-
模拟或存根数据库连接:为了在测试期间将应用程序与实际数据库隔离,开发者可以使用模拟或存根技术来模拟数据库的行为。这允许他们在不实际连接到数据库的情况下测试应用程序与数据库的交互,从而使测试更快、更可靠。
-
测试数据库迁移:在那些使用数据库迁移来管理模式变更的应用程序中,开发者可以测试迁移脚本,以确保它们正确应用变更且不会引入问题或数据丢失。
通过对数据库进行集成测试,开发者可以确保他们的应用程序正确地与数据库交互,并且数据被可靠地处理和存储,为软件的整体功能提供坚实的基础。
集成测试的挑战和局限性
集成测试的挑战和局限性主要源于系统组件之间交互的复杂性增加。由于涉及的依赖关系,集成测试通常需要更多的时间和资源来设置、执行和维护。创建与生产环境相似度高的测试环境可能既耗时又昂贵。此外,集成测试可能不太可靠,因为它们更容易受到外部因素(如网络延迟或第三方服务中断)引起的问题的影响。此外,集成测试通常范围更广且更复杂,这使得确定失败的根本原因更加困难,从而导致调试时间增加。
端到端测试
E2E 测试是软件测试过程中的一个关键方面,它从用户的角度测试整个应用程序流程。此类测试验证了应用程序的所有组件是否能够无缝协作,确保应用程序满足其预期功能并提供流畅的用户体验。E2E 测试有助于识别可能来自各个组件之间交互的问题,这些问题可能在单元或集成测试中无法检测到。
编码用户旅程
E2E 测试涉及将现实生活中的用户旅程或工作流程编码为测试用例,这些测试用例模拟用户与应用程序的交互。这些用户旅程涵盖了应用程序的完整流程,从初始用户输入到最终输出或结果。通过模拟用户旅程,E2E 测试确保应用程序按预期行为,并在部署前检测和解决可能在实际使用中出现的任何问题。
设计有效的 E2E 测试场景
创建有效的 E2E 测试场景需要仔细考虑各种因素。开发者应专注于识别应用程序最重要的和最常用的工作流程或功能,以及覆盖边缘情况和潜在的故障点。测试场景应包括可能揭示隐藏问题的非常见或异常情况。根据测试场景的重要性、复杂性和对应用程序整体功能可能产生的影响进行优先排序也是必不可少的。最后,确保测试的可维护性很重要——测试场景应易于理解、更新和维护,随着应用程序的发展。
E2E 测试的挑战和局限性
虽然 E2E 测试是软件开发过程中的一个重要部分,但它也伴随着某些挑战和局限性。E2E 测试可能耗时且资源密集,尤其是在模拟复杂的用户旅程或测试大型应用程序时。由于网络延迟、超时或不可预测的用户行为等因素,有时可能会出现测试不稳定的情况,导致结果不一致。随着应用程序的发展,E2E 测试可能需要频繁更新以反映应用程序功能和工作流程的变化,这可能会使测试维护更具挑战性。此外,可能无法在 E2E 测试中涵盖所有可能的用户旅程和场景,这可能导致未检测到的问题。
尽管存在这些挑战,E2E 测试仍然是软件测试过程中的一个关键组成部分,有助于确保应用程序正确运行并提供可靠的用户体验。通过设计有效的 E2E 测试场景并解决挑战和局限性,开发者可以构建高质量、健壮的应用程序。
SaaS 应用程序测试工具和框架概述
可用于运行测试的工具和框架数量庞大,每个都有自己的优点和缺点。然而,在本节中,我们将我们的焦点限制在适用于 Microsoft 技术的框架上,例如我们在演示应用程序中使用过的那些。通过缩小范围,我们可以为使用这些技术在他们的 SaaS 应用程序中工作的开发者提供更具体和相关的讨论。
.NET 应用程序的一般测试
当使用 .NET 开发 SaaS 应用程序(或任何应用程序!)时,确保代码经过良好的测试和可靠是非常重要的。.NET 中最受欢迎的两个测试框架是 xUnit 和 NUnit。这两个框架都是开源的,被广泛使用,并且得到了 .NET 社区的良好支持。它们提供了一套丰富的特性和功能,使开发者能够为他们的应用程序编写全面的测试。
xUnit 是一个专为 .NET 设计的现代和可扩展的测试框架。它是 .NET Core 和 ASP.NET Core 项目的默认测试框架,对于在现代 .NET 应用程序上工作的开发者来说是一个极佳的选择。它的一些关键特性包括以下内容:
-
编写测试的简洁简单语法
-
支持并行测试执行,这可以加快测试过程
-
一套强大灵活的断言和测试属性
NUnit 是 .NET 中另一个流行的测试框架,在 .NET 社区中有着悠久的使用历史。尽管它不是 .NET Core 和 ASP.NET Core 项目的默认测试框架,但它仍然得到了广泛的支持,并为编写单元测试提供了一套坚实的功能。NUnit 的一些关键特性包括以下内容:
-
编写测试的熟悉语法,尤其是对于在其他测试框架中具有经验的开发者来说
-
支持并行测试执行
-
一套全面的断言和测试属性
两者之间实际上几乎没有区别,选择使用哪一个将主要取决于个人偏好,并且几乎不会对你的项目产生重大影响。
除了 xUnit 和 NUnit,还有其他有用的工具和库可以用于测试 .NET 应用程序,例如:
-
Moq: 这是一个流行的 .NET 模拟库,可用于创建模拟对象并在测试中设置它们行为的期望。
-
FluentAssertions: 这是一个提供更易读和表达性语法的库,用于在测试中编写断言,使得理解测试的意图更加容易。
-
NSubstitute: NSubstitute 是 Moq 的替代品,是 .NET 中另一个流行的模拟库。它提供了一种简单直观的语法来创建模拟对象并在测试中定义它们的行为。NSubstitute 可以与 NUnit、xUnit 以及其他测试框架一起使用。
-
AutoFixture: AutoFixture 是一个帮助自动化生成单元测试测试数据的库。它可以创建具有随机或自定义值的对象,使得设置测试场景时手动配置最小化,从而简化了测试场景的设置。AutoFixture 可以与 NUnit 和 xUnit 等其他测试框架一起使用。
-
Shouldly: Shouldly 是一个类似于 FluentAssertions 的断言库,旨在为测试中的断言提供更易于阅读和表达的语言语法。它简化了编写断言的过程,并使得理解测试的意图更加容易。
-
SpecFlow: SpecFlow 是一个针对 .NET 的 BDD 工具,允许开发者使用 Gherkin 语法以自然语言格式编写测试。它允许非技术利益相关者理解和参与测试场景,弥合开发团队和业务团队之间的差距。
测试 API
当涉及到编写 Web API 的自动化测试时,Postman 和 Newman 等工具可以非常有价值。Postman 是一个强大的 API 测试工具,允许开发者向 API 端点发送 HTTP 请求并检查响应,这使得在开发过程中调试和验证 API 的行为变得更加容易。另一方面,Newman 是 Postman 的命令行伴侣工具,允许你直接从命令行或作为 持续集成/持续部署 (CI/CD) 管道的一部分运行 Postman 收集。
在本书的示例中,我们一直使用 Thunder Client,主要是为了将所有内容都包含在 Visual Studio Code (VSCode) 内。Postman 提供了一些更高级的功能,例如预请求脚本和文档生成。随着你的 SaaS 项目增长,使用 Postman 而不是 Thunder Client 可能会有一些优势。Thunder Client 是一个轻量级且易于使用的选项,适合想要将简单的 API 测试工具集成到 VSCode 环境中的开发者。另一方面,Postman 是一个功能更强大、特性更丰富的工具,适合高级 API 测试场景和团队协作。你在这两个之间的选择将取决于你的具体需求和个人偏好。
在测试 API 时模拟 HTTP 客户端可能会有些棘手,但有一些库如 Moq 和 HttpClient Interception 可以帮助简化这一过程。API 测试也可以被视为一种集成测试的形式,因为它涉及到验证 API 各个组件之间的正确交互。
测试 Blazor 应用程序
由于技术的特性,测试 Blazor 应用程序可能有点更具挑战性。然而,有一些工具和库可以帮助简化这个过程:
-
bUnit: 这是一个专门为 Blazor 应用程序设计的测试库,允许开发者编写单元和组件测试
-
Playwright:这是一个浏览器自动化库,可用于编写 Blazor 应用程序的端到端测试,模拟用户交互并验证应用程序的行为
-
Selenium:虽然不是专门为 Blazor 设计的,但 Selenium 是一个流行的浏览器自动化工具,也可以用于编写 Blazor 应用程序的端到端测试
编写数据库测试的挑战
由于与数据库一起工作的固有复杂性,测试数据库相关代码可能具有挑战性。专门为数据库交互编写测试相对较少见,但有一些原因和一般性指南需要考虑:
数据库可能会引入状态性和外部依赖性到测试中,使得维护隔离和确定性的测试环境更加困难。
可能更有效的是专注于测试应用程序的数据访问层和业务逻辑,而不是直接测试数据库本身。
当测试与数据库交互的代码时,请考虑使用诸如模拟或存根等技术来隔离数据库相关代码并模拟数据库的预期行为。
为了更有效地测试数据库相关代码,请考虑使用专门的数据库测试工具,如针对 SQL Server 的 tSQLt,它允许您为数据库对象(如存储过程、函数和触发器)编写单元测试。
通过考虑这些因素并采用适当的工具和技术,您可以通过对应用程序所有方面的全面测试来提高您 SaaS 应用程序的质量。
实际演示
虽然本书的范围不包括提供完整的测试套件,但我们可以通过实际演示一些在本章中讨论的工具和技术,向我们所构建的GoodHabits应用程序添加一些单元测试。
让我们从添加一个测试项目开始。我们将使用 xUnit,因为它通常被推荐用于现代.NET 应用程序。我们还将向此项目添加 Moq 和 Fluent Assertions,并查看我们如何使用它们。
使用以下脚本执行此操作:
mkdir test; \
cd test; \
dotnet new xunit --name GoodHabits.HabitService.Tests; \
cd GoodHabits.HabitService.Tests; \
dotnet add reference ../../GoodHabits.HabitService/
GoodHabits.HabitService.csproj; \
dotnet add package Moq; \
dotnet add package FluentAssertions; \
rm UnitTest1.cs ; \
touch HabitsControllerTest.cs ; \
cd ../..
dotnet sln add ./tests/GoodHabits.HabitService.Tests/
GoodHabits.HabitService.Tests.csproj;
上述脚本将添加一个名为HabitsControllerTest.cs的文件,我们将使用它来测试HabitsController。添加以下代码:
using Moq;
using FluentAssertions;
using GoodHabits.HabitService.Controllers;
using Microsoft.Extensions.Logging;
using GoodHabits.HabitService;
using AutoMapper;
using Microsoft.AspNetCore.Mvc;
public class HabitsControllerTests
{
private readonly HabitsController _habitsController;
private readonly Mock<ILogger<HabitsController>>
_loggerMock;
private readonly Mock<IHabitService> _habitServiceMock;
private readonly Mock<IMapper> _mapperMock;
public HabitsControllerTests()
{
_loggerMock = new Mock<Ilogger
<HabitsController>>();
_habitServiceMock = new Mock<IHabitService>();
_mapperMock = new Mock<IMapper>();
_habitsController = new HabitsController
(_loggerMock.Object, _habitServiceMock.Object,
_mapperMock.Object);
}
[Fact]
public async Task GetVersion_ReturnsExpectedVersion()
{
var result = await _habitsController.GetVersion();
var okResult = result.Should().BeOfType
<OkObjectResult>().Subject;
okResult.Value.Should().Be("Response from version
1.0");
}
}
您现在可以通过打开终端并输入dotnet test来运行测试。您应该会看到以下内容,表明测试已通过:

图 9.3 – 第一次测试通过
上述测试是一个非常简单的测试,以确保我们从版本端点获取正确的字符串。但我们已经展示了某些高级测试技术。我们使用了Moq包来创建控制器所有依赖项的模拟。
我们还使用了FluentAssertions库来使测试非常易于阅读。仅从阅读这一行就可以非常明显地看出其意图!
okResult.Value.Should().Be("Response from version 1.0")
这只是一个对测试的温和介绍——还有很多可以做的事情来证明HabitsController类的正确操作!开始构建这个测试套件并可能为其他项目添加一些测试将是一个极好的练习。或者甚至添加一些集成和端到端测试!
摘要
在本章中,我们探讨了测试在 SaaS 应用程序开发和维护中的重要作用。通过了解各种测试类型——单元测试、集成测试和端到端测试——以及它们在开发过程中的相应角色,你现在更有能力为你的应用程序实施全面的测试策略。
我们还讨论了 TDD 及其好处,如提高代码质量、加快开发周期和更容易维护。通过将 TDD 纳入你的开发过程,你可以进一步提高 SaaS 应用程序的可靠性和功能性。
我们还通过查看一些底层技术和你可以用来应用这些技术的工具,对测试进行了高级概述。
本章为你提供了对测试在 SaaS 应用程序开发中扮演的重要角色的全面理解。我们希望你现在可以自信地将这些概念和实践应用到自己的项目中,从而产生更健壮、可靠和高质量的 SaaS 应用程序。
随着你继续开发和部署你的 SaaS 应用程序,监控它们的性能并记录相关信息以确保平稳运行和快速解决可能出现的任何问题是至关重要的。
在下一章中,我们将讨论监控和日志记录,涵盖维护和优化生产环境中 SaaS 应用程序的必要工具和最佳实践。
进一步阅读
-
什么是单元测试?
smartbear.com/learn/automated-testing/what-is-unit-testing/ -
集成测试:是什么,类型及示例:
www.guru99.com/integration-testing.html -
在 ASP.NET Core Blazor 中测试 Test Razor 组件:
learn.microsoft.com/en-us/aspnet/core/blazor/test?view=aspnetcore-7.0&viewFallbackFrom=aspnetcore-7.0 -
什么是测试驱动开发?TDD 与 BDD 与 SDD 的比较:
testrigor.com/blog/what-is-test-driven-development-tdd-vs-bdd-vs-sdd/ -
单元测试:为什么值得去做?
www.cmsdrupal.com/blog/unit-testing-why-bother
问题
-
测试金字塔中的三种主要测试类型是什么,每种类型的主要目的是什么?
-
TDD 如何提高代码质量、开发速度和可维护性?
-
微软生态系统中有哪些流行的测试工具和框架,以及它们的主要用途是什么?
-
单元测试如何帮助确保 SaaS 应用程序中单个组件的正确性和可靠性?
-
为什么在验证 SaaS 应用程序中不同组件和服务之间的交互时,集成测试很重要?
-
端到端测试如何有助于确保 SaaS 应用程序的整体功能和使用体验?
第十章:监控和日志记录
一个典型的软件即服务(SaaS)应用将服务于数百万用户,他们全天候访问该平台。当意外问题出现时,诊断、重现和解决这些问题可能极其困难。监控和日志记录是解决这一挑战的关键工具,它们通过提供对在生产环境中运行并被实际使用的应用程序的健康和性能的宝贵见解。
监控侧重于通过实时收集和分析关键指标来主动观察系统的健康和性能。这是一个“自上而下”的视角,涵盖了整个系统的整体健康状况,包括资源利用率等方面。这个过程使开发者能够识别潜在问题,优化资源利用率,并保持无缝的用户体验。例如,应用洞察和应用 Azure Monitor 等技术提供定制解决方案,以有效地在 SaaS 应用中实施监控策略,确保可靠性和高性能。
相反,日志记录对于捕捉应用程序内部发生的广泛事件至关重要。日志记录比监控更细致,通常捕获应用程序代码中发生的问题和事件。关于错误、用户操作和系统事件的详细信息,使开发者能够有效地诊断和解决问题,同时为安全和合规目的维护全面的审计跟踪。通过利用日志库,开发者可以无缝地将日志集成到他们的 SaaS 应用程序中。
本章将涵盖以下主题:
-
监控
-
日志记录
-
SaaS 应用的监控和日志记录考虑因素
本章深入探讨了 SaaS 应用环境下的监控和日志记录,突出了由此产生的独特挑战和考虑因素。将提供实用指南,帮助实施高效的监控和日志记录策略,帮助 SaaS 应用的开发者维护高性能和可靠的应用程序。
让我们从两者的概述开始,然后更详细地探讨它们。
概述
监控和日志记录都是你可以用来查看你的 SaaS 应用性能的工具。由于 SaaS 应用通常有许多动态部分,获得整个系统的健康状况的整体视图可能非常困难,可能涉及多种不同的技能集。
例如,如果用户报告应用程序“运行缓慢”,这可能是以下原因造成的:
-
如果用户的个人电脑或网络运行缓慢,在这种情况下,我们对此无能为力!
-
连接到云服务的网络连接缓慢,在这种情况下,我们需要网络专家来解决,可能还需要网络提供商的帮助来增加带宽。
-
API 可能难以处理多个并发请求,在这种情况下,我们需要后端开发者来识别问题,以及 DevOps 专家来正确扩展 API。
-
用户界面可能对 API 进行了非常低效的请求,导致性能缓慢。这需要前端和后端开发者的协调努力来解决。
-
数据库可能是瓶颈。也许数据库的索引不足,因此需要数据库管理员(DBA)来识别和纠正问题。
我可以继续!在 SaaS 应用程序中诊断客户问题可能非常困难,并且可能需要广泛的专家来识别和解决问题。
当应用程序在生产环境中运行时,应用程序的开发者对应用程序中出现的任何问题了解非常有限,因此通常使用监控和日志记录技术来实时跟踪发生的情况。没有这些工具,诊断生产应用程序中的问题几乎完全是猜测,并且是一个耗时且令人沮丧的练习。
从一个非常广泛的概述来看,我们可以说监控和日志记录都能让我们了解当应用程序在生产环境中运行时发生了什么。这些洞察力被开发者和维护团队用来更快地诊断和纠正用户问题。
然而,这有点过于宏观,所以我们将深入探讨一些细节!
监控通常从外部视角关注应用程序及其组件的整体健康和性能,包括应用程序服务、网络和数据库。它提供了系统操作的概览,并识别潜在的问题和瓶颈。监控通常是主动的,可以用来在问题出现之前识别问题,例如存储设备空间不足或可用带宽开始接近其限制。你可以把监控看作是一个自上而下的过程,它是从应用程序外部向内看的。监控是从外部向内部看。
另一方面,日志记录更侧重于捕获关于应用程序代码内部发生的事件、错误和事务的详细信息。这些详细数据有助于开发者诊断、排除故障和理解与应用程序内部工作相关的特定问题。因此,虽然监控提供了一个更广泛的从外向内的视角,但日志记录深入到应用程序代码的细微之处,并记录其行为。日志记录总是回顾性的。它是存储应用程序中已经发生的事情的地方。你可以把日志记录看作是一个更精细的过程,它在应用程序中得到备份,向某些外部存储此类动作的存储库报告事件和动作。日志记录是从内部向外看。
下表显示了监控和日志记录之间的区别:
| 监控 | 日志记录 |
|---|---|
| 实时观察性能和资源使用 | 记录事件、错误和事务 |
| 关注系统健康和可用性 | 关注详细信息和审计跟踪 |
| 主动检测异常和潜在问题 | 对历史数据的回顾性分析 |
| 优化资源利用率 | 应用程序问题的诊断和故障排除 |
| 应用程序组件的高级视图 | 应用程序代码行为的深入了解 |
| 从外部看内部 | 从内部看外部 |
表 10.1 – 监控与日志记录的区别
您可以看到,虽然这两个主题相关,但每个主题的功能和目的之间存在着相当大的差异。我们现在将详细探讨这两个主题,从监控开始。
监控
监控是指持续观察和测量系统的各个方面,如性能、资源利用率和可用性,以确保其最佳运行并识别潜在问题。在 SaaS 应用程序的背景下,监控涉及实时收集和分析关键指标和事件,使开发者能够主动检测异常、优化资源并保持无缝且可靠的用户体验。
监控是维护 SaaS 应用程序健康和性能的关键方面。在一个每天 24 小时有数百万用户访问平台,执行各种操作的环境中,主动观察系统变得至关重要。本节将探讨实施有效监控在 SaaS 应用程序中的关键概念、工具和策略。
监控的关键方面
在为您的应用程序构建监控系统时,有几个关键考虑因素需要记住:
-
性能指标对于衡量 SaaS 应用程序的响应性和效率至关重要。这些指标可以包括响应时间、吞吐量、错误率和延迟等。通过密切监控这些参数,开发者可以识别瓶颈和优化区域,确保流畅且令人满意的用户体验。
-
资源利用率监控涉及跟踪应用程序如何消耗系统资源,例如 CPU、内存、磁盘空间和网络带宽。通过监控资源消耗,开发者可以检测和预防与资源竞争或耗尽相关的问题,这些问题可能会对应用程序的性能和稳定性产生负面影响。这种洞察力还有助于做出关于扩展和基础设施管理的明智决策。在我们现在工作的以云为先的世界中,资源利用率对企业的成本有显著影响,因此现在在所有时候都掌握这一点变得更加重要。
-
应用程序可用性和健康监控侧重于评估应用程序及其组件的运行状态。这包括监控系统的正常运行时间、错误率和单个服务或组件的性能。通过跟踪应用程序的健康状况,开发者可以主动检测并解决问题,在问题升级之前进行干预,最小化停机时间并保持对用户的高服务水平。
-
长期趋势和容量规划涉及分析一段时间内的历史监控数据,以识别模式和预测未来的系统需求。通过了解用户增长、资源消耗和性能指标的趋势,开发者可以做出关于基础设施投资的明智决策,优化资源,并为增加的需求准备应用程序。这种前瞻性使 SaaS 提供商能够提供始终如一可靠且性能良好的服务,即使随着时间的推移,用户基础和工作负载发生变化。
如果您牢记这四个关键考虑因素,您应该已经朝着为您的 SaaS 应用程序提供成功的监控系统迈出了坚实的步伐。当然,这不仅仅是这些!因此,我们现在将探讨作为 SaaS 开发者可能会遇到的某些细微差别。
监控工具
我们已经讨论了监控您的 SaaS 应用程序的重要性。现在,我们将探讨您可以使用哪些工具来完成这项重要任务。
通常,建议使用现成的监控解决方案,而不是尝试自定义构建此功能。监控工具可以与其监控的应用程序一样复杂!这些工具提供高度专业化的功能,通常最好留给专业人士来实现。监控有许多不同的选项,但一般来说,一个好的监控工具应该提供以下功能:
-
收集和显示相关数据:这些都是监控的绝对基础!一个好的监控工具应该能够收集和显示各种相关数据,包括服务器性能指标、特定于应用程序的指标和用户行为数据。
-
提供实时监控:实时监控对于快速检测和响应出现的问题至关重要。一个好的监控工具应该能够提供关于您应用程序状态和性能的实时更新,这些更新可以通过仪表板或其他类似方式查看。
-
警报和通知:当检测到问题时,工具应该能够通过电子邮件、短信或 Slack 等聊天工具等方式进行警报和通知。期望团队成员全天候监控仪表板是不合理的,因此警报系统可以用来通知团队有问题发生。工具还应提供可定制的警报阈值,以便您可以设置不同类型问题的适当紧急程度。这很重要,因为频繁的非重要错误消息会导致人们忽略所有消息,从而错过重要的消息。
-
启用主动监控:除了对发生的问题做出反应外,一个好的监控工具还应通过在问题影响用户之前提供对潜在问题的洞察来启用主动监控。这可以通过预测分析和趋势分析等特性实现,使团队能够提前采取行动,防止问题发生。
-
支持定制化:没有两个 SaaS 应用是完全相同的,因此工具应该允许高度定制和配置,以满足您应用的具体需求。这包括创建自定义仪表板和报告的能力,以及将它们与其他工具和系统(尤其是我们将在本章后面讨论的日志系统)集成。
-
提供可扩展性和可靠性:一个好的监控工具应该能够处理大量数据,并在高负载下提供可靠的性能。它还应该能够根据需要向上或向下扩展,以适应您应用使用模式的变化。
-
促进协作:一个好的监控工具应该促进维护和改进您应用的各个团队和利益相关者之间的协作。随着应用的增长,将有多个团队对应用整体健康状况的不同方面感兴趣。每个用户类别都应能够通过使用基于角色的访问控制功能和共享仪表板和报告的能力,从监控工具中获得所需的信息。
推荐使用特定的监控工具非常困难,因为最佳选择将取决于正在实施的技术堆栈。鉴于本书专注于.NET 和 Microsoft 技术堆栈,可以说基于 Azure 的系统,如应用洞察或 Azure Monitor,将是最有用的。
这里有一些常用的监控工具列表,您可能希望考虑。请注意,这里有一些与日志记录工具的重叠,正如我们将在本章后面看到的:
-
应用洞察:一个基于 Microsoft Azure 的监控服务,为.NET 应用提供全面的应用性能监控和诊断。
-
Azure Monitor:一个 Microsoft Azure 服务,用于收集、分析和对来自各种 Azure 和本地资源的遥测数据进行操作,包括应用程序和基础设施监控。
-
Datadog:一个基于云的监控和分析平台,提供跨应用程序、基础设施和云服务的全栈可观察性。
-
New Relic:一个全面的应用性能监控和管理平台,提供对应用程序和基础设施性能和健康状况的实时可见性。
-
Prometheus:一个开源的监控和警报工具包,主要设计用于可靠性和可扩展性,通常与 Kubernetes 等容器编排系统一起使用。
-
Grafana:一个流行的开源可视化和分析平台,允许用户使用来自各种监控工具的数据创建和共享交互式仪表板和警报。
-
Elasticsearch、Logstash、Kibana (ELK 堆栈):一个流行的开源日志管理和分析平台,结合了 Elasticsearch 用于搜索和分析、Logstash 用于日志处理和 Kibana 用于数据可视化。
如何做到这一点
我们已经讨论了很多可用于此目的的工具,但并没有太多关于如何实际操作的讨论!以下是一份您在设置监控策略时可能想要考虑的步骤列表。记住,监控是“从外部看内部”:
-
定义对您的特定应用程序重要的指标。这里没有一刀切的方法;您需要仔细考虑哪些信息可能对您的应用程序有用。
-
选择一个工具。再次强调,没有一种“最佳”的工具可以使用。研究您可用的选项,并决定哪个最适合。这些工具通常是付费服务,因此创建发票并购买工具。
-
配置监控工具以收集定义的指标。根据您选择的工具,这可能涉及在您的服务器上安装代理、配置 API 集成或设置自定义脚本。
-
为您监控的指标设置适当的阈值、警报和通知。这将帮助您在这些问题影响用户之前,主动检测异常、性能问题或潜在的瓶颈。
-
将您的监控工具与现有的开发和运维工作流程集成,例如您的缺陷跟踪系统、CI/CD 管道和通信平台。这将确保您的团队能够及时了解任何问题,并立即采取行动。
-
随着应用程序的发展,持续审查和改进您的监控策略。随着新功能的添加、性能要求的改变或用户期望的增长,您可能需要相应地调整您的监控方法。
-
定期分析收集到的监控数据,以识别趋势、模式和潜在的优化区域。这将帮助您就应用程序的架构、资源分配和未来的开发优先级做出明智的决策。
通过遵循这些步骤并根据您 SaaS 应用程序的独特需求定制监控策略,您将充分准备,以维护一个可靠、高性能和具有弹性的平台,为您的用户提供服务。
监控最佳实践
监控对于任何应用程序来说都可能具有挑战性,在 SaaS 应用程序的背景下,复杂性显著增加。在本节中,我们将探讨一系列针对有效监控 SaaS 应用程序的最佳实践,以提高您成功的可能性:
-
定义相关指标和阈值:在监控使用微软技术构建的 SaaS 应用程序时,定义准确反映应用程序健康和性能的相关指标和阈值至关重要。这可能包括响应时间、错误率、资源利用率和吞吐量等指标。为这些指标建立适当的阈值将帮助您在问题升级并影响用户体验之前发现潜在问题。
-
实施主动监控和警报:主动监控涉及持续观察应用程序的性能和健康,让您能够及早发现问题并采取纠正措施。利用微软技术,可以使用 Application Insights 和 Azure Monitor 等工具设置主动监控和警报。通过根据预定义的阈值配置警报,您可以在问题出现时立即通知您的团队,最小化停机时间并保持高质量的用户体验。
-
确保多租户环境中的数据隐私和合规性:SaaS 应用程序通常在单个应用程序实例中为多个租户提供服务,这引发了数据隐私和合规性问题。在监控多租户应用程序时,保持适当的数据隔离并确保租户特定的性能数据不可访问给其他租户至关重要。例如,Azure Monitor 等微软技术可以帮助您在遵守隐私和合规要求的同时实施租户特定的监控。
-
将监控数据与日志和其他诊断工具集成:监控和日志通过提供对应用程序性能和健康的不同见解而相互补充。将监控数据与日志和其他诊断工具集成可以帮助您更全面地了解应用程序的行为,并更有效地识别问题的根本原因。例如,Application Insights 和 Azure Monitor 等工具可以与 ELK Stack 或 Azure Log Analytics 等日志平台集成,使您能够关联监控和日志数据以进行更深入的分析。
-
监控中的警报和通知:除了收集和分析监控数据外,为您的 SaaS 应用建立有效的警报和通知系统至关重要。警报涉及为相关指标配置预定义的阈值,当这些阈值被突破时,通知会被发送给适当的团队成员,使他们能够快速响应并减轻对用户体验的潜在影响。例如,Microsoft 的技术,如 Application Insights 和 Azure Monitor,提供了强大的警报功能,可以根据您应用的独特需求进行定制。通过将这些警报功能与通信工具(如电子邮件、短信或协作平台,如 Microsoft Teams 或 Slack)集成,您可以确保您的团队能够了解任何关键问题,并能够及时采取行动解决这些问题。
-
持续优化和改进监控策略:随着应用需求和时间推移,监控策略应与您的应用同步发展。持续审查和优化您的监控策略确保您始终关注最相关的指标,并能够主动解决新兴问题。通过利用 Application Insights 和 Azure Monitor 等监控工具提供的洞察力和分析,您可以持续改进您的监控方法,并保持高性能、可靠的 SaaS 应用。
在本节中,我们首先探讨了监控的初衷,考虑了其在 SaaS 应用中的适用性,审视了可用的工具,并讨论了最佳实践。现在,我们将转向思考日志记录。
日志记录
日志记录:与监控相比,日志记录侧重于捕获关于应用内部事件、用户行为和系统行为的详细信息。虽然监控提供了对应用性能和健康状况的高级视图,但日志记录允许您深入了解特定事件和发生情况,从而实现有效的故障排除并维护全面的审计记录,以用于安全和合规目的。
日志记录是捕获和记录系统内部发生的事件、错误和用户行为的详细信息的实践,为开发者提供了宝贵的洞察力,用于故障排除和诊断问题。虽然监控侧重于对系统健康和性能的实时观察,但日志记录更关注于维护应用事件和活动的全面记录,以便未来分析。
日志在维护和改进 SaaS 应用程序中发挥着不可或缺的作用,因为它允许开发者理解应用程序内部发生的复杂交互和过程。随着数百万用户持续与平台互动,拥有详细的系统事件日志对于确定问题的根本原因和确保平稳运行变得至关重要。本节将深入探讨实施有效的 SaaS 应用程序日志记录的关键概念、工具和技术。通过采用针对 SaaS 环境独特需求的日志实践,开发者可以增强其诊断和解决问题的能力,维护强大的审计跟踪以符合安全和合规要求,并最终向用户提供可靠且性能卓越的服务。
日志的关键方面
在本节中,我们将探讨实施全面有效的 SaaS 应用程序日志策略的关键方面,使开发者能够获得宝贵的见解,维护强大的审计跟踪,并确保应用程序性能最优化。
任何日志系统的基本是能够从各种来源收集信息,例如应用程序、数据库以及一组微服务或容器。高效执行此操作的能力是任何成功日志策略的基础。一个设计良好的日志收集系统应该能够处理由您的应用程序生成的各种类型和数量的日志数据,同时最大限度地减少对应用程序性能的影响。确保所有相关的日志数据都被捕获并可用于分析。
一旦收集到日志数据,就需要将其存储在集中且易于访问的位置。有效的日志存储策略侧重于数据保留,确保日志数据能够保存适当的时间长度,并在需要时能够快速检索。可扩展性也是一个至关重要的考虑因素,因为日志存储系统必须能够随着你的 SaaS 应用程序的扩展而增长,以适应不断增长的数据量。不要低估日志系统可以收集的数据量!相应地规划,因为数据在云基础设施中的存储可能非常昂贵。
如果难以从日志中提取任何可用的信息,那么收集和存储数据就毫无意义。应建立一个系统,允许相关方阅读和分析日志数据,以识别模式、趋势和异常。这可以帮助开发者更有效地诊断和排除问题,优化资源利用,甚至识别潜在的安全威胁——希望是在它们发生之前!为了促进快速洞察和决策,以图表、图形和仪表板等易于消化的格式呈现日志数据非常重要。Kibana、Grafana 和 Azure Monitor 等日志可视化工具可以帮助将原始日志数据转换为有意义的视觉表示,使开发者和运维团队能够更容易地了解应用程序的状态并识别改进领域。这些工具还可以根据您的特定 SaaS 应用程序定制,以突出显示最相关的信息。
随着 SaaS 应用程序产生的日志数据量巨大,过滤掉无关或嘈杂的日志数据,专注于最关键和可操作的信息至关重要。日志过滤技术可以在日志过程的各个阶段(从收集到分析)使用,以减少噪声并提高信噪比。通过实施有效的日志过滤策略,开发者可以节省时间和资源,专注于最相关的日志数据,并确保重要事件不会在噪声中丢失。
确保日志数据的机密性、完整性和可用性是日志的关键方面,因为它涉及遵守数据保护法规和遵循行业最佳实践。日志安全措施可能包括加密、访问控制和数据备份策略,所有这些旨在保护日志数据免受未经授权的访问、篡改或丢失。
基于特定的日志事件或模式配置警报对于主动识别您 SaaS 应用程序中的潜在问题至关重要。日志警报功能可以在检测到潜在问题时及时向适当的团队成员发送通知,从而允许迅速采取行动解决问题。
最后,没有必要永远保留所有日志数据,但保留一些数据较长时间可能是有用的。为了未来的参考、分析或合规目的,保留某种形式的日志数据历史记录可能非常有用,并且在构建日志系统时应予以考虑。
日志工具
通常,建议使用现成的日志解决方案。日志现在是一个相当成熟且被充分理解的概念,因此自己构建定制实现通常没有太多好处。在本节中,我们将探讨一些一般性指南,以帮助选择一个好的日志工具,然后考虑一些具体的工具:
-
收集和存储日志: 一个好的日志工具应该能够从各种来源收集和存储日志,例如服务器、应用程序和数据库。它还应该能够处理大量日志并以可扩展和高效的方式存储它们。
-
提供搜索和分析功能: 一个好的日志工具应该提供强大的搜索和分析功能,使您能够轻松搜索和过滤日志以识别问题和解决问题。它还应支持高级查询和过滤,以实现更复杂的分析。
-
启用实时监控: 一个好的日志工具应该提供实时监控功能,以便您能够跟踪日志的生成流程。这可以帮助您在问题发生时及时发现并实时采取纠正措施。
-
提供集中式管理: 一个好的日志工具应该提供日志的集中式管理,使您能够轻松管理来自不同来源的日志,并跟踪日志数据随时间的变化。它还应提供访问控制和权限设置,以确保日志只能由授权人员访问。
-
支持定制: 一个好的日志工具应该可定制以满足您应用程序的特定需求。这包括自定义日志格式和字段的能力,以及与其他工具和系统集成的能力。
-
启用日志关联: 一个好的日志工具应该使您能够关联来自不同来源的日志,并识别日志数据之间的模式和关系。这可以帮助您深入了解应用程序的性能,并识别潜在的问题。
-
提供审计和合规性功能: 一个好的日志工具应该提供审计和合规性功能,帮助您满足监管要求和内部政策。这包括访问控制、记录用户操作以及生成审计报告的能力。
与监控工具一样,很难推荐用于日志记录的具体工具,因为这将根据所使用的特定技术堆栈以及应用程序的用途而有所不同。以下是一些在开始构建日志系统之前您可以进行研究的一些工具,其中.NET/Microsoft 堆栈工具再次排在最前面!请注意,Microsoft 提供了一个旨在与各种内置和第三方日志提供者一起工作的日志 API:
-
.NET 内置提供者: 这通常适用于小型应用程序,但您可能会发现它提供的功能集不如列表中的其他一些工具丰富。这是一个有用的入门工具,但您的应用程序可能会很快超出其范围。
-
Serilog: 这是一个流行的.NET 应用程序结构化日志库,支持多个接收器和增强器,以增强日志功能。
-
NLog:一个灵活且高性能的.NET 日志库,提供高级路由和过滤选项,用于日志事件。
-
log4net:一个广泛使用的.NET 应用程序日志库,灵感来源于流行的 Java 日志库 log4j,提供各种日志目标和灵活的配置选项。
-
Seq:一个集中的日志服务器和结构化日志数据查看器,通常与 Serilog 一起使用,提供强大的查询和可视化功能,用于分析日志事件。
-
ELK Stack:一个流行的开源日志管理平台,结合了 Elasticsearch 进行索引和搜索、Logstash 进行日志处理和路由、以及 Kibana 进行日志数据的可视化和分析。
-
Application Insights:一个 Microsoft Azure 服务,提供应用程序性能监控、诊断和日志功能,易于集成到.NET 应用程序中。
-
Azure Log Analytics:Azure 中的一种日志管理和分析服务,可以收集、存储和分析来自各种来源的日志数据,包括应用程序日志、Azure 资源和虚拟机。
这些工具和服务针对日志的不同方面,从应用程序代码中使用的库到集中的日志管理和分析平台。工具的选择将取决于您的 SaaS 应用程序的具体需求和约束,以及您首选的开发生态系统。
如何做到这一点
实施强大的日志策略对于任何 SaaS 应用程序都是至关重要的。虽然我们已经讨论了可用于日志的各种工具,但了解设置有效日志策略的过程也同样重要。以下是在您的应用程序中实施日志时需要遵循的步骤列表。请记住,日志的重点是记录应用程序代码中发生的事件:
-
识别您应用程序中需要记录的事件和信息。这可能包括错误、用户操作、系统事件以及其他有助于您理解应用程序行为、解决问题以及为安全和合规目的维护审计跟踪的相关数据。
-
选择最适合您应用程序需求和技术堆栈的日志工具或库。有众多日志工具可供选择,每个工具都有其自身的优点和缺点。请确保选择一个与您的应用程序兼容并提供必要功能的工具。
-
配置日志工具以捕获步骤 1 中确定的有关事件和数据。这可能涉及在应用程序代码中设置日志级别、过滤器以及自定义日志条目,以确保您捕获了正确的信息。
-
在您的应用程序中建立一致的日志格式和结构,以便更容易分析和关联日志数据。这可能包括使用标准化的时间戳、日志级别和消息格式,以确保一致性。
-
设置日志聚合和存储,以便集中和保留日志数据以供分析。这可能涉及配置您的日志工具将日志数据发送到中央日志管理系统,将日志存储在数据库中,或使用基于云的日志存储服务。
-
实施日志分析和监控,以主动检测日志数据中的问题和趋势。这可能涉及使用日志分析工具,根据日志事件或模式设置警报,并定期审查日志数据以获取见解。
-
将您的日志策略与现有的开发和运维工作流程集成,例如您的缺陷跟踪系统、CI/CD 管道和通信平台。这将确保您的团队能够意识到任何问题并相应地采取行动。
-
随着应用程序的发展,持续审查和改进您的日志策略。随着新功能的添加、安全要求的改变或用户期望的增长,您可能需要调整您的日志方法以捕获必要的信息。
通过遵循这些步骤并根据您的 SaaS 应用程序的具体需求定制日志策略,您将能够维护应用程序事件的全面记录,更有效地诊断和解决问题,并确保为用户提供一个安全且合规的平台。
日志最佳实践
与监控类似,在 SaaS 应用程序中正确设置日志可能具有挑战性。在设计日志系统时,以下是一些需要考虑的最佳实践:
-
定义日志级别:建立清晰的日志级别,根据日志事件的严重性或重要性对其进行分类,这是非常重要的。这些级别可以包括调试、信息、警告、错误和关键,并有助于您根据对应用程序的影响识别和优先处理问题。
-
使用结构化日志:实施结构化日志可以使得您能够以机器可读的格式捕获日志事件,这使得过滤、搜索和分析日志数据变得更容易。通过在日志中包含结构化数据,您可以提供关于事件的额外上下文和信息,这使得识别和解决问题变得更加容易。
-
包含上下文:确保您的日志消息提供足够的信息,以便识别问题的来源。这可能包括相关的变量值、用户 ID 或时间戳。通过提供这些信息,您可以在问题发生时更容易地识别和解决问题。
-
日志关联:在分布式系统或微服务架构中,追踪请求流和识别多个服务之间的问题可能具有挑战性。使用关联 ID 或跟踪 ID 将相关的日志事件链接起来,可以更容易地识别和解决不同服务和组件之间的问题。
-
集中日志管理:将来自多个来源的日志聚合到集中的日志管理系统可以提供对应用程序性能的全面视图,并使监控和分析日志数据更加容易。这可以使您更快地识别问题并解决问题。
-
实施日志保留策略:根据存储限制、合规要求和历史日志数据的有用性定义日志数据的保留策略。根据需要存档或删除日志,可以降低存储成本并确保符合监管要求。
-
保护敏感信息:避免记录敏感信息,例如个人身份信息(PII)或身份验证凭据,以防止数据泄露并确保符合数据隐私法规。通过实施适当的安全措施,如加密和访问控制,您可以保护您的日志数据免受未经授权的访问。
-
实时监控日志:设置实时日志监控和警报可以帮助您主动检测和解决问题。通过实时监控日志并基于特定的日志事件设置警报,您可以在问题影响用户或系统性能之前快速识别并解决它们。
-
优化日志性能:确保日志记录不会对应用程序的性能产生负面影响。这可能包括根据需要使用异步日志记录、批处理和节流来优化日志性能并防止与日志相关的性能问题。
-
回顾和优化:定期回顾和优化您的日志策略可以帮助您识别改进区域,并根据需要调整日志级别、消息格式或保留策略。通过持续改进您的日志系统,您可以确保它随着时间的推移保持有效和高效。
接下来,我们将探讨一些您必须牢记的 SaaS 特定考虑因素。
SaaS 应用程序的监控和日志考虑因素
正如我们在本书中发现的,开发 SaaS 应用程序可能具有挑战性,在使用各种类型的技术时有许多具体考虑因素。在本节中,我们将探讨可能更具体于 SaaS 应用程序的监控和日志考虑因素:
-
多租户是在构建 SaaS 应用程序时常用的一种技术。在多租户环境中进行监控和日志记录需要仔细关注,以确保租户数据的适当隔离并跟踪特定租户的性能指标。开发者需要设计能够有效识别影响特定租户的问题,同时保持数据隐私和合规性的监控和日志记录策略。正如我们在第三章中讨论的那样,在多租户系统中保持数据隔离既困难又极其重要。如果处理不当,集中数据收集的系统,如监控或日志系统,很容易成为链条中的薄弱环节。
-
微服务已成为构建可扩展和可维护 SaaS 应用程序的流行架构风格。监控和日志记录微服务需要一种细粒度方法来捕获应用程序中各个服务的性能、健康和事件。这可能会使构建监控和日志记录基础设施变得具有挑战性,因为微服务星座中可能有多个不同的服务,每个服务都有自己的要求。在微服务应用程序中调试运行时错误可能会迅速变成一场噩梦。尽管这增加了一些挑战,但为微服务应用程序构建健壮的监控和日志记录仍然极其重要。
-
可扩展性是 SaaS 应用程序的一个关键方面,因为用户基础和工作负载可以迅速增长。我们将在第十二章中详细讨论大规模运营。监控和日志记录系统应设计为适应规模的变化,确保它们即使在应用程序增长的情况下也能继续提供准确和及时的洞察。这包括监控资源消耗、负载均衡和自动扩展功能,以保持最佳性能和资源分配。日志系统还应能够处理不断增长的数据量和用户负载。
-
分布式架构涉及多个组件和服务在不同物理或虚拟位置协同工作。监控和日志记录此类系统需要一种全面的方法,能够捕获和关联来自各种来源的事件和指标,使开发者能够全面了解应用程序的健康状况、性能和事件历史。分布式跟踪、日志聚合和集中式监控等技术可以帮助管理分布式架构的复杂性。
-
与云服务的集成在 SaaS 应用程序中很常见,因为它们通常利用云平台提供的存储、数据库和消息传递等服务。监控和记录这些集成涉及跟踪这些云服务的性能、可用性和使用情况,确保它们满足应用程序的要求和 SLA。开发人员还应考虑云平台本身提供的监控和日志记录能力和工具,以获得对集成服务的更深入了解。
-
合规性在 SaaS 应用程序中发挥着至关重要的作用,尤其是在处理敏感数据或在受监管的行业中运营时。确保合规性意味着遵守由特定行业组织、政府机构或国际机构制定的既定规则、标准或法规。监控和日志记录系统需要考虑到合规性,捕获与安全相关的指标、事件和审计跟踪,以证明符合这些要求。合规性还可能规定特定的日志保留策略、访问控制措施和加密实践,以保护敏感信息。通过将专注于合规性的监控和日志记录实践集成到您的 SaaS 应用程序中,您不仅保护了客户的资料和隐私,还减轻了与不合规相关的潜在法律和财务风险。
-
最后,安全和合规性在 SaaS 应用程序中极为重要,尤其是在处理敏感数据或在受监管的行业中运营时。监控和日志记录应包括与安全相关的指标和事件,例如身份验证失败、未经授权的访问尝试和政策违规。这种关注有助于开发人员主动识别潜在的安全威胁,遵守行业标准法规,并确保满足特定租户的日志记录要求或偏好,例如日志级别、数据保留策略或警报阈值。
摘要
在本章中,我们探讨了监控和日志记录在 SaaS 应用领域中的关键重要性,尤其是在考虑与 Microsoft 技术一起工作时出现的复杂性和独特挑战。由于 SaaS 应用程序服务于数百万用户,全天候运行,并处理各种不同的操作,因此实施强大的监控和日志记录系统对于维护这些应用程序的可靠性、性能和安全至关重要。
我们深入探讨了监控和日志记录之间的区别,强调监控是一种主动技术,专注于观察系统的健康和性能,而日志记录主要关注记录事件和数据,以便进行有效的故障排除和分析。这两种技术都是 SaaS 开发人员工具箱中的补充工具,确保提供无缝且可靠的用户体验。
在本章中,我们探讨了监控和日志记录的关键方面,讨论了性能指标、资源利用率、应用可用性和健康状态的重要性,以及日志级别、结构化日志和上下文的相关性。我们还考察了 SaaS 应用中出现的独特考虑因素,例如多租户、微服务、可扩展性、分布式架构、与云服务的集成、安全性和合规性。
我们介绍了 SaaS 应用中监控和日志记录的最佳实践,强调了定义相关指标和阈值的必要性,实施主动监控和警报,确保在多租户环境中数据隐私和合规性,以及将监控数据与日志和其他诊断工具集成。此外,我们还强调了持续改进监控和日志记录策略的重要性,以适应 SaaS 应用不断变化的需求和需求。
本章还介绍了在 Microsoft 开发生态系统中常用的一些监控和日志记录工具和技术。我们讨论了 Application Insights、Azure Monitor 和 Azure Log Analytics 在监控方面的效用,并探讨了日志库,如 Serilog、NLog 和 log4net,以及日志管理解决方案,如 ELK Stack。
在我们结束本章时,重要的是要记住,监控和日志记录不是静态的过程。为了在 SaaS 应用中取得成功,开发者必须持续审查、适应和改进他们的监控和日志记录策略,以应对新的挑战、用户行为的改变和技术格局的演变。通过这样做,他们可以保持其应用的最高可靠性、性能和安全水平,确保数百万用户的满意度和信任。
在下一章中,我们将探讨构建和发布管道——这是构建 SaaS 应用时另一个非常重要的考虑因素!
进一步阅读
-
什么是 SaaS 监控?:
www.comparitech.com/net-admin/what-is-saas-monitoring/ -
SaaS 业务/应用中审计日志的最佳实践:
chrisdermody.com/best-practices-for-audit-logging-in-a-saas-business-app/ -
日志记录:
learn.microsoft.com/en-us/dotnet/core/extensions/logging -
log4net .NET 日志指南:
stackify.com/log4net-guide-dotnet-logging/
问题
-
对于您的 SaaS 应用,最重要的监控指标是什么?为什么?
-
如何在详细日志记录的需求与遵守数据隐私法规的需求之间取得平衡?
-
在实施日志和监控系统时,你遇到了哪些常见的挑战,你是如何克服它们的?
-
配置警报和通知有哪些最佳实践,以确保你能够及时地被通知到问题,同时不会被误报所困扰?
-
你如何确保你的日志和监控系统随着应用的扩展而具有可扩展性,并能处理日益增长的数据量?
第十一章:经常发布,尽早发布
快速有效地适应和响应市场需求对于 SaaS 应用的成功至关重要。在软件即服务(SaaS)应用中,客户满意度和用户体验是增长和保留的关键驱动因素,而满足这些需求的最有效方式之一是采用持续集成(CI)和持续部署(CD)实践,统称为 CI/CD。
CI/CD 是一套开发实践,强调频繁集成代码、持续测试以及以最小延迟部署应用更新的重要性。通过自动化这些流程,CI/CD 帮助开发团队减少将新功能、改进和错误修复交付给用户所需的时间,同时提高软件的整体质量和可靠性。
本章将提供对 CI/CD 概念及其在 SaaS 应用背景下的重要性的高层次理解。重点将放在可以应用于各种 CI/CD 工具的原则和实践上,而不是深入到特定工具(如 Azure Pipelines 或 GitHub Actions)的详细说明。通过使讨论更加通用,目标是使读者能够获得使用他们首选工具实施 CI/CD 流程并适应其独特项目需求所需的知识和见解。
在本章中,我们将介绍 CI/CD 管道的关键组件,包括源代码控制集成、构建和发布触发器、容器化和部署策略。我们还将讨论在 SaaS 应用中有效实施 CI/CD 的最佳实践和技巧,确保安全性和合规性,扩展管道,以及监控和优化 CI/CD 流程。
本章将涵盖以下主要内容:
-
理解 CI/CD
-
配置 CI/CD 管道
-
CI/CD 管道和工具概述
-
SaaS 特定考虑因素
在本章结束时,你应该对 SaaS 应用中 CI/CD 的重要性有一个牢固的理解,并准备好在你的项目中实施这些实践,无论你选择的具体 CI/CD 工具是什么。这些知识将帮助你创建更高效、可靠和适应性强,以满足客户不断变化需求的 SaaS 应用。
理解 CI/CD
CI 和 CD 是旨在通过自动化流程的各个阶段来简化软件开发生命周期的开发实践。CI 专注于自动化代码更改的集成,确保开发者频繁合并他们的工作,并且生成的代码库持续受到测试。另一方面,CD 自动化将集成和测试的代码部署到生产环境的过程,使新功能和错误修复尽可能快地提供给用户。
在本节中,我们将更深入地探讨这两种流水线类型,并了解它们如何协同工作,使开发 SaaS 应用程序时每个人的生活变得更轻松。
CI
CI 流水线通常在开发者将本地修改过的代码提交到集中式仓库时被触发。CI 流水线的目的是确保传入的更改与现有工作兼容,并且没有引入回归或新的错误。通常(尽管不是强制性的),传入的代码在 CI 流水线成功完成所有任务之前,不会合并到现有代码中,这表明新代码是安全的。
当新代码提交时,CI 流水线通常会自动运行,并执行以下任务:
-
下载代码:CI 流水线必须做的第一件事是定位和下载代码。代码通常托管在云中的某个 Git 仓库中。仓库和流水线位于同一系统(如 Azure DevOps)中是常见的,但这不是必须的。例如,使用 GitHub 托管仓库,使用 CircleCI 运行流水线。在流水线能够进行任何操作之前,它必须首先获取源代码!
-
构建代码:CI 流水线通常会执行构建步骤,以确保新提交的代码确实可以编译。这一步骤将在配置了构建项目所需工具的虚拟机(VM)或容器中执行。这个 VM 或容器还需要管理依赖项,因此可能需要互联网访问,以便下载所需的任何包。
-
运行测试:假设新提交的代码可以构建,CI 流水线接下来会运行测试。正如我们在第九章中讨论的,测试大致分为三类:单元测试、集成测试和端到端(E2E)测试。所有这些测试都可以由 CI 流水线运行,尽管在某些情况下,可能会跳过集成和 E2E 测试。通常,至少单元测试总是由 CI 流水线执行。
-
测试覆盖率报告:如果项目配置了测试覆盖率,这也会由 CI 流水线运行。这可以帮助确保开发者在进行单元测试时非常勤奋,确保应用程序的代码覆盖率保持在一定百分比以上。
-
静态分析和代码检查:如果静态分析或代码检查工具定义了代码标准,这些标准通常也会由 CI 流水线运行。这确保了代码以一致的方式编写,无论团队中的谁编写了代码。
-
安全测试:如果有自动化的安全测试,CI 流水线也会运行它们。
-
自动版本控制:CI 管道负责创建随后发布的工件。这确保了版本控制策略始终得到遵守,并且每个发布都是唯一可识别的。
-
工件创建和存储:最后,假设所有前面的任务都已通过,CI 管道将打包构建的应用程序及其所有依赖项到可部署的工件中。通常,这些可部署的工件将由 CD 管道用于部署应用程序。这些工件是 CI 管道的输出,也是 CD 管道的输入。
这不是一份详尽的列表——你可能想要运行的工作有无数种,确保它们通过后再允许新代码合并到仓库中。你可以看到 CI 管道可能相当繁忙!
CD
当 CI 管道成功完成其任务并生成可部署的工件后,CD 管道就会被启动。CD 管道的目的是确保应用程序以一致、高效和自动化的方式部署和发布,降低人为错误的风险,并最大限度地减少将新功能和错误修复带给用户所需的时间。CD 管道通常涉及几个阶段,例如部署到各种环境(例如,预发布、生产)、运行部署后测试和监控应用程序,如下列所示:
-
部署到环境:CD 管道通常会按顺序将应用程序部署到不同的环境。它通常从部署到测试环境开始,这是一个生产环境的副本。这允许团队在一个类似生产的环境中验证应用程序的行为、性能以及与其他服务或组件的兼容性。
-
运行部署后测试:在应用程序部署到环境后,CD 管道可以运行额外的测试,例如冒烟测试或回归测试,以确保部署成功,并且应用程序的关键功能仍然按预期工作。
-
监控应用程序性能:CD 管道应包括监控工具,收集有关应用程序性能、资源使用和错误率的指标。这些信息可用于识别潜在问题或改进领域,有助于保持高质量的用户体验。我们在第十章中讨论了监控和日志记录——一些这些任务可以由 CD 管道启动或至少配置。
-
管理配置和环境变量:CD 管道应处理特定环境的配置,例如 API 密钥或数据库连接字符串,确保每个环境使用适当的值。
-
回滚策略:设计良好的 CD 管道应包括回滚机制,允许团队在部署后发现问题后回滚到应用程序的先前版本。这有助于最小化停机时间并减轻任何问题的冲击。在数据库平台上,这可能特别具有挑战性。一旦数据库应用了更新,即使有 Entity Framework 的帮助,回滚也可能很困难!
-
通知和报告:持续交付(CD)管道应发送有关部署状态的通知,并生成有关部署过程的报告。这有助于让团队保持知情,并使他们能够快速解决部署过程中出现的任何问题。
前面的任务只是 CD 管道的起点,你可以根据应用程序的具体需求和需求调整流程。通过实施强大且自动化的 CD 管道,你可以简化发布过程,提高应用程序的可靠性和稳定性,并确保新功能和修复尽可能快速、安全地交付给用户。
环境
环境是应用程序的独立实例,每个环境都有其自己的配置、资源和基础设施。它们被用来复制软件开发生命周期的各个阶段,使开发者能够在将应用程序发布给最终用户之前对其进行测试、验证和优化。使用不同的环境有助于降低风险、及早发现问题,并确保应用程序的稳定性、性能和安全。
通常,一个功能或错误修复将依次通过一系列环境,从开发者的笔记本电脑开始,最终结束在用户手中的生产环境。随着新代码通过环境,bug 的后果会增加,因此批准部署所需的权限也会增加。项目上的开发者可能希望完全控制本地开发环境,但可能无法访问生产环境,这可能需要高级经理的批准才能进行更改。
以下是在软件开发过程中环境的一个常见设置:
-
开发环境:这个环境是开发者在其本地机器上工作的地方,他们在构建应用程序的过程中编写代码和测试功能。它被配置为允许快速迭代和调试,并且与其他环境相比,通常具有更宽松的安全约束。
-
测试环境:测试环境用于运行各种类型的测试,例如单元测试、集成测试和端到端测试。它被设置得与生产环境非常相似,以便测试可以在现实条件下验证应用程序的行为、兼容性和性能。测试环境通常由质量保证(QA)团队管理,并且与开发者的本地机器分开,以确保测试结果的一致性。
-
预发布环境:预发布环境是生产环境的近似副本,包括配置、基础设施和资源。它用于在将应用程序部署到生产之前执行最终测试和验证。这个环境有助于识别和解决在测试环境中可能未被发现的问题,从而降低向最终用户部署有缺陷软件的风险。
-
生产环境:这是应用程序向最终用户提供的实时环境。生产环境对安全性、性能和可靠性要求最为严格,因为任何问题或停机都可能直接影响到用户和业务。向生产环境的部署应谨慎管理并监控,以确保应用程序的稳定性和性能。
在某些情况下,组织还可能有额外的环境,例如以下内容:
-
预生产环境:这个环境用于在部署到生产环境之前执行最终检查,例如负载测试或安全测试。这是一个可选的环境,可以用来进一步降低发布新软件相关的风险。
-
灾难恢复环境:这个环境是生产环境的备份,用于在发生灾难性故障或灾难时快速恢复应用程序。它确保业务连续性,并在不可预见的事件中最大限度地减少停机时间。
使用多个环境允许组织在各个阶段检测和修复问题,从而提高应用程序的整体质量,并降低向最终用户部署有缺陷软件的可能性。
采用 CI/CD 的好处
如果你认为这一切听起来工作量很大——你是对的。这是一项很大的工作,但构建 CI/CD 系统也有许多非常显著的好处。这些好处最终有助于提高软件开发过程的效率、可靠性和敏捷性,确保向用户交付高质量的软件。
CI/CD 可以显著缩短代码编写与功能交付给客户之间的时间。对客户有价值的代码应尽可能快地部署,但代码在仓库中闲置数月,没有任何价值的情况非常普遍。除了添加新功能外,CI/CD 还将导致错误修复和补丁的快速推出。
除了部署速度的提升,CI/CD 还将极大地减少部署失败,甚至可能完全消除。在发布过程中通常遇到的问题几乎都会在 CI 或 CD 流水线中被立即捕捉到,几乎每次都能保证发布成功。
CI/CD 的好处并不只是针对应用程序的用户。对于开发者来说,也有显著的好处。通常,流水线将通过自动化测试、代码审查和静态分析等工具强制执行更高的代码质量和一致性。这使得所有参与产品工作的人都能够享受到更加愉快的体验,并最终导致开发者侧在新的功能和错误修复上的周转时间更快。
流水线将提高整个团队对发布和快速回滚发布的信心。这种额外的安心感对团队来说是一大好处。
我们将在第十二章中详细讨论应用程序的扩展,但在此处值得提及的是,当您的应用程序开始扩展,您需要部署和管理多个应用程序实例以管理服务器负载时,CI/CD 是必不可少的。手动实现这一点几乎是不可能的,因此当您开始认真扩展应用程序时,CI/CD 几乎是强制性的。
虽然实施 CI/CD 流水线可能需要大量的时间和资源投入,但它们在效率、可靠性和整体软件质量方面提供的益处使它们成为现代软件开发流程的必要组成部分。拥抱 CI/CD 将帮助您的组织保持竞争力,更快地向用户交付价值,并为未来的增长奠定坚实的基础。
CI/CD 是 DevOps 吗?
您经常会听到 CI/CD 和 DevOps 被放在同一句话中提到,尽管这两个概念相关,但 CI/CD 并不完全等同于 DevOps。DevOps 是一个更广泛的概念,它包括文化转变、协作和实践,旨在将软件开发和 IT 运维团队结合起来,以提高效率、减少交付软件的时间并提高整体软件质量。
CI/CD 专注于自动化代码更改集成、测试并将应用程序部署到生产环境的过程。通过实施 CI/CD 流水线,开发和运维团队能够更紧密、更迭代地工作,这与 DevOps 哲学是一致的。
简而言之,CI/CD 是 DevOps 方法的关键组成部分,但 DevOps 包含了更广泛的做法、工具和文化转变,旨在弥合开发和运维团队之间的差距。
配置 CI/CD 流水线
虽然 CI/CD 的概念相对简单明了,但要完全掌握它们可能是一个看似庞大且复杂的主题。所需技能是开发人员和 IT 运营(即 DevOps)的某种混合体,因此正确实施可能具有挑战性。有许多流行的系统可以完成这项工作;'三大'是 Azure DevOps、GitHub Actions 和 Jenkins,每个都有自己的优缺点。在本节中,我将提供一些通用的建议,这些建议应该适用于您选择的任何系统来构建管道。
源代码控制集成
首先也是最重要的事情是集成源代码控制与管道。这是基础性的,因为没有源代码控制,管道就没有运行的目标。这种集成必须允许管道检测代码库的变化(通常是通过提交或拉取请求),然后启动适当的构建、测试和部署流程。在设置源代码控制集成时,请确保它支持团队使用的各种分支和工作流程,以实现无缝协作和高效开发。
构建触发器和构建代理
构建触发器和构建代理在自动化构建过程中发挥着至关重要的作用。构建触发器确定管道何时开始构建应用程序,通常是对新代码提交、拉取请求或计划的响应。构建代理负责在专用机器或基于云的环境中执行构建任务,确保应用程序根据指定的配置进行构建、测试和打包。在配置构建触发器和代理时,请考虑代码更改的频率、构建应用程序所需的资源以及所需的并行程度。
构建代理通常是在某个云环境中运行的虚拟机或容器。与云中的所有事物一样,您必须为构建代理按使用付费。这可能会迅速累积费用!在较小的项目中,每次代码提交到仓库时可能都进行一次构建,但随着团队的增长和每日提交次数的增加,进行单个夜间构建甚至周构建可能更有意义。
定义构建任务和阶段
定义构建任务和阶段对于组织和管理工作流中的各个构建步骤以及保持一切整洁至关重要。您的管道配置本质上只是更多的代码,因此它应该像实际的应用程序代码一样得到妥善管理。
构建任务是在构建过程中执行的单个操作,例如编译代码、运行测试或打包应用程序。
阶段代表一系列相关任务,这些任务通常一起执行,通常对应于开发生命周期的不同阶段,如开发、测试和生产。在定义构建任务和阶段时,确保它们与团队的开发实践相一致,并且支持所需的自动化和测试水平。
发布触发器和环境
发布触发器和环境控制着应用程序部署到各种环境,例如预发布或生产环境。发布触发器决定何时创建和部署新版本,通常是对成功构建、预定时间或人工干预等事件的响应。环境代表目标部署目的地,包括它们的配置、资源和访问控制。在配置发布触发器和环境时,考虑因素包括期望的发布频率、部署过程的复杂性以及在生产部署前进行预发布和测试的需求。
在测试或预发布环境中进行部署通常是相当常见的,可以完全自主进行,或者得到团队任何成员的批准。而部署到生产环境很少会完全自动进行,通常需要团队高级成员的批准。通常需要多个批准,例如来自管理层、质量保证团队和开发经理。
部署到多个租户
部署到多个租户是 SaaS 应用的关键方面,因为它允许你使用相同的代码库为多个客户提供服务,同时保持数据隔离和定制。为了实现这一点,配置你的 CI/CD 管道以支持特定租户的部署,使你能够同时或选择性地将更新和新功能部署到所有租户。这可能涉及参数化部署任务,使用特定租户的配置,或利用 CI/CD 工具或托管平台提供的功能。
我们在第三章中详细讨论了多租户,因此你会欣赏到管理具有许多租户的应用程序可能有多么具有挑战性,其中一些租户可能需要完全隔离的安装。
在 SaaS 应用中部署微服务
在 SaaS 应用中部署微服务是至关重要的,因为它们使你能够构建可扩展、灵活且易于维护的系统。微服务架构允许你将应用程序划分为小型、独立的组件,每个组件负责特定的功能或特性。这使得你可以独立地开发、测试和部署这些组件,从而降低了与单体应用相关的复杂性和风险。我们在第六章中详细讨论了微服务,因此你会欣赏到管理基于这种架构构建的 SaaS 应用所关联的益处和挑战。
在 CI/CD 管道的背景下,部署微服务需要仔细的协调和管理,以确保每个服务都能以一致和可靠的方式构建、测试和部署。这可能包括配置您的 CI/CD 管道以处理多个仓库,使用特定于服务的构建和部署任务,以及利用容器化技术,如 Docker,来打包和部署您的微服务。
此外,在 SaaS 应用程序中部署微服务可能涉及与其他组件的集成,例如 API、数据库和第三方服务。这要求您的 CI/CD 管道管理每个微服务的依赖项、版本控制和配置设置,以确保应用程序所有组件之间的无缝交互。
质量控制审批和门控
质量控制审批和门控对于确保您的应用程序在部署到生产之前达到所需标准至关重要。审批涉及指定团队成员的手动签字,而门控是在进入管道的下一阶段之前必须通过的自动化检查。门控的例子包括成功的测试结果、性能指标或安全扫描。通过实施审批和门控,您可以最大限度地降低部署有缺陷或不安全的代码的风险,确保您的 SaaS 应用程序保持高水平的质量和可靠性。
CI/CD 管道和工具概述
可用于构建您的 CI/CD 管道的工具和系统相当多。通常,CI/CD 工具将与您使用的源代码控制工具一起提供,但这并不一定如此,了解可用的工具以便您做出选择是值得的。
这些工具的功能重叠相当广泛,您会发现我们在这章中讨论的所有主要功能都在所有主流工具中都有提供。工具的选择将主要取决于个人偏好。在企业环境中最常用的三个工具是 Azure DevOps、GitHub Actions 和 Jenkins。开源社区更常用 CircleCI 或 Travis CI。
流行的 CI/CD 工具
微软的 Azure DevOps 是一个涵盖整个开发生命周期的综合工具套件,从规划、编码到构建、测试和部署。它提供了一系列服务,包括 Azure Repos 用于源代码控制、Azure Boards 用于项目管理以及 Azure Pipelines 用于 CI/CD。Azure DevOps 与其他 Microsoft 服务提供无缝集成,并支持各种编程语言、平台和框架。它特别适合那些已经使用 Microsoft 技术并寻求紧密集成的 CI/CD 解决方案的团队。
GitHub Actions 是一个直接构建在 GitHub 中的 CI/CD 解决方案,对于已经使用 GitHub 进行源代码控制的团队来说,它是一个有吸引力的选择。使用 GitHub Actions,你可以使用各种预构建动作创建自定义工作流程,或者创建自己的动作。这些工作流程可以由各种事件触发,例如提交、拉取请求或计划事件。GitHub Actions 提供了一个市场,你可以在这里找到各种社区贡献的动作,使你能够快速构建和自定义你的 CI/CD 管道。它还支持多种编程语言、平台和框架。
Jenkins 是一个在软件开发行业中广泛采用的开放源代码 CI/CD 服务器。由于其庞大的插件和集成生态系统,它提供了高度的可灵活性和可扩展性。Jenkins 支持各种构建工具、版本控制系统和部署平台,使其成为具有多样化技术堆栈的团队的多功能选择。使用 Jenkins 的管道作为代码功能,你可以创建自定义构建管道,允许你在源代码控制系统中管理你的管道配置。
Travis CI 是一个广受欢迎的 CI/CD 平台,以其易用性和与 GitHub 无缝集成为人所知。它提供基于云和本地部署的选项,为具有不同需求的组织提供灵活性。Travis CI 支持广泛的编程语言、平台和框架,使其成为各种项目的多功能选择。与其他 CI/CD 工具一样,Travis CI 允许你将构建管道定义为代码,这可以在你的仓库中进行版本控制和管理工作。
CircleCI 是另一个强调速度和简单的流行 CI/CD 平台。它提供基于云的解决方案以及针对具有特定安全或合规性要求的团队的本地托管选项。CircleCI 支持广泛的编程语言和平台,并提供了一套强大的与其他开发工具的集成。其管道作为代码的方法,与其他 CI/CD 工具类似,允许你在源代码控制系统中管理你的管道配置,这使得随着项目的演变轻松维护和更新。
这些工具,包括 Azure DevOps、GitHub Actions、Jenkins、Travis CI 和 CircleCI,为 CI/CD 提供了多样化的选项。最适合你特定需求的选择将取决于你的现有技术堆栈、团队规模和项目需求。这些工具中的每一个都提供了独特的功能和好处,因此根据你团队的需求和偏好进行评估非常重要。
选择 CI/CD 工具时需要考虑的因素
为你的项目选择正确的 CI/CD 工具是构建成功管道的关键步骤。一个精心选择好的 CI/CD 工具可以提高团队的生产力,简化你的流程,并帮助你保持高质量的代码库。
在选择 CI/CD 工具时,考虑其与当前源代码控制、问题跟踪和其他开发工具无缝集成的能力是最关键的方面之一。这确保了流畅和高效的流程,减少了管理不同系统的工作量。在选择 CI/CD 工具之前,评估其与现有工具和服务的兼容性,并考虑集成的简便性。
我们将在下一章详细讨论扩展性,但在此处值得提及的是,随着你的 SaaS 应用程序的增长,你的 CI/CD 管道应该能够与之一起扩展。考虑工具处理大型项目和多个团队同时工作的能力是至关重要的。一个可扩展的 CI/CD 工具应该能够支持不断增长的工作负载、更多用户和更复杂的管道,而不会影响性能或可靠性。
根据你项目的具体要求,你可能需要一个提供高度定制和可扩展性的 CI/CD 工具。这可能通过插件、集成或自定义脚本来实现。一个可定制的 CI/CD 工具允许你根据独特需求调整管道,实现自定义逻辑,并与利基工具或服务集成。考虑扩展工具功能的选择以及实施这些定制的简便性。
最后,这一切都不是免费的!比较不同 CI/CD 工具的定价模式和可用的支持选项。团队规模、部署频率和预算限制等因素可能会显著影响你的决策。许多 CI/CD 工具提供一系列定价层,包括具有有限功能的免费计划和具有高级功能和支持的企业计划。此外,在评估 CI/CD 工具时,还应考虑文档质量、社区支持和供应商提供的支持。具有强大支持资源的工具可以帮助你的团队更有效地解决问题和采用最佳实践。
构建灵活且适应性强的 CI/CD 流程
在选择合适的 CI/CD 工具至关重要时,设计一个灵活且能适应你项目独特需求的 CI/CD 流程同样重要。一个结构良好且敏捷的 CI/CD 流程可以提高团队的生产力,缩短新功能上市时间,并帮助你保持高质量的代码库。
在团队成员之间培养协作和沟通对于成功的 CI/CD 流程至关重要。鼓励就管道及其目标进行开放讨论有助于形成对流程的共同理解和所有权。定期的会议、代码审查和共享文档可以促进更好的沟通和协作,使解决问题和进行改进变得更加容易。
持续改进是所有开发过程的一个基本组成部分,CI/CD 也不例外。根据需要整合团队成员的反馈,并适应项目需求或工具的变化。通过定期审查和改进您的 CI/CD 流程,您可以确保它们保持高效、最新并与项目目标保持一致。
确保您的 CI/CD 管道有良好的文档记录,并且新团队成员可以轻松理解。清晰的文档使您的管道随着时间的推移更容易维护和扩展,减少新团队成员的学习曲线,并使团队更容易进行更新和改进。此外,通过记录管道的配置、最佳实践和故障排除指南,您可以为您团队创建一个有价值的资源。
监控 CI/CD 管道的性能和有效性对于识别改进领域和优化流程至关重要。使用构建成功率、部署频率和变更的领先时间等指标来评估管道的效率和有效性。定期分析这些指标以发现趋势、检测瓶颈并确定可以改进的领域。通过积极监控和优化您的 CI/CD 流程,您可以确保它保持稳健、高效,并能够满足项目不断变化的需求。
如果您了解可用的工具,做出明智的选择,并构建适当程度的灵活性,那么您在 CI/CD 方面应该已经走上了成功的道路。正如与 SaaS 相关的所有事物一样,有一些特定的考虑因素值得关注。我们将在下一节中讨论这些内容。
SaaS 特定考虑因素
SaaS 应用带来了一组独特的挑战和需求。因此,在构建和部署这些应用时,仔细考虑 SaaS 的特定方面至关重要。本节将探讨在设计和管理您的 CI/CD 管道时应牢记的关键 SaaS 特定考虑因素。
容器化
我们已经利用容器化技术构建了开发者环境,但这在 SaaS 应用背景下并非唯一的用例。在开发 SaaS 应用时,由于这类系统的固有复杂性和规模,容器化特别有价值。通过将每个微服务打包到自包含的容器中,开发者可以确保他们的应用程序在不同环境中的一致运行,减少因依赖项或配置差异而产生问题的可能性。此外,容器化提高了资源利用率,并使得独立扩展应用程序的各个组件变得更加容易,从而实现更高效和成本效益更高的 SaaS 解决方案。
要在您的 SaaS 应用中利用容器化,首先为您的应用中的每个微服务创建容器镜像。这些镜像是通过 Dockerfile 构建的,它定义了基本镜像、应用程序代码、依赖项和运行时配置。通过为每个微服务创建单独的镜像,您可以确保它们保持隔离,从而允许您独立于其他服务更新、扩展和部署每个服务。
管理多容器应用可能很复杂,因为它通常涉及协调多个相互连接服务的部署、扩展和通信。为了简化此过程,使用编排工具,如 Docker Compose、Kubernetes 或 Amazon 弹性容器服务(ECS),这些工具允许您使用配置文件定义多容器应用,并自动化容器化服务的管理。这些工具帮助您保持应用程序状态的一致性,并促进容器之间的通信,使开发和管理大规模 SaaS 应用变得更加容易。
将容器化集成到您的 CI/CD 管道中对于自动化构建、测试和部署您的容器化 SaaS 应用至关重要。为了实现这一点,配置您的 CI/CD 管道,以便在代码更改集成时为每个微服务构建容器镜像,并自动对这些镜像运行测试以验证其功能和性能。一旦测试通过,管道应使用所选的编排工具将更新的镜像部署到适当的环境。通过将容器化纳入您的 CI/CD 管道,您可以简化开发和部署流程,使向客户交付高质量、可扩展的 SaaS 解决方案变得更加容易。
升级
一个周密的升级策略对于 SaaS 微服务应用的重要性不容小觑。因为这些应用通常服务于多个具有不同需求和高度可用性预期的客户,一个无缝的升级策略确保了新功能、改进和错误修复可以在不干扰用户体验的情况下交付。
无停机部署是成功升级策略的一个关键组成部分。通过最小化更新对应用程序可用性和性能的影响,无停机部署确保用户可以在不间断的情况下继续使用服务。实现无停机部署有几种方法:
-
蓝绿部署:这种方法涉及维护两个相同的生产环境,分别称为“蓝”和“绿”。在任何给定时间,一个环境处于活动状态,为用户提供服务,而另一个处于空闲状态。在升级期间,更改部署到空闲环境,然后进行测试和验证。一旦升级被认为成功,流量将逐渐切换到更新后的环境。如果出现问题,这种方法允许快速回滚,因为流量可以轻松地重新定向回原始环境。
-
金丝雀发布:在这种方法中,升级首先部署到一小部分用户或“金丝雀”实例,然后再推广到整个用户群。这使开发者能够以受控的方式监控升级的性能和稳定性,并在问题影响所有用户之前识别任何问题。如果升级成功,它将逐渐部署到剩余的实例。
-
滚动更新:滚动更新涉及增量地将升级部署到实例,通常是逐个或以小批量进行。随着每个实例的更新,它将暂时退出服务,流量被重新定向到剩余的实例。这个过程一直持续到所有实例都已升级。虽然这种方法可能比其他方法慢,但它最大限度地减少了广泛问题的风险,并允许更容易地进行故障排除。
在 SaaS 应用程序中管理数据库模式更改可能特别具有挑战性,因为更新通常需要在不破坏现有数据或损害应用程序完整性的情况下进行。为了处理这些更改,考虑使用迁移、版本控制或功能标志等工具和技术,这些工具和技术允许对数据库模式进行增量且可逆的更新。此外,确保您的数据库设计支持多租户,允许在所有租户之间无缝升级。
监控和回滚策略对于快速识别和解决失败的升级至关重要。通过在升级期间及之后密切监控应用程序的性能和稳定性,您可以早期发现问题并采取适当的行动。实施一个回滚策略,以便在升级过程中出现问题时,您可以快速回滚到应用程序的先前稳定版本。通过拥有一个明确的监控和回滚计划,您可以最小化失败升级对用户的影响,并保持 SaaS 微服务应用程序的高质量和可靠性。
CI/CD 管道中的安全和合规性
在 SaaS 应用程序中,确保软件的安全性和合规性至关重要,因为它涉及处理敏感数据并满足行业特定的法规。通过在 CI/CD 管道中实施严格的安全措施和合规性检查,您可以在遵守所需标准的同时保护应用程序及其用户。
要将安全和合规措施纳入你的 CI/CD 管道,请考虑以下最佳实践:
-
自动化安全测试:将自动化安全测试工具,如静态应用安全测试(SAST)和动态应用安全测试(DAST),集成到你的管道中。这些工具有助于识别代码中的漏洞和潜在的安全风险,使你能够在它们达到生产环境之前解决这些问题。
-
实施安全的编码实践:鼓励你的开发团队遵循安全的编码最佳实践和指南。这包括遵守最小权限原则、输入验证和安全的存储数据。你还可以将代码分析工具集成到你的管道中,以强制执行这些实践并识别潜在的安全问题。
-
监控和审计你的管道:定期监控和审计你的 CI/CD 管道,以确保它保持安全和合规。这包括检查未经授权的访问、跟踪管道配置的变化以及审查安全日志。实施访问控制和基于角色的权限也可以帮助防止对管道的未经授权修改。
-
安全地管理秘密和凭证:通过使用秘密管理工具或安全存储服务,安全地存储敏感数据,如 API 密钥、密码和令牌。避免将这些凭证嵌入到你的代码或配置文件中,并确保它们在静态和传输过程中都得到加密。
-
定期进行漏洞扫描和更新:通过定期扫描漏洞并应用必要的补丁,保持你的 CI/CD 基础设施的最新状态。这包括更新你的构建工具、依赖项和运行时环境,以减轻已知安全问题的风险。
-
合规性检查:将自动合规性检查纳入你的管道,以确保你的应用程序符合所需的行业标准法规。这可能涉及将你的应用程序与预定义的合规性策略进行验证或与专门的合规性工具集成。
通过将安全和合规措施纳入你的 CI/CD 管道,你可以主动应对潜在风险,并保持对你 SaaS 应用程序的高度信任。这不仅保护了你的用户,还确保了你的应用程序保持可靠并符合行业标准。
摘要
总结,本章涵盖了在 SaaS 应用程序中实施 CI/CD 的基本概念和最佳实践。我们探讨了 CI/CD 在增强开发周期和提高 SaaS 应用程序质量方面的好处。我们讨论了各种 CI/CD 管道、工具以及在选择 CI/CD 工具时需要考虑的因素,强调了构建灵活和适应性强的流程的重要性。
我们检查了 CI/CD 管道的配置,包括源代码控制集成、构建触发器和代理、定义构建任务和阶段、发布触发器和环境、部署到多个租户以及纳入质量控制的批准和门控。我们还强调了容器化在 SaaS 应用中的价值,讨论了使用 Docker 和容器编排工具来管理和部署容器化微服务。
我们深入探讨了升级 SaaS 微服务应用,讨论了良好规划升级策略的重要性以及各种零停机部署技术,如蓝绿部署、金丝雀发布和滚动更新。我们还解决了管理数据库模式变更的挑战以及失败升级的监控和回滚策略的需求。
最后,我们提供了 SaaS 应用中 CI/CD 的最佳实践和技巧,强调了自动化和测试的重要性,确保安全性和合规性,为大规模应用扩展 CI/CD 管道,并持续监控和优化管道。通过遵循本章中提供的指导,您可以构建高效且有效的 CI/CD 管道,以支持高质量、可扩展和可靠的 SaaS 应用的开发和部署。
在下一章中,我们将探讨如何扩展您的 SaaS 应用。
进一步阅读
-
SaaS 企业的最佳实践:
www.missioncloud.com/blog/five-best-practices-for-saas-businesses-deploying-devops-as-your-secret-weapon -
使用 Azure Pipelines 的 CI/CD 基准架构:
learn.microsoft.com/en-us/azure/architecture/example-scenario/apps/devops-dotnet-baseline -
如何用四个简单步骤使用 GitHub Actions 构建 CI/CD 管道:
github.blog/2022-02-02-build-ci-cd-pipeline-github-actions-four-steps/ -
Jenkins 是什么?
phoenixnap.com/kb/what-is-jenkins
问题
-
实施 CI/CD 在 SaaS 应用中的关键好处是什么?
-
如何通过容器化(如 Docker)改进 SaaS 应用的开发和部署?
-
哪些零停机部署技术可以用于升级 SaaS 微服务应用?
-
选择 CI/CD 工具时,您应该考虑哪些因素?
-
您如何确保 SaaS CI/CD 过程中的安全性和合规性?
-
为什么监控和回滚计划对于处理 SaaS 应用程序中的失败升级至关重要?
-
扩展 CI/CD 管道以适应大规模 SaaS 应用程序的最佳实践有哪些?
第十二章:成长之痛——大规模运营
随着软件即服务(SaaS)应用程序的成长和用户数量的增加,它们不可避免地会面临与性能、可扩展性、安全性和可用性相关的新挑战。这些障碍统称为大规模运营的挑战。到目前为止,在这本书中,我们已经深入探讨了使用微软技术构建 SaaS 应用程序的基础知识,包括数据建模、微服务架构、Web API、Entity Framework、Blazor 以及安全的身份验证和授权。尽管我们没有明确地解决扩展这些元素的问题,但我们通过遵循最佳实践并构建一个坚实的基石,为将来扩展应用程序提供了宝贵的价值。
在本章中,我们将更深入地探讨与大规模运营相关的挑战,一如既往地,特别强调使用微软技术扩展 SaaS 应用程序。我们将从对扩展各个方面的全面概述开始,接着详细探讨扩展数据库、API 和用户界面(UI)的技术。此外,我们还将讨论监控和警报的至关重要性、实施有效的 DevOps 实践以及稳健的灾难恢复计划。
通过研究这些方面,我们的目标是为您提供必要的知识和工具,以便自信地应对随着您的 SaaS 应用程序扩展而出现的挑战。我们的目标是让您深入了解扩展的复杂性,确保您的应用程序即使在为不断增长的用户群体提供服务时,也能保持高性能、可靠性和安全性。
本章涵盖的主要内容包括以下几方面:
-
大规模运营的挑战
-
扩展数据库
-
扩展 API
-
扩展 UI
-
监控和警报
-
扩展 SaaS 应用程序的 DevOps 实践
-
灾难恢复计划
大规模运营的挑战
当 SaaS 应用程序需要扩展时,这是一个令人兴奋的里程碑,因为这意味着该应用程序取得了成功,并为业务创造了收入。然而,随着这种增长,也会带来挑战,因此准备好应对这些挑战至关重要,以便应用程序可以继续成功。随着用户基础的扩大,您的应用程序必须始终保持可用性,能够处理对资源的增加需求,并继续提供卓越的性能和安全性。
大规模运营的挑战可以广泛地分为几个领域,包括基础设施的可扩展性、性能优化、安全性和合规性、可用性和正常运行时间、成本和资源管理,以及为扩展和增长进行规划。在本节中,我们将详细探讨这些领域,讨论您可能遇到的特定挑战以及您可以使用哪些策略来克服它们。我们将考虑这些领域如何影响应用程序的主要层,从数据库到 API,最后到 UI。
在您实际开始大规模运营之前,理解大规模运营的挑战并制定应对这些挑战的计划,您就可以构建并运营一个成功的 SaaS 应用程序,该应用程序能够满足不断增长的用户群体的需求。
在这本书的到目前为止的部分,我们一直专注于应用的开发以及在当地开发者的笔记本电脑上本地运行代码。虽然我们已经意识到我们最终需要大规模运行,但在开发环境中,规模显然不是问题!本章中的大部分技巧和技术都指的是托管在云上的生产环境。因为我们主要处理的是微软技术,所以我将重点关注 Azure 作为云平台,但本节中的通用建议同样适用于其他云服务提供商。
性能和响应时间
在大规模运营 SaaS 应用程序时,最关键的方面之一是确保用户获得最佳的性能和响应时间。为了提供性能良好的 UI,UI 下每一层都必须是高性能的——应用程序的性能取决于其最不性能的部分!快速而高效的用户体验对于用户满意度至关重要,因为它直接影响到他们对应用程序质量可靠性的感知。研究表明,用户往往会放弃性能缓慢的应用程序或网站,导致收入和用户参与度下降。因此,保持高性能和快速的响应时间是保留用户并支持您的 SaaS 应用程序增长的关键。
随着用户基础和数据量的增长,对您应用程序的基础设施和资源的需求也会相应增加。如果不妥善管理,这会导致性能下降。通过主动监控和解决性能和响应时间,您可以创造一个积极的用户体验,使客户保持参与和忠诚于您的 SaaS 应用程序。您可以采取一些实际步骤来保持应用程序性能,从而保持您的用户满意。
定期监控和剖析您的应用程序对于识别性能瓶颈和优化响应时间至关重要。使用性能监控工具,如适用于.NET 应用程序的应用洞察,来收集和分析与响应时间、吞吐量和资源利用率相关的指标。剖析工具可以帮助您确定代码库中可能引起性能问题的特定区域,使您能够进行有针对性的优化。所有这些都应该尽可能自动化,理想情况下是完全自动化的,当系统检测到性能下降时发出警报。
实施缓存策略以减少应用程序和数据库服务器的负载。利用各种缓存技术,如内存、分布式和输出缓存,来存储频繁请求的数据,并更快地为用户提供服务。内容分发网络(CDNs)也可以用于从地理位置分布的服务器缓存和提供静态资产,如图像和脚本,从而减少延迟并提高全球用户的响应时间。这是一个非常复杂的话题,可能需要单独占用整整一章!只要作为 SaaS 应用程序的开发者,你意识到这一点,那么在需要时你将能够充分利用它。我们将在接下来的章节中更详细地探讨缓存,重点关注数据库、API 和 UI。
通过实施适当的索引、微调查询和使用连接池来优化数据库性能。定期审查和更新数据库索引以改进查询执行时间。分析运行缓慢的查询并使用 SQL Server Query Store 或 SQL Server Management Studio 内置的性能工具进行优化。
实施负载均衡,以在您的应用程序的多个实例之间均匀分配流量,防止任何单个实例成为瓶颈。可以使用像 Azure Load Balancer 或 Application Gateway 这样的技术来实现。与许多此类建议一样,负载均衡必须是完全自动的。利用自动扩展根据当前负载动态调整应用程序实例的数量。这确保了在高峰时段您的应用程序保持响应,同时在低使用期间降低成本。
将耗时任务卸载到异步进程中,这些进程可以在后台运行而不会阻塞主应用程序流程。这有助于提高面向用户的操作响应时间,因为它们不需要等待这些任务完成。可以使用像 Azure Service Bus 或 RabbitMQ 这样的消息队列来管理和分配这些任务到后台工作服务中。你会记得我们在关于微服务的章节中查看过 RabbitMQ。这项相同的技术,使我们能够干净地分离我们的应用程序,也可以用来提高或维护性能。
如你或许能猜到的,在扩展 SaaS 应用程序时,可以使用许多不同的技巧、工具和技术!
可靠性和可用性
可靠性和可用性也是在大规模运营 SaaS 应用程序时非常重要的组成部分,因为它们直接影响用户的信任和满意度。一个可靠的应用程序会持续执行其预期功能,而不会出现意外的故障或错误;而应用可用性则指应用在用户需要时能够访问和运行的能力。确保高可靠性和可用性对于用户保留和建立你 SaaS 应用的正面声誉至关重要。
随着应用的普及,增长往往是非线性的,伴随着一段时间的平稳期,随后偶尔会出现需求激增,这些需求可能会逐渐减少、减弱或持续存在。在这样的环境中保持应用的正常运行时间是极具挑战性的!随着你的应用规模扩大,设计容错性、冗余性和有效的监控变得越来越重要,以最小化停机时间并确保在需求波动或急剧增加的时期也能提供无缝的用户体验!
通过在数据存储、计算资源和网络连接等多个层面实现冗余,设计你的应用以具备容错性。这可以通过在不同地理区域或可用区部署你应用的多个实例来实现。在某个实例发生故障的情况下,其他实例可以继续不间断地为用户提供服务。
此外,确保你的数据在多个地点进行复制,以防止数据丢失并便于快速恢复。例如,Azure SQL 数据库和 Azure 存储服务提供了内置的数据复制功能,这可以帮助你相对容易地实现这种级别的冗余。
无论你的系统多么出色,最终都可能会出现需要从备份中恢复数据的故障。为了使这种可能性尽可能无缝,实施定期备份你的应用数据和配置,以便在数据丢失或损坏的情况下快速恢复。使用 Azure 备份或 SQL Server 备份等工具来自动化数据备份过程,并确保你的备份安全存储,且独立于主数据存储。此外,制定灾难恢复计划,概述在发生重大事件时恢复应用所需的步骤。别忘了定期测试你的灾难恢复计划,以验证其有效性并进行必要的调整。
仅进行备份是不够的——您还应该实施定期执行恢复的常规做法,其中数据从备份中恢复并检查一致性。已经记录了许多实例,其中“备份”并不像预期的那样完整,这一事实仅在数据丢失后的恢复过程中被发现。
备份和恢复可以在数据丢失后拯救您。但应采取措施防止这种情况首先发生。建立全面的健康监控和警报,以在问题影响用户之前检测和响应潜在问题。使用监控工具收集应用程序、基础设施和网络的数据、日志和跟踪。根据预定义的阈值配置警报和通知,使您的团队能够及时解决问题并最小化停机时间。
即使拥有最先进的日志记录和监控,以及稳固的备份和恢复策略,也难免会有时候(遗憾的是)您的应用程序会在新用户激增的压力下崩溃。设计您的应用程序以优雅地处理高负载或部分故障。实施诸如断路器、超时和重试等技术,以控制方式处理错误和故障,防止级联故障,并确保即使在某些组件或服务不可用的情况下,用户仍然可以访问核心功能。
安全和合规
在大规模运营 SaaS 应用程序时,安全和合规至关重要,因为它们保护您的用户数据、应用程序的完整性和公司的声誉。一个安全的应用程序可以保护敏感数据免受未经授权的访问,防止恶意攻击,并维护用户数据的机密性、完整性和可用性。合规性确保您的应用程序遵守适用的法律、法规和行业标准,降低风险,并在用户之间建立信任。
随着您的应用程序增长,潜在的攻击面增加,因此实施强大的安全措施并保持与相关标准的合规性至关重要。通过积极应对安全和合规性问题,您可以创建一个安全的环境,保护您的用户和业务,同时满足不断增长的用户群的需求。
第一道防线是实施强大的身份验证和授权机制,以控制对您的应用程序及其资源的访问。我们已在之前的章节中讨论了这一点,并给出了一些如何将.NET 集成到微服务架构中的示例。根据该示例,您永远不应该尝试构建自己的基础设施——始终使用经过实战检验的解决方案,如 OAuth 2.0、OpenID Connect 或 Azure Active Directory 进行用户身份验证,并以标准方式实现基于角色的访问控制(RBAC)或基于声明的授权,以在应用程序内强制执行细粒度的权限。
如果你的应用程序以未加密的方式传输数据,那么担心身份验证和授权几乎是没有意义的。通过实施强大的加密方法,保护在传输中和静止状态下的敏感数据。使用如传输层安全性(TLS)这样的加密协议来保护传输中的数据,以及如 Azure 存储服务加密、Azure 磁盘加密或透明数据加密(TDE)这样的加密技术来加密 SQL Server 中的静止数据。此外,使用如 Azure Key Vault 等服务安全地管理加密密钥。在开发环境中处理机密信息时,要特别小心。已经有许多实例表明,生产环境的机密信息被意外提交到公共存储库中而泄露了!
即使你认为你已经从一开始就保护了你的应用程序,仍然非常重要的是要定期进行安全审计和漏洞评估,以识别应用程序安全中的潜在弱点。使用如 Azure Security Center 或第三方漏洞扫描器等工具来检测和修复安全漏洞。此外,进行渗透测试以模拟现实世界的攻击并评估应用程序承受这些攻击的能力。
渗透测试是一个复杂的话题,需要非常特定的技能集。通常建议咨询领域专家来执行渗透(pen)测试。
设置持续监控和日志记录,以便及时检测和响应安全事件。利用如 Azure Monitor、Azure Sentinel 或第三方安全信息和事件管理(SIEM)解决方案来汇总和分析来自各种来源的日志,例如应用程序、服务器和网络日志。制定一个事件响应计划,概述识别、控制和从安全事件中恢复的步骤,以及与受影响用户和利益相关者的沟通。
最后,尽管众多的合规要求可能看起来像是不必要的负担,但这些规定存在是有原因的。确保你的应用程序遵守相关的法律、法规和行业标准,例如通用数据保护条例(GDPR)、健康保险可携带性和问责法案(HIPAA)或支付卡行业数据安全标准(PCI DSS)。定期审查和更新应用程序的隐私政策、数据处理程序和安全措施,以保持合规。考虑使用 Azure Compliance Manager 等工具来跟踪和管理你的合规要求。
基础设施可扩展性
基础设施的可扩展性曾经是一个巨大的挑战。在应用运行在物理服务器上的时代,唯一的扩展方式是联系您的硬件供应商,并订购一卡车新的服务器!这个过程可能需要数月时间——根本无法对使用高峰的瞬间做出反应,而且故障非常常见。应对需求小幅度波动的唯一方法是拥有 99%时间未使用的额外容量——这对托管应用的公司来说是一种极其昂贵的低效。
幸运的是,在云服务普及的今天,许多这些问题现在已经被历史所淘汰。然而,仍然有一系列新的挑战需要解决!
当大规模运营 SaaS 应用时,基础设施的可扩展性是否会迅速成为一个关键因素,因为它确保了您的应用能够适应不断变化的需求,并继续提供高质量的用户体验?可扩展的云基础设施可以动态地增长或缩小,以满足您应用的变化需求,使其能够处理不断增长的负载,而不会牺牲性能、可靠性或可用性。同样,当需求下降时,例如在您最活跃的地区夜间,云基础设施也可以再次缩小规模。这使得应用的操作者能够极其高效地使用资源,只需维护一小部分始终在线的缓冲区以应对使用高峰。随着您的应用用户基础和资源需求的增长,设计和实施能够垂直和水平扩展的基础设施变得越来越重要。通过主动解决基础设施的可扩展性问题,您可以创建一个适应性强、支持应用增长并继续满足不断增长的用户群需求的环境。水平扩展是指设计您的应用以在多个实例或节点上运行,这些实例或节点可以根据需要添加或删除,以适应不断变化的负载。为了实现这一点,采用我们在前面章节中讨论的微服务架构非常有用。微服务架构允许您独立扩展单个组件或服务,从而提高资源利用和管理效率。还建议使用容器化技术,如 Docker,以及编排平台,如 Kubernetes 或Azure Kubernetes Service(AKS),以简化微服务的部署和管理。
垂直扩展是指根据需要增加分配给应用组件的资源,如 CPU、内存或存储,以处理增加的需求。定期分析和优化应用资源使用,以确保其高效地使用可用资源。使用 Azure Monitor 或 Application Insights 等工具跟踪资源利用情况并识别潜在瓶颈。
如果你的应用程序设计得可以轻松地进行水平扩展,并且你的云基础设施可以垂直扩展,那么你已经为自己应对需求波动的峰值提供了最佳的机会!
这些峰值可能随时发生,不分昼夜,而且往往发生得非常快。没有时间组建团队来处理,并且必须自动构建水平和垂直扩展以响应额外的需求。使用自动化服务来定义基于预定义指标(如 CPU 利用率或请求速率)的扩展规则和触发器。结合自动扩展和负载均衡,使用如 Azure 负载均衡器或应用程序网关等技术,在实例之间均匀分配流量,确保最佳性能和资源利用率。
一种非常现代且非常巧妙的方法来帮助促进自动扩展,是采用基础设施即代码(IaC)实践来自动化基础设施的供应、配置和管理。IaC 允许你将基础设施定义为代码,对其进行版本控制,并在各个环境中一致地应用更改。使用如Azure 资源管理器(ARM)模板、Terraform 或 Ansible 等工具来实现 IaC,并简化你的基础设施管理。
最后,再次强调,无论你的流程和实践多么出色,总会不可避免地出现一些意外问题。为了减轻这种影响,持续监控你的基础设施性能、资源利用率和容量,以便做出关于扩展的明智决策。使用 Azure Monitor、Application Insights 或第三方解决方案等监控工具来收集和分析基础设施指标。定期审查容量规划,以估计未来的资源需求,并确保你的基础设施能够应对预期的增长。通过这样做,你将给自己最大的机会在问题发生之前捕捉到它们,或者至少在它们发生时能够迅速响应!
成本和资源管理
在上一节中,我们讨论了通过为你的应用程序添加额外资源来消费来实现水平和垂直扩展。即使是在谈论云基础设施时,添加资源也会产生额外的费用,并且随着应用程序的扩展,这可能会变得极其昂贵。
因此,在规模运营 SaaS 应用程序时,有效的成本和资源管理至关重要,因为它使你的组织能够优化资源的使用,减少开支,并维持可持续和盈利的业务模式。随着你的应用程序的用户基础和基础设施的增长,实施有助于你监控、控制和优化与运行和扩展应用程序相关的成本的策略变得越来越重要。
通过积极应对成本和资源管理,你可以创建一个适应性强且成本效益高的环境,支持你的应用程序的增长,同时最大化投资回报率。
这一点首先是从简单关注成本开始。定期分析和优化应用程序的资源使用,以确保其高效地使用可用资源。使用监控工具,如 Azure Monitor、Application Insights 或第三方解决方案,以跟踪资源利用情况并识别潜在的瓶颈或未充分利用的资源。实施自动扩展和负载均衡策略,如基础设施可伸缩性部分所述,以优化资源分配和利用。
与本章中的许多建议一样,使用 Azure Cost Management、AWS Cost Explorer 或第三方成本管理解决方案等工具持续监控应用程序的成本非常重要。设置成本警报和通知,以使团队了解成本趋势和潜在的预算超支。定期审查和分析成本报告,以识别成本优化的机会,并确保应用程序的支出符合预算和业务目标。
根据应用程序的具体需求和用法模式选择合适的基础设施和资源具有挑战性,并且经常被只想构建酷炫应用程序的技术团队所忽视!但是,应用程序的成功最终是盈利能力的函数,因此应谨慎选择最合适的云服务。定期审查您的基础设施选择,并调整资源以确保您没有过度配置或未充分利用资源。
数据一致性和完整性
数据一致性和完整性是大规模运营 SaaS 应用程序的关键方面,因为它们直接影响到应用程序处理和存储的数据的质量和可靠性。确保数据一致性意味着无论数据存储在何处或如何访问,应用程序都向所有用户提供一致的数据视图。数据完整性是指在整个生命周期内保持数据的准确性、完整性和一致性。
随着应用程序的用户基础和数据量的增长,实施确保应用程序组件和服务之间数据一致性和完整性的策略变得越来越重要。通过积极解决数据一致性和完整性问题,您可以创建一个可靠的环境,保持数据质量并支持应用程序的增长。
在构建 SaaS 应用时,与分布式数据系统或微服务一起工作是非常常见的。使用这些技术,你应该考虑采用最终一致性模型来维护多个数据存储或服务之间的数据一致性。在这个模型中,数据更新可以在不同的组件之间异步传播,最终达到一致状态。实施机制,如消息队列(例如在微服务章节中演示的 RabbitMQ)或事件驱动架构,以传播数据更新并在应用的服务之间强制一致性。
在数据库层拥有一个稳固的数据模型非常重要,但同样重要的是尝试防止不良数据首先进入数据库。为了实现这一点,在 UI、API 和数据库级别实施数据验证和清理过程,以确保仅存储和处理的准确且格式良好的数据。使用输入验证技术,如数据类型约束、范围检查和模式匹配,在存储或处理之前验证传入的数据。此外,清理数据以删除任何可能有害的内容或格式,从而防止诸如 SQL 注入或跨站脚本(XSS)攻击等安全漏洞。
如在可靠性和可用性部分所述,定期备份应用数据以防止数据丢失或损坏。实施包括多个级别冗余的备份策略,如完整备份、差异备份和增量备份。并且不要忘记定期测试备份和恢复过程,以确保它们有效,并在发生故障时能够恢复数据完整性。
在所有这些扩展考虑因素中,一个共同的主题是持续监控和审计应用的数据操作,以检测和应对可能影响数据一致性和完整性的潜在问题。定期审查数据审计日志以识别趋势和模式,以及确保符合相关法规和标准。
规划扩展和增长
到目前为止,本章主要关注技术技巧,但考虑非技术元素也同样重要,这些元素涉及到应用扩展,以便它能处理需求的变化。制定扩展和增长的计划是成功运营大规模 SaaS 应用的关键方面,因为它确保了你的应用能够准备好应对不断增长的用户群体的需求,并能够持续提供高质量的用户体验。通过积极规划增长,你可以创建一个灵活且具有弹性的环境,支持应用的增长并帮助维持高水平的客户满意度。
第一步是与技术团队以及其他利益相关者坐下来,定期审查你的应用程序的容量规划和资源需求,根据历史趋势、用户增长预测和资源利用率模式来估计未来的需求。在技术领域,没有什么会长期保持静止,所以定期更新你的容量计划,以确保你的应用程序和基础设施为预期的增长做好准备。
为了验证你的假设并为增长规划会议提供输入,定期进行性能测试和基准测试,以评估你的应用程序处理增加的工作负载和用户并发的能力。使用负载测试和压力测试工具来模拟真实世界的使用场景,并识别潜在的瓶颈或性能问题。建立性能基线并设置目标指标,以帮助指导你的扩展工作,并确保你的应用程序在扩展过程中继续满足性能要求。
当然,在过程中可能会出现问题。团队对这些问题的意外程度越低,它们的影响就越小,因此制定全面的灾难恢复和业务连续性计划,以确保你的应用程序能够从意外故障中恢复,并继续为用户提供服务。正如在可靠性和可用性部分所讨论的,实施备份和恢复策略、冗余和故障转移机制,以最大限度地减少停机时间和数据损失。定期测试和更新你的灾难恢复计划,以确保其有效性并与其应用程序的增长和不断变化的需求保持一致。
很容易专注于扩展应用程序所涉及的技术挑战,但这一点不应是唯一的考虑因素。在这方面为未来做规划将证明非常有价值,因为你的应用程序会不断增长!
拥抱 DevOps 和自动化
到目前为止,我们本章所讨论的一切都基于对 DevOps 和自动化的扎实理解。在使用手动流程时,几乎不可能跟上对现代 SaaS 应用不断变化的需求。
拥抱 DevOps 和自动化使你的团队能够简化开发和运营流程,提高效率,并最小化潜在风险。通过整合开发和运营团队并利用自动化工具和实践,你可以确保你的应用程序在增长过程中保持敏捷、可靠和适应性强。
通过积极地将 DevOps 和自动化融入你组织的文化和流程中,你可以创造一个协作高效的环境,支持你的应用程序增长并帮助保持高水平的客户满意度。
其核心是持续集成和持续部署(CI/CD),我们将在下一章中详细讨论!CI/CD 流水线自动化构建、测试和部署应用程序的过程,对于这个过程来说是基础性的,因为它们显著减少了发布新功能和改进所需的时间和精力,同时最大限度地降低了引入错误、回归或性能问题的风险。
CI 流水线应始终通过运行一系列自动化测试来验证代码的正确性。这包括单元测试、集成测试和端到端测试。自动化测试与强大的 CI 流水线相结合,可以大幅降低在应用程序扩展过程中引入错误或性能问题的风险。
现代云基础设施使我们能够采用 IaC(基础设施即代码)实践,使用代码和配置文件而不是手动流程来管理和配置应用程序的基础设施。IaC 使您能够自动化基础设施的配置和提供,确保一致性、可重复性和可扩展性。例如,可以使用 Terraform 等工具来促进这一过程。
正如我们在本书的演示应用程序环境中所展示的,配置开发者环境即代码(DEaC)并将所有开发者依赖项构建到 Docker 设置中也是可能的。
继续延续“自动化一切”的主题,实现配置管理工具和实践以自动化管理应用程序的设置、依赖和环境配置的过程也是非常方便的。配置管理有助于确保应用程序组件和服务的一致性和可靠性,同时简化部署更新和扩展基础设施的过程。自动化配置还可以最大限度地减少在生产环境中重要配置细节意外共享或推送到不安全环境的风险。
最后,正确实施 DevOps 还有很大的非技术成分。通过鼓励开放沟通、共同目标和联合问题解决,在开发和运维团队之间培养协作文化。实施促进协作和信息共享的工具和实践,例如项目管理工具如 Jira 或 Trello,以及通信平台如 Microsoft Teams 或 Slack。定期举行跨职能会议和回顾会议,以审查进度、讨论挑战并确定改进的机会。
DevOps 近年来迅速发展,这是有充分理由的。DevOps 实践在成功运营大规模 SaaS 应用中发挥着关键作用。通过将开发和运维团队结合起来,DevOps 促进了无缝协作,并确保软件快速、可靠、安全地交付。使用 DevOps,开发者可以持续部署新功能和更新,而运维团队能够保持应用的高可用性和可靠性。这在运营规模较大时尤为重要,因为任何停机或中断都可能对用户体验和收入产生重大影响。因此,使用 DevOps 实践对于确保大规模 SaaS 应用的平稳运行至关重要。
总之,在大规模运营 SaaS 应用时,开发团队必须解决众多挑战,以确保应用的持续成功和增长。通过理解和积极应对这些挑战,您可以创造一个可扩展、高效且具有弹性的环境,使应用随着用户基础的扩大而蓬勃发展。
在本节中,我们探讨了关键领域,包括性能和响应时间、可靠性和可用性、数据一致性和完整性、安全性和合规性、基础设施可扩展性、成本和资源管理、规划扩展和增长,以及拥抱 DevOps 和自动化。通过实施本节提供的实用技巧和策略,您的团队能够应对大规模运营的挑战,保持高水平的客户满意度,并推动 SaaS 应用的持续成功。
随着您继续扩大和扩展 SaaS 应用,定期审查和调整您的策略和实践,以应对不断变化的需求、新技术和不断发展的用户期望,这一点非常重要。通过保持敏捷、适应性强并专注于持续改进,您的开发团队能够成功应对大规模运营的挑战,并确保 SaaS 应用的长期成功和可持续性。
现在,我们将查看应用各层级的特定扩展考虑因素。
扩展数据库
在本节中,我们将深入探讨扩展您的 SaaS 应用程序数据库层的关键任务。作为您应用程序构建的基础,数据库在系统的整体性能、可靠性和可扩展性中起着至关重要的作用。随着应用程序的增长,处理更大的数据量和更多的用户请求,有效管理数据库变得越来越重要。我们将讨论包括分片、水平扩展、缓存、分区、归档、索引和查询优化、连接池和复制在内的基本策略和技术。通过掌握这些方法,您将加强数据库基础,并确保一个性能优良、可扩展且具有弹性的 SaaS 应用程序,以满足不断增长的用户群体的需求。
分片
分片是一种数据库扩展技术,涉及将大型数据集划分为更小、更易于管理的片段,称为分片。每个分片包含数据的一部分,并存储在单独的数据库服务器上,从而分散负载并提高整体性能。对于 SaaS 应用程序来说,分片特别有益,因为处理不断增长的数据量和用户需求对于增长和成功至关重要。
分片主要有两种方法:
-
水平分片(数据分区):这种方法通过行来划分数据集,每个分片包含一组独特的记录子集。水平分片通常基于特定属性,如用户 ID 或地理位置。
-
垂直分片(模式分区):在这种方法中,数据集被分为列,每个分片包含表属性的一个子集。当某些列比其他列更频繁地被访问或具有不同的扩展需求时,通常使用垂直分片。
在实施分片时,选择合适的分片键以确定数据如何在分片中分布是至关重要的。分片键的选择可以显著影响性能,因此需要考虑查询模式、数据分布和可扩展性需求等因素。常见的分片策略包括以下几种:
-
基于范围的分片:数据根据分片键的值范围(例如,日期范围或字母范围)进行分区。
-
基于哈希的分片:对分片键应用哈希函数,并根据产生的哈希值将数据分布在分片中。这种方法通常提供更均匀的数据分布。
-
基于目录的分片:使用单独的查找服务或目录将分片键映射到特定的分片,提供在数据分布和分片管理方面的更大灵活性。
虽然分片可以显著提高数据库性能和可扩展性,但重要的是要意识到潜在挑战和考虑因素:
-
数据一致性:确保跨分片的一致性可能很复杂,尤其是在分布式事务或处理最终一致性模型时。
-
查询复杂性:分片可能会增加查询的复杂性,因为某些查询可能需要在多个分片及其结果组合中执行。
-
重新平衡和重新分片:随着您的应用程序的增长,您可能需要重新分配数据到分片或添加新的分片。这个过程,称为重新平衡或重新分片,可能很耗时,可能需要仔细的计划和执行。
-
跨分片操作:跨越多个分片的操作,如连接或事务,可能比单个分片内的操作更复杂且性能更低。
扩展
与涉及将数据分区成更小的子集并分布到单独数据库服务器的分片不同,扩展侧重于增加数据库基础设施的容量以处理增加的工作负载。扩展数据库有两种主要方法:水平扩展和垂直扩展。
水平扩展,也称为向外扩展,涉及向您的基础设施添加更多服务器或节点以处理增加的负载并提高性能。在数据库的上下文中,水平扩展涉及在整个数据库中复制到多个服务器或节点,并将负载在他们之间分配。负载均衡和数据复制技术通常被用于实现水平扩展。
垂直扩展,或向上扩展,涉及通过添加更多资源(如 CPU、内存和存储)来增加现有服务器的容量,以处理增加的工作负载并提高性能。在垂直扩展数据库时,您升级硬件或增加分配给数据库服务器的资源。这可能包括升级到更强大的服务器、添加更多随机存取存储器(RAM)、增加存储容量或分配更多 CPU 核心。
水平扩展和垂直扩展都有其优势和局限性。水平扩展允许更好的容错性和可能更大的整体容量,而垂直扩展可以在不管理多个服务器复杂性的情况下提供即时的性能提升。然而,垂直扩展在资源可用性和潜在的单点故障方面存在固有的局限性。
这些扩展技术是提高您数据库基础设施处理增长工作负载容量的重要技术。通过了解这些方法之间的差异以及各自的优缺点,您可以做出关于扩展您的 SaaS 应用程序数据库层的最佳方法的明智决策。
分区
之前我们讨论了分片作为一种在多个数据库系统或集群之间分配数据的技术,以实现更大的可扩展性和容错性。另一方面,分区是一个相关但不同的概念,它涉及根据特定标准将单个数据库系统中的大表划分为更小、更易于管理的部分。虽然分区和分片都旨在提高性能和管理性,但分区在单个数据库系统内操作,对应用程序是透明的,而分片则需要跨多个数据库系统进行显式管理和协调。
分区是一种通过将数据拆分为更小、更易于管理的部分来管理数据库中大型数据集的技术。这种方法可以帮助提高您 SaaS 应用程序数据库层的性能、可维护性和可扩展性。分区可以在表和索引级别应用,具体取决于所使用的特定数据库系统。
在扩展数据库时,需要考虑两种主要的分区类型:
-
水平分区:如前所述,水平分区涉及根据特定标准(如值范围或哈希函数)将表行拆分为更小的子集。每个分区包含行的一个独特子集,可以存储在单独的数据库服务器或表空间中,这可以通过允许并行处理和减少竞争来提高性能。
-
垂直分区:在垂直分区中,表的列被拆分为更小的子集,每个分区包含列的一个子集。这种方法对于具有许多列的大表或特定列经常一起访问的情况特别有用。垂直分区可以帮助减少获取数据所需的输入/输出(I/O)量,从而提高查询性能。
在实现分区时,应考虑以下几个因素:
-
分区键:选择一个合适的分区键,以确保数据在分区之间得到平衡分布。选择不当的键可能会导致数据分布不均,从而对性能产生负面影响。
-
分区方案:根据数据访问模式、查询性能要求和维护考虑等因素,确定最适合您数据的最合适的分区方案。
-
数据管理:实施数据管理策略,例如分区维护,以确保您的分区保持优化并保持最新。这可能包括添加或合并分区、重新组织分区或更新分区统计信息等任务。
-
查询优化:优化您的查询以利用分区,使用如分区消除和分区内连接等特性,这些特性可以显著提高查询性能。
分区是一种有效的技术,用于管理大型数据集并提高 SaaS 应用程序数据库层的性能和可伸缩性。通过了解不同类型的分区及其相关考虑因素,您可以实施优化查询性能、便于数据管理和使数据库随着应用程序的增长而扩展的分区策略。
缓存
缓存是一种用于通过在称为缓存的临时存储区域中存储频繁使用的数据或资源密集型操作的结果来提高 SaaS 应用程序性能和响应性的技术。通过使用缓存,应用程序可以快速检索数据,而无需重新计算或从数据库中重新获取,从而减少数据库的负载并最小化响应时间。
您可以采用几种缓存策略来优化 SaaS 应用程序的数据库性能:
-
内存缓存:这种方法涉及将频繁访问的数据存储在应用程序服务器的内存中,从而允许更快的数据检索。内存缓存可以使用内置的.NET 缓存机制或第三方库,如 Redis 来实现。
-
分布式缓存:在分布式缓存设置中,缓存存储在多个服务器上,通常使用专门的缓存服务,如 Redis 或 Memcached。这种方法对于大规模应用程序特别有用,因为它允许缓存水平扩展并保持多个应用程序服务器之间的一致性。
-
数据库缓存:数据库缓存涉及使用数据库系统本身提供的内置缓存机制,例如 SQL Server 的缓冲区缓存或 Azure SQL 数据库的内存在线事务处理(OLTP)功能。这种方法通过减少从磁盘获取数据所需的时间来帮助优化查询性能。
-
查询结果缓存:通过缓存频繁执行的查询的结果,可以减少对数据库重复查询的需求并提高性能。这可以通过应用级缓存或利用数据库级缓存功能来实现,例如 SQL Server 的查询存储功能。
在实施缓存时,以下因素是至关重要的:
-
缓存失效:确定何时以及如何使缓存数据失效或更新,以确保应用程序提供准确和最新的信息。
-
缓存过期:为缓存数据定义适当的过期策略,以防止向用户提供过时数据并优化缓存使用。
-
缓存粒度:选择适当的缓存粒度,平衡性能改进的需求与精细粒度缓存条目管理的潜在复杂性。
-
监控和指标:实施监控和指标以跟踪缓存性能、命中率以及资源使用情况,这使您能够优化缓存策略,并就容量规划和扩展做出明智的决策。
缓存是提高您的 SaaS 应用程序数据库层性能和可扩展性的强大技术。通过了解各种缓存策略及其相关考虑因素,您可以有效地减少数据库负载,最小化响应时间,并为用户提供更好的整体体验。
索引和查询优化
我们在本书的数据库章节中已经提到了这一点。索引和查询优化是扩展您的 SaaS 应用程序数据库的关键方面,因为它们有助于确保您的数据库查询运行高效,并最小化对性能的影响。低效的查询可能会对应用程序的性能产生巨大影响,并可能显著增加运行数据库的云资源成本。正确处理这一点尤为重要!
索引是一种数据库对象,它通过提供更有效的数据访问路径来帮助加快从表中检索行,从而提高检索速度。可以在表的一个或多个列上创建索引,并且它们使数据库引擎能够快速定位所需的行,而无需执行完整的表扫描。为您的应用程序创建正确的索引可以显著提高查询性能并减少数据库负载。
这里是索引的类型:
-
聚集索引:聚集索引确定表中数据存储的物理顺序。每个表只能有一个聚集索引,它可以显著提高按索引定义的顺序检索数据的查询性能。
-
非聚集索引:非聚集索引存储索引列的单独副本,以及对应表中行的引用。您可以在每个表上创建多个非聚集索引,并且它们可以帮助提高基于索引列进行筛选、排序或连接数据的查询性能。
-
列存储索引:列存储索引以列格式存储数据,这可以为分析查询和大规模数据聚合任务提供显著的性能改进。列存储索引特别适合数据仓库和报告场景。
除了索引之外,优化您的查询是数据库性能调优的重要方面,因为它确保您的应用程序能够高效地从数据库中检索数据。以下是一些查询优化的技术:
-
使用特定列的
SELECT语句而不是SELECT * -
利用索引:确保您的查询利用现有的索引,并考虑创建额外的索引以支持频繁执行的查询。
-
使用
LIMIT、OFFSET或TOP子句来限制查询返回的行数,这有助于减少应用程序传输和处理的请求数据量。 -
根据
INNER JOIN或OUTER JOIN的数据需求。 -
分析查询计划:使用诸如 SQL Server 的查询分析器或 Azure SQL 数据库的查询性能洞察等工具来分析查询执行计划,并识别潜在的瓶颈或不效率。
索引和查询优化在提高您的 SaaS 应用程序数据库层的性能和可扩展性方面发挥着至关重要的作用。通过了解不同类型的索引并采用有效的查询优化技术,您可以确保应用程序高效地检索数据,最小化对数据库性能的影响,并提供更好的用户体验。
数据存档和保留
随着您的 SaaS 应用程序的增长,数据库中存储的数据量必然会增加,这可能导致性能下降和更高的存储成本。实施数据存档和保留策略可以帮助您管理数据增长,同时确保您的应用程序保持响应和成本效益。
数据存档涉及将历史数据或很少访问的数据从您的主数据库移动到单独的、更具成本效益的存储系统。此过程通过减少主数据库需要管理和查询的数据量,从而允许您保持主数据库的性能。存档数据在需要时仍然可以访问,尽管可能速度较慢,并且可用于报告、分析或合规目的。
在实施数据存档策略时,考虑以下因素:
-
确定要存档的数据:确定哪些数据可以安全地移动到存档中,而不会影响应用程序的功能或用户体验。这可能包括历史交易数据、已完成的项目或非活跃用户账户。
-
选择合适的存储解决方案:选择满足您的成本、性能和合规要求的存储解决方案,例如 Azure Blob 存储、Azure 数据湖或其他存档存储服务。
-
自动化存档过程:实施一个过程,定期将合格数据从主数据库移动到存档存储系统,确保您的数据保持最新,主数据库保持精简。
数据保留是在数据库或存档存储系统中定义数据在永久删除之前应存储多长时间的实践。一个定义良好的数据保留策略可以帮助您管理存储成本,遵守数据保护法规,并降低数据泄露的风险。
在制定数据保留策略时,考虑以下因素:
-
了解您的法律和监管义务:根据您的行业、管辖区域以及任何适用的法规(如 GDPR 或 HIPAA),确定不同类型数据的最低和最高保留期限。
-
根据业务需求定义保留期限:根据您的业务需求,为每种类型的数据建立保留期限,考虑数据价值、访问频率和存储成本等因素。
-
实施数据删除流程:开发流程以自动删除已达到保留期限结束的数据,确保您的数据存储符合您的保留政策。
一个执行良好的数据归档和保留策略可以帮助您在保持数据库性能和控制存储成本的同时,管理 SaaS 应用程序数据的增长。通过仔细考虑哪些数据需要归档,选择适当的存储解决方案,并实施明确的数据保留政策,您可以确保随着应用程序的增长,您的应用程序保持可扩展性和成本效益。
扩展数据库是确保您的 SaaS 应用程序成功和增长的关键方面。随着用户基础的扩大和数据量的增加,实施有助于您保持性能、可靠性和成本效益的策略至关重要。
在本节中,我们介绍了各种技术和最佳实践,用于扩展您的数据库,包括分片、水平扩展和垂直扩展、缓存、分区、数据归档和保留、监控以及性能调整。每种方法都有其自身的优势和权衡,最适合您应用程序的具体技术组合将取决于您的独特需求和限制。
在您继续构建和扩展 SaaS 应用程序的过程中,请记住这些策略,并根据需要持续评估和调整您的做法。通过积极应对数据库扩展的挑战并采用正确的技术组合,您可以确保您的应用程序保持高性能、可靠性和成本效益,为不断增长的用户群体提供高质量的服务。
扩展 API
在本节中,我们将探讨在 SaaS 应用中扩展 API 的具体考虑因素。一个设计良好的 API 对于保持应用在增长过程中的性能、可靠性和灵活性至关重要。既然你已经构建了 Good Habits 演示应用,并实现了包含 WebAPI、Ocelot 作为 API 网关和 RabbitMQ 用于异步通信的微服务架构,你已经为 API 的扩展奠定了坚实的基础!然而,你还需要考虑其他方面,以确保随着系统需求的增加,你的 API 仍然能够保持响应性和高效性。我们将讨论各种策略和最佳实践,例如负载均衡、API 版本控制、速率限制、缓存和监控。通过理解和实施这些技术,你可以有效地扩展 API 以满足不断增长的用户群体的需求,并继续为你的客户提供高质量的服务体验。
负载均衡和 API 网关优化
负载均衡是扩展 API 的关键方面,因为它有助于将传入请求均匀地分配到可用资源中,确保没有单个实例成为瓶颈。通过实施负载均衡和优化 API 网关,你可以提高 API 的性能和可靠性,随着应用的扩展。
这里有一些你可能想要考虑的负载均衡策略:
-
轮询:这种策略将请求均匀地分配到 API 的所有实例中,无论它们的当前负载或响应时间如何。这种方法简单易行,但可能没有考虑到实例性能或容量的差异。
-
最少连接数:这种策略将请求路由到活动连接最少的实例。这种策略有助于确保具有较少连接的实例可以处理更多请求,从而可能提高整体性能。
-
基于延迟:这种策略将请求路由到延迟最低或响应时间最短的实例。这种方法可以帮助最小化网络延迟对 API 性能的影响。
API 网关优化涉及很多方面,但这本书的范围不包括详细探讨。以下是一些需要考虑的一般性要点:
-
连接池:通过重用 API 网关和 API 实例之间的现有连接,你可以减少建立新连接的开销,从而提高性能并降低延迟
-
缓存:在 API 网关级别实现缓存,以存储和提供频繁访问的数据或响应,减少对 API 实例的负载并提高响应时间
-
速率限制:在 API 网关级别实施速率限制,以保护 API 实例免受单个客户端或恶意攻击带来的过多请求的影响
-
安全性:在网关级别实现安全功能,如身份验证、授权和 API 密钥管理,将这些责任从您的 API 实例中卸载,从而提高其性能
通过采用负载均衡策略和优化您的 API 网关,您可以有效地分配传入请求,提高 API 的性能和可靠性,并确保不断增长的用户群获得高质量的体验。
API 版本化和向后兼容性
随着您的 SaaS 应用程序的演变和新功能的添加,API 的变更可能是必要的。确保向后兼容性和管理 API 版本化是扩展 API 以保持客户和用户一致可靠体验的关键方面。
我们已经在 API 章节中介绍了 API 版本化策略。以下是一些关键策略的快速提醒:
-
/v1/users或/v2/users。这种方法简单易懂,但对于客户来说可能导致 URI 杂乱,并需要仔细管理资源和路由 -
/users?version=1或/users?version=2。这种方法使 URI 保持简洁,并允许更灵活的版本化,但对于客户来说可能不太直观 -
X-API-Version: 1或X-API-Version: 2。这种方法使 URI 保持简洁,并将版本化问题与资源表示分离,但对于客户来说可能不太容易被发现
一旦 API 投入生产,非常重要的一点是不要引入任何可能导致任何消费应用程序出现错误的破坏性变更。为了确保 API 保持向后兼容,您可以考虑以下措施:
-
避免破坏性变更:尽可能设计您的 API 变更以实现向后兼容,允许现有客户在无需修改的情况下继续运行
-
弃用策略:如果需要引入破坏性变更,请提供一个清晰的弃用策略和时间表,以便通知客户何时将不再支持旧版本的 API
-
优雅降级:为新 API 功能实现回退机制,允许不支持最新版本的客户端以减少功能或特性的方式继续运行
-
文档:为每个 API 版本维护清晰和全面的文档,帮助客户了解版本之间的差异以及迁移过程
通过管理 API 版本化和确保向后兼容性,您可以在继续演进和扩展 SaaS 应用程序的同时,最小化对客户和用户的干扰。这种方法允许您在 API 增长和适应不断变化的需求时,保持一致和可靠的体验。
速率限制和节流
随着您的 SaaS 应用程序扩展并吸引更多用户,对 API 的请求数量也会增加。实施速率限制和节流策略有助于防止滥用,保护 API 免受过度负载的影响,并确保客户之间公平使用。
如果你的应用程序正在面临间歇性的重负载,你可以考虑以下速率限制策略:
-
全局速率限制:这将在指定时间周期内设置所有客户端允许的最大请求数量。这种方法可以帮助保护你的 API 免受过度负载的影响,但可能无法考虑个别客户端的使用模式。
-
按客户端速率限制:在指定时间周期内为每个客户端设置允许的最大请求数量。这种策略可以帮助确保客户端之间的公平使用,但可能需要更复杂的跟踪和执行机制。
-
分层速率限制:根据客户端订阅级别或访问层提供不同的速率限制。这种方法允许你提供差异化的服务级别,并鼓励客户端升级到更高层以获得更好的 API 访问。
除了前面提到的速率限制策略,你还可以考虑以下节流技术:
-
漏桶:实现一个算法,该算法累积传入的请求并以固定速率处理它们。这种方法可以平滑请求峰值,并确保你的 API 不会过载。
-
令牌桶:使用令牌来调节客户端可以发起请求的速率。客户端必须拥有令牌才能发起请求,并且令牌以固定速率生成。这种方法允许在处理请求突发时具有更大的灵活性和适应性。
-
指数退避:鼓励客户端在遇到速率限制或错误时逐渐增加重试之间的时间间隔。这种技术有助于在时间上分散重试,从而降低压倒你的 API 的机会。
通过实施速率限制和节流策略,你可以保护你的 API 免受过度负载,防止滥用,并确保用户获得高质量的服务体验。这些技术有助于维护 API 的性能和可靠性,随着你的 SaaS 应用的增长和为更大的用户群体提供服务,这些技术尤为重要。
API 性能的缓存策略
我们已经讨论了数据库层的缓存,现在我们将介绍 API 层的缓存。缓存是一种提高 API 性能和响应性的基本技术,尤其是在你的 SaaS 应用扩展时。通过存储和提供频繁访问的数据或缓存中的响应,你可以减少 API 实例的负载并提高响应时间。在 API 层进行缓存意味着根本不与数据库层接触,因此整个堆栈都能感受到好处。
以下是一些缓存策略的示例:
-
客户端缓存:通过提供适当的缓存控制头(例如,Cache-Control,ETag),鼓励客户端在本地缓存 API 响应。这种方法减少了发送到你的 API 的请求数量,并将缓存责任转移到客户端。
-
服务器端缓存:这将在服务器端存储频繁访问的数据或响应,无论是在内存中还是在外部缓存服务(例如,Redis 或 Memcached)中。这种方法可以通过减少耗时数据检索或处理的需求,显著提高 API 的性能。
-
边缘缓存:这利用 CDN 在客户端附近缓存和提供 API 响应。这种方法可以帮助减少延迟并提高响应时间,特别是对于远离您的 API 实例的客户端。
-
缓存失效:这实现了在底层数据更改时使缓存条目失效的策略,确保客户端接收到的信息是最新的。可以采用缓存过期、缓存版本化或事件驱动缓存失效等技术来维护数据一致性。
通过将缓存策略集成到您的 API 中,您可以提高性能、减少延迟并最小化对后端系统的负载。随着您的 SaaS 应用程序扩展并服务于更多用户,有效的缓存变得越来越重要,以确保为您的客户和用户提供高质量体验。
异步通信和消息队列
SaaS 应用程序通常很复杂,需要计算密集型的 API 调用。这些可能会对其性能和响应性产生负面影响,并急剧增加云资源的成本。实现异步处理和后台作业可以帮助从主 API 请求/响应周期中卸载这些任务,确保用户获得流畅的体验。
为了保持您的应用程序平稳运行,您可以考虑以下这些异步处理策略和技术,用于在后台运行作业:
-
消息队列:这利用消息队列(例如,RabbitMQ、Azure Service Bus)将 API 与处理任务解耦。客户端向 API 发送请求,然后 API 将任务推送到队列中,由专门的工人服务进行处理。
-
事件驱动架构:这实现了一个基于特定事件或系统内操作的事件驱动架构来触发处理。这种方法使您能够构建可扩展且具有弹性的系统,这些系统可以随着您的应用程序需求的发展而发展。
-
计划任务:这安排在特定间隔运行后台作业,例如夜间数据处理、每周报告生成或每日清理任务。这种技术有助于您在时间上更均匀地分配系统负载。
-
优先级队列:这为后台作业队列中的任务分配不同的优先级级别,确保关键任务首先被处理。这种方法有助于您更有效地管理系统资源并提高整体用户体验。
-
重试和回退机制:这为可能因暂时性错误(如网络问题或临时资源限制)而失败的背景作业实现了重试和回退机制。这种技术有助于确保任务最终完成,并且您的系统对故障具有弹性。
通过利用异步处理和后台作业,您可以将资源密集型任务从 API 中卸载,帮助保持其性能和响应性,随着您的 SaaS 应用程序扩展。这种方法使您能够在高效管理系统资源的同时,为用户提供高质量的服务体验。
无状态和幂等 API 设计
在扩展 SaaS 应用程序时,设计无状态和幂等 API 至关重要,因为它确保您的系统更具可预测性、易于管理,并且更不容易出错。在本节中,我们将探讨无状态性和幂等性及其在可扩展应用程序 API 设计中的重要性。
无状态 API 在请求之间不维护任何客户端特定状态,这意味着每个请求都是自包含的,并且独立于之前的请求。实现无状态 API 提供了以下好处:
-
简化扩展:无状态 API 更容易进行横向扩展,因为您可以在多个实例之间分配请求,而无需担心维护会话状态
-
提高可靠性:无状态 API 对故障具有更强的抵抗力,因为任何实例都可以处理请求,而不依赖于其他实例的状态
-
增强性能:无状态 API 可以更好地利用缓存机制,因为响应不依赖于客户端特定的状态
要设计无状态 API,请考虑以下实践:
-
避免服务器端会话,而是使用令牌(例如,JSON Web Token(JWT))来验证和授权请求
-
在客户端或外部存储(如数据库或缓存)中存储任何所需的状态
幂等 API 操作在多次调用时,如果使用相同的输入,将产生与只调用一次相同的结果和副作用。设计幂等 API 确保您的系统行为可预测,并且由于网络重试、超时或其他问题而导致的错误更少。
要设计幂等 API,请考虑以下实践:
-
使用适当的 HTTP 方法,例如
GET、PUT和DELETE,这些方法本身是无状态的 -
对于非幂等操作,如
POST,实现幂等键或令牌,允许客户端安全地重试请求,而不会造成意外的副作用 -
确保您的 API 内部逻辑可以处理重复请求,而不会创建重复记录或执行不希望的操作
通过设计无状态和幂等 API,您可以构建更可扩展、可靠和可预测的 SaaS 应用程序。这些设计原则有助于确保您的系统可以处理增加的负载,并在您的应用程序增长时为用户提供高质量的服务体验。
规模化安全性和身份验证
随着您的 SaaS 应用程序的增长,确保 API 的安全性和适当的身份验证变得更加关键。在早期章节中,我们讨论了将身份验证集成到您的应用程序中。扩展应用程序可能会引入新的安全挑战,因此实施强大的安全措施来保护用户数据和维持他们的信任至关重要。在本节中,我们将讨论在扩展 API 时增强安全和身份验证的关键考虑因素和最佳实践。
使用如 OAuth 2.0 或 OpenID Connect 之类的集中式身份验证和授权系统,您可以有效地管理用户对 API 的访问。实施单点登录(SSO)使用户能够使用一组凭据访问应用程序内的多个服务。此外,利用身份提供者(如 Azure Active Directory)可以减轻对用户身份和身份验证流程的管理负担,有助于确保一个安全且可扩展的解决方案。
正确的 API 密钥管理对于维护 API 的安全性至关重要。这包括 API 密钥的生成、分发和撤销。确保 API 密钥具有适当的访问级别和作用域,以限制其使用到特定的资源和操作。定期轮换 API 密钥,并鼓励客户端也这样做,以降低未经授权访问的风险。
使用 HTTPS 进行所有 API 通信以保护传输中的数据,并考虑使用诸如HTTP 严格传输安全(HSTS)等技术来强制执行安全连接。使用强大的加密算法和密钥管理实践在静态中对敏感数据进行加密。实施适当的数据处理程序以最大限度地降低数据泄露或违规的风险。
为登录应用速率限制和节流策略,以保护 API 免受滥用、拒绝服务(DoS)攻击和过度资源消耗。根据用户角色、API 密钥或 IP 地址等因素自定义速率限制,以提供公平和安全的 API 体验。
定期进行安全审计和漏洞评估,以识别 API 和基础设施中可能存在的潜在弱点。建立处理识别出的安全问题的流程,并持续改进您的安全态势。
通过在扩展 API 时关注安全和身份验证,您可以保护用户数据,维持他们的信任,并确保您的 SaaS 应用程序持续成功。实施强大的安全措施对于为不断增长的用户群提供安全可靠的 API 体验至关重要。
扩展您的 SaaS 应用的 API 是确保系统整体性能、可靠性和安全性的关键方面。通过解决诸如无状态和幂等 API 设计、负载均衡、版本控制、速率限制、缓存、异步通信和安全等方面的关键领域,您可以构建一个强大且可扩展的 API,能够满足不断增长的用户群体的需求。
在本节中,我们探讨了各种技术和最佳实践,以确保您的 API 能够适应成功 SaaS 应用增加的需求。通过实施这些策略,您不仅提高了 API 的性能和效率,还确保了用户获得一致且安全的服务体验。
随着应用的持续增长,监控和优化您的 API 扩展策略,适应新的挑战和不断变化的需求至关重要。通过这样做,您将确保 SaaS 应用的长期成功和可持续性,同时为用户提供高质量的服务体验。
扩展 UI
在覆盖了数据库和 API 扩展之后,我们现在将探讨 UI 层的扩展技术。UI 是您 SaaS 应用的关键组件,因为它是用户直接与之交互的层!用户对您整个应用的印象将基于他们对使用您 UI 的喜爱程度(或不喜欢程度)!确保随着应用的增长,用户体验流畅且响应迅速,对于维持用户满意度和参与度至关重要。在本节中,我们将讨论扩展 UI 层的各种技术和最佳实践,重点关注性能优化、静态资产的效率管理以及实施有效的缓存策略。希望这些技术能让您的用户面带微笑,并持续回到您的 SaaS 应用中!
设计可扩展性和性能的最佳实践
良好的设计是支撑应用扩展所有方面的基础,包括良好的数据库设计和后端稳健的架构原则。然而,前端的设计是多方面的,因为设计不仅要技术上可靠,还要让最终用户在使用时感到愉悦。
设计一个性能良好且可扩展的 UI 是复杂的。这涉及到设计 UI 和用户体验(UX)以适应不断增长的用户基础和应用的日益复杂性。通过遵循最佳实践,您可以为客户提供响应迅速、高效且愉悦的体验。在本节中,我们将探讨各种 UI 和 UX 最佳实践,以帮助您设计一个可扩展的 UI。
尽可能使 UI 简单直观,以减少用户的心理负担。这听起来显然且简单,但在实践中,这可能极具挑战性。尝试专注于核心功能,最小化视觉杂乱,并优先考虑用户工作流程。简洁直观的 UI 还可以帮助减少客户端所需的处理和渲染量,从而提高性能。
确保您的应用程序的用户界面能够无缝适应不同的屏幕尺寸、分辨率和设备类型。实施响应式设计技术,如流体网格、灵活的图像和 CSS 媒体查询,以在各种设备上创建一致的用户体验。这种方法可以提高可用性,并帮助您的应用程序适应新设备和屏幕尺寸的出现。
UI 是用户真正看到的全部,他们将以 UI 的性能来评判整个应用程序的性能。通过优化渲染和减少不必要的重渲染来提高 UI 性能。例如,使用虚拟文档对象模型(DOM)、防抖和节流等技术可以帮助最小化更新频率和对性能的影响。此外,考虑使用更轻量级的 UI 框架和库。
最后,始终牢记可访问性。随着您的应用程序用户基础的扩大,使用应用程序的不同能力或残疾的人的数量也将相应增加。以可访问性为设计理念,确保应用程序可以被具有各种能力和残疾的个人使用。这扩大了您的用户基础,并使您的应用程序更加用户友好和多功能。利用语义 HTML、辅助富互联网应用程序(ARIA)角色和键盘导航来增强可访问性。
优化静态资源和打包
静态资源,如图像、样式表和 JavaScript 文件,在您的 UI 性能和响应性方面发挥着重要作用。正确优化和打包这些资源可以缩短加载时间,提高整体 UX,并减轻云资源的负载。在本节中,我们将讨论几种优化静态资源并高效打包的技术。
通过删除不必要的字符、空格和注释来精简 CSS 和 JavaScript 文件可以显著减小其大小。这反过来又减少了下载和解析这些文件所需的时间。此外,使用 Gzip 或 Brotli 等算法压缩文件可以进一步减小文件大小,从而加快加载时间。
优化图像以减小其文件大小,同时不牺牲质量。使用适当的格式(例如,JPEG 用于照片,PNG 用于具有透明度的图形,SVG 用于矢量图像),并确保图像被压缩以最小化其文件大小。此外,利用响应式图像,根据用户的设备和屏幕分辨率提供不同的图像大小。
将多个 CSS 和 JavaScript 文件合并成一个单独的包,以减少客户端发出的 HTTP 请求次数。这有助于提高页面加载时间,可以使用 webpack、Rollup 或 Parcel 等构建工具来实现。您还可以使用代码拆分技术将包拆分成更小的块,以便只加载特定页面或功能的必要代码。
就像在数据库和 API 层一样,我们可以利用缓存来优化 UI。为您的静态资产设置适当的缓存头,以便浏览器缓存这些文件,减少在后续访问时再次下载的需求。配置缓存控制头,如 Cache-Control 和 ETag,以确保高效的缓存行为。这可以减轻服务器的负载,并通过更快地交付资产来改善用户体验。
CDN 从地理位置分布的服务器上提供您的静态资产。通过从更靠近用户位置的服务器上提供资产来减少延迟。CDN 还有助于平衡服务器的负载,提高性能和可伸缩性。
实施最新的 HTTP 协议版本 HTTP/2,以实现客户端和服务器之间更快、更高效的通信。HTTP/2 提供了多路复用、头部压缩和服务器推送等好处,可以显著提高静态资产的加载和渲染速度。
优化静态资产并高效地打包它们可以对 UI 的性能产生巨大影响,并显著减轻(因此成本)云系统的负担。
实施渐进式加载和懒加载技术
在开始构建 UI 时,通常会简单地将用户需要用于某个页面的所有内容在初始页面加载时发送出去。这似乎一次解决了所有加载问题,并允许实现性能最高的 UI。但是,采取这种方法可能会消耗大量带宽并增加云系统的成本。渐进式和懒加载技术可以通过最小化最初加载的数据和资源量来帮助减轻这一问题,从而加快初始页面加载速度并减少服务器/云的带宽需求。
渐进式加载涉及分阶段加载内容,从低分辨率或简化版本开始,并在需要时逐渐用更高品质或更详细的版本替换它们。这种方法特别适用于图像和其他媒体,允许用户在内容完全加载之前开始与之交互。实现渐进式加载的一种方法是通过使用低质量图像占位符(LQIP)或模糊缩略图,当可用时用全分辨率图像替换。可能有些图像根本不需要加载全分辨率版本,最终减少带宽消耗并加快最终用户的 UI 加载速度。
相反,懒加载将非关键或屏幕外资源的加载推迟到需要时。这项技术减少了初始负载大小,从而加快了页面加载时间。对于图像和媒体,您可以通过为img和iframe元素使用loading="lazy"属性来在现代浏览器中启用原生的懒加载。如果原生懒加载不可用或您需要更多定制,您还可以使用 JavaScript 库(如 Intersection Observer API)实现自定义懒加载,该 API 可以检测元素何时在屏幕上可见,并在必要时加载它们。
除了图像和媒体之外,懒加载还可以应用于应用程序的其他部分,例如按需加载组件或模块。这对于具有众多功能或组件的大型应用程序尤其有益,因为它允许您在需要时仅加载应用程序的必要部分,从而减少初始加载时间和整体资源使用。
例如,在 Blazor WebAssembly 应用程序中,您可以使用内置的代码拆分和懒加载功能按需加载特定的组件或整个程序集。通过利用这项技术,您的应用程序可以变得更加模块化和高效,从而更容易在长期内进行扩展和维护。
在您的应用程序中实现渐进式加载和懒加载技术可以显著提高其性能、响应速度和整体用户体验。通过最小化最初加载的资源和数据,并专注于在需要时仅提供必要的内容,您可以确保用户获得流畅且快速的体验!
利用 UI 组件的缓存策略
再次强调,缓存是提高您的 SaaS 应用程序 UI 性能和响应速度的重要技术,尤其是在它扩展时。通过存储和重用之前获取或计算的数据,缓存减少了冗余请求的需求,减轻了服务器的负载,并改善了整体用户体验。
对于 UI 组件,最有效的缓存策略之一是客户端缓存。通过在浏览器缓存中存储频繁使用的数据或渲染的组件,您的应用程序可以快速访问这些信息,而无需额外的服务器请求。HTML5 本地存储和 IndexedDB 是可用于缓存数据的两种客户端存储机制。
另一种缓存技术涉及记忆化,这是一种基于函数调用输入参数缓存函数结果的策略。在 UI 组件的上下文中,记忆化可以用于缓存计算成本高或频繁执行函数的输出,减少冗余计算的需求。许多现代 UI 库,如 Blazor,都提供了内置的记忆化支持,这使得在您的应用程序中实现它变得更加容易。
当利用缓存策略时,在缓存数据以获得性能好处和确保数据保持新鲜和更新之间取得平衡至关重要。为了保持数据一致性,你应该实施缓存失效策略,在数据不再有效或底层数据发生变化时过期或更新缓存数据。一些缓存失效的方法包括为缓存数据设置过期时间,使用版本或时间戳来检测变化,以及监听指示数据更新的服务器端事件。
在分布式环境中,例如基于微服务的架构中,缓存也可以在服务器端实现。例如缓存 API 响应或使用分布式缓存,如 Redis 或 Memcached,可以帮助减少后端服务的负载,并提高应用程序的整体性能。在实现服务器端缓存时,务必考虑数据一致性、缓存一致性和容错性等因素。
缓存总是很难做对,当考虑到 UI 层的缓存时,这一点也不例外。为 UI 组件精心规划和实施缓存策略,考虑性能带来的好处以及缓存可能引入的潜在复杂性,这是至关重要的。通过选择合适的缓存技术,在性能和数据新鲜度之间取得平衡,你可以在扩展时显著提高 SaaS 应用程序的用户体验。记住要随着时间的推移监控和评估缓存策略的有效性,根据需要做出调整,以确保最佳性能和可伸缩性。
在你的 SaaS 应用程序中扩展 UI 层是确保随着应用程序的增长用户体验平滑和响应的关键方面。通过关注性能优化,高效管理和交付静态资产,实施渐进式和懒加载技术,以及利用 UI 组件的缓存策略,你可以在扩展以适应更多用户的同时显著提高应用程序的性能和响应性。
随着你的应用程序继续增长,持续监控和优化你的 UI 扩展策略以确保最佳性能和用户体验是至关重要的。记住,采用数据驱动的性能优化方法,分析用户反馈,并跟上最新的行业最佳实践将帮助你保持竞争优势,并为用户提供高质量的服务。通过深思熟虑地规划和执行 UI 扩展策略,你的 SaaS 应用程序将能够应对增长和扩张带来的挑战。
摘要
本章重点介绍使用微软技术构建的 SaaS 应用程序在规模扩展过程中面临的挑战和最佳实践。本章分为四个主要部分:一般概述、数据库扩展、API 扩展和 UI 扩展。
第一部分提供了关于在规模扩展过程中遇到的挑战的一般讨论,包括基础设施可扩展性、性能优化、安全性和合规性、可用性和正常运行时间,以及成本和资源管理。
第二部分涵盖了数据库扩展,包括分区、分片、存档和缓存等子部分。通过实施这些技术,你可以确保你的数据库能够处理增加的需求,并为你的应用程序提供可靠且高性能的数据访问。
第三部分涵盖了 API 扩展,包括负载均衡、微服务、缓存和监控等子部分。通过实施这些技术,你可以确保你的 API 能够处理增加的需求,并为你的应用程序提供一个可靠且高性能的数据访问层。
第四部分涵盖了 UI 扩展,包括性能优化、缓存、负载测试、用户体验优化、监控和自动化扩展以及安全考虑。通过实施这些技术,你可以确保即使在用户基础增长和需求增加的情况下,你的 UI 仍然保持高性能和响应性。
总结来说,在规模上运营 SaaS 应用程序会带来一些挑战,但通过实施适当的技术和最佳实践,你可以确保你的应用程序能够处理增加的需求,并为你的客户提供可靠且高性能的用户体验。
我们即将结束使用微软技术对 SaaS 应用程序的学习!在最后一章中,我们将回顾我们已经涵盖的内容,并总结我们的学习成果!
进一步阅读
-
设计和扩展 SaaS 软件时应了解的 36 件事:
medium.com/@mikesparr/things-i-wish-i-knew-when-starting-software-programming-3508aef0b257 -
可扩展性:
learn.microsoft.com/en-us/sql/relational-databases/in-memory-oltp/scalability?view=sql-server-ver16 -
API 管理实用指南:
www.softwareag.com/en_corporate/resources/api/guide/api-management.html -
ASP.NET Core Blazor 性能最佳实践:
learn.microsoft.com/en-us/aspnet/core/blazor/performance?view=aspnetcore-7.0
问题
-
扩展 SaaS 应用程序时面临的关键挑战是什么?
-
分片如何帮助提高数据库的可扩展性?
-
数据库的水平扩展和垂直扩展之间有什么区别?
-
实施速率限制和节流如何有助于 API 的可扩展性?
-
在 UI 扩展中,渐进式加载和懒加载技术的目的是什么?
-
缓存如何提高 UI 组件和后端服务的性能?
第五部分:总结思考
本节通过一个章节来结束本书,回顾我们所学的知识,并提供一些如何应用新获得知识的指导!
本节包含以下章节:
- 第十三章,总结
第十三章:总结
首先,恭喜你走到了这一步!在这本书中,我们涵盖了大量的内容!构建 SaaS 应用并不容易,而你能够走到这一步,已经展现了极大的奉献和毅力。我相信这本书中的章节对你来说既有趣又有启发性。我希望你现在已经对使用 Microsoft 技术构建企业级 SaaS 应用所必需的工具和技术有了坚实的理解。
随着我们接近 SaaS 旅程这一段的结束,承认并重视你在这方面的投入至关重要。通过参与这些材料,你是在投资自己,你所获得的知识不仅将帮助你构建强大且可扩展的 SaaS 应用,还将为你的职业成长和个人发展开辟新的机会。
在本章中,我们将涵盖三个主要主题,这些主题将帮助你巩固你的学习,并为你未来在 SaaS 开发领域的努力做好准备。我们将首先庆祝你的成就,并认可你在整本书中学到的技能。接下来,我们将回顾每一章最重要的教训,强调这些新技能在 SaaS 领域的价值。然后,我们将讨论你如何在现实世界项目中利用你的专业知识,并概述可能的职业发展。最后,我们将探索你持续学习和成长的下一步,为你提供资源和技巧,以保持对不断发展的 SaaS 行业的了解和联系。
让我们深入探讨,了解如何最大限度地利用你新获得技能和知识!
干得好!
再次恭喜你来到这本书的最后一章。写这本书对我来说非常有趣,我希望它对你,作为读者,也是一个既有趣又有信息量的旅程。
在本节中,我们将花一些时间来认可通过这本书的学习所取得的成就。
学习的承诺
通过拿起这本书,并逐章学习,你通过投入时间和精力去理解复杂的概念,并使用所展示的各种 Microsoft 技术和工具来应用这些概念,展现了强烈的求知承诺。这种对自我提升的承诺值得高度赞扬,并且使你在众多人中脱颖而出,花时间学习和构建作为 SaaS 开发者的技能集。这种终身学习的承诺对于开发者来说是一个极其重要且极具价值的技能,可以帮助他们保持领先,并在不断变化且竞争激烈的软件开发世界中保持有价值的资产。
除了对终身学习的承诺和提升你的技术技能之外,你还展现了对成长心态的承诺,这在 SaaS 开发这样多元、广阔且快速发展的领域中是无价的。拥有这种心态将帮助你适应新技术和方法,并允许你将挑战转化为成长的机会。随着你职业生涯的进步,这种学习和适应的意愿将成为你最大的资产,确保你始终能够保持相关性并在你的职业中出类拔萃!
掌握 SaaS 开发技能
对终身学习的承诺本身就是一件好事,但除此之外,你在 SaaS 开发方面也奠定了非常坚实的基础。我们研究了具体的技术,例如 Docker、C# WebAPI、Blazor、SQL Server 和 Entity Framework。我们还探讨了使用这些技术的最佳实践,例如 RESTful API,并覆盖了一些 SaaS 特定的挑战,如多租户、微服务、身份验证和授权。这些技能不仅会使你成为一个更灵活、更有效的开发者,还会提高你在日益重要的 SaaS 开发领域参与和领导项目的能力。
除去你已掌握的技术技能,你还对支配 SaaS 开发的底层原理有了更深入的理解。这种理解使你能在设计和实施解决方案时做出明智的决策,确保它们可扩展、可维护且安全。随着你未来项目的开展,你将能够运用这些知识构建更高效、更健壮的应用程序,这最终将导致更好的用户体验和更高的客户满意度。重要的不仅仅是工具和技术,还有那些使你成为杰出 SaaS 开发者的原则和实践!
遵循行业最佳实践
这本书的一个重要收获是能够在你的 SaaS 项目中学习和实施行业最佳实践。通过整合有效的测试策略、监控、日志记录、性能优化、身份验证和授权,你已经成为一个更全面、更优秀的开发者,而不仅仅局限于 SaaS 的特定焦点。掌握多租户和微服务等高级概念将使你在许多年内都受益匪浅,而了解 CI/CD 在软件工程中具有普遍适用性。
在一个不断发展的行业中,跟上最佳实践对于长期成功至关重要。你适应并整合这些实践到工作中的能力证明了你对为用户提供最佳解决方案的承诺。随着你作为开发者的成长,继续寻找并拥抱可以帮助你优化 SaaS 应用程序的新方法、工具和技术。对卓越的追求不仅会导致更高质量的软件,还会使你成为一个具有敏锐理解当前和新兴趋势的前瞻性开发者。
个人和职业成长
完成这本书无疑对你的个人和职业成长做出了贡献。你所获得的技能和知识提高了你作为开发者的市场价值,而你建立起的信心将帮助你应对更复杂的 SaaS 项目。此外,你建立的基础为你在这个行业中承担领导角色或专业职位奠定了基础,为职业发展开辟了新的机会。通过持续学习和成长,你将准备好在 SaaS 开发世界中产生有意义的积极影响。
然而,通过完成这本书所取得的个人和职业成长只是你旅程的开始!随着你继续学习、磨练技能并接受新的挑战,你会发现自己在成为一名开发者之外,还成为了一个领导者和其他人的导师。拥抱这种成长,并积极寻找分享知识、与同行合作以及在软件开发社区中做出贡献的机会。通过这样做,你不仅会提升自己的职业生涯,还会为 SaaS 开发世界的整体进步和创新做出贡献。
你学到了什么?
在这本书的整个过程中,你已经获得了关于 SaaS 开发世界的宝贵知识和洞察。让我们花一点时间回顾一下你所学到的最重要的概念和技能。
理解 SaaS 是你旅程的起点。通过掌握 SaaS 的基本原理、其优势和架构,你已经建立了一个强大的基础,这让你能够应对 SaaS 开发的复杂性。你学习了 SaaS 应用程序如何为各种规模的企业提供成本效益高、可扩展和易于访问的解决方案,使它们在现代软件景观中成为一个有吸引力的选择。
认识到 SaaS 在当代软件生态系统中的重要性,让你能够欣赏到对熟练的 SaaS 开发者的日益增长的需求。随着越来越多的组织采用基于云的服务和订阅模式,你对 SaaS 核心原则的理解将使你在构建和部署应用程序时做出明智的决定,确保你能提供最佳解决方案来满足当今用户的需求。
随着你在这本书中的学习进展,你更深入地研究了多租户和可扩展性等基本概念,这些在 SaaS 应用的成功中起着至关重要的作用。你理解了多租户的概念,即允许多个客户端使用单个应用实例同时保持数据隔离。了解多租户在 SaaS 应用中的重要性,为你提供了宝贵的见解,帮助你设计和构建满足不同客户和行业独特需求的解决方案。
你已经学习了构建可扩展和数据丰富应用的战略,确保你的 SaaS 解决方案能够随着用户需求的增长而发展。在这个过程中,你熟悉了数据库和 Entity Framework,这是一个简化数据访问并允许你更高效地与数据库工作的基本工具。此外,你还探讨了微服务的重要性,这是一种促进模块化、独立部署服务开发的架构模式。通过采用微服务,你可以创建更易于维护、可扩展和有弹性的 SaaS 应用,为用户提供更好的体验,同时简化软件的持续管理。
当你涉足 SaaS 应用的前端开发领域时,你发现了创建直观和用户友好界面的重要性。一个设计良好的用户界面不仅能提升用户体验,还能促进你的 SaaS 解决方案的整体成功和普及。在这个背景下,你学习了 Blazor,这是一个使用 C#构建交互式 Web 应用的有力框架。通过利用 Blazor,你可以创建一个无缝且一致的开发体验,在前端和后端使用相同的语言和工具。
除了前端开发,你还深入研究了认证和授权的关键方面。在 SaaS 的世界里,确保用户数据的安全和维持适当的访问控制至关重要。你了解了各种认证机制,例如 OAuth 和 OpenID Connect,这些机制能够实现安全的登录过程并帮助保护用户数据。通过实施强大的授权策略,你可以确保用户只能访问与其角色和权限相关的资源和操作。
通过掌握这些概念和工具,你已经获得了创建强大、安全且视觉上吸引人的 SaaS 应用所需的必要技能。将前端开发技术与你对后端技术的了解相结合,你现在已经具备了构建和部署满足广泛客户和行业需求的全面 SaaS 解决方案的能力。
在这本书的整个过程中,你还在关键软件工程实践中获得了洞察,这些实践超越了 SaaS 开发的特定领域。这些实践对于交付高质量的软件至关重要,并且可以应用于各种类型的项目和领域。
测试是这种基本实践之一,你已经学会了在整个堆栈中进行彻底测试的重要性,以确保软件的可靠性和正确性。通过采用各种测试策略,如单元测试、集成测试和端到端测试,你可以验证应用程序的功能,并在开发过程中早期识别问题。
监控和日志记录是维护和排除软件故障的关键组成部分。通过将有效的监控和日志记录解决方案集成到你的 SaaS 应用程序中,你可以快速识别并解决性能瓶颈、错误和其他潜在问题。这些技术使你能够主动管理你的应用程序,最小化停机时间,并为你的用户提供一致、高质量的体验。
你还探讨了持续集成和持续部署(CI/CD)的概念,这促进了“频繁发布,尽早发布”的方法。CI/CD 管道自动化了构建、测试和部署应用程序的过程,减少了人工干预,并提高了软件发布的整体质量和速度。
最后,你了解了在规模上运营的挑战,因为 SaaS 应用程序通常需要服务于不断增长的用户数量并处理越来越多的数据。通过理解可扩展性原则并实施管理增长痛点的策略,你可以确保你的 SaaS 解决方案即使在扩展以满足更大用户群的需求时,也能保持性能、弹性和可靠性。
通过掌握这些通用的软件工程实践,你已经变成了一位更加灵活和高效的开发者,能够交付高质量、可扩展和可维护的软件解决方案,以满足当今用户的需求。
你如何使用这些技能?
拥有 SaaS 开发和通用软件工程实践的广泛技能和知识,你现在准备将这些能力应用于各种情境。无论你是寻求改进你的当前项目、探索新的机会,还是在开发社区中做出贡献,这些技能为你打开了众多可能性,让你在 SaaS 世界及其之外产生重大影响。在本节中,我们将讨论各种利用和利用你在本书中学到的专业知识的方法。
将你的知识应用于你的当前工作或项目
这在我看来是你开始将这本书中学到的知识付诸实践的最明显和最简单的方式。当然,如果你的当前雇主正在开发一个 SaaS 应用程序,你现在可以自信地承担该应用程序开发中的强大技术角色,那将是理想的。当然,情况可能并非如此。然而,你在本书中学到的许多技能都可以应用于任何软件工程项目。你可能可以利用从第九章学到的技能开始构建一些额外的自动化测试,或者通过构建或改进我们在第十一章中学到的 CI/CD 管道来给你的团队留下深刻印象。
更普遍地说,你现在可能能够改进你团队中的一些现有实践。优化围绕软件开发实际工作的流程本身是一项技能,而你从这本书中学到的知识应该能够很好地服务于这项努力,如果你选择去尝试的话。
我相信这本书中分享的见解将为你提供执行 SaaS 项目所需的所有必要技能。我也希望书中涵盖了足够的一般性良好建议,这样你就可以开始立即将至少一些这些技能付诸实践,并通过对我们所涵盖主题的实际应用来加深你的知识和理解。
自由职业或咨询机会
软件工程领域广阔而多样,虽然你可能会立即开始在你当前的工作场所使用你新获得的知识,但这并不适用于每个人!
另一个你可以探索的新领域,是利用你新获得的 SaaS 开发专业知识进入自由职业或咨询行业。许多企业正在寻找有技能的专业人士来帮助他们开发、维护或改进他们的基于云的解决方案。作为一名自由职业开发者或顾问,你可以根据客户的需求,以项目为基础提供你的服务,或者提供持续的支持。
通过帮助企业过渡到基于云的解决方案,你可以在他们的数字化转型旅程中扮演关键角色。你的 SaaS 开发知识,加上你对行业最佳实践和软件工程原则的理解,可以使你能够为客户提供有价值的指导。你可以帮助他们优化现有的应用程序,识别创新的机会,简化他们的开发流程,同时提供能够改善他们财务状况的实质性成果。
作为一名自由职业者或顾问,你也有机会建立一个多元化的作品集,与各种客户合作,并应对新的挑战。这可以是一个极好的方式来进一步扩展你的技能组合,接触不同的行业和技术,并对企业和他们的客户产生有意义的积极影响。
构建自己的 SaaS 产品或初创公司
自由职业是一种很好的方式来提升你的技能并为各种项目做出贡献。然而,你也可能想要考虑将你的想法扩展出去。启动一家初创公司无疑是一种具有挑战性的方法!初创公司需要大量的工作,所需的技能组合远远超出了开发 SaaS 应用程序的技术能力。然而,沿着这条路走下去的潜在回报也是巨大的。
如果你确实决定走这条路,首先确定一个你的 SaaS 解决方案可以解决的利基市场或问题。这可能是特定行业中的痛点,或者是企业和用户共同面临的更普遍的挑战。通过专注于独特而有价值的提议,你可以创造一个在市场上脱颖而出的产品。一条非常好的、非常常见的建议是“挠自己的痒处”,我的意思是构建一些可以解决你自己的问题的事物。这样,你可以成为你自己的首要客户,并利用你对问题领域的洞察力为你的后续客户制作出优秀的产品!
作为一家 SaaS 初创公司的创始人,你需要扮演许多角色。你的责任不仅包括创建应用程序的技术方面,还要涉及制定商业模式、定价策略和上市计划。此外,你还需要监督产品开发、市场营销、销售和客户成功。这些技能并不总是与软件工程师相关联!不要害怕在创业旅程中引入他人来填补这些空缺。
发展自己的 SaaS 业务可能既是挑战也是回报。在你导航创业的起伏过程中,你将有机会从你的经验中学习,适应不断变化的市场条件,并组建一个与你共享愿景的团队。通过应用你在本书中学到的技能,你可以创造一个成功的 SaaS 产品,它不仅能解决一个关键问题,还能产生可持续的收入和增长。
参与开源项目
自由职业和启动初创公司都是开始使用你的 SaaS 技能的绝佳方式,但无疑都需要大量的工作。我们中的许多人生活忙碌,根本无法抽出时间来从事这样的任务。另一种与 SaaS 应用程序合作的方式是参与开源项目。
参与开源项目可以让你在回馈社区的同时提升自己的技能。通过分享你的知识和专业知识,你可以帮助改进现有项目,并在软件行业中推动创新。开源项目通常拥有多元化和欢迎的社区,在那里你可以找到指导、支持和友谊。
与开源项目中的其他开发者合作也提供了一个宝贵的从他们的专业知识中学习的机会。你可以接触到新技术、编程语言或框架,以及不同的编码风格和最佳实践。这可以显著拓宽你的视野并进一步磨练你的开发技能。
参与开源项目也有其他好处,例如构建你的作品集、扩展你的专业网络,甚至可能带来工作机会。通过投入一些你的时间和精力来为这些项目做出贡献,你可以在同时推进你职业生涯的同时,对软件开发社区产生持久的影响。
关注行业趋势和最佳实践
通过简单地拿起并阅读这本书,你已经展示了你维护你的技能集并跟上最新技术进步和行业最佳实践的承诺。在软件工程领域,这种追求是一个终身的事业,不会随着我们接近这本书的结尾而结束。总有更多东西要学习,我鼓励你继续你的学习和成长之旅,超越这些页面。跟上行业趋势和最佳实践的一个最好的方法是参加会议、研讨会和网络研讨会。这些活动为你提供了从专家那里学习、发现新工具和技术以及与其他领域的专业人士建立联系的机会。通过积极参与这些聚会,你可以确保你的知识和技能在软件开发的不断变化的世界中保持相关。通常,这些会议也在寻找演讲者。你可以通过自愿在会议上发言来挑战自己!
此外,养成关注行业领袖、博客和社交媒体渠道的习惯。这将帮助你了解新兴趋势、创新解决方案以及 SaaS 和软件工程领域的思想领袖的最新见解。
对于任何软件专业人士来说,关注行业最新发展都很重要。通过投入时间和精力进行持续学习和成长,你可以保持你的技能敏锐,并更好地准备适应软件工程和 SaaS 开发的不断变化的环境。
指导他人并教授他们
最后,保持技能敏锐的一个非常有回报的方法是指导他人并教授他们。分享你的知识和专业知识不仅帮助他人在其职业生涯中成长,而且也加强了你对概念和实践的理解。教学可以是一个极好的反思你自己的经验、识别改进领域并保持与 SaaS 开发和软件工程基础紧密联系的方式。
考虑与工作中的同事或在在线社区,如论坛、社交媒体群组或 Stack Overflow 等平台分享你的知识。通过提供指导、回答问题或提供反馈,你可以为开发社区的共同成长做出贡献,并在你的领域内建立自己作为受信任的专家。
此外,你可以提供研讨会和培训课程,或开发教育内容,如博客文章、文章或视频教程。这不仅有助于他人从你的经验中学习,而且还能让你磨练沟通和演讲技巧,这在任何专业环境中都非常有价值。
指导和教授他人可以是一项充实且互利共赢的事业。通过分享你的专业知识并支持他人的成长,你可以在软件工程领域产生持久的影响,同时也能保持你的技能敏锐和与时俱进。
接下来要做什么
现在你已经完成了这本书,并开始考虑作为 SaaS 开发者旅程中的下一步,让我们考虑一些具体的方法,你可以通过这些方法扩展你的技能、专业知识和专业网络。
作为开发者,我们首先必须做的是始终保持技术敏锐。深入特定技术或领域,如 C#、Entity Framework 或 Blazor,的专长总是一个好主意。这将使你能够应对更复杂的项目和挑战,甚至可能为开源项目做出贡献或创建教育内容。此外,你可能还想探索替代数据库、架构或云平台,以扩展你的知识并多样化你的技能组合。
在加强你的技术专长的同时,探索相关学科,如 DevOps、数据科学或机器学习,同样重要。在这些领域以及其他编程语言或框架中获得熟练程度,将使你成为一个更全能的开发者,开辟新的机会和挑战。获得相关的认证,如微软认证解决方案开发者(MCSD)、Azure 开发者或 Azure 解决方案架构师认证,可以进一步增强你在该领域的信誉和市场竞争力。
与其他专业人士建立联系对于保持与行业的联系并从同行的经验中学习至关重要。参与当地聚会、用户组或在线社区,以及参加行业活动、会议和研讨会,可以帮助你深化你的专业知识,同时使你能够与更广泛的社区分享你的知识和经验。这可能包括创建文章、博客文章或视频内容,这不仅使你成为 SaaS 开发的专家,而且对领域内的其他人也有益。
最后,设定个人和职业目标是指导你的职业道路并保持方向感的关键。通过确定短期和长期目标,你可以持续评估你的进度并根据需要调整你的计划。通过积极探索这些机会并致力于持续成长,你可以确保你在 SaaS 开发世界以及更广阔的领域中的成功。
摘要
本章总结了我们在本书中涵盖的所有内容,并展望了你现在作为 SaaS 开发者可能面临的未来。我们探讨了开发 SaaS 应用程序的许多方面,包括多租户、微服务、UI 设计、测试、CI/CD 等!我们还探讨了你可以继续扩展你的技能、专业知识和专业网络的许多途径。从深化你在特定技术方面的专业知识、探索相关学科和获得相关认证,到与其他专业人士建立联系、分享你的知识以及设定个人和职业目标,你有许多机会可以追求。
当你开始这段激动人心的旅程时,请记住要保持好奇心、对新挑战持开放态度,并适应软件开发的不断变化的世界。你从本书中获得的知识和经验将成为你未来项目的坚实基础,无论这些项目涉及自由职业工作、为开源项目做出贡献、启动初创公司还是指导他人。
没有更多的话可说了!我衷心感谢你拿起这本书,并抽出时间把它读完到最后一页。我希望这已经是一次有趣且富有信息量的阅读。我也非常希望这将成为你在 SaaS 开发世界中可以现在开始着手进行的多项项目的起点。
在你所有的未来努力中祝你好运!


浙公网安备 33010602011771号