为什么推荐你用 C# 构建大型后端应用?(二)

语言: CN / TW / HK

不畏浮云遮望眼,只缘身在此山中。--王安石《登飞来峰》

笔者在上一篇 《为什么推荐你用 C# 构建大型后端应用?- Part 1》 展示了 C# 的受欢迎程度,并介绍了一些 C# 特有的语法特性,并以此为引子分析 C# 对于构建后端应用的优势。在本篇文章中,我们将继续探究 C# 的其他一些特性,进一步展示用 C# 做后端开发的优势和适用场景。本篇文章是 C# 系列文章的第二篇,我们将以 C# 跨平台框架 .NET Core 为例,介绍如何用 C# 编写大型后端应用项目。

.NET Core 简介

在前一篇文章我们提到,C# 是 .NET 的主要支持语言,.NET 是微软开发并开源的平台开发框架,跟 Java 的 Spring 类似,是非常全面的软件开发框架,能够开发 Web 应用、API、云函数、移动端等,集成了开发者所需要的各种套件。而 .NET 之前一直为人诟病的原因之一就是它只支持在 Windows 操作系统上部署,这为普及 C# 和 .NET 带来了麻烦。所幸的是,微软在 2016 年发布了 .NET Core,支持跨平台运行 C# 应用,不仅能在 Windows 上部署,还可以在 MacOS 和 Linux 上运行 C# 开发的程序。.NET Core 的发布,让 C# 从 Windows 独立出来,成为不受操作系统限制的企业级编程语言,让其能够与基于 JVM 跨平台运行的 Java 平起平坐。甚至,有很多地方 C# 比 Java 更有优势,例如语法特性方面。

现在很多企业都已经采用 .NET Core 来作为后端开发框架,因为它足够简单、全面,很多功能模块能开箱即用,还能够部署在 CentOS、Ubuntu 等操作系统,或以容器部署于分布式集群中。随着容器编排技术、微服务等技术的发展,.NET Core 作为一个现代后端开发框架,可以被用于开发可扩展的、易维护的大型后端系统,而且相应的生态技术也能够让其适用于更多的业务场景。

后端框架概览

前后端分离是现代 Web 应用的主流架构设计,前端一般是 React、Vue 等前端框架开发的单页应用(SPA),而后端一般是基于 HTTP 的 RESTful 风格 API,前后端数据交互通过 AJAX 请求来完成。而后端 API 开发正是 .NET Core 擅长的应用领域。

下面是 Web API 开发中常规的模块或功能,这里只列出了后端应用常用的模块功能。

  • 路由

  • 中间件

  • 鉴权/授权

  • 数据库操作

  • 配置

  • 依赖注入

  • 日志

  • 错误处理

  • 单元测试

  • RPC

可以看到,成熟后端 Web 框架还是需要涵盖不少内容的。而 .NET Core 作为现代化后端框架,包含所有这些常规的模块和功能。因此,如果直接采用 .NET Core 来开发后端 Web API,会简化不少搭建工作,因为这些功能都能开箱即用,很容易被应用到项目中来。另外,微软为 .NET Core 编写了非常详尽的文档,对于首次接触或不熟悉 .NET Core 的入门开发者来说,可以通过阅读相关的文档解释说明,来帮助自己迅速掌握基础知识和相关 API,以加速入门使用 .NET Core 开发后端 API 应用。

其他编程语言的 Web 框架(例如 Spring Boot、Gin)也包含类似的功能模块,一些基础模块是非常类似的。不过,笔者认为 .NET Core 在某些方面有提升开发体验的一定优势,例如数据库操作、鉴权/授权、依赖注入等。本篇文章将就这些模块来介绍 .NET Core 的主要特性。

下面,笔者将着重介绍 .NET Core 用于开发后端 API 的这些主要特性。

.NET Core 主要特性

这里我们主要介绍一些 .NET Core 中对开发者比较友好的特性,同时也是提升开发体验和效率的主要特性。我们将略过路由、中间件、单元测试等常规特性,这些各个框架都大同小异,大家可以参考官方文档来查看详情。

数据库操作

.NET Core 主要使用的数据库操作框架是 「Entity Framework」 (简称 EF,中文名为 实体框架 )。EF 是 C# 专属现代化 ORM 框架,主要用于操作关系型数据库中的数据、表、列、字段等数据库对象。Entity Framework 可以配合 LINQ(一种类似 SQL 的查询语言,在前一篇文章 《为什么推荐你用 C# 构建大型后端应用?- Part 1》 详细介绍过)在代码中轻松完成 ORM 查询和操作。

下面是一个 EF 简单的查询语言例子。

using (var context = new BloggingContext())
{
var blogs = context.Blogs
.Where(b => b.Url.Contains("dotnet"))
.ToList();
}

其中 BloggingContext 是 EF 中对应的数据库上下文类,其中包含了相关的数据库表、列、字段、索引、外键等信息,可以直接在 C# 代码中对其进行操作。这里 context.Blogs 是代表博客文章对应的表, Where 表示对该表进行筛选,其中用 b => b.Url.Contains("dotnet") 这样的匿名函数来定义筛选条件,函数参数 b 是表中每一行记录,其包含 Url 这个字段。

当然,Entity Framework 的查询能力远不限于此,它能够做 SQL 能做的几乎所有事情。

下面是一个包含 JOIN 相对复杂的 LINQ 查询语句。

var query = from photo in context.Set<PersonPhoto>()
join person in context.Set<Person>()
on new { Id = (int?)photo.PersonPhotoId, photo.Caption }
equals new { Id = person.PhotoId, Caption = "SN" }
select new { person, photo };

在这个查询语句里, PersonPhotoPerson 这两个表在 PersonPhotoIdPhotoId 这两个字段上进行了 JOIN,而且还筛选出 Caption 等于 SN 的记录,最后将 personphoto 这两个模型输出出来。最后的结果是一个列表,列表项包含 personphoto 这两个模型。

可以看到, 「在 EF 中配合 LINQ 对关系型数据库进行查询操作是非常简单的,就跟写 SQL 一样简单」 。而且, 「我们在 C# 代码中用 ORM 的方式操作数据库的主要目的是为了约束变量类型,以静态类型的方式保证代码的健壮性」

对于一个通用的 Web API 来说,增删改查(CURD)模块几乎是不可或缺的,前面介绍了 EF 在查询方面的便捷性,现在我们介绍 EF 其他的数据操作方式,也就是增、删、改。

下面是 EF 中简单的增删改例子。

// 添加
using (var context = new BloggingContext())
{
var blog = new Blog { Url = "http://example.com" };
context.Blogs.Add(blog);
context.SaveChanges();
}

// 删除
using (var context = new BloggingContext())
{
var blog = context.Blogs.First();
context.Blogs.Remove(blog);
context.SaveChanges();
}

// 更新
using (var context = new BloggingContext())
{
var blog = context.Blogs.First();
blog.Url = "http://example.com/blog";
context.SaveChanges();
}

上面的增删改操作是通过 context.BlogsAddRemove 以及属性赋值( blog.Url = ... )来实现的,相对来说也非常简洁。

语法一致性

请注意,上面提到的这些是 C# 中 ICollection 接口以及类型实例的基础语法,没有任何附加的东西,也就是说,Entity Framework 是完全兼容 C# 基础类型的,具有很强的 「语法一致性」

为什么说语法类型一致性是提升开发体验的重要特性呢?第一,从大脑工作的角度来看,这让开发者减少了不必要的工作记忆开销,因为你不需要更多的工作记忆区域(例如新的语法知识)来协助完成工作,因而能提高开发效率,避免工作记忆切换带来的效率问题;第二,语法一致性让开发者能够更合理的规范代码,能够利用基础语法中的编程模式来规范 ORM 中的代码;第三,学习成本降低,因为开发者不需要学习新的知识。

相反,如果你去学习 Java 的 MyBatis、JPA 这样的 ORM 框架,需要掌握很多额外的知识,才能有效完成数据库的增删改查操作。

笔者推荐用 C# 写后端应用的其中一个重要原因就是 Entity Framework。在 .NET Core 中用 EF 操作数据库会很好的满足语法一致性,提高开发体验以及工作效率。

依赖注入

.NET Core 跟其他大型后端框架一样,也有 「依赖注入」 (Dependency Injection,简称 DI)的功能。什么是依赖注入?其实依赖注入不是一个新的概念,是软件工程中的一个设计模式,主要用于降低模块间依赖关系的复杂度,达到 「解耦」 的目的。简单来说,依赖注入允许开发者将所有依赖都 “注入”(也可以叫注册)到容器里,如果需要某个依赖直接通过类型、接口引用即可,完全不用开发者考虑各模块之间的依赖关系,达到自动加载的目的。这在面向对象编程(OOP)语言中是非常有用的。

其实依赖注入不仅限于 OOP。例如前端框架 Angular 就是以依赖注入为模块化的核心功能,在 Go 语言中也有依赖注入的框架。读者可以参考微软关于依赖注入的 官方文档 [1] 来对其概念进行深入了解。

.NET Core 中提倡用依赖注入的方式规范项目结构。对于大型项目来说,模块化、分层是很重要的,这个可以通过依赖注入来做到。

下面是一个利用依赖注入将负责处理 HTTP 请求响应的控制器层与负责实现核心业务逻辑的服务层分层的例子。

我们首先定义好模型、服务接口、服务类。

// ./Interfaces/IUserService.cs
...
namespace DotNetExamples.Interfaces
{
public interface IUserService
{
IEnumerable<User> GetUserList();
...
}
}

// ./Services/UserService.cs
...
namespace DotNetExamples.Services
{
public class UserService : IUserService
{
public IEnumerable<User> GetUserList()
{
// 获取数据的核心代码
...
}
...
}
}

然后,我们在启动代码中 Startup.cs 注册这个服务。

// ./Startup.cs
...
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();

// 注册服务
services.AddScoped<IUserService, UserService>();
}
...

其中, services.AddScoped<IUserService, UserService>(); 就是注册用户服务的代码。 AddScoped 是指该依赖的生命周期为 scoped ,也就是在只存在于单次上下文中。.NET Core 一个好处就是可以通过配置依赖的生命周期来进行性能调优。关于依赖注入生命周期这里限于篇幅原因不打算详细介绍,感兴趣的读者可以到微软 .NET Core 官网查看。

然后,我们就可以在控制器代码里通过接口来引用服务了。下面是控制器 UserController 的代码。

// ./Controllers/UserController.cs
...
namespace DotNetExamples.Controllers
{
[ApiController]
[Route("[controller]")]
public class UserController : ControllerBase
{
// 声明依赖
private readonly IUserService _userService;

public UserController(IUserService userService)
{
// 注入依赖
_userService = userService;
}

[HttpGet]
public IEnumerable<User> GetList()
{
// 调用依赖
return _userService.GetUserList();
}
}
}

可以看到,相关的依赖 IUserService 是通过构造函数的形式来注入的。只需要声明私有变量,然后在构造函数中将依赖注入进去,从而忽略了依赖与依赖之间的细节,直接引用即可,非常直观方便。

如果您仔细看上面的例子,可以看到控制器 UserController 只是起声明路由的作用,其核心获取数据的代码在被注入的 UserService 类型的依赖 _userServiceGetUserList 方法中。这样设计,就轻松将负责 HTTP 数据交互的 Controller 层与负责实际业务逻辑的 Service 层剥离开来,各司其职,以实现软件工程中关于 「低耦合」 的设计理念。

本篇文章主要通过介绍 C# 跨平台框架 .NET Core 的其中两个主要特性,来帮助读者理解用 C# 开发后端项目的优势。其中,Entity Framework 因为其语法一致性以及配合 LINQ 加持的语法简洁性,使得用 .NET Core 编写 CURD 应用变得非常轻松和灵活。另外,我们还介绍了 .NET Core 的依赖注入,它让项目代码分层、模块化以及实现低耦合设计变得更容易,Controller 层和 Service 层可以相互分开而不受影响,这增强了可维护性以及可扩展性。

本篇文章是 C# 系列文章的第二篇,在后面笔者将介绍更多 C# 构建大型后端应用的实践内容,以帮助感兴趣的读者更能了解和熟悉 C#,帮助大家拓宽知识面以及增加软件工程特别是后端开发中的知识广度,让开发者在后端编程语言中能够有更多的选择,例如 C#。

Reference

[1]

官方文档: https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/dependency-injection