写在前面
有段日子没有来更新博客了,笔记积攒了蛮多。工作模式调整了一下之后,时间富裕了不少。
之后会分几个具体的系列把最近半年的一些实践经验和有趣的库进行分享。
这篇算是个关于.Net 6系列的引子,作为未来的长期版本,也是跨平台全栈的重要版本, .Net 6在继承了 .Net Core发展路线里的各个重要功能的同时,也实现了更轻巧的语法、更高的性能,并且自.Net5起又回归的Winform和WPF(仅Windows平台),以及MAUI(还未正式发布),各位坚守在.NET阵营的小伙伴们有了继续坚持的动力。此刻我只想惊呼 微软牛逼!
摘要
本篇主要作为Start up类的指南,入门级别的说明一下在.Net 6下构建Web API的过程。原本不在计划中,但是临时写了个测试,糅杂了 webAPI、IConfiguration、Swagger配置、EFCore和Microsoft.Extensions.DependencyInjection的内容,因此还是打算简单写一下作为开篇。
作为一个入门级别的Demo,我的实际工程还非常简单,后续在一个个人项目的构建时,会继续完善本篇。
我的场景非常简单,使用appsettings.json 作为系统配置,EFCore作为基本的数据访问,实现了一组自定义的Controller,分别使用DI注入EFCore和常规的 new DbContext方式调用db。
在之后的推进中,这个基本框架将逐步演进为一个完整的、工程化的Web API,用来完成我自己的一套小工具。
Before Start
在开始之前,希望您已经准备好一个测试用的 MSSQL Server 环境(SqlExpress或Mysql亦可);并且已经配置好具备ASP.NetCore功能的VS环境。我的环境是VS 2022社区版。
我的这篇文章毕竟是以记录过程为主,并非事无巨细的帮助文档。因此我提议,在继续之前,或在阅读过程中有任何疑问,请首先阅读官方的教程。本篇文章保证一定不会比官方更详细。
作为没有大量写代码的人来说,不保证本文所有内容都高度准确无错。因此如果有我理解错误,或者表述不准确、错误,敬请留言告知,十分感谢。
Step 1, 新建项目
无它,项目类型选正确即可。
当然,第一次进行Web开发的话,注意在项目模板的检索条件处选择 Web。
Web相关的模板很多,取决于你需要的项目类型。当然对于纯API开发而言,我更建议直接选择 ASP.Net Core Web API应用。
有几个长得比较像的选项,选择的时候注意一下:
- ASP.Net Core Web 应用 // 默认的Web项目模板,包含Razor示例
- ASP.Net Core 空 // 空的Web项目模板,适合老手
- ASP.Net Core Web 应用(模型-视图-控制器) // ASP.NetCore MVC, 写过ASP.Net MVC的话一定不陌生
- ASP.Net Core Web API // 主角登场
后续的设置按需即可,程序框架如果是VS 2022的话会默认为 .Net 6.0,当然也不建议选择其他。 6.0提出了最小API的功能,并且在项目启动器(Program.cs)的语法和既往完全不同。
【注】关于最小API的说明,请移步官方文档: https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/minimal-apis?ocid=_reactor_nov_shanghai_social_organic_youtube_dotnet-klo&view=aspnetcore-6.0
在一个Minimal API 中,最少可以通过4行代码完成一个API功能:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
最小API的应用场景应该说比较丰富,参照官方说明:“构建最小 API,以创建具有最小依赖项的 HTTP API。 它们非常适合于需要在 ASP.NET Core 中仅包括最少文件、功能和依赖项的微服务和应用。”, 可以想象在一些IoT设备中,Minimal API 的意义可以说是非凡的。
那么回到我们自己的项目,会发现整个Program.cs 的代码并没有像从前,通过一个Program 或者App 的 Class中的Main函数作为入口,甚至不包括命名空间的定义。基于以上代码,可以看出来整个程序由一个 WebApplication的实例(即app对象)负责完成配置、依赖注入和启动。
Step2, 引入依赖包
根据我在摘要里描述的架构,我们需要引入如下几个Nuget Package: (以下几个包直接通过命令行添加即可,相关依赖会自行加载)
- Microsoft.EntityFrameworkCore.SqlServer // 如果你使用其他数据库,确定Nuget中存在对应的Provider包。
- Microsoft.Extensions.DependencyInjection
- Microsoft.Extensions.Options.ConfigurationExtension
- Microsoft.Extensions.Configuration.Json
完成上述依赖的添加后,基本上就满足构建API的所有准备了。
Step3,添加中间件
如果做过 ASP.Net Core 2/3.1 开发的话,对于中间件和ServiceProvider应该非常熟悉了。在最新版本里,同样采用的是中间件注册的方式添加诸如配置管理、Logger、Swagger和一些功能性组件。
在本例中,IServiceCollection是属于 WebApplicationBuilder 对象的,因此在CreateBuilder后分别进行添加即可。
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
//builder.Services.AddSwaggerGen();
// Overwrite Swagger Configs
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo
{
Version = "v1.1.0302",
Title =" MiniAPI"
});
});
// Add IConfiguration
IConfiguration configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json",false,true)
.AddEnvironmentVariables()
.Build();
builder.Configuration.AddConfiguration(configuration);
//builder.Services.AddSingleton<IConfiguration>(configuration);
var connStr = configuration.GetConnectionString("DefaultConnection");
// Add EFCore
builder.Services.AddDbContext<CoreContext>(
options => options.UseSqlServer(connStr));
var app = builder.Build();
如果需要注册的Service比较多(比如包含了业务Service的注入) 也可以自己实现一个方法进行注册,此处不赘述。
使用ServiceCollection需要注意,ServiceCollection是需要注入的依赖的全局管理器,而各个依赖自身的生命周期需要显式指定,一种方法是在各个中间件的 AddXXX方法的参数中指定(有些中间件并不提供相关参数,实际可能是仅能以单例或按需时调用),或者使用ServiceCollection 的 AddSinglton
例如,对于EFCore的注入,官方的方法签名如下:
//
// 摘要:
// Registers the given context as a service in the Microsoft.Extensions.DependencyInjection.IServiceCollection.
// Use this method when using dependency injection in your application, such as
// with ASP.NET Core. For applications that don't use dependency injection, consider
// creating Microsoft.EntityFrameworkCore.DbContext instances directly with its
// constructor. The Microsoft.EntityFrameworkCore.DbContext.OnConfiguring(Microsoft.EntityFrameworkCore.DbContextOptionsBuilder)
// method can then be overridden to configure a connection string and other options.
// Entity Framework Core does not support multiple parallel operations being run
// on the same Microsoft.EntityFrameworkCore.DbContext instance. This includes both
// parallel execution of async queries and any explicit concurrent use from multiple
// threads. Therefore, always await async calls immediately, or use separate DbContext
// instances for operations that execute in parallel. See Avoiding DbContext threading
// issues for more information.
// See Using DbContext with dependency injection for more information.
//
// 参数:
// serviceCollection:
// The Microsoft.Extensions.DependencyInjection.IServiceCollection to add services
// to.
//
// optionsAction:
// An optional action to configure the Microsoft.EntityFrameworkCore.DbContextOptions
// for the context. This provides an alternative to performing configuration of
// the context by overriding the Microsoft.EntityFrameworkCore.DbContext.OnConfiguring(Microsoft.EntityFrameworkCore.DbContextOptionsBuilder)
// method in your derived context.
// If an action is supplied here, the Microsoft.EntityFrameworkCore.DbContext.OnConfiguring(Microsoft.EntityFrameworkCore.DbContextOptionsBuilder)
// method will still be run if it has been overridden on the derived context. Microsoft.EntityFrameworkCore.DbContext.OnConfiguring(Microsoft.EntityFrameworkCore.DbContextOptionsBuilder)
// configuration will be applied in addition to configuration performed here.
// In order for the options to be passed into your context, you need to expose a
// constructor on your context that takes Microsoft.EntityFrameworkCore.DbContextOptions`1
// and passes it to the base constructor of Microsoft.EntityFrameworkCore.DbContext.
//
// contextLifetime:
// The lifetime with which to register the DbContext service in the container.
//
// optionsLifetime:
// The lifetime with which to register the DbContextOptions service in the container.
//
// 类型参数:
// TContext:
// The type of context to be registered.
//
// 返回结果:
// The same service collection so that multiple calls can be chained.
public static IServiceCollection AddDbContext<TContext>(this IServiceCollection serviceCollection, Action<DbContextOptionsBuilder>? optionsAction = null, ServiceLifetime contextLifetime = ServiceLifetime.Scoped, ServiceLifetime optionsLifetime = ServiceLifetime.Scoped) where TContext : DbContext
{
return serviceCollection.AddDbContext<TContext, TContext>(optionsAction, contextLifetime, optionsLifetime);
}
其中,ServiceLifetime 参数指示了注入的生命周期为 Scoped。
Step4, 编写Controller代码
对于极简API而言,除了使用Minimal API形式,直接在Program.cs中完成接口及方法调用的代码外,常规的形式仍然是自己定义继承了 ControllerBase的API Controller Class。
参考模板生成的WeatherForecastController,代码如下:
这是一个非常常规的Controller,它提供一个HttpGet的API,该API的对外名称为 “GetWeatherForecast”。在自动生成的Swagger文档中,显示如下:
可以看见,这个模板Controller通过构造函数注入的方式注入了logger(虽然并未在实际中使用)。依赖注入在应用层代码中的价值也完全体现,using代码中并不需要去指定对具体Logger的引用,而是只需要一个实现了ILogger接口的对象注入即可。具体使用的Logger则是在program.cs中,通过ServiceCollection的添加完成的。
当然目前为止,我们并没有注入这个东西,那么正常来说,运行时在强制调用ILogger的记录方法时应该报错。事实上,进行调试时发现并不会出现异常,这是由于在.Net 6中提供了内置日志记录器。而这些记录器是随SDK提供的。
当然同样可以使用第三方日志记录器,例如NLog等。
有关.Net中的日志记录器细节,请移步: https://docs.microsoft.com/zh-cn/dotnet/core/extensions/logging-providers
那么既然ILogger是通过注入形式,那么EFCore必然也是可以通过注入添加到Controller的。
接下来进行Controller的接口声明。
现在REST API是主流的规约,但是很多场景下并不能完全适用。例如最常见的登录接口,按照REST的设想,把一个Controller的内容当作资源,那么登录这个行为究竟是在访问什么样的资源呢?你可以讲,登录是请求用户信息,所以可以以用户信息作为背后的资源;你也可以讲,登录这个操作要提交一系列认证数据,通常还要在登录日志中新建记录、修改一些例如终端状态的表,那么到底是考虑为POST还是PUT呢?
我估计此时大多数开发就会陷入困境,然后决定采用POST一把梭方案。
关于Controller定义时使用的动词和对应场景,我觉得最近复习的左耳朵耗子的文章讲的比较合理,推荐移步:https://coolshell.cn/articles/22173.html
大多数情况下,更多人优先考虑的逻辑是能否支持复杂参数传递(这个考虑首先能排除GET),其次是所谓的安全,再次是所谓的通用(这两条基本锁定了POST),之后会在有空的情况下适当考虑下REST的设想,极少会再从是否有幂等的要求来进一步考虑。陈皓这篇文章也很新,我觉得提供了一种更好的思路,既绕开了诸多理想REST难以适应的场景,又能够改善POST一把梭的现实。
一个Controller需要遵循单一职责原则,并且由于访问的格式通常是 api/scope/method,因此结合以上对如何考虑Http动词的讨论,至少至此,可以有一个基本的设计了。
那么作为一个入门级别的Demo,考虑把几种动词都用上,其实背后的程序逻辑很简单,连数据库,进行对应的数据操作即可。于是我们得到以下的接口定义:
HTTP动词 | 接口声明 | 接口功能 |
---|---|---|
HttpGet | Get() | 获取某id对应的对象 |
HttpPost | Post(object newObject) | 添加新建的newObject对象 |
HttpPut | Put(object ResObject) | 对既有的ResObject进行对象更新 |
HttpDelete | Delete() | 删除某id对应的对象 |
HttpPatch | Patch(Dictionary<string, object> kvs) | 通过属性-值字典,局部更新某id对应的对象字段 |
HttpHead | - | |
HttpOptions | - |
对于以上定义,可以发现我提到的几处涉及【对应ID】的内容并没有在参数中指定id,当然这和API访问方式有关。 对于GET、DELETE和Patch,建议可以为其Url添加{id}通配符来进行访问。 而Post和PUT,通常是以完整的对象进行提交。如此设计,可以降低部分高频请求的数据通信压力。
在我的经验之中,最佳的API规划是,只提供最必要的参数作为API的入参,而不是优先考虑复用对象来作为参数(图方便)。虽然大多数的业务系统并不会轻易触发到性能瓶颈。
Step5,运行吧!
在完成Controller代码编写后,事实上目前我们的Demo已经完全具备了完整的功能: 可以与数据库交互,具备完整的增删查改API,并且基于项目模板自己填充的Swagger,还拥有一个可以进行测试的前端页面。
运行非常容易,在VS中直接启动调试/运行,或者找到生成目录,找到其中唯一和你项目名称一致的exe,运行即可。
启动时Console中没有其他报错的情况下,你应该发现有一个弹出的浏览器页面,并自动跳转至SwaggerUI界面,如下:
通过界面上各个接口的 Try it out, 你可以非常轻松的按照你的接口定义构建测试的入参,并测试接口调用。
End
作为入门的Demo,至此就完成了最基本的WebAPI项目构建。
当然这里我们的代码没有任何分层可言,也没有详细展开如何通过EFCore访问数据。作为Demo而言,合格,作为工程而言,连框架都不算。
所以之后这个系列会继续完善以下内容:
(先挖坑,后边迟早要写)
- EFCore -- Code First 与 DB First两种模式的快速构建
- EFCore -- DbContext的追踪行为、异常处理
- ConfigurationManager -- 配置管理、绑定、配置修改与重载、多环境配置调用
- ILogger -- 内建日志记录器的功能、如何引入第三方记录器
- DI -- 原生用法、自动注入和使用思考
- IDentityServer整合
- Core Web项目的多种部署方式