|
前言:前段时间根据 [老张的哲学] 大佬讲解的视频做的笔记,讲的很不错。此文主要记录JWT/DI依赖注入/AOP面向切面编程/DTO/解决跨域等相关知识,还包含一些.NET Core项目实战的一些案例。我是西瓜程序猿,感谢大家的支持!
一、ASP.NET Core基础
1.1-.NET Core概述
1.1.1-.NET Croe简介
(1)为什么要学习.NET Core?
.NET Core是为了重新启动某些Framework组件而为其他人提供平台工作的机会,由于.NET Framework主要以委托(C#)代码位基础构建了因此这些部分不需要改代码即可移至.NET Core。
(2).NET Core运用的多吗?
微信支付、网易游戏、三星电子、Adobe、Stackoverflow等
(3)什么是.NET Core?
跨平台、自托管、开源、高性能,.NETCore是基于Kestrel执行的控制台程序。
(4)中间件的执行过程?
(5)中间件3种写法?
1.1.2-进程内托管 和 进程外托管
1.2-JWT详解
1.2.1-为什么要保护API?
- 防泄漏
- 防攻击
- 防伪装攻击(案例:在公共网络环境里,第三方有意或者无意的调用我们的接口)
- 放篡改工具(案例:在公共网络环境里,请求头/查询字符串/内容在传输过程中被修改)
- 防重放工具(案例:在公共环境网络里,请求呗捕获后,稍后被重放或者多次)
- 收益化:将数据进行开发,然后进行付费才能使用。
1.2.2-设计原则
- 轻量级
- 易于开发、测试和部署
- 适合于异构系统(跨操作系统、多语言简易实现)
- 所有写操作接口(增删改查)
- 非公开的读接口(如:泄密/敏感/隐私等)
1.2.3-加密算法
- ES:对称可逆加密算法
- RSA:非对称可逆算法
- Base64
- MD5:不可逆加密
1.2.4-有哪些方式保护方式
1.2.5-JWT的好处?
- 服务无状态
- 解决跨域问题:这是基于Token的访问策略可以克服Cookie的跨域问题。
- 服务端无状态可以横向扩展,Token可完成认证,不许存储Seesion
- 系统解耦,Token携带所有的用户信息,无需绑定一个特定的认证方案,只需要知道加密的方法和秘钥就可以进行加密解密,有利于解耦。
- 防止跨站点脚本攻击,没有Cokkie技术,无需考虑跨站请求的安全问题。
1.2.6-如何使用JWT?
1.3-DI依赖注入(IOC的思想)
1.3.1-相关基本概念
- 他距离我们不遥远:类和对象——实例化的过程
- 依赖注入实现了new,不是为了省一个new
- 解除依赖的思想是如何产生的。
- 控制反装:
- 是一种设计原则,最早由Martin Fowler提出,因为其理论提出时间和成熟时间比较晚,所以并没有包含在Gof的《设计模式》中。
- 对依赖的控制从自己,转到自己的调用者上。
- 依赖注入是实现控制反转思想的一种实现(不要弄混淆了),依赖的对象注入到调用者,你不应该自己创建他,而是通过由你的调用者给你——容器。
1.3.2-常见的依赖注入有哪些?
- 微软自带的DI(建议使用)
- Autofac:貌似目前.NET、.NET Core下用的最多(建议使用)
- Ninject:目前好像没什么人用了
- Unity:也是比较常用
1.3.3-三种注入方法
- 构造函数注入——遵循显示依赖原则
- 属性注入
- 方法注入
1.3.4-三种声明周期-注册要保持一致
- AddTransient:(服务级别的)每次注入或请求时都会转瞬即逝的服务(每次访问Services都会创建一个实例对象)。
- AddScoped:(会话级别的)是按照范围创建的,在Web应用程序中,每个Web请求都会创建一个新的服务范围(每次访问的HTTP范围内使用的是同一个对象,然后到下一个接口又会产生一个新的对象)。
- AddSingleton:(容器/项目级别的)每个DI容器创建一个单例服务,通常意味着他们在每个应用程序只创建一次,然后用于整个应用程序生命周期。
1.3.5-为什么要使用依赖注入?
- 依赖注入实现了new,不是为了省去一个new!
- 修改配置文件不需要重启服务器(配置到文件中)。
- 减少代码量,更灵活。
- 防止重构——参数的变化
- 忽略内部复杂依赖(不管我依赖的内容里面又依赖了什么,这个咱们不管的)
- 项目代码松耦合
- 管理生命周期——防止内存泄漏
- 单元测试——Mock
- 方便进行代理——AOP
1.3.6-依赖注入步骤
1.4-AOP面向切面编程(思想)
1.4.1-什么是AOP?
- AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译的方式和运行代码实现程序功能的同意维护的一种技术。
- AOP是OOP(面向对象程序设计)的延续。是软件开发中的一个热点,是函数式编程的一种衍生泛型,利用AOP可以对业务逻辑的各个部分进行隔离,从而是业务逻辑各部分之间进行隔离,从而是业务逻辑个部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
1.4.2-AOP的特点?
- 分散的
- 毫无关联的(有关联、可拔插)
- 却是又有联系的
- 关注点的不同导致切面的产生。
1.4.3-思想的来源
- 面向过程编程——POP——c/c++:关注的是过程和行为,动作步骤
- 面向对象编程——OOP——封装、继承、多态等概念来简历一种对象层次结构,用已模拟公共行为的一个集合。当我们需要分散的对象引入公共行为的时候,OOP则显得无能为力:具备功能的对象、分工(解决面向过程总耦合的问题、面向过程关注的是事件,太多太复杂)。
- 面向抽象编程——接口
- Gof四人23中设计模式——还是OOP的方位
- 面向切面编程:解决面向对象编程中,出现的那些不是自己公共行文的一些行为
1.4.4-AOP只是流程
- Join Point:连接点、关注点
- Pointcut:切点——我们日志功能和服务相交叉的点。
- Aspect:切面——日志记录、异常处理就是一个一个的切面。
- Advice:通知——访问前、中、后的三个步骤
1.4.5-AOP应用场景(不是业务逻辑的逻辑、是公共逻辑)
1.4.6-多种思想应用的区别
1.4.7-AOP有哪些优势?
- 隔离:核心和关注点,减少关联的错误。
- 可以深入到具体的对象内部,各种操作。
- 动态修改静态的OO模型,快速迭代。
1.4.8-AOP的使用
1.5-DTO与多模型
1.5.1-什么是DTO?
数据传输对象(DTO全称为Data Transfer Object):是一种设计模式之间传输数据的软件应用系统。数据传输对象往往是数据访问对象从数据库检索数据。数据传输对象与数据交互对象或数据访问对象之间的差异是一个以下不具有任何行为除了存储和检索的数据(范文和存取器)。
1.5.2-为什么要使用数据传输对象DTO?
- 简洁直观
- 数据安全性:业务和UI解耦,防止错误——表现层不引用Model层的东西
- 更精简——领域模型带有业务
- 效率更好——扁平化
- 跨平台——多平台的字符类型不一致
1.5.3-DTO和ViewModel(视图模型)是一回事么?
- 相同点:
- Dto和ViewModel都是将数据从数据库中拿出来进行前端展示的。
- 不同点:
- ViewModel视图模型比DTO数据模型更靠近页面。
1.5.5-多种模型概论
- BO(Business object)——业务对象:主要作用是把业务逻辑封装成一个对象,这个对象可以包括一个或者多个其他的对象。
- DAO(data sccess object)——数据访问对象
- DO(domain object)——领域对象
- DTO(data tansfer object)——数据传输对象
- PO(persistant object)持有对象——从db拿出来的展示数据,没有操作
- POCO(plain old CLR Object)——简单无规则CLR对象
- VO(View Object)——视图对象:用于展示层,他的作用就是吧某个指定页面(组件)的所有数据封装起来。
1.5.6-如何使用DTO?
1.6-跨域
1.6.1-跨域的相关概念
域名组成:
1.6.2-JSONP
1.6.3-Proxy--接口级别
1.6.4-Cors--项目级别
1.6.5-Nginx
1.6.6-Socket--非HTTP请求
1.6.7-其他跨域操作
二、ASP.NET Core项目实战案例
2.1-Autofac依赖注入
(1)西瓜程序猿创建的项目结构如下:
项目相关依赖如下:
(2)在【Autofac依赖注入】层导入相关Nuget包:
(3)在Program.cs使用Autofac工厂:
(4)在AutofacModuleRegister写入以下代码,继承自Modele,并重写Load方法。- protected override void Load(ContainerBuilder builder) { var basePath = AppContext.BaseDirectory; #region 带有接口层的服务注入 var servicesDllFile = Path.Combine(basePath, "Autofac.Service.dll"); var repositoryDllFile = Path.Combine(basePath, "Autofac.Repository.dll"); if (!(File.Exists(servicesDllFile) && File.Exists(repositoryDllFile))) { var msg = "Repository.dll和service.dll 丢失,因为项目解耦了,所以需要先F6编译,再F5运行,请检查 bin 文件夹,并拷贝。"; throw new Exception(msg); } // 获取 Service.dll 程序集服务,并注册 var assemblysServices = Assembly.LoadFrom(servicesDllFile); builder.RegisterAssemblyTypes(assemblysServices) .AsImplementedInterfaces() .InstancePerDependency(); // 获取 Repository.dll 程序集服务,并注册 var assemblysRepository = Assembly.LoadFrom(repositoryDllFile); builder.RegisterAssemblyTypes(assemblysRepository) .AsImplementedInterfaces() .PropertiesAutowired() .InstancePerDependency(); #endregion }
复制代码 (5)在控制器中使用构造函数依赖注入。
(6)然后编译项目,最后运行项目试试,可以发现报错了,因为Repository.dll和service.dll 丢失,因为项目解耦了。解决方案如下:
将这下面2个实现层输出位置如下:
2.2-AutoMapper对象映射
(1)引入包。
(2)创建对应的数据实体,和要返回的视图模型实体。
实体:
视图模型实体:
(3)创建一个【CustomProfile】类,需要继承自【Profile】,用来创建关系映射。
(4)创建一个【AutoMapperConfig】类,用来静态全局 AutoMapper 配置文件。
(5)创建一个【AutoMapperSetup】类,用于Automapper的启动服务。
(6)然后在【Startup.cs】的ConfigreServices方法注册相关服务。
2.3-IP Limit限流解析
2.3.1-什么是限流?
2.3.2-时间窗口算法
2.3.3-漏斗算法
2.3.4-令牌算法
2.3.5-时间窗口(代码实现)
(1)安装Nuget包。
(2)创建【IpPolicyRateLimitSetup】类,用于限流,启用服务。
(3)在【Startup.cs】的ConfigreServices方法注册相关服务。
(4)在【appsettings.json】中根节点写入一下配置。- "IpRateLimiting": { "EnableEndpointRateLimiting": true, //false: 全局执行API——true:针对每个API "StackBlockedRequests": false, //False:应在另一个计数器上记录拒绝次数 "RealIpHeader": "X-Real-IP", "ClientIdHeader": "X-ClientId", "IpWhitelist": [], //添加白名单 "EndpointWhitelist": [ "get:/api/xxx", "*:/api/yyy" ],//添加API的白名单 "ClientWhitelist": [ "dev-client-1", "dev-client-2" ],//添加客户端的白名单 "QuotaExceededResponse": { "Content": "{{"status":429,"msg":"时间限流:访问过于频繁,请稍后重试","success":false}}", "ContentType": "application/json", "StatusCode": 429 }, "HttpStatusCode": 429, //返回状态码 "GeneralRules": [ //api规则,结尾一定要带* { //针对blog的API,1分钟最多访问20次 "Endpoint": "*:/api/blog*", //规则 "Period": "1m", //时间 "Limit": 20 //次数 }, { //无论什么API,1秒最能请求3次 "Endpoint": "*/api/*", "Period": "1s", "Limit": 3 }, { //无论什么API,1分钟最能请求30次 "Endpoint": "*/api/*", "Period": "1m", "Limit": 30 }, { //无论什么API,12小时秒最能请求50次 "Endpoint": "*/api/*", "Period": "12h", "Limit": 500 } ] }
复制代码 (5)写一个中间件,用于IP限流。
(6)在【Startup.cs】的Configure中配置中间件。
(7)平凡请求接口时。
2.4-CORS跨域与"钓鱼"
2.4.1-没有跨域会导致什么问题?
2.4.2-CORS跨域代码实现
(1)安装Nuget包。
(2)创建【CorsSetup】类,用于限流,启用服务。
(3)在【Startup.cs】的ConfigreServices方法注册相关服务。
(4)在【appsettings.json】中根节点写入一下配置。- "Cors": { "PolicyName": "CorsIpAccess", //策略名称 "EnableAllIPs": true, //当为true时,开放所有IP均可访问。 // 支持多个域名端口,注意端口号后不要带/斜杆:比如localhost:8000/,是错的 // 注意,http://127.0.0.1:1818 和 http://localhost:1818 是不一样的 "IPs": "http://127.0.0.1:2364,http://localhost:2364" },
复制代码 (5)在【Startup.cs】的Configure中配置中间件。- // CORS跨域app.UseCors(Appsettings.app(new string[] { "Startup", "Cors", "PolicyName" }));
复制代码 2.5-Swagger接口文文档
(1)导入Nuget包。
(2)创建【SwaggerSetup】类,启动Swagger服务。- /// /// Swagger 启动服务 /// public static class SwaggerSetup { private static readonly ILog log =LogManager.GetLogger(typeof(SwaggerSetup)); public static void AddSwaggerSetup(this IServiceCollection services) { if (services == null) throw new ArgumentNullException(nameof(services)); //获取项目的根路径 var basePath = AppContext.BaseDirectory; //获取项目名 var ApiName = Appsettings.app(new string[] { "Startup", "ApiName" }); services.AddSwaggerGen(c => { //遍历出全部的版本,做文档信息展示 typeof(ApiVersions).GetEnumNames().ToList().ForEach(version => { c.SwaggerDoc(version, new OpenApiInfo { Version = version, //RuntimeInformation.FrameworkDescription:运行时版本 Title = $"{ApiName} 西瓜程序猿 - 接口文档——{RuntimeInformation.FrameworkDescription}", Description = $"{ApiName} HTTP API " + version, //Contact:联系 //License:声明 }); c.OrderActionsBy(o => o.RelativePath);//接口排序 }); try { //这个就是刚刚配置的xml文件名【文档API的注释】 var xmlPath = Path.Combine(basePath, "Blog.Core.xml"); //默认的第二个参数是false,这个是controller的注释,记得修改 c.IncludeXmlComments(xmlPath, true); //这个就是Model层的xml文件名【Model相关的注释】 var xmlModelPath = Path.Combine(basePath, "Blog.Core.Model.xml"); c.IncludeXmlComments(xmlModelPath); } catch (Exception ex) { log.Error("Blog.Core.xml和Blog.Core.Model.xml 丢失,请检查并拷贝。\n" + ex.Message); } // 开启加权小锁 c.OperationFilter(); c.OperationFilter(); // 在header中添加token,传递到后台 c.OperationFilter(); // ids4和jwt切换 if (Permissions.IsUseIds4) { //接入identityserver4 c.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme { Type = SecuritySchemeType.OAuth2, Flows = new OpenApiOAuthFlows { Implicit = new OpenApiOAuthFlow { AuthorizationUrl = new Uri($"{Appsettings.app(new string[] { "Startup", "IdentityServer4", "AuthorizationUrl" })}/connect/authorize"), Scopes = new Dictionary { { "blog.core.api","ApiResource id" } } } } }); } else { // Jwt Bearer 认证,必须是 oauth2 c.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme { Description = "描述:西瓜程序猿 - JWT授权(数据将在请求头中进行传输) 直接在下框中输入Bearer {token}(注意两者之间是一个空格)"", Name = "Authorization",//jwt默认的参数名称 In = ParameterLocation.Header,//jwt默认存放Authorization信息的位置(请求头中) Type = SecuritySchemeType.ApiKey }); } }); } } /// /// 自定义版本 /// public class CustomApiVersion { /// /// Api接口版本 自定义 /// public enum ApiVersions { /// /// V1 版本 /// V1 = 1, /// /// V2 版本 /// V2 = 2, } }
复制代码 (3)在【Startup.cs】的ConfigreServices方法注册相关服务。
(4)创建一个【UseSwaggerMildd】类,开启并处理Swagger中间件。- private static readonly ILog log = LogManager.GetLogger(typeof(SwaggerMildd)); public static void UseSwaggerMildd(this IApplicationBuilder app, Func streamHtml) { if (app == null) throw new ArgumentNullException(nameof(app)); app.UseSwagger(); app.UseSwaggerUI(c => { //根据版本名称倒序 遍历展示 var ApiName = Appsettings.app(new string[] { "Startup", "ApiName" }); typeof(ApiVersions).GetEnumNames().OrderByDescending(e => e).ToList().ForEach(version => { c.SwaggerEndpoint($"/swagger/{version}/swagger.json", $"{ApiName} {version}"); }); c.SwaggerEndpoint($"https://petstore.swagger.io/v2/swagger.json", $"{ApiName} pet"); // 将swagger首页,设置成我们自定义的页面,记得这个字符串的写法:{项目名.index.html} if (streamHtml.Invoke() == null) { var msg = "index.html的属性,必须设置为嵌入的资源"; log.Error(msg); throw new Exception(msg); } c.IndexStream = streamHtml; if (Permissions.IsUseIds4) { c.OAuthClientId("blogadminjs"); } // 路径配置,设置为空,表示直接在根域名(localhost:8001)访问该文件,注意localhost:8001/swagger是访问不到的,去launchSettings.json把launchUrl去掉,如果你想换一个路径,直接写名字即可,比如直接写c.RoutePrefix = "doc"; c.RoutePrefix = ""; }); }
复制代码 (5)在【Startup.cs】中配置中间件,注意顺序。
(6)index.html记得设置为【嵌套的资源】
2.6-MiniProfiler性能分析
(1)导入Nuget包。
(2)创建【AddMiniProfilerSetup】类,启动Swagger服务。- public static void AddMiniProfilerSetup(this IServiceCollection services) { if (services == null) throw new ArgumentNullException(nameof(services)); if(Appsettings.app(new string[] { "Startup", "MiniProfiler", "Enabled" }).ObjToBool()) { services.AddMiniProfiler(); } // 3.x使用MiniProfiler,必须要注册MemoryCache服务 // services.AddMiniProfiler(options => // { // options.RouteBasePath = "/profiler"; // //(options.Storage as MemoryCacheStorage).CacheDuration = TimeSpan.FromMinutes(10); // options.PopupRenderPosition = StackExchange.Profiling.RenderPosition.Left; // options.PopupShowTimeWithChildren = true; // // 可以增加权限 // //options.ResultsAuthorize = request => request.HttpContext.User.IsInRole("Admin"); // //options.UserIdProvider = request => request.HttpContext.User.Identity.Name; // } //); }
复制代码 (3)在【Startup.cs】的ConfigreServices方法注册相关服务。
(4)创建一个【UseMiniProfilerMildd】类,开启并处理Swagger中间件。- private static readonly ILog log = LogManager.GetLogger(typeof(MiniProfilerMildd)); public static void UseMiniProfilerMildd(this IApplicationBuilder app) { if (app == null) throw new ArgumentNullException(nameof(app)); try { if (Appsettings.app("Startup", "MiniProfiler", "Enabled").ObjToBool()) { // 性能分析 app.UseMiniProfiler(); } } catch (Exception e) { log.Error($"An error was reported when starting the MiniProfilerMildd.\n{e.Message}"); throw; } }
复制代码 (5)在【Startup.cs】中配置中间件,注意顺序。
2.7-StackEx.Redis安装与使用
(1)导入Nuget包。
(2)创建【RedisCacheSetup】类,启动Redis服务。
(3)在【Startup.cs】的ConfigreServices方法注册相关服务。
(4)创建一个名为【IRedisCacheManager】接口,用于Redis缓存。- /// /// Redis缓存接口 /// public interface IRedisCacheManager { //获取 Reids 缓存值 string GetValue(string key); //获取值,并序列化 TEntity Get(string key); //保存 void Set(string key, object value, TimeSpan cacheTime); //判断是否存在 bool Get(string key); //移除某一个缓存值 void Remove(string key); //全部清除 void Clear(); }
复制代码 (5)创建一个【RedisCacheManager】类,并实现【IRedisCacheManager】接口- public class RedisCacheManager : IRedisCacheManager { private readonly string redisConnenctionString; public volatile ConnectionMultiplexer redisConnection; private readonly object redisConnectionLock = new object(); public RedisCacheManager() { string redisConfiguration = Appsettings.app(new string[] { "AppSettings", "RedisCachingAOP", "ConnectionString" });//获取连接字符串 if (string.IsNullOrWhiteSpace(redisConfiguration)) { throw new ArgumentException("Redis配置为空", nameof(redisConfiguration)); } this.redisConnenctionString = redisConfiguration; this.redisConnection = GetRedisConnection(); } /// /// 核心代码,获取连接实例 /// 通过双if 夹lock的方式,实现单例模式 /// /// private ConnectionMultiplexer GetRedisConnection() { //如果已经连接实例,直接返回 if (this.redisConnection != null && this.redisConnection.IsConnected) { return this.redisConnection; } //加锁,防止异步编程中,出现单例无效的问题 lock (redisConnectionLock) { if (this.redisConnection != null) { //释放redis连接 this.redisConnection.Dispose(); } try { var config = new ConfigurationOptions { AbortOnConnectFail = false, AllowAdmin = true, ConnectTimeout = 15000,//改成15s SyncTimeout = 5000, //Password = "Pwd",//Redis数据库密码 EndPoints = { redisConnenctionString }// connectionString 为IP:Port 如”192.168.2.110:6379” }; this.redisConnection = ConnectionMultiplexer.Connect(config); } catch (Exception) { throw new Exception("Redis服务未启用,请开启该服务,并且请注意端口号,本项目使用的的6319,而且我的是没有设置密码。"); } } return this.redisConnection; } /// /// 清除 /// public void Clear() { foreach (var endPoint in this.GetRedisConnection().GetEndPoints()) { var server = this.GetRedisConnection().GetServer(endPoint); foreach (var key in server.Keys()) { redisConnection.GetDatabase().KeyDelete(key); } } } /// /// 判断是否存在 /// /// /// public bool Get(string key) { return redisConnection.GetDatabase().KeyExists(key); } /// /// 查询 /// /// /// public string GetValue(string key) { return redisConnection.GetDatabase().StringGet(key); } /// /// 获取 /// /// /// /// public TEntity Get(string key) { var value = redisConnection.GetDatabase().StringGet(key); if (value.HasValue) { //需要用的反序列化,将Redis存储的Byte[],进行反序列化 return SerializeHelper.Deserialize(value); } else { return default(TEntity); } } /// /// 移除 /// /// public void Remove(string key) { redisConnection.GetDatabase().KeyDelete(key); } /// /// 设置 /// /// /// /// public void Set(string key, object value, TimeSpan cacheTime) { if (value != null) { //序列化,将object值生成RedisValue redisConnection.GetDatabase().StringSet(key, SerializeHelper.Serialize(value), cacheTime); } } /// /// 增加/修改 /// /// /// /// public bool SetValue(string key, byte[] value) { return redisConnection.GetDatabase().StringSet(key, value, TimeSpan.FromSeconds(120)); } }
复制代码 (6)修改【appsetting.json】配置文件。
(4)创建一个【UseSwaggerMildd】类,开启并处理Swagger中间件。- private static readonly ILog log = LogManager.GetLogger(typeof(SwaggerMildd)); public static void UseSwaggerMildd(this IApplicationBuilder app, Func streamHtml) { if (app == null) throw new ArgumentNullException(nameof(app)); app.UseSwagger(); app.UseSwaggerUI(c => { //根据版本名称倒序 遍历展示 var ApiName = Appsettings.app(new string[] { "Startup", "ApiName" }); typeof(ApiVersions).GetEnumNames().OrderByDescending(e => e).ToList().ForEach(version => { c.SwaggerEndpoint($"/swagger/{version}/swagger.json", $"{ApiName} {version}"); }); c.SwaggerEndpoint($"https://petstore.swagger.io/v2/swagger.json", $"{ApiName} pet"); // 将swagger首页,设置成我们自定义的页面,记得这个字符串的写法:{项目名.index.html} if (streamHtml.Invoke() == null) { var msg = "index.html的属性,必须设置为嵌入的资源"; log.Error(msg); throw new Exception(msg); } c.IndexStream = streamHtml; if (Permissions.IsUseIds4) { c.OAuthClientId("blogadminjs"); } // 路径配置,设置为空,表示直接在根域名(localhost:8001)访问该文件,注意localhost:8001/swagger是访问不到的,去launchSettings.json把launchUrl去掉,如果你想换一个路径,直接写名字即可,比如直接写c.RoutePrefix = "doc"; c.RoutePrefix = ""; }); }
复制代码 (5)在【Startup.cs】中配置中间件,注意顺序。
原文链接:https://www.cnblogs.com/kimiliucn/p/17641252.html
来源:https://www.cnblogs.com/kimiliucn/archive/2023/08/18/17641252.html
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作! |
|