ASP.NET与ASP.NET Core很类似,但它们之间存在一些细微区别以及ASP.NET Core中新增特性的使用方法,在此之前也写过一篇简单的对比文章ASP.NET MVC应用迁移到ASP.NET Core及其异同简介,但没有进行深入的分析和介绍,在真正使用ASP.NET Core进行开发时,如果忽略这些细节可能会出现奇怪的问题,特此将这些细节进行分享。
本文主要内容有:
- 无处不在的依赖注入(Dependency Injection, DI)
- Configuration&Options
- ASP.NET Core 请求管道建立
- ASP.NET Core Mvc
- Web API
- Signalr
- 小结
注:本文基于ASP.Net Core 2.1版本,.Net Core SDK版本需要2.1.401+。长篇预警( ╯□╰ )
无处不在的依赖注入
ASP.NET与ASP.NET Core之间最大区别之一就是内置了依赖注入机制,虽然ASP.NET中也有DI机制,但没有内置容器,一般都需要使用第三方的容器来提供服务,另外依赖注入的概念也不像ASP.NET Core中这样无处不在。
简单来说依赖注入的目的是为了让代码解耦以提高代码的可维护性,同时也要求代码设计符合依赖导致原则使得代码更加灵活,而其原理实际上就是在应用程序中添加一个对象容器,在应用初始化时将实际的服务“放”到容器中,然后当需要相应服务时从容器中获取,由容器来组装服务。
服务的注册
ASP.NET Core的Startup(注:Startup仅仅只是约定名称,实际使用是在Program类型中创建 WebHost时使用的),该类型中包含两个方法分别是ConfigureServices和Configure,其中ConfigureServices的主要作用就是用来将服务“放”置到容器中
代码来自:https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-2.1
替换默认的依赖注入容器
ASP.NET Core的默认容器仅提供了构造注入功能,如果需要使用属性注入等功能或者在迁移时原有应用依赖于其它容器,那么可以通过使用第三方容器实现。
将默认容器替换为其它容器仅需三步:
1. 将ConfigureServices方法的返回类型改为IServiceProvider。
2. 将ASP.NET Core中的服务注册到第三方容器中。
3. 使用第三方容器实现IServiceProvider接口并返回。
官方文档以Autofac为例,Autofac已经实现了ASP.NET Core服务注册到Autofac容器中,以及Autofac容器的IServiceProvider接口封装,仅需安装Autofac以及Autofac.Extensions.DependencyInjection包即可。
详情参考:https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-2.1#default-service-container-replacement
使用windsor或其它容器可以参考:
https://*.com/questions/47672614/how-to-use-windsor-ioc-in-asp-net-core-2
将Controller注册为服务
虽然Controller在激活时是通过容器来获取Controller的依赖(即构造方法需要的参数),在代码运行的时候给人一种Controller是从容器中组装的错觉,但是实际上默认情况下Controller的组装过程不是直接由容器组装的,如果要让Controller从容器组装,那么在配置MVC服务时需要通过.AddControllerAsServices()方法将Controller注册到容器中:
注:一般情况下是否将Controller注册为服务对Controller的开发和代码的运行并没有很大区别,但是如果当容器变更为其它容器,并且使用了容器提供的如属性注入等功能时,如果没有将Controller注册为服务,那么相应的属性注入的过程也不会被触发,简单来说就是只有将Controller注册为服务,那么实例化Controller的工作才会由容器完成,才会触发或者使用到容器提供的其它特性。
服务的获取
前面介绍了服务的注册,现在来介绍一下在ASP.NET Core中有哪些方法可以获取服务:
1. Controller构造方法参数。
2. 通过Controller注入IServiceProvider类型,通过IServiceProvider来获取服务:
3. 在Action方法或者Mvc过滤器(过滤器的上下文参数中包含HttpContext)中通过HttpContext的RequestServices对象获取服务:
4. 在View上通过@inject注入服务:
5. 在Action方法中,通过FormServices特性注入:
注:一般来说尽可能显式的标明类型的依赖(即通过构造参数的方式声明当前类型所依赖的组件),上面的2和3两点分别都是通过服务提供器在方法内部来获取依赖,这样做依赖对于外界来说是不可知的,可能会对代码的可维护、可测试性等造成一定影响,这种模式被称为Service Locator模式,在开发过程中尽可能避免Service Locator模式的使用。
常用的服务
ASP.NET Core相对于ASP.NET来说取消了一些常用的静态类型,比如HttpContext、ConfigurationManager等,取而代之的是通过将类似的组件以服务的形式注册到容器中,使用时通过容易来获取相应的服务组件,这些常用的服务有:
1. IHostingEnvironment:包含了环境名称、应用名称以及当前应用程序所处的根目录及Web静态内容的根目录(默认wwwroot)。
2. IHttpContextAccessor:从名字可以看出,它用来访问当前请求的HttpContext。
3. IConfiguration:ASP.NET Core配置信息对象。
4. IServiceProvider: ASP.NET Core服务提供器。
5. DbContext: 这里的DbContext指的是EFCore的DbContext,在ASP.NET Core中,EFCore的DbContext也是在ConfigureServices方法中进行配置并添加到容器,使用时直接从容器中获取(但要注意的是对于分层结构的开发风格来说,DbContext不会直接被Controller依赖,而是被Controller中依赖的业务服务类型所以来,就是说编写Controller代码的时候不会直接与DbContext发生直接交互)。
Configuration&Options
在ASP.NET的开发中,通常某个变量需要从配置文件读取,一般都是在相应类型的构造方法中,通过静态类型ConfigurationManager的AppSettings方法来读取并初始化变量。虽然ASP.NET Core也可以在类型中注入IConfiguration实例来直接读取配置文件,但该方法由于Options模式的出现已经不再建议使用,使用组件通过依赖相应的组件Options可以做到关注点分离,提高程序的灵活性、可拓展性,Options使用方法见文档:https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-2.1
ASP.NET Core 请求管道建立
ASP.NET由于是基于IIS请求管道的,ASP.NET应用程序仅仅是管道中的一个处理环节,管道中还包含如身份验证、静态文件处理等环节,但ASP.NET Core不一样,它脱离了IIS处理管道,所以整个管道的建立均需要靠程序自身完成,而ASP.NET Core建立管道的代码就是Startup类型的Configure方法,该方法通过IApplicationBuilder实例来添加不同功能的中间件,通过中间件的串联形成处理管道,下图是ASP.NET Mvc模板生成的管道代码:
图片来自:https://docs.microsoft.com/en-us/aspnet/core/fundamentals/startup?view=aspnetcore-2.1#the-configure-method
该管道主要包含了错误处理(开发环境显示异常信息,其它环境跳转错误页面)中间件、静态文件处理中间件以及Mvc中间件。
更多中间件可参考文档:https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/index?view=aspnetcore-2.1
ASP.NET Core Mvc
ASP.NET Core Mvc与ASP.NET Mvc相比整体上区别不大,但仍然有很多细节上的变化,下面就开始一一介绍:
路由
路由的作用是将请求根据Url映射到“对应”的处理器上,在Mvc中请求的终点就是Controller的Action方法,而这里所谓的“对应”指的是Url与路由模板的匹配,ASP.NET Core Mvc通过以下的方式添加路由模板:
上图中的路由模板是最常用的路由模板,使用花括号内的内容为路由参数及其默认值,Url中通过路由参数控制器名称、活动方法名称来匹配到相应控制器的活动方法。
在注册路由时可以为相应路由添加默认值、路由参数约束以及对应路由的相关附加数据(datatokens):
路由的功能除了处理请求匹配外,还具有链接生成的功能,特别是Mvc程序的View中使用IUrlHelper或TagHelper来生成页面的超链接:
其生成原理是通过链接参数(如上图所示的Controller和Action)去路由表中匹配,然后使用匹配结果中的第一个路由(可能会匹配到多个路由对象,具体内容在后续Area章节介绍)来生成链接。
更多路由信息及路由模板定义参考文档:https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-2.1
控制器
ASP.NET Core Mvc的Controller一般继承Controller类型实现,基类Controller中包含了Mvc中常用的返回方法(如Json以及View等)以及用于数据存储的ViewBag、ViewData、TempData。
Area
Area是Mvc应用中用来进行功能拆分或分组的一种方式,Area一般有自己的命名空间和目录结构,一般Area的默认目录结构如下:
ASP.NET Core Mvc和ASP.NET Mvc中的概念和用法基本上是一致的,但也存在一些区别:
1. Area下面的Controller需要使用Area特性标明当前Controller属于哪一个Area:
注:Area的目录结构不是必须的,只需要通过特性标记的Controller都会被正确识别,但目录结构的改变会导致无法找到View,关于View的查找路径会在后续介绍。
2. Area的路由注册也是在UseMvc方法中完成:
注:携带Area的路由模板需要放在前面,否则在生成通过IUrlHelper或TagHelper生成链接时,由于Controller以及action会匹配到没有area的模板并使用该模板生成链接,导致area参数被忽略,而生成类似:/controller/action?area=area的结果(在生成Url时,ASP.NET Core会将多余的路由参数放置到查询字符串中)
View
View是基于Razor的HTML模板,Razor的详细语法参考文档:
https://docs.microsoft.com/en-us/aspnet/core/mvc/views/razor?view=aspnetcore-2.1
ASP.NET Core Mvc的View与ASP.NET Mvc中的使用方法基本一致,主要区别如下:
1. 引入了TagHelper,使用TagHelper可以让View的代码更接近Html。更多TagHelper信息参考文档:https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro?view=aspnetcore-2.1
2. Controller将参数传输到View的方法添加了ViewData特性,使用方法如下:
View中访问被ViewData标记的方式:
更多详情参考文档:https://docs.microsoft.com/en-us/aspnet/core/mvc/views/overview?view=aspnetcore-2.1#passing-data-to-views
3. 新增View组件:https://docs.microsoft.com/en-us/aspnet/core/mvc/views/view-components?view=aspnetcore-2.1
配置View的查找路径:
ASP.NET Core可以在ConfigureServices方法中对RazorViewEngineOptions进行配置,如下图所示,在默认查找位置基础上添加了View以及AreaView的查找路径:
模型绑定
模型绑定指的是ASP.NET Core Mvc将请求携带的数据绑定到Action参数的过程,ASP.NET Core Mvc的模型绑定数据源默认使用Form Values、Route Values以及Query Strings,所有值都以Name-Value的形式存在,模型绑定时主要通过参数名称、参数名称.属性名称、参数名称[索引]等方式与数据源的Name进行匹配。
除了默认的数据源之外还可以从Http请求Header、Http请求Body甚至从依赖注入容器中获取数据,要从这些数据源中获取数据需要在相应参数上使用[FromHeader]、[FromBody]、[FromServices]特性。
如果需要获取的数据在不同数据源中都存在时(Name存在于多个数据源中),还可以通过特性指明从哪一个数据源中获取,如[FromForm]、[FromQuery]及[FromRoute]。
需要注意的是[FromBody]默认只支持Json格式的内容,如果需要支持其它格式,如XML需要添加相应的格式化器,添加方法如下图所示:
更多模型绑定及验证内容请参考文档:https://docs.microsoft.com/en-us/aspnet/core/mvc/models/model-binding?view=aspnetcore-2.1
https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-2.1
其中模型验证的使用方式与ASP.NET Mvc一致,仍然是通过相应的验证特性对模型或模型属性进行标记。
Action的返回值与Json序列化
说完Action方法参数的绑定,再来看一下Action方法的返回类型,在ASP.NET Mvc中Controller提供了返回页面内容的View方法以及返回Json内容的Json方法(当然还有文件、重定向、404等等其它内容返回方法,详见Controller与ControllerBase类型)。
这里有一个需要注意的地方是当使用Json方法返回一个对象实例时,默认使用首字母小写的驼峰命名方式序列化实例的属性名称,如下图所示:
访问结果:
要使用大写驼峰形式命名需要在配置Mvc服务时添加以下代码来修改Json默认的序列化配置:
注:同样的问题也存在于WebAPI的Ok方法以及Signalr的Json格式协议。
静态资源
由于ASP.NET Core已经不再使用IIS请求管道,所以对于静态资源的访问来说需要在请求管道中添加相应的处理中间件来完成:
默认的无参UseStaticFiles方法将wwwroot目录作为静态资源存放目录,如果要添加其它静态内容目录可以再次使用UseStaticFiles方法,并通过StaticFileOptions对目录的访问路径以及实际路径进行配置:
注:由于ASP.NET Core可以在Linux下运行,所以对于Linux来说路径是大小写敏感的,另外由于Windows和Linux类系统的路径分隔符也不一致,所以为了保证路径的统一,可以使用Path.Combine方法,该方法会根据操作系统的不同对路径进行不同的处理。
另外对于css及js资源文件的打包、压缩功能,最新版本(ASP.NET Core 2.1)的应用模板以及不会自动添加相关功能,需要在拓展工具中添加Bunlder& Minifier拓展:
然后通过右键js等资源文件来创建bundleconfig.json文件:
WebAPI
API控制器的创建
ASP.NET Core将Mvc和WebAPI进行了合并,它们的实现都直接或间接继承了ControllerBase类型,只不过Mvc的基类Controller在ControllerBase的基础上添加了一些用于处理View的功能。
用ASP.NET Core开发WebAPI时,Controller类型直接继承ControllerBase。然后这个API的Controller就具有了基类的特性,返回一个结果仅需要使用Ok方法即可,如下图所示:
然后在路由表中添加路由:
即可通过/api/default/index访问到这个API:
但对于REST风格的API来说,它需要通过ApiController特性对Controller类型进行标记,并且通过Route特性来设置路由:
然后就可以通过HTTP谓词来访问API:
但要注意的是在ASP.NET Core中实现的REST风格的Controller,它不会再根据action方法的名称来匹配谓词,所以存在多个方法时会,那怕对方法进行了命名,但仍然会出现以下错误:
为了解决这个问题,需要通过添加谓词特性解决:
模型绑定
WebAPI中的模型绑定与MVC存在一些区别,首先当使用ApiController标记Controller类型时,如果模型绑定验证未通过,会直接返回400错误,不会执行Action方法(免去了使用!ModelState.IsValid进行判断):
执行结果:
其次使用ApiController标记的Controller在执行模型绑定时会使用默认的推断规则,该规则分别从Body、Form、Header、Query、Route、Services(它们分别对应FromBody、FromForm、FromHeader、FromQuery、FromRoute、FromServices特性)中推断获取数据并绑定,为什么说推断?
因为有一些特殊的规则:
1. FromBody用于复杂类型推断,如果不是复杂类型(如int、string等)以及特殊的内置类型(IFormCollection文档例子),则不会从Body中获取数据,除非通过[FromBody]特性指明,例子如下:
请求结果:
当使用[FormBody]指明参数数据源后可以正常访问:
注:当请求参数为简单类型时,请求体内容类型需要为application/json,内容不能为Json字符串,使用参数值作为内容即可(上图id没有提供的异常并不是因为Json格式问题,而是没有指明从body中获取数据导致的)。
2. 只能存在一个参数从Body中获取数据,如果出现多个参数时,只能保证一个参数从Body中获取数据,其它参数需要指明获取数据的位置:
该API的调用方式如下:
3. FromForm默认只推断文件(IFormFile)及文件集合类型(IFormFileCollection),其余类型默认均不会从Form中获取。
4. 使用FromForm特性时会推断multipart/form-data请求内容类型。
以上推断行为可以通过如下配置禁用:
更多信息参考文档:https://docs.microsoft.com/zh-cn/aspnet/core/web-api/?view=aspnetcore-2.1
SignalR
SignalR是用于客户端服务器实时通信的工具库,从ASP.NET中就具有该功能,ASP.NET Core中的SignalR概念与用法与原来基本一致,但也存在一些区别:
1. 支持更多的客户端,.Net客户端、Java客户端、Js客户端以及非官方的C++客户端、Swift客户端。
2. 当链接SignalR并通过身份验证后,SignalR会保存当前用户链接SignalR的ID以及通过验证后的用户名,可以通过用户名向用户客户端推送消息。
3. 在应用程序中可以通过IHubContext<HubType>方式,对SignalR上下文进行注入,并且可以直接通过该上下文推送数据给已经链接的客户端,IHubContext<HubType>实际上是GlobalHost.ConnectionManager.GetHubContext<HubType>()的替代方式。
4. ASP.NET Core中通过app.UserSignalR以及route参数来映射一个Hub,每一个Hub拥有独立的上下文,因此如果要使用IHubContext<HubType>来向客户端推送信息,那么必须准确注明Hub的类型,如下图代码应该使用IHubContext<ChatHub>,不能使用除ChatHub以外的类型(基类也不行)。
5. SignalR默认使用Json协议传输数据,默认情况下使用首字母小写的驼峰命名方式序列化对象,要更改该默认行为需要通过一下代码,替换默认的序列化行为:
6. ASP.NET Core的客户端代码(特指Js客户端)有变更,需要对应版本使用。
关于更多SignalR内容请参考文档:https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction?view=aspnetcore-2.1
小结
本文主要介绍了ASP.NET Core中Mvc、WebAPI以及SignalR开发时与原来ASP.NET中的一些细小区别和新特性,整体来说ASP.NET Core与ASP.NET从使用方式上基本上是一致的,这也使得从ASP.NET迁移到ASP.NET Core变得更加容易,但可能因为这些细小的问题往往会向代码中埋入一些坑,所以特别编写了本文来解释这些问题。
总的来说ASP.NET Core的文档相当齐全,本文中大部分内容实际都是文档中提到的,所以建议大家在使用ASP.NET Core开发时,首先第一步就是熟读文档,避免遗漏细节。希望本篇文章对大家有帮助(*^_^*)
参考:
https://docs.microsoft.com/en-us/aspnet/core/?view=aspnetcore-2.1