47

从壹开始学习NetCore 44 ║ 最全的 netcore 3.0 升级实战方案 - 老张的哲学

 4 years ago
source link: https://www.cnblogs.com/laozhang-is-phi/p/11520048.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

从壹开始学习NetCore 44 ║ 最全的 netcore 3.0 升级实战方案

系列文章列表,点击展示/隐藏

正文

1、哈喽大家中秋节(后)好呀!感觉已经好久没有写文章了,但是也没有偷懒哟,我的视频教程《系列一、NetCore 视频教程(Blog.Core)》也已经录制八期了,还在每周末同步更新中,欢迎大家多多指教。

2、除此之外呢,我也在平时的时间帮朋友开发了一个小项目,就是使用 .net mvc+vue+ele+mongo 框架写的项目,之前一直想着用mvc结合着vue写,这次也终于上手了,不过是一个小的demo,因为是朋友的项目,所以就不开源了。

 

1468246-20190924111426840-732619681.png

 ✔ 如果要使用 netcore 3.0 正式版的话,一定要更新 vs2019 最新版:16.3.0

当然你可以先看看3.x到底更新了哪些东西:  https://docs.microsoft.com/zh-cn/aspnet/core/release-notes/aspnetcore-3.0

言归正传,👉  从2018年8月就开始听说 netcore 要准备3.0了,👉 到了近期 v3.0.0-preview9 的发布(截止目前,3.0已经发布,地址 https://dotnet.microsoft.com/download),官方也最终定稿不会再更新了,(这里存疑,不过功能更新至少是不会有了),不过9月23号会发布最终版本, 👉  接着马上 在下周 9月23日至25日 .NET Conf 社区大会上,会正式推出 netcore3.0 版本, (最后 👉 微软会将 .netcore 和 .net 进一步融合,推出完美跨平台 net 5.0 版本,这里暂时先不说),单单从这一年里 netcore 3.0 的快速发展、迭代以及接受用户的反馈进一步修改中,我们就能感觉的到,微软是如何的有希望并且有信心在未来的发展中,将微软系产品进一步融入到广大开发者的心中,我们也要有信心微软能做到这一点。

在netcore 3.0 马上要到来之际,我也要尝尝鲜,我肯定不是第一个吃螃蟹的人,博客园这两个月也是一直轰轰烈烈的进行 3.0 的更新和迭代,不过过程是怎样的吧,至少结果目前还是可以的,也可以作为一个成功案例给大家提供一些建议和思路。

感觉尝试就是成功的一半,所以我在中秋节这两天,也把 Blog.Core  项目给提升到了 3.0 版本,大家现在看的我的在线地址(国外的服务,可能加载比较慢,后期会做处理 http://apk.neters.club/index.html)  就是netcore 3.0 的,总体看起来,可能没有什么差别,而且运行中也没有发现任何问题(管理后台 http://vueadmin.neters.club/),不过这次官方更新的东西还是稍微挺多的,所以我这里就统一做下记录,方便大家吧,希望每一个在使用 netcore 的小伙伴都能从这里得到一些帮助,虽然官网也有一些记录,但是我看了看,英文的可能有些小伙伴不好理解,尽管有中文翻译版,可是看着不是很通顺,而且也不是很全,大家可以看看:地址

当然不仅仅包括下边的这几点,我还在慢慢更新,如果你使用到了我 blog.core 项目中没有用到的技术,并且自己在更新 3.0 的时候出现了问题,可以和我聊聊,我在下边补充下,争取达到一个最全的解决方案合集。

 

1468246-20190915104628071-793074083.png

好啦,废话到此结束,马上开始今天的迁移报告内容!🌈🌈🌈

零、NetCore3.0 有哪些新特性

1468246-20191003101351717-717681800.png

netcore 1.0 到 2.0 主要的是网络和云服务的升级,那 net core 从2.0 到 3.0 更新的是哪些呢?

这里我就简单的列举了下这一年来netcore 3.0 更新的比较热门的特性,当然还有其他的,因为本篇文章主要是讲解升级实战,所以对以下特性就不过多的铺开讲解。

先看看官网提到了哪些更新内容:https://docs.microsoft.com/zh-cn/aspnet/core/release-notes/aspnetcore-3.0?WT.mc_id=DOP-MVP-5003704&view=aspnetcore-3.1

1、性能、性能、性能,重要的地方说三遍

2、在机器学习,AI等很好的支持

3、对Winform、WPF的支持

4、gRPC的添加

5、支持 API 授权在单页面应用 (Spa) 中提供身份验证、实现 Open ID Connect 的IdentityServer结合。

6、Worker Service 模板,为开发做服务或监控微服务相关Bus

7、Microsoft.Data.SqlClient:独立存在于.NET Framework和.NET Core中

8、ReadyToRun

9、HttpClient支持HTTP/2

10、Json.NET 不在内置在框架内,使用System.Text.Json

11、HostBuilder 替换掉WebHostBuilder

12、Blazor 是一个用于使用 .NET 生成交互式客户端 Web UI 的框架,用c#开发前端

13、.NET Framework不支持.NET Standard 2.1

14、IL linker

15、发布成单个程序 dotnet publish -r win10-x64 /p:PublishSingleFile=true

16、And so on......

那下面我就针对我的 Blog.Core 项目,坐下迁移的说明,一共八个方面,不是很多,大家可以一一对比下。

一、项目启动部分

操作前必备:备份文件,这个很重要,我们要玩儿新花样,肯定要做好备份文件,可别因为升级失败,而不好回退。

当然我的操作是直接操作的 Blog.Core 项目,因为项目在 git 上,如果不成功,就直接回退,这种资源管理工具还是很有必要的。

1、安装SDK

 首先可以查看自己的本地 SDK 是什么版本的,比如我的目前只有 2.1和 2.2 :

1468246-20190914220729292-1892969763.png

所以,如果我们要升级 3.0 的话,就肯定要安装指定的 SDK 了,下载地址:https://dotnet.microsoft.com/download/visual-studio-sdks

 ✔ 如果要使用 netcore 3.0 正式版的话,一定要更新 vs2019 最新版:16.3.0

1468246-20190924115356001-1710340185.png

选择指定版本的 SDK ,然后进行安装,最后我们就可以看到我们的本地已经安装好了:

 1468246-20190924115410487-890070809.png

 这里我们可以看到我们的 3.0 的 SDK 已经安装好了,最后再做个验证,就是在我们的 VS 2019 中,查看是否有 3.0 的框架:

1468246-20190914231934608-633614695.png

竟然没有??!!别慌,这里有两个方法:

1、工具 -> 选项 -> 项目与解决方案 -> 右侧,勾选预览版(这个方案是2019 最旧版本的,已取消请忽略)。

1468246-20190916103107355-1120586106.png

2、在工具 -> 选项 -> 环境里(正规是使用这个):

1468246-20190914235549809-1991606216.png

然后我们把 vs 重新启动一下,发现已经有了:

1468246-20190914235701041-250719064.png

安装好了 SDK,我们就已经是成功了一半了,下边我们就正式开始升级打怪之路。

但是这里还有一个问题,就是打开的项目属性里,虽然有了 3.0 的框架,但是新建的项目,依然没有 3.0 的部分,那这个是为什么呢?

1468246-20190915001639886-864527169.png

这里网上的方案是:不要用preview8或者9,这两个版本出不来core3.0的选项,preview7没有问题。如果非要用最新版,可以用dotnet new创建项目,或者等下星期的 net core 3.0正式版出来,这样就不用来来回回勾选了。

Tips:感谢 @迷失的猫叔 给出建议

https://dotnet.microsoft.com/download/dotnet-core/3.0
这个页面的Tips已经说了,有可能下周就是Core3.0和VS 2019的16.3一起发布,猜测应该就是更新就行了,目前我的VS版本是16.2.5

更新:确实现在已经创建了,要注意:

1、安装 netcore 3.0 正式版;

2、更新 vs2019 16.3.0 最新版;

3、重启 vs2019 即可;

1468246-20190924130322174-1410650010.png

2、更新框架以及所有依赖

刚刚我们已经成功的安装好了 3.0 的 SDK ,那接下来就是正式开始升级项目,首先呢,就是需要更新我们的目标框架,这里有两种方法:

第一种是直接修改我们的项目文件 .csproj ,修改节点 <TargetFramework>netcoreapp3.0</TargetFramework>,并移除关于 Aspnetcore 2.2 相关的包;

第二种就是直接右键项目,属性,应用程序,修改目标框架到 netcore 3.0 就行,就是上文截图中显示的那个,我个人采用的是这种方法。

1468246-20190915120807793-2073542025.png

 直接手动删除即可

记得要把项目从底层开始更新,比如从 Model 层和 Common 层开始更新,然后最后更新 API 层,就是从下向上,(这里有个小问题,就是出现修改了,CTRL S 保存后,又重新回到2.2了,可以重启下项目,重启下vs就行了)。

代码修改对比图:

1468246-20190915113837523-1266947195.png

(netcore 3.0 修改sdk框架)

 接下来,就是把项目中用到的所有nuget包都更新到最新的版本,这里再强调一句,一定是最新的,包括预发行rc版!!!,因为有些是为了迎接 netcore 3.0,做了相应的修改,比如下午说到的 swagger ,一定要更新到 5.0+版本。

 到了这里,我们的项目已经把框架和依赖升级完成了,是不是很简单,重新编译,运行,这里肯定会有错误,别着急,接下来我们就进一步修改 Code 中出现的bug。 

3、宿主机变化(Program.cs)

netcore 3.0,对 host 做了调整,底层没有发生太多的变化,这里不细说,主要说要修改的地方,更新的内容,我会在视频里,详细给大家讲解。

在 Program.cs 文件中,修改HostBuilder生成方法,注意在main 方法里引用也要做相应的修改

代码修改对比图:

1468246-20190915121112833-1556110433.png

 (Program.cs 修改 host 宿主机)

CODE:

 public static IHostBuilder CreateHostBuilder(string[] args) =>
   Host.CreateDefaultBuilder(args)
     .ConfigureWebHostDefaults(webBuilder =>
     {
         webBuilder
           .UseStartup<Startup>()
           .UseUrls("http://localhost:8081")
           //这里是配置log的
           .ConfigureLogging((hostingContext, builder) =>
           {
               builder.ClearProviders();
               builder.SetMinimumLevel(LogLevel.Trace);
               builder.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
               builder.AddConsole();
               builder.AddDebug();
           });
     });

4、Host 环境变量(Startup.cs)

从上边我们也可以看得出来,官方更新了 host ,所以自然而然的,也更新了部分的命名空间,这样就出现了一个问题:

当 Microsoft.Extensions.Hosting 在 2.1 中被引入时,某些类型 IHostingEnvironment 和 IApplicationLifetime 是从 Microsoft.AspNetCore.Hosting 复制的。某些 3.0 更改会导致应用同时包含 Microsoft.Extensions.Hosting 和 Microsoft.AspNetCore.Hosting 两个命名空间。当同时引用两个命名空间时,对这些重复类型的任何使用都会导致"不明确的引用"编译器错误。

所以官方就对某些命名空间和类做了修改:

Obsolete types (warning):

Microsoft.Extensions.Hosting.IHostingEnvironment
Microsoft.AspNetCore.Hosting.IHostingEnvironment
Microsoft.Extensions.Hosting.IApplicationLifetime
Microsoft.AspNetCore.Hosting.IApplicationLifetime
Microsoft.Extensions.Hosting.EnvironmentName
Microsoft.AspNetCore.Hosting.EnvironmentName

New types:

Microsoft.Extensions.Hosting.IHostEnvironment
Microsoft.AspNetCore.Hosting.IWebHostEnvironment : IHostEnvironment
Microsoft.Extensions.Hosting.IHostApplicationLifetime
Microsoft.Extensions.Hosting.Environments

这个不用记忆,到时候使用的时候,会有提示的,那我们的项目中,有哪个地方需要修改呢,就是配置中间件的时候有一个环境变量配置需要修改下:

1468246-20190924104753595-1282801228.png

1、将 IHostingEnvironment env 改成 IWebHostEnvironment env,这个时候会报错,因为命名空间变了;

2、所以需要引用新的命名空间: using Microsoft.Extensions.Hosting;

到了这里,我们就完全修改好了宿主机的部分,现在项目还不能正常的使用,还需要继续修改 mvc 部分,别着急,慢慢往下看。

二、MVC 部分

刚刚我们修改了宿主机 host ,启动项目的时候,还是会有错误,主要提示我们的中间件 .UseMvc() 已经不能被使用了,3.0后,对mvc做了较大的修改,主要从两个方面,一个是服务注册,一个是中间件的拆分:

1、MVC 服务注册(Startup.cs)

在 netcore 3.0 中,官方对 mvc 服务做了细分,主要有以下几个部分:

  services.AddMvc();// 我们平时2.2使用的,最全面的mvc服务注册
  services.AddMvcCore();// 稍微精简的mvc注册
  services.AddControllers();// 适用于api的mvc部分服务注册
  services.AddControllersWithViews();//含有api和view的部分服务注册
  services.AddRazorPages();//razor服务注册

我们看出来,如果我们的项目是webapi的,那只需要注册 .AddControllers() 这个就够了,.AddMvc() 里边服务太多,会造成浪费,而大材小用。

代码修改对比图:

1468246-20190915124239908-1712208802.png

2、MVC 中间件的拆分(Startup.cs)

除了上边的 mvc 服务注册以外,我们还需要对 UseMvc() 中间件做修改。

官方已经正式去掉了Mvc()这个短路中间件,取代他的是 .UseEndpoints() 方法,我们可以做以下修改:

代码修改对比图:

1468246-20190915135622940-414712611.png

CODE:

 app.UseRouting();//路由中间件
 // 短路中间件,配置Controller路由
 app.UseEndpoints(endpoints =>
 {
     endpoints.MapControllerRoute(
         name: "default",
         pattern: "{controller=Home}/{action=Index}/{id?}");
 });

到了这里,我们已经完成了 netcore 2.2 到 net core 3.0 的最简单的升级,如果你想尝试下,可以自己手动建立一个空的 2.2 项目,实现到 3.0 的迁移,我们运行项目,可以看到已经成功的启动起来,还是很成功的。这里要注意下中间件的顺序,一般的顺序是这样的:

  app.UseStaticFiles();

  app.UseRouting();

  app.UseCors();

  app.UseAuthentication();
  app.UseAuthorization();

  app.UseEndpoints(endpoints =>
  {
      endpoints.MapHub<ChatHub>("/chat");
      endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
  });

那是不是到了这里已经完成了呢,答案当然是否定的,我们的项目不可能这么简单,肯定还会有其他的依赖,还有各种各样的中间件,那我们在升级的过程中,还会有哪些地方需要做处理呢,就比如下边的这些。

3、注意:中间件的顺序

不多说,注意下中间件的顺序 。

1468246-20191101232948464-1690803466.png

三、Swagger 部分

在 netcore 3.0 中,要求我们使用的是 swagger 5.0 ,而且变化的内容也挺多的,但是原理和思路都是一样的,大家一看就知道了,所以我就直接贴代码了。

1、代码修改对比图(ConfigureServices)

 这次的修改,主要是服务的注册部分,中间件没有变化,所以我们直接在 startup.cs 中的 configureService 中,做下调整:

这里要注意下,需要引用最新的5.0两个 Nuget 包(预览版):Swashbuckle.AspNetCore 和 Swashbuckle.AspNetCore.Filters,这里用的是新的类Openinfo

1468246-20190915155522040-670205363.png

2、修改后的完整代码

services.AddSwaggerGen(c =>
{
    typeof(ApiVersions).GetEnumNames().ToList().ForEach(version =>
    {
        // swagger文档配置
        c.SwaggerDoc(version, new OpenApiInfo
        {
            Version = version,
            Title = $"{ApiName} 接口文档",
            Description = $"{ApiName} HTTP API " + version,
            Contact = new OpenApiContact { Name = ApiName, Email = "[email protected]", Url = new Uri("https://www.jianshu.com/u/94102b59cc2a") },
            License = new OpenApiLicense { Name = ApiName, Url = new Uri("https://www.jianshu.com/u/94102b59cc2a") }
        });
        // 接口排序
        c.OrderActionsBy(o => o.RelativePath);
    });

    // 配置 xml 文档
    var xmlPath = Path.Combine(basePath, "Blog.Core.xml");
    c.IncludeXmlComments(xmlPath, true);
    var xmlModelPath = Path.Combine(basePath, "Blog.Core.Model.xml");
    c.IncludeXmlComments(xmlModelPath);

    c.OperationFilter<AddResponseHeadersFilter>();
    c.OperationFilter<AppendAuthorizeToSummaryOperationFilter>();
    // 很重要!这里配置安全校验,和之前的版本不一样
    c.OperationFilter<SecurityRequirementsOperationFilter>();

    // 开启 oauth2 安全描述,必须是 oauth2 这个单词
    c.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
    {
        Description = "JWT授权(数据将在请求头中进行传输) 直接在下框中输入Bearer {token}(注意两者之间是一个空格)\"",
        Name = "Authorization",
        In = ParameterLocation.Header,
        Type = SecuritySchemeType.ApiKey
    });
});

代码中,变化比较大的地方,我已经用红色标注,某些又做了注解,不过大部分的内容还是和之前是一样的,相信大家都能看得懂。

四、Autofac 部分

 关于依赖注入框架 Autofac 的变化,整体来说不是很大,主要是在依赖容器的使用上,在 2.2 的时候,我们是直接修改的的 ConfigureServices ,然后将容器实例给 return 出去,但是 3.0 之后,ConfigureServices 不能是返回类型了,只能是 void 方法,那我们就不用 return 出去了,官方给我们提供了一个服务提供上工厂,我们从这个工厂里拿,而不是将服务配置 return 出去。

1、代码修改对比图

 1、首先我们需要在 Program.cs 中的 CreateHostBuilder 中,添加Autofac的服务工厂:

1468246-20190915161251654-256634103.png

2、然后在 startup.cs 文件中,新建一个 ConfigureContainer(ContainerBuilder builder) 的方法,里边的内容就是我们之前写的 Autofac 的代码,把之前在 configureService 中的代码都删掉。

只不过我们这里是注入了 builder 的实例对象,不用new了,然后也不用 build 容器了,交给了 hotst 帮助我们一起 build。

1468246-20190915161120715-153680348.png

如果有任何看不懂的,请查看我的 Blog.Core 项目中的代码。

2、修改后的完整代码

 Startup.cs 中,新增 ConfigureContainer 方法,删除 ConfigureService中,所有有关 Autofac 的内容:

     public void ConfigureContainer(ContainerBuilder builder)
        {
            var basePath = Microsoft.DotNet.PlatformAbstractions.ApplicationEnvironment.ApplicationBasePath;

            //注册要通过反射创建的组件
            //builder.RegisterType<AdvertisementServices>().As<IAdvertisementServices>();
            builder.RegisterType<BlogCacheAOP>();//可以直接替换其他拦截器
            builder.RegisterType<BlogRedisCacheAOP>();//可以直接替换其他拦截器
            builder.RegisterType<BlogLogAOP>();//这样可以注入第二个

            // ※※★※※ 如果你是第一次下载项目,请先F6编译,然后再F5执行,※※★※※

            #region 带有接口层的服务注入

            #region Service.dll 注入,有对应接口
            //获取项目绝对路径,请注意,这个是实现类的dll文件,不是接口 IService.dll ,注入容器当然是Activatore
            try
            {
                var servicesDllFile = Path.Combine(basePath, "Blog.Core.Services.dll");
                var assemblysServices = Assembly.LoadFrom(servicesDllFile);//直接采用加载文件的方法  ※※★※※ 如果你是第一次下载项目,请先F6编译,然后再F5执行,※※★※※

                //builder.RegisterAssemblyTypes(assemblysServices).AsImplementedInterfaces();//指定已扫描程序集中的类型注册为提供所有其实现的接口。


                // AOP 开关,如果想要打开指定的功能,只需要在 appsettigns.json 对应对应 true 就行。
                var cacheType = new List<Type>();
                if (Appsettings.app(new string[] { "AppSettings", "RedisCachingAOP", "Enabled" }).ObjToBool())
                {
                    cacheType.Add(typeof(BlogRedisCacheAOP));
                }
                if (Appsettings.app(new string[] { "AppSettings", "MemoryCachingAOP", "Enabled" }).ObjToBool())
                {
                    cacheType.Add(typeof(BlogCacheAOP));
                }
                if (Appsettings.app(new string[] { "AppSettings", "LogAOP", "Enabled" }).ObjToBool())
                {
                    cacheType.Add(typeof(BlogLogAOP));
                }

                builder.RegisterAssemblyTypes(assemblysServices)
                          .AsImplementedInterfaces()
                          .InstancePerLifetimeScope()
                          .EnableInterfaceInterceptors()//引用Autofac.Extras.DynamicProxy;
                                                        // 如果你想注入两个,就这么写  InterceptedBy(typeof(BlogCacheAOP), typeof(BlogLogAOP));
                                                        // 如果想使用Redis缓存,请必须开启 redis 服务,端口号我的是6319,如果不一样还是无效,否则请使用memory缓存 BlogCacheAOP
                          .InterceptedBy(cacheType.ToArray());//允许将拦截器服务的列表分配给注册。 
                #endregion

                #region Repository.dll 注入,有对应接口
                var repositoryDllFile = Path.Combine(basePath, "Blog.Core.Repository.dll");
                var assemblysRepository = Assembly.LoadFrom(repositoryDllFile);
                builder.RegisterAssemblyTypes(assemblysRepository).AsImplementedInterfaces();
            }
            catch (Exception ex)
            {
                throw new Exception("※※★※※ 如果你是第一次下载项目,请先对整个解决方案dotnet build(F6编译),然后再对api层 dotnet run(F5执行),\n因为解耦了,如果你是发布的模式,请检查bin文件夹是否存在Repository.dll和service.dll ※※★※※" + ex.Message + "\n" + ex.InnerException);
            }
            #endregion
            #endregion


            #region 没有接口层的服务层注入

            ////因为没有接口层,所以不能实现解耦,只能用 Load 方法。
            ////注意如果使用没有接口的服务,并想对其使用 AOP 拦截,就必须设置为虚方法
            ////var assemblysServicesNoInterfaces = Assembly.Load("Blog.Core.Services");
            ////builder.RegisterAssemblyTypes(assemblysServicesNoInterfaces);

            #endregion

            #region 没有接口的单独类 class 注入
            ////只能注入该类中的虚方法
            builder.RegisterAssemblyTypes(Assembly.GetAssembly(typeof(Love)))
                .EnableClassInterceptors()
                .InterceptedBy(typeof(BlogLogAOP));

            #endregion

            //这里不要再 build 了
            //var ApplicationContainer = builder.Build();

        }

program.cs

 public static IHostBuilder CreateHostBuilder(string[] args) =>
   Host.CreateDefaultBuilder(args)
     .UseServiceProviderFactory(new AutofacServiceProviderFactory()) //<--NOTE THIS
     .ConfigureWebHostDefaults(webBuilder =>
     {
         webBuilder
           .UseStartup<Startup>()
           .UseUrls("http://localhost:8081")
           .ConfigureLogging((hostingContext, builder) =>
           {
               builder.ClearProviders();
               builder.SetMinimumLevel(LogLevel.Trace);
               builder.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
               builder.AddConsole();
               builder.AddDebug();
           });
     });

3、变化的核心点

就是将我们的Autofac的容器,从 configureService 中,转向了我们的宿主机中了,步骤是:

1、删除 ConfigureService 中的所有 Autofac 配置内容;

2、将刚刚删除的配置内容,拷贝到新建一个 ConfigureContainer  方法中;

3、在 ConfigureContainer 方法中,不要进行 build 操作,然后 Main 入口方法中的 Build() 去执行。

4、在 Program.cs 的 CreateHostBuilder 中,新增服务工厂实例。

好了,到现在,我们可以尝试看看 Autofac 依赖注入框架,已经可以正常的使用了。

五、Sqlsugar 部分

随着netcore 3.0 的更新,sqlsugar当然也要做相应的优化处理,主要是为了配合 3.0 做处理,作者凯旋兄还是很负责的,及时做了调整,目前 sqlsugar 的版本是 5.0.10 ,我们如果使用 netcore 3.0 的话,就必须要使用。

1、这个 5.0.10 的版本,如果不使用的话,可能会有一个映射错误:

1468246-20190916101937224-1239208336.png

 如果遇到了这个错误,直接不要问,更新到最新版本就行。

2、如果更新了以后,发现还有错误,一个《未将对象引用到对象的实例》:

1468246-20190916102109442-152676990.png

这个时候,你可以尝试重新生成下数据库,好像只需要创建下表结构就行,数据可以导入,记得做好生产环境数据库备份

其他还没有发现什么问题。

六、Authorization 部分

1、swagger是如何增加校验功能呢?

这个地方其实很简单,刚刚在讲 swagger 的时候,我也说到了,有一个地方需要我们注意, 就是安全校验的配置上,现在发生了变化,从服务添加变成了过滤器:

1468246-20190915164452523-1342932157.png

具体的代码,在上边讲 swagger 的时候,已经粘贴完整了,你可以直接复制即可。

注意一定要使用 "oauth2" 安全描述:

1468246-20191101233953165-805246771.png

2、接口上又是如何配置策略权限的呢?

之前我的 Blog.Core 项目使用了权限过滤器公约,这样就算 controller 没有配置 Authorize 的话,也会默认采用这种权限过滤器,感觉很方便。

但是现在不行了,必须要在每一个 controller/action 上配置,才能在 swagger 中出现那个 小锁 的标志,所以我又都在 controller 上,加上了 [Authorize(Permissions.Name)]

如果不配置的话,是没有小锁标志,也就不会启动权限认证的作用的,只有配置了的才有:

1468246-20190915173214156-347091029.png

3、修改授权处理器PermissionHandler

不用说其他的,直接看代码吧:

// 重写异步处理程序
        protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
        {
            var httpContext = _accessor.HttpContext;

            if (!requirement.Permissions.Any())
            {
                var data = await _roleModulePermissionServices.RoleModuleMaps();
                var list = (from item in data
                            where item.IsDeleted == false
                            orderby item.Id
                            select new PermissionItem
                            {
                                Url = item.Module?.LinkUrl,
                                Role = item.Role?.Id.ObjToString(),
                            }).ToList();
                requirement.Permissions = list;
            }

            //请求Url
            if (httpContext != null)
            {
                var questUrl = httpContext.Request.Path.Value.ToLower();
                //判断请求是否停止
                var handlers = httpContext.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
                foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync())
                {
                    if (await handlers.GetHandlerAsync(httpContext, scheme.Name) is IAuthenticationRequestHandler handler && await handler.HandleRequestAsync())
                    {
                        context.Fail();
                        return;
                    }
                }
                //判断请求是否拥有凭据,即有没有登录
                var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync();
                if (defaultAuthenticate != null)
                {
                    var result = await httpContext.AuthenticateAsync(defaultAuthenticate.Name);
                    //result?.Principal不为空即登录成功
                    if (result?.Principal != null)
                    {
                        // 将最新的角色和接口列表更新

                        // 这里暂时把代码移动到了Login获取token的api里,这样就不用每次都请求数据库,造成压力.
                        // 但是这样有个问题,就是如果修改了某一个角色的菜单权限,不会立刻更新,
                        // 需要让用户退出重新登录,如果你想实时更新,请把下边的注释打开即可.

                        //var data = await _roleModulePermissionServices.RoleModuleMaps();
                        //var list = (from item in data
                        //            where item.IsDeleted == false
                        //            orderby item.Id
                        //            select new PermissionItem
                        //            {
                        //                Url = item.Module?.LinkUrl,
                        //                Role = item.Role?.Name,
                        //            }).ToList();
                        //requirement.Permissions = list;

                        httpContext.User = result.Principal;

                        //权限中是否存在请求的url
                        //if (requirement.Permissions.GroupBy(g => g.Url).Where(w => w.Key?.ToLower() == questUrl).Count() > 0)
                        //if (isMatchUrl)
                        if (true)
                        {
                            // 获取当前用户的角色信息
                            var currentUserRoles = (from item in httpContext.User.Claims
                                                    where item.Type == "role"
                                                    select item.Value).ToList();

                            var isMatchRole = false;
                            var permisssionRoles = requirement.Permissions.Where(w => currentUserRoles.Contains(w.Role));
                            foreach (var item in permisssionRoles)
                            {
                                try
                                {
                                    if (Regex.Match(questUrl, item.Url?.ObjToString().ToLower())?.Value == questUrl)
                                    {
                                        isMatchRole = true;
                                        break;
                                    }
                                }
                                catch (Exception)
                                {
                                    // ignored
                                }
                            }

                            //验证权限
                            //if (currentUserRoles.Count <= 0 || requirement.Permissions.Where(w => currentUserRoles.Contains(w.Role) && w.Url.ToLower() == questUrl).Count() <= 0)
                            if (currentUserRoles.Count <= 0 || !isMatchRole)
                            {
                                context.Fail();
                                return;
                            }
                        }

                        //判断过期时间(这里仅仅是最坏验证原则,你可以不要这个if else的判断,因为我们使用的官方验证,Token过期后上边的result?.Principal 就为 null 了,进不到这里了,因此这里其实可以不用验证过期时间,只是做最后严谨判断)
                        if ((httpContext.User.Claims.SingleOrDefault(s => s.Type == "exp")?.Value) != null && DateHelper.StampToDateTime(httpContext.User.Claims.SingleOrDefault(s => s.Type == "exp")?.Value) >= DateTime.Now)
                        {
                            context.Succeed(requirement);
                        }
                        else
                        {
                            context.Fail();
                            return;
                        }
                        return;
                    }
                }
                //判断没有登录时,是否访问登录的url,并且是Post请求,并且是form表单提交类型,否则为失败
                if (!questUrl.Equals(requirement.LoginPath.ToLower(), StringComparison.Ordinal) && (!httpContext.Request.Method.Equals("POST") || !httpContext.Request.HasFormContentType))
                {
                    context.Fail();
                    return;
                }
            }

            context.Succeed(requirement);
        }

4、自定义状态返回

1、修改AddAuthentication服务注册

 标红的部分

 // 开启Bearer认证
 services.AddAuthentication(o=> {
     o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
     o.DefaultChallengeScheme = nameof(ApiResponseHandler);
     o.DefaultForbidScheme = nameof(ApiResponseHandler);
 })
   添加JwtBearer服务
  .AddJwtBearer(o =>
  {
      o.TokenValidationParameters = tokenValidationParameters;
      o.Events = new JwtBearerEvents
      {
          OnAuthenticationFailed = context =>
          {
              // 如果过期,则把<是否过期>添加到,返回头信息中
              if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
              {
                 context.Response.Headers.Add("Token-Expired", "true");
              }
              return Task.CompletedTask;
          }
      };
  })
  .AddScheme<AuthenticationSchemeOptions, ApiResponseHandler>(nameof(ApiResponseHandler), o => { });

2、添加Api返回对象

 public class ApiResponse
 {
     public int Status { get; set; } = 404;
     public object Value { get; set; } = "No Found";

     public ApiResponse(StatusCode apiCode, object msg = null)
     {
         switch (apiCode)
         {
             case StatusCode.CODE401:
                 {
                     Status = 401;
                     Value = "很抱歉,您无权访问该接口,请确保已经登录!";
                 }
                 break;
             case StatusCode.CODE403:
                 {
                     Status = 403;
                     Value = "很抱歉,您的访问权限等级不够,联系管理员!";
                 }
                 break;
             case StatusCode.CODE500:
                 {
                     Status = 500;
                     Value = msg;
                 }
                 break;
         }
     }
 }

 public enum StatusCode
 {
     CODE401,
     CODE403,
     CODE404,
     CODE500
 }

3、添加api响应处理器

  public class ApiResponseHandler : AuthenticationHandler<AuthenticationSchemeOptions>
  {
      public ApiResponseHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
      {
      }

      protected override Task<AuthenticateResult> HandleAuthenticateAsync()
      {
          throw new NotImplementedException();
      }
      protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
      {
          Response.ContentType = "application/json";
          Response.StatusCode = StatusCodes.Status401Unauthorized;
          await Response.WriteAsync(JsonConvert.SerializeObject(new ApiResponse(StatusCode.CODE401)));
      }

      protected override async Task HandleForbiddenAsync(AuthenticationProperties properties)
      {
          Response.ContentType = "application/json";
          Response.StatusCode = StatusCodes.Status403Forbidden;
          await Response.WriteAsync(JsonConvert.SerializeObject(new ApiResponse(StatusCode.CODE403)));
      }

  }

七、JSON 部分

 1、接口返回格式

 在netcore 3.0 中,它内置了一个 json 工具—— System.Text.Json,而作为改善 ASP.NET Core 共享框架的工作的一部分,已从 ASP.NET Core 共享框架中删除Json.NET 。 如果你的应用程序使用Newtonsoft.Json特定的功能(如 JsonPatch 或转换器),或者如果它是特定于格式 Newtonsoft.Json的类型,那我们就需要重新引用它。

简单来说,就是 3.0 内置了 Text.Json 框架,你可以直接使用,但是我没有用这个,因为我好像中间出现了一个序列化错误,而且我还要取消默认的驼峰命名,所以我还是采用的之前的 Newtonsoft.json,具体的使用方法请看:

1、如果使用 .net core 3.0 内置的 System.Text.Json ,配置方法如下:

services.AddMvc().AddJsonOptions(options =>
{
    options.JsonSerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All);
    options.JsonSerializerOptions.PropertyNamingPolicy = null;
});

2、如果使用 Newtonsoft.Json ,配置方法如下:

services.AddControllers()
    .AddNewtonsoftJson(options =>
        options.SerializerSettings.ContractResolver = new DefaultContractResolver());

 2、中文乱码问题

原理就不多说了,直接上代码

public string[] ReturnChinese()
{
    var jsonObject = new { chinese = "老张的哲学" };
    string aJsonString = Newtonsoft.Json.JsonConvert.SerializeObject(value: jsonObject);
    string bJsonString = System.Text.Json.JsonSerializer.Serialize(
        value: jsonObject,
        options: new System.Text.Json.JsonSerializerOptions
    {
        Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
    });
        
      return new string[]{aJsonString,bJsonString};
    
}

八、SignalR 部分

这个很简单,官方中间件取消了  UseSignalR 中间件,而放到了  UseEndpoints 短路中间件中,配置如下:

 app.UseRouting();
 app.UseEndpoints(endpoints =>
 {
     endpoints.MapHub<ChatHub>("/api2/chatHub");

     endpoints.MapControllerRoute(
         name: "default",
         pattern: "{controller=Home}/{action=Index}/{id?}");
 });

在使用中,会出现json格式的问题,这里可以做下修改,依然使用 Newtonsoft:

1、修改服务注册       services.AddSignalR().AddNewtonsoftJsonProtocol();
2、引用 nuget 包  Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson

九、CORS 部分

 CORS变化其实不大,整体来说和 2.2 一样的,具体的按照 之前的写法来写就行。

只是已经不支持在使用 AllowCredentials 的时候向所有域名开放了,感谢网友提醒@wswind,会有错误提示:

The CORS protocol does not allow specifying a wildcard (any) origin and credentials at the same time. Configure the CORS policy by listing individual origins if credentials needs to be supported.”

所以下边的 Policy 可以删除了,其他的不用变化:

  c.AddPolicy("AllRequests", policy =>
  {
      policy
      .AllowAnyOrigin()//允许任何源
      .AllowAnyMethod()//允许任何方式
      .AllowAnyHeader()//允许任何头
      .AllowCredentials();//允许cookie
  });

然后就是要注意中间件的顺序,这里记得还要带上 policy 的名称 ,还是 app.UseCors("LimitRequests");:

1468246-20190915162534702-2038743046.png

十、MiniProfiler 部分

服务注册:

  services.AddMiniProfiler(options =>
  {
      options.RouteBasePath = "/profiler";
      options.PopupRenderPosition = StackExchange.Profiling.RenderPosition.Left;
      options.PopupShowTimeWithChildren = true;
  });

1、升级nuget包,并配合修改js调用

这里还有一个问题,就是最新版的nuget包情况下,会报一个错误:

1468246-20190926183423235-1319439489.png

这是一个小问题,主要是因为 netcore 3.0 升级以后,miniprofiler 之前版本对 System.Text.json 没有很好的兼容导致,但是官方新版本已经更新,我们只需要升级下版本即可:

1、升级 MiniProfiler 到最新版本 4.1.0;

2、swagger 的 index.html 文件中,对 MiniProfiler 的js引用也要同步更新;

3、保证当前 id 不能为空,可以自己随意定义一个Guid值;

最终的代码是这样的:

<!--1、版本号要与nuget包一致;2、id不能为空-->
<script async="async" id="mini-profiler" src="/profiler/includes.min.js?v=4.1.0+c940f0f28d"
        data-version="4.1.0+c940f0f28d" data-path="/profiler/"
        data-current-id="4ec7c742-49d4-4eaf-8281-3c1e0efa8888" data-ids="4ec7c742-49d4-4eaf-8281-3c1e0efa8888"
        data-position="Left"
        data-authorized="true" data-max-traces="5" data-toggle-shortcut="Alt+P"
        data-trivial-milliseconds="2.0" data-ignored-duplicate-execute-types="Open,OpenAsync,Close,CloseAsync">
</script>

多参考官网开源项目:https://github.com/MiniProfiler/dotnet/tree/master/samples

其他错误:

1、缺少服务注册

InvalidOperationException: Error while validating the service descriptor 'ServiceType: Microsoft.Extensions.Options.IConfigureOptions`1[StackExchange.Profiling.MiniProfilerOptions] Lifetime: Singleton ImplementationType: Microsoft.Extensions.DependencyInjection.MiniProfilerOptionsDefaults': Unable to resolve service for type 'Microsoft.Extensions.Caching.Memory.IMemoryCache' while attempting to activate 'Microsoft.Extensions.DependencyInjection.MiniProfilerOptionsDefaults'

直接把红色的服务注入就行了。

2、注入IMemoryCache服务  

这是一个小知识点,之前也没有发现,我们使用 MiniProfiler 的时候,那些数据其实是存放到内存里的,因此用到了IMemoryCache,所以我们必须要对其进行注入,

之前2.2版本的时候,我们直接使用的是  services.AddMvc(); 这里已经包含了对内存接口的注入,但是升级3.0以后,我们可能会仅仅使用更轻量级的 services.AddControllers();这里是没有IMemoryCache服务的,所以我们必须单独注入:

services.AddSingleton<IMemoryCache>(factory =>
{
    var cache = new MemoryCache(new MemoryCacheOptions());
    return cache;
});

当然,你也可以直接使用 services.AddMvc();效果是一样的。

十一、其他补充中

如果你有其他的用到的,是我没有使用到的, 或者我上文没有提到的注意点,

欢迎想问提问和反馈,我会在这里,给你署名写上,让更多的小伙伴可以学会学号。

1、守墓老人

#37楼 2019-12-29 05:07 守墓老人

win server2008r2 安装.net core 安装了sdk
控制台输入dotnet --version 提示not found 看了环境变量都有
服务器重启了两次也一样,搜索到一个答案那个网友名称忘记了(实在抱歉)
环境变量C:\Program Files\dotnet\ 一定要在C:\Program Files (x86)\dotnet\;前面 自动生成的却刚好相反 (真鸡儿疼)问题解决
dotnet-hosting-3.1.0-win.exe 安装导致老项目的程序池停止
安装 vc_redist.x64.exe 最后还是停止,其他网上方法试了很多种 省略,
最后发现老项目都启用了32位应用程序,改成false就好了

END、Github && Gitee

https://github.com/anjoy8/Blog.Core

https://gitee.com/laozhangIsPhi/Blog.Core

相关文章:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK