ASP.NET Core 菜鸟之路:从Startup.cs说起
1.前言
本文主要是以Visual Studio 2017 默认的 WebApi 模板作为基架,基于Asp .Net Core 1.0,本文面向的是初学者,如果你有 ASP.NET Core 相关实践经验,欢迎在评论区补充。
与早期版本的 ASP.NET 对比,最显著的变化之一就是配置应用程序的方式, Global.asax、FilterConfig.cs 和 RouteConfig.cs 统统消失了,取而代之的是 Program.cs 和 Startup.cs。Program.cs 作为 Web 应用程序的默认入口,不做任何修改的情况下,会调用同目录下 Startup.cs 中的 ConfigureServices 方法 和 Configure 方法。
对于初学者来说,第一次面对 Startup.cs 往往无从下手,本文将一步步介绍作者的经验,但是不会涉入到内部的代码实现以及相关的原理,那并不是本文想要讨论的范畴。
相信我,这将是你迈出构建灵活而强大的ASP.NET Core 应用程序的第一步。
2.配置参数选项
在官方文档中提供多种方式来配置参数选项:
- 文件格式(INI,JSON和XML)
- 命令行参数
- 环境变量
- 内存中的 .NET 对象
- 用户机密存储
- Azure 键值
- 自定义提供程序
虽然提供了很多选择,但是我们只选择其中的JSON文件和环境变量来提供配置参数。
2.1 Json配置参数选项
参考官方文档的示例,我们在 appsettings.json 加入如下的参数:
与此同时,我们还需要一个类来映射这些配置参数:
思考一下 subsection 应该是字典还是一个对象?如果是字典,是否可以为<string,dynamic>或者<string,object>?
好了,现在就差怎么让他们联系起来,只需在 ConfigureServices 方法中将他们配对:
services.Configure<MyOptions>(Configuration);
最后就是解决怎么使用这些配置参数的问题了,举个最简单的例子,我们可以在某个控制器中把我们的所有参数打印出来:
不知道你有没有发现 MyOptions 类中有些属性首字母大写,有些属性没有,并不是与json文件中完全一致,也就是说可以忽略大小写的。
回到之前的匹配环节,我们可以发现 services.Configure 的方法中似乎还有更多选择,比如我们把之前的那一行代码替换为:
services.Configure<MyOptions>(Configuration.GetSection("wizards"));//选择wizards节点
我们可以发现返回的对象里面的属性都为null,这是因为json中的 "wizards"的节点并不能与我们的类匹配。
那么问题来了,如果匹配的代码如下,又会产生什么样的结果呢?先别急着回答,我会在下一节中给出答案。
2.2环境变量
环境变量,或者说系统变量,在windows中我们可以在系统属性中配置:
在Linux环境中也有相应的配置,但是在开发过程中,我们可以在 Visual Studio 中配置:
在这之前,我们的Json文件中已经有 "option1" 和 "option2"的参数选项,那么会产生什么样的结果呢?
显然我们可以看到环境变量的参数覆盖了Json文件的参数。而引起这种变化的原因还是需要回到Startup的初始化:
public Startup(IHostingEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)//必须的json文件,并且自动重载 .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)//不必须的json文件 .AddEnvironmentVariables();//启用环境变量 Configuration = builder.Build(); }
从中我们可以看出环境变量的配置在读取 Json 文件参数之后,这样就会覆盖已经存在的同名参数,而已经从 Json 文件被匹配的参数并不会被清空(同样适用于前一节提出的问题)。从另一方面来说,如果你不需要环境变量,则需要去掉 "AddEnvironmentVariables() ",以免覆盖预期参数。
回到本节的中心,我们为什么会需要环境变量呢?我个人会在Dockerfile中配置一些环境变量,比如某种服务的访问地址、某中功能的开关等等。下面举例说说两个常用的环境变量:
ASPNETCORE_URLS 如果你没有指定对应的 Url 监听地址,可以通过该参数修改,如设置为 "http://*:80"。
ASPNETCORE_ENVIRONMENT 开发环境(Development)、预演环境(Staging)、生产环境(Production),将在工作环境一节中做详细介绍。不同的工作环境将使得整个软件流程变得清晰。
2.3配置参数小贴士
- 参数有多种来源,如不需要勿增来源。
- 要注意"最近原则",避免参数同名引起冲突。
- 参数的key可以忽略大小写,所以环境变量中的 "OPTION2" 可以引起覆盖 Json文件中的 "option2" 的效果。
- 为复杂参数选择合适的类型很重要,比如字典还是对象的取舍。
3.依赖注入
依赖注入在 ASP.NET Core 中无处不存在,在之前打印参数的例子中同样用到。依赖注入好处都有啥?为什么我们需要依赖注入?在 ASP .NET Core 中文文档中依赖注入的章节 很好地解释了:
你应该设计你的依赖注入服务来获取它们的合作者。这意味着在你的服务中避免使用有状态的静态方法调用(代码被称为 static cling)和直接实例化依赖的类型。当选择实例化一个类型还是通过依赖注入请求它时,它可以帮助记住这句话, New is Glue。通过遵循 面向对象设计的 SOLID 原则,你的类将倾向于小、易于分解及易于测试。
3.1注册服务以及简单使用
为了方便下一节测试,我准备三个文件,简单的接口、该接口的实现类,拥有接口成员的类:
接下来,我们使用 ASP.NET Core 自带的 DI 来注册服务:
可以看到,注册对象有很多种方法,并且我们可以管理对象的生命周期。注册完对象,我们就可以在我们需要的地方注入对应的对象了,还是最简单的例子——在控制器中使用:
对于控制器,我们有三种方式注入对象:构造函数、控制器动作、属性注入。然而,在一般的类中,使用自带的 DI 只能是构造函数注入。到底是哪种方式好,见仁见智。
3.2生命周期
ASP.NET Core 服务可以被配置为以下生命周期:
- 瞬时(Transient) 在它们每次请求时都会被创建。这一生命周期适合轻量级的,无状态的服务。
- 作用域 (Scoped) 在每次请求中只创建一次。
- 单例(Singleton) 在它们第一次被请求时创建(或者如果你在 ConfigureServices运行时指定一个实例)并且每个后续请求将使用相同的实例。
我们将通过逐步更改 IRepository 的生命周期来看看会发生什么事情。
首先是瞬时:
接着是作用域:
最后是单例:
瞬时很好理解,类似每次都会new了一个对象。而对于作用域,如果一次请求中,有两个甚至多个非单例对象引用到同一个作用域类型时,他们将会收获同一个实例。单例也很好理解,从头到尾都是同一个实例。
控制单一变量,如果只是改变 ProductTotalizer 的生命周期而不是改变 IRepository 的生命周期的话,会发生什么情况呢?
3.3依赖注入小贴士
- 遵循 SOLID 原则,考虑一下哪些是需要依赖注入的。
- 合理考虑生命周期,假如某个类型在不同上下文中需要不同生命周期时,是否需要显式命名区分?还是考虑结构是否合理?
- 避免静态访问服务。
- 避免静态访问 HttpContext 。
4.启用扩展
在项目中我们往往会添加许多扩展,比如用于API文档说明的Swagger、计划任务的Hangfire、压缩响应的GZIP、跨域访问、日志扩展等等。他们的共同点就是需要先安装相应的nuget包,然后在 ConfigureServices() 方法中配置服务,最后在 Configure() 方法中启用。
我们以Swagger为例,首先是安装对应的 nuget 包—— Swashbuckle。
接着是配置扩展:
最后就是启用 Swagger 了:
我们访问 Swagger 的地址看看效果:
5.中间件
中间件是用于组成应用程序管道来处理请求和响应的组件。管道内的每一个组件都可以选择是否将请求交给下一个组件、并在管道中调用下一个组件之前和之后执行某些操作。请求委托被用来建立请求管道,请求委托处理每一个 HTTP 请求。
举一个简单的例子(更复杂的可以在中间件依赖注入对象),从 cookie 中获取 token 并附加到请求头中:
与启用扩展一样,我们同样是需要在 Configure()方法中启用中间件:
app.UseMiddleware<JWTInHeaderMiddleware>();
如果我们有多个中间件呢,中间件的顺序可能会影响到响应结果,但并不是总是线性相关的。例如,我们新增一个对响应状态码处理的中间件:
app.UseMiddleware<StatusCodeMiddleware>(); //.... app.UseMiddleware<JWTInHeaderMiddleware>();
虽然对状态码处理的中间件是最前面,但可以在请求的最后关头对请求结果进行处理。当然,如果中间有某个中间件短路了(没有传递到下一个中间件),就会让我们前功尽弃。
6.过滤器
与中间相似,过滤器同样可以对请求的前后执行特定代码,但是过滤器可以配置为全局有效、仅对控制器有效或是仅对 Action 有效,比中间件更具有灵活性。
另外,过滤器从类型上还能分为:授权过滤器、资源过滤器、Action过滤器、结果过滤器。很容易实现面向切面编程,降低了耦合。
这里举一个我最喜欢的过滤器——对请求的模型进行验证:
当然,模型验证的过滤器往往具有全局性,所以我一般是加在 services.AddMvc 中:
services.AddMvc(config=> { config.Filters.Add(new ValidateModel()); });
7.工作环境
ASP.NET Core 提供了许多功能和约定来允许开发者更容易的控制在不同的环境中他们的应用程序的行为。当发布一个应用程序从开发到预演再到生产,为环境设置适当的环境变量允许对应用程序的调试,测试或生产使用进行适当的优化。
在软件开发的生命周期中,在不同的工作环境中往往是不同的状态。比如说,在测试或者预演环境中,启用Swagger扩展、控制台打印日志、允许跨域,而在生产环境中,往往处于安全、性能或者其他考虑,这些功能是需要禁止的。对于 MVC 开发者来说,在开发过程中会使用本地的JS、Css、图片等文件,而在生产环境中这些文件往往是从CDN中获取。
8.结语
ASP.NET Core 提供了强大而灵活的配置机制,每个点展开都像是一片新的天地。即使是经验丰富的开发者,也不能自称完全掌握全部机制。随着 .NET Core 的完善和发展,Startup.cs 也将越来越复杂。越是复杂,就越是要小心,如无需要,勿增实体!