diff --git a/handbook/.gitignore b/handbook/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..50b2292ed58926d9b0822c819e593b31159a4014 --- /dev/null +++ b/handbook/.gitignore @@ -0,0 +1,20 @@ +# Dependencies +node_modules + +# Production +build + +# Generated files +.docusaurus +.cache-loader + +# Misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/handbook/README.md b/handbook/README.md new file mode 100644 index 0000000000000000000000000000000000000000..85593a25d0f22568467586c8d1e245d9a6ff5f25 --- /dev/null +++ b/handbook/README.md @@ -0,0 +1,17 @@ +文档基于 [https://www.docusaurus.io/](https://www.docusaurus.io/) 构建。 + +### 本地运行 + +```bash +# 安装依赖 +npm install + +# 本地浏览 +npm run start +``` + +### 发布部署 + +```bash +npm run build +``` diff --git a/handbook/babel.config.js b/handbook/babel.config.js new file mode 100644 index 0000000000000000000000000000000000000000..e00595dae7d69190e2a9d07202616c2ea932e487 --- /dev/null +++ b/handbook/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: [require.resolve('@docusaurus/core/lib/babel/preset')], +}; diff --git a/handbook/blog/2021-02-01-httpcontext.mdx b/handbook/blog/2021-02-01-httpcontext.mdx new file mode 100644 index 0000000000000000000000000000000000000000..afb698dba7684ee2c5a498c567a1684538c1bd86 --- /dev/null +++ b/handbook/blog/2021-02-01-httpcontext.mdx @@ -0,0 +1,79 @@ +--- +slug: httpcontext +title: 1. HttpContext 应用 +author: dotNET China +author_title: 让 .NET 开发更简单,更通用,更流行。 +author_url: https://gitee.com/dotnetchina +author_image_url: https://i.loli.net/2021/01/19/M8q5a3OTZWUKicl.png +tags: [furion, furos, .net, .netcore, .net5, httpcontext] +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +## HttpContext 重大调整 + +在 `ASP.NET` 的时代,我们通常通过 `HttpContext` 全局静态类获取请求上下文,但在 `ASP.NET Core` 中,`HttpContext` 是一个非静态的抽象类,无法手动创建,也无法通过静态获取。 + +虽然在 `ASP.NET Core` 中无法直接获取 `HttpContext` 对象。但是微软也提供了注入 `IHttpContextAccessor` 方式获取。 + +## HttpContext 多种获取方式 + + + +### 在 `ControllerBase` 派生类中 + +在 `ControllerBase` 派生类中,我们可以直接通过 `HttpContext` 属性获取 `HttpContext` 对象。 + +### 通过注入 `IHttpContextAccessor` + +在 `Furion` 框架中,默认已经注册了 `IHttpContextAccessor` 服务,所以我们可以通过构造函数注入该对象获取。 + +```cs showLineNumbers {3,5} +public class AppService +{ + public AppService(IHttpContextAccessor httpContextAccessor) + { + var httpContext = httpContextAccessor.HttpContext; + } +} +``` + +### 通过 `App.HttpContext` + +`App` 静态类也提供了 `App.HttpContext` 获取 `HttpContext` 对象。 + +## `HttpContext` 拓展方法 + +`Furion` 框架也提供了一些常用的 `HttpContext` 拓展方法 + +### 获取当前请求的特性 + +```cs showLineNumbers +var attribute = httpContext.GetMetadata(); +``` + +### 设置 `Swagger` 自动授权 + +```cs showLineNumbers +httpContext.SigninToSwagger("你的token"); +``` + +### 退出 `Swagger` 授权 + +```cs showLineNumbers +httpContext.SignoutToSwagger(); +``` + +### 获取本地 IP 地址 + +```cs showLineNumbers +var ipv4 = httpContext.GetLocalIpAddressToIPv4(); +var ipv6 = httpContext.GetLocalIpAddressToIPv6(); +``` + +### 获取客户端 IP 地址 + +```cs showLineNumbers +var ipv4 = httpContext.GetRemoteIpAddressToIPv4(); +var ipv6 = httpContext.GetRemoteIpAddressToIPv6(); +``` diff --git a/handbook/blog/2021-02-02-fileupload-download.mdx b/handbook/blog/2021-02-02-fileupload-download.mdx new file mode 100644 index 0000000000000000000000000000000000000000..b3511d55f0cac6de013d3a7944a86eea00a2e212 --- /dev/null +++ b/handbook/blog/2021-02-02-fileupload-download.mdx @@ -0,0 +1,82 @@ +--- +slug: fileupload-download +title: 2. 文件上传下载 +author: dotNET China +author_title: 让 .NET 开发更简单,更通用,更流行。 +author_url: https://gitee.com/dotnetchina +author_image_url: https://i.loli.net/2021/01/19/M8q5a3OTZWUKicl.png +tags: [furion, furos, .net, .netcore, .net5, upload, download] +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +## 文件下载 + +```cs showLineNumbers +[HttpGet, NonUnify] +public IActionResult FileDownload(string path, string fileName) +{ + string filePath = "这里获取完整的文件下载路径"; + return new FileStreamResult(new FileStream(filePath, FileMode.Open), "application/octet-stream") { FileDownloadName = fileName }; +} +``` + +:::note 关于前端获取文件名 + +如果前端获取不到文件夹,可添加以下配置: + +```cs showLineNumbers +_httpContextAccessor.HttpContext.Response.Headers.Add("Content-Disposition", $"attachment; filename={文件名}"); +_httpContextAccessor.HttpContext.Response.Headers.Add("Access-Control-Expose-Headers", "Content-Disposition"); +``` + +::: + + + +## 文件上传 + +```cs showLineNumbers +[HttpPost, NonUnify] +public async Task UploadFileAsync(List files) +{ + // 保存到网站根目录下的 uploads 目录 + var savePath = Path.Combine(App.HostEnvironment.ContentRootPath, "uploads"); + if(!Directory.Exists(savePath)) Directory.CreateDirectory(savePath); + + long size = files.Sum(f => f.Length); + + foreach (var formFile in files) + { + if (formFile.Length > 0) + { + // 避免文件名重复,采用 GUID 生成 + var filePath = Path.Combine(savePath, Guid.NewGuid().ToString("N") + Path.GetExtension(formFile.FileName)); // 可以替代为你需要存储的真实路径 + + using (var stream = System.IO.File.Create(filePath)) + { + await formFile.CopyToAsync(stream); + } + } + } + + // 在动态 API 直接返回对象即可,无需 OK 和 IActionResult + return Ok(new { count = files.Count, size }); +} +``` + +:::note 关于使用axios上传文件,方法获取到参数files.Count=0 +axios请求配置 + +```cs showLineNumbers + let formData = new FormData(); + formData.append("files", this.file); //files需与方法里的参数files名称一样 + let config = { + headers: { + "Content-Type": "multipart/form-data", + }, + }; + axios.post(this.uploadURL, formData, config).then((res) => {//需引入axios + console.log(res); + }); +``` \ No newline at end of file diff --git a/handbook/blog/2021-06-18-net6-preview5.mdx b/handbook/blog/2021-06-18-net6-preview5.mdx new file mode 100644 index 0000000000000000000000000000000000000000..ff501532ca8f053d54f43834f03df25ae1aa93ac --- /dev/null +++ b/handbook/blog/2021-06-18-net6-preview5.mdx @@ -0,0 +1,55 @@ +--- +slug: net6-preview5 +title: 3. .NET 6 Preview 5 尝鲜 +author: dotNET China +author_title: 让 .NET 开发更简单,更通用,更流行。 +author_url: https://gitee.com/dotnetchina +author_image_url: https://i.loli.net/2021/01/19/M8q5a3OTZWUKicl.png +tags: [furion, furos, .net, .netcore, .net5, .net6] +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +**2021 年 06 月 18 日,微软发布了 `.NET 6 Preview 5` 版本,`Furion` 在当天第一时间适配了该版本,并发布 `Furion v3.0.0-preview.5.21301.9` 版本。** + +[Furion .NET 6 Preview 5 源码地址](https://gitee.com/dotnetchina/Furion/tree/net6.0-preview5/) + +## 等不及尝鲜了 + +### 安装 `.NET 6 Preview 5 SDK` + +尝鲜之前,首先先安装 `.NET 6 Preview 5 SDK`,下载地址:[https://dotnet.microsoft.com/download/dotnet/6.0](https://dotnet.microsoft.com/download/dotnet/6.0) + +下载对应系统和处理器版本即可。 + +### 升级 `Visual Studio 2019` + +目前 `.NET 6 Preview 5` 支持使用 `Visual Studio 2019 Preview 16.11.0 Preview 2.0` 版本,**注意是 `Preview` 最新版本**。下载地址:[https://visualstudio.microsoft.com/zh-hans/thank-you-downloading-visual-studio/?sku=enterprise&ch=pre&rel=16](https://visualstudio.microsoft.com/zh-hans/thank-you-downloading-visual-studio/?sku=enterprise&ch=pre&rel=16) + +已经安装了 `Visual Studio 2019 Preview` 版本的朋友直接升级到最新版即可。 + +:::note 特别说明 + +如果使用 `Visual Studio Code` 开发,可忽略此选项。 + +::: + +### 第一个例子 + +`Furion v3.0.0-preview.5.21301.9` 目前提供了所有类型的脚手架,版本号统一 `3.0.0-preview.5.21301.9`。 + +安装脚手架,打开 `CMD/Powershell` 执行以下命令: + +```bash showLineNumbers +dotnet new --install Furion.Template.Api::3.0.0-preview.5.21301.9 +``` + +创建项目 + +```bash showLineNumbers +dotnet new furionapi -n FurionNET6 +``` + +### 打开并启动项目 + +打开浏览器查看效果即可。 diff --git a/handbook/blog/2021-07-15-net6-preview6.mdx b/handbook/blog/2021-07-15-net6-preview6.mdx new file mode 100644 index 0000000000000000000000000000000000000000..147f1ddd139052249ec896a0d50734e77fd75dee --- /dev/null +++ b/handbook/blog/2021-07-15-net6-preview6.mdx @@ -0,0 +1,61 @@ +--- +slug: net6-preview6 +title: 4. .NET 6 Preview 6 尝鲜 +author: dotNET China +author_title: 让 .NET 开发更简单,更通用,更流行。 +author_url: https://gitee.com/dotnetchina +author_image_url: https://i.loli.net/2021/01/19/M8q5a3OTZWUKicl.png +tags: [furion, furos, .net, .netcore, .net5, .net6] +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +**2021 年 07 月 15 日,微软发布了 `.NET 6 Preview 6` 版本,`Furion` 在当天第一时间适配了该版本,并发布 `Furion v3.0.0-rc.1` 版本。** + +[Furion .NET 6 Preview 6 源码地址](https://gitee.com/dotnetchina/Furion/tree/3.0.0-rc.1/) + +## 安装 `.NET 6 Preview 6 SDK` + +尝鲜之前,首先先安装 `.NET 6 Preview 6 SDK`,下载地址:[https://dotnet.microsoft.com/download/dotnet/6.0](https://dotnet.microsoft.com/download/dotnet/6.0) + +下载对应系统和处理器版本即可。 + +## 旧项目升级 + +编辑所有 `.csproj` 项目,修改 `net5.0` 为 `net6.0`。 + +同时升级所有 `Microsoft` 和 `Furion` 包为最新版本,`Furion` 最新版本为:`v3.0.0-rc.1`。 + +## 新项目使用 + +### 升级 `Visual Studio 2019` + +目前 `.NET 6 Preview 6` 支持使用 `Visual Studio 2019 Preview 16.11.0 Preview 3.0` 版本,**注意是 `Preview` 最新版本**。下载地址:[https://visualstudio.microsoft.com/zh-hans/thank-you-downloading-visual-studio/?sku=enterprise&ch=pre&rel=16](https://visualstudio.microsoft.com/zh-hans/thank-you-downloading-visual-studio/?sku=enterprise&ch=pre&rel=16) + +已经安装了 `Visual Studio 2019 Preview` 版本的朋友直接升级到最新版即可。 + +:::note 特别说明 + +如果使用 `Visual Studio Code` 开发,可忽略此选项。 + +::: + +### 第一个例子 + +`Furion v3.0.0-rc.1` 目前提供了所有类型的脚手架,版本号统一 `3.0.0-rc.1`。 + +安装脚手架,打开 `CMD/Powershell` 执行以下命令: + +```bash showLineNumbers +dotnet new --install Furion.Template.Api::3.0.0-rc.1 +``` + +创建项目 + +```bash showLineNumbers +dotnet new furionapi -n FurionNET6 +``` + +### 打开并启动项目 + +打开浏览器查看效果即可。 diff --git a/handbook/blog/2021-07-24-console.mdx b/handbook/blog/2021-07-24-console.mdx new file mode 100644 index 0000000000000000000000000000000000000000..c54d629339fc13511704e0c1eba4f8e0bdb829a5 --- /dev/null +++ b/handbook/blog/2021-07-24-console.mdx @@ -0,0 +1,68 @@ +--- +slug: console +title: 5. 在控制台中使用 +author: dotNET China +author_title: 让 .NET 开发更简单,更通用,更流行。 +author_url: https://gitee.com/dotnetchina +author_image_url: https://i.loli.net/2021/01/19/M8q5a3OTZWUKicl.png +tags: [furion, furos, .net, .netcore, .net5, .net6] +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +:::warning 内容过时 + +当前内容已过时,请查看 [2.1 入门指南 - 2.1.10.3 `Console` 初始化](/docs/serverun#21103-console-%E5%88%9D%E5%A7%8B%E5%8C%96) + +::: + +`Furion` 从 `v2.15.3+` 版本开始,支持全平台应用程序开发,包括 `Web`,`控制台`,`WinForm`,`WPF`,`Xamarin/MAUI` 等。 + +在控制台中使用示例: + +```cs showLineNumbers +using Furion; +using Furion.DependencyInjection; +using Furion.RemoteRequest.Extensions; +using Microsoft.Extensions.DependencyInjection; +using System; + +namespace ConsoleApp1 +{ + class Program + { + static void Main(string[] args) + { + // 创建一个服务容器 + var services = Inject.Create(); + // 注册服务 + services.AddRemoteRequest(); + // 所有服务注册完毕后调用 Build() 构建 + services.Build(); + + // 使用 + var helloService = App.GetService(); + Console.WriteLine(helloService.SayHello()); + + Console.WriteLine("============="); + + var baidu = "https://www.baidu.com".GetAsStringAsync().GetAwaiter().GetResult(); + Console.WriteLine(baidu); + } + } + + public interface IHelloService + { + string SayHello(); + } + public class HelloService : IHelloService, ITransient + { + public string SayHello() + { + return "Hello Furion."; + } + } +} +``` + +以上代码通过 `var services = Inject.Create();` 创建一个服务集合,最后通过 `services.Build()` 即可完成初始化。 \ No newline at end of file diff --git a/handbook/blog/2021-08-11-net6-preview7.mdx b/handbook/blog/2021-08-11-net6-preview7.mdx new file mode 100644 index 0000000000000000000000000000000000000000..7086dc430012f82c9e04575ae9268c90df0041b1 --- /dev/null +++ b/handbook/blog/2021-08-11-net6-preview7.mdx @@ -0,0 +1,61 @@ +--- +slug: net6-preview7 +title: 6. .NET 6 Preview 7 尝鲜 +author: dotNET China +author_title: 让 .NET 开发更简单,更通用,更流行。 +author_url: https://gitee.com/dotnetchina +author_image_url: https://i.loli.net/2021/01/19/M8q5a3OTZWUKicl.png +tags: [furion, furos, .net, .netcore, .net5, .net6] +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +**2021 年 08 月 11 日,微软发布了 `.NET 6 Preview 7` 版本,`Furion` 在当天第一时间适配了该版本,并发布 `Furion v3.0.0-rc.2` 版本。** + +[Furion .NET 6 Preview 7 源码地址](https://gitee.com/dotnetchina/Furion/tree/3.0.0-rc.2/) + +## 安装 `.NET 6 Preview 6 SDK` + +尝鲜之前,首先先安装 `.NET 6 Preview 7 SDK`,下载地址:[https://dotnet.microsoft.com/download/dotnet/6.0](https://dotnet.microsoft.com/download/dotnet/6.0) + +下载对应系统和处理器版本即可。 + +## 旧项目升级 + +编辑所有 `.csproj` 项目,修改 `net5.0` 为 `net6.0`。 + +同时升级所有 `Microsoft` 和 `Furion` 包为最新版本,`Furion` 最新版本为:`v3.0.0-rc.1`。 + +## 新项目使用 + +### 升级 `Visual Studio 2019` + +目前 `.NET 6 Preview 7` 支持使用 `Visual Studio 2019 Preview 16.11.0 Preview 4.0` 版本,**注意是 `Preview` 最新版本**。下载地址:[https://visualstudio.microsoft.com/zh-hans/thank-you-downloading-visual-studio/?sku=enterprise&ch=pre&rel=16](https://visualstudio.microsoft.com/zh-hans/thank-you-downloading-visual-studio/?sku=enterprise&ch=pre&rel=16) + +已经安装了 `Visual Studio 2019 Preview` 版本的朋友直接升级到最新版即可。 + +:::note 特别说明 + +如果使用 `Visual Studio Code` 开发,可忽略此选项。 + +::: + +### 第一个例子 + +`Furion v3.0.0-rc.2` 目前提供了所有类型的脚手架,版本号统一 `3.0.0-rc.2`。 + +安装脚手架,打开 `CMD/Powershell` 执行以下命令: + +```bash showLineNumbers +dotnet new --install Furion.Template.Api::3.0.0-rc.2 +``` + +创建项目 + +```bash showLineNumbers +dotnet new furionapi -n FurionNET6 +``` + +### 打开并启动项目 + +打开浏览器查看效果即可。 diff --git a/handbook/blog/2022-05-31-global-usings.mdx b/handbook/blog/2022-05-31-global-usings.mdx new file mode 100644 index 0000000000000000000000000000000000000000..81db77184c9d6f4f8dbb8d48bc33be1e7975e9d8 --- /dev/null +++ b/handbook/blog/2022-05-31-global-usings.mdx @@ -0,0 +1,103 @@ +--- +slug: global-usings +title: 7. GlobalUsings 的使用 +author: dotNET China +author_title: 让 .NET 开发更简单,更通用,更流行。 +author_url: https://gitee.com/dotnetchina +author_image_url: https://i.loli.net/2021/01/19/M8q5a3OTZWUKicl.png +tags: [furion, furos, .net, .netcore, .net5, .net6] +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +### 简介 + +在 `.NET6/C#10` 之后,微软新增了 `GlobalUsings` 机制,可以在项目的根目录下创建一个 `GlobalUsings.cs` 文件,把常用的 `using` 放置其中。 + +这样 `GlobalUsings.cs` 所在的项目 `.cs` 文件就无需重复 `using` 了,大大的提高开发效率,也让代码变的更加简洁。 + +### 必要配置 + +**启用 `GlobalUsings` 机制需要以下两个步骤:** + +1. 在你需要全局 `using` 的项目层根目录创建 `GlobalUsings.cs` 文件,如果多个项目层需要,则每个层都应该有一个 `GlobalUsings.cs` +2. 编辑项目的 `.csproj` 文件,添加 `enable`,注意是在 `` 中添加,通常和 `` 同父同级 + +### 基本使用 + +配置之后,现在就可以把常用的 `using` 放到 `GlobalUsings.cs` 中了,写法如下: + +```cs showLineNumbers title="Furion 推荐的全局命名空间" +global using Furion; +global using Furion.DatabaseAccessor; +global using Furion.DataEncryption; +global using Furion.DataValidation; +global using Furion.DependencyInjection; +global using Furion.DynamicApiController; +global using Furion.Extensions; +global using Furion.FriendlyException; +global using Mapster; +global using Microsoft.AspNetCore.Authorization; +global using Microsoft.AspNetCore.Http; +global using Microsoft.AspNetCore.Mvc; +global using Microsoft.CodeAnalysis; +global using Microsoft.EntityFrameworkCore; +global using System.ComponentModel.DataAnnotations; +``` + +**注意必须以 `global` 开头!** + +:::tip 小知识 + +一般推荐把实体类的命名空间也放进去,因为仓储 `IRepository` 使用的频率非常高。 + +另外推荐大家在 `Visual Studio` 中安装 `CodeMaid` 插件哦,自动清理解决方案所有无用的 `using`,结合 `GlobalUsings.cs` 非常棒! + +::: + +:::important 个别情况 + +可能由于 `Visual Studio` 版本的问题,导致 `GlobalUsings.cs` 定义出错,这时候需要在 `using` 后面加 `global::`,如: + +```cs showLineNumbers +global using global::Furion; +``` + +::: + +接下来在代码中使用: + +```cs showLineNumbers {1} +// 无需 using Furion 的命名空间了哦,清爽了不少 + +namespace Your.Application; + +public class DefaultAppService : IDynamicApiController +{ + private readonly IRepository _boardCardRepository; + private readonly IRepository _boardGroupRepository; + private readonly IRepository _boardCardAttachmentRepository; + private readonly IRepository _boardCardUserRepository; +} + +// .... +``` + + + +### 默认全局 `using` + +**实际上微软已经自动把一些常用的 `using` 在编译后的代码中自动补上了**,路径在 `项目/obj/Debug/net6.0/项目.GlobalUsings.cs` 文件中,文件内容如下: + +```cs showLineNumbers +// +global using global::System; +global using global::System.Collections.Generic; +global using global::System.IO; +global using global::System.Linq; +global using global::System.Net.Http; +global using global::System.Threading; +global using global::System.Threading.Tasks; +``` + +**也就是以上的 `using` 无需写在你创建的 `GlobalUsings.cs` 中了,微软会在编译时自动合并。** diff --git a/handbook/docs/appstartup.mdx b/handbook/docs/appstartup.mdx new file mode 100644 index 0000000000000000000000000000000000000000..c4f500e6a54ce1c8ac99b71d102d7ef7d6dc1211 --- /dev/null +++ b/handbook/docs/appstartup.mdx @@ -0,0 +1,340 @@ +--- +id: appstartup +title: 3.1 AppStartup 启动 +sidebar_label: 3.1 AppStartup 启动 +description: 合理的配置每一个项目层的服务依赖 +--- + +## 3.1.1 `Startup` 类 + +`Startup` 类是 `ASP.NET Core` 应用程序启动默认调用的类,该类是在 `Program.cs` 中配置: + +**.NET5** 方式 + +```cs showLineNumbers {18} title="Furion.Web.Entry\Program.cs" +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Furion.Web.Entry +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) + { + return Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } + } +} +``` + +**.NET6 方式** + +在 `.NET6` 中微软已不再推荐 `UseStartup()` 方式。 + +### 3.1.1.1 `Startup` 两个重要方法 + +`Startup` 默认有两个重要的方法: + +- `ConfigureServices`:配置应用所需服务,在该方法中可以添加应用所需要的功能或服务 +- `Configure`:配置应用请求处理管道 + +默认代码如下: + +```cs showLineNumbers {9,13} title="Furion.Web.Entry\Startup.cs" +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; + +namespace Furion.Web.Entry +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + } + } +} +``` + +在这里,不打算详细讲 `Startup` 类的具体功能和作用。 + +:::note 了解更多 + +想了解更多 `Startup` 知识可查阅 [ASP.NET Core - Startup 类](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/startup?view=aspnetcore-3.1) 章节。 + +::: + +## 3.1.2 `AppStartup` + +在 `Furion` 框架中,提供了更为灵活的 `Startup` 类配置方式,**无需在 `Web 启用层` 中配置,可将配置放到任何项目层。** + +可能会有读者有疑问,为什么要多此一举呢?原因有几点: + +- `Startup` 类默认和 `Web 应用层` 绑定在一起,这样就会导致如果我创建了新的 `Web 应用层`,`Startup` 又要重新配置 +- 随着业务的增长,需要集成越来越多的第三方服务,这时候 `Startup` 类就会变得越来越臃肿,难以维护 +- `Startup` 类无法与其他项目类型进行共用 + +所以,`Furion` 提供了更加灵活的配置方式:`AppStartup`。 + +:::warning 注意事项 + +如果 `AppStartup` 的派生类所在的项目层没有被启动层直接或间接添加引用,那么这个 `Startup.cs` 就会被忽略,也就是不会自动载入注册。 + +::: + +### 3.1.2.1 如何配置 `AppStartup` + +`AppStartup` 是一个抽象的空类,没有任何定义成员。正是因为这样,才提供更加灵活的配置方式。 + +### 3.1.2.2 `AppStartup` 约定 + +`AppStartup` 派生类只有两个小约定: + +- 任何公开、非静态、返回值为 `void` 且方法第一个参数是 `IServiceCollection` 类型,那么他就是一个 `ConfigureServices` 方法 +- 任何公开、非静态、返回值为 `void` 且方法第一个参数是 `IApplicationBuilder` 类型,第二个参数是 `IWebHostEnvironment` 类型,那么他就是一个 `Configure` 方法 + +所以,我们可以自由的编写方法,只要遵循约定即可,如: + +```cs showLineNumbers {5,7,12,18,23} +using Microsoft.Extensions.DependencyInjection; + +namespace Furion.EntityFramework.Core +{ + public class MyStartup : AppStartup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddDataValidation(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseSwagger(); + } + + // 可以随意定义名字和方法 + public void XXXXName(IServiceCollection services) + { + } + + // 可以随意定义名字和方法 + public void ZZZName(IApplicationBuilder app, IWebHostEnvironment env) + { + } + } +} +``` + +### 3.1.2.3 `AppStartup` 配置顺序 + +默认情况下,`AppStartup` 配置顺序由所在程序集的名称进行正序调用,如果我们需要配置执行顺序,只需要在 `AppStartup` 派生类中贴 `[AppStartup(order)]` 特性即可。 + +`order` 数值越大,越在前面调用,如: + +```cs showLineNumbers {5} +using Microsoft.Extensions.DependencyInjection; + +namespace Furion.EntityFramework.Core +{ + [AppStartup(10)] + public class FirstStartup : AppStartup + { + public void ConfigureServices(IServiceCollection services) + { + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + } + } +} +``` + +```cs showLineNumbers {5} +using Microsoft.Extensions.DependencyInjection; + +namespace Furion.EntityFramework.Core +{ + [AppStartup(9)] + public class SecondStartup : AppStartup + { + public void ConfigureServices(IServiceCollection services) + { + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + } + } +} +``` + +`FirstStartup` 会在 `SecondStartup` 之前调用。 + +### 3.1.2.4 `AppStartup` 方法调用顺序 + +`AppStartup` 方法调用顺序和方法的书写先后有关,越在前面的方法越先调用。 + +## 3.1.3 `Startup` 配置最佳实践 + +:::important `v3.6.3+` 说明 + +在 `Furion v3.6.3+` 版本之后无需创建空 `Startup.cs` 类,内部已实现 `FakeStartup` 模式。 + +::: + +建议 `Web` 启动层的 `Startup.cs` 保持为空方法体,如: + +```cs showLineNumbers {9-11,13-15} title="Furion.Web.Entry\Startup.cs" +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; + +namespace Furion.Web.Entry +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + } + } +} +``` + +将所有 `Web 应用层` 配置迁移到 `Furion.Web.Core.Startup.cs` 中,如: + +```cs showLineNumbers title="Furion.Web.Core\Startup.cs" +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Furion.Web.Core +{ + public sealed class FurWebCoreStartup : AppStartup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddCorsAccessor(); + + services.AddControllers().AddInject(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseCorsAccessor(); + + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseInject(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } +} +``` + +这样,后续更换 `Web 应用层` 也无需重新配置 `Startup.cs` + +## 3.1.4 构造函数注入说明 + +**`AppStartup` 的派生类并未提供依赖注入的功能,也即是无法通过构造函数进行注入服务**。原因是 `AppStartup` 是个空类,目的是用来查找 `Startup` 的。 + +那如何像 `Startup.cs` 一样使用服务呢? + +- 获取配置 `IConfiguration` 实例:通过 `App.Configuration` +- 解析服务:通过 `App.GetService()` 或 `app.ApplicationServices.GetService()` + +:::important 关于 `Configure` 方法注入 + +`AppStartup` 针对 `Configure` 方法提供了参数解析注入功能,也就是只要在方法中声明接口参数即可自动注入,如: + +```cs showLineNumbers {1,2} +// app 和 env 会自动注入 +public void Configure(IApplicationBuilder app, IWebHostEnvironment env) +{ +} +``` + +::: + +## 3.1.5 关于 `appsettings.json` + +在默认情况下,`ASP.NET Core` 配置放在 `appsettings.json` 中配置,但是这样的方式和 `Startup.cs` 配置一样的道理,一旦我们更换了 `Web 应用层`,那么 `appsettings.json` 又要重新配置一次。 + +所以,`Furion` 框架提供了更加灵活的方式配置 `appsettings.json`,**只需要在任何项目层根目录下创建 `.json` 文件即可。`Furion` 框架最后会自动合并所有分散的配置文件。** + +如我们在 `Furion.EntityFramework.Core` 层创建 `dbsettings.json` 配置数据库连接字符串,如: + +```json showLineNumbers title="Furion.EntityFramework.Core\dbsettings.json" +{ + "ConnectionStrings": { + "DbConnectionString": "Server=localhost;Database=Furion;User=sa;Password=000000;MultipleActiveResultSets=True;", + "Sqlite3ConnectionString": "Data Source=./Furion.db" + } +} +``` + +**无需在 `appsettings.json` 中配置**,下面是 `appsettings.json` 默认代码: + +```json showLineNumbers +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "Microsoft.EntityFrameworkCore": "Information" + } + }, + "AllowedHosts": "*" +} +``` + +这样我们把配置文件分散在不同项目层之后,就可以实现共用和共享了。 + +:::caution 特别注意 + +其他层的配置文件不能以 `appsettings.json` 命名,会导致覆盖启动层的配置。 + +另外,在其他层创建的 `*.json` 文件必须设置文件属性为 `始终复制或较新复制`。 + +::: + +## 3.1.6 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/audit.mdx b/handbook/docs/audit.mdx new file mode 100644 index 0000000000000000000000000000000000000000..6a67d32fa940d0cd34d47a25ed9e204ef9b03e19 --- /dev/null +++ b/handbook/docs/audit.mdx @@ -0,0 +1,153 @@ +--- +id: audit +title: 5.4 请求审计日志 +sidebar_label: 5.4 请求审计日志 (Audit) +description: 任何苍蝇都不能放过 +--- + +:::tip 小知识 + +`Furion` 提供了非常强大的 `LoggingMonitor` 审计日志功能,可直接使用:[LoggingMonitor 文档](logging.mdx#1811-loggingmonitor-监听日志) + +::: + +## 5.4.1 审计日志 + +在一个企业应用系统中,用户对系统所有的操作包括请求、数据库操作等等都应该记录起来,那么这些日志我们称为操作日志,也可以说审计日志。 + +:::tip 关于数据库操作审计日志 + +如需实现 `sql` 操作,`数据库操作` 的审计日志可查阅 【[9.23 审计日志章节](./dbcontext-audit.mdx)】 + +::: + +## 5.4.2 请求审计日志 + +:::note 实现原理 + +在这里,结合 【[5.3 筛选器](./filter.mdx)】 实现请求审计日志功能。 + +::: + +请求审计日志通常指的是记录请求地址,来源地址,操作人,传递参数等。这个主要是通过 `IAsyncActionFilter` 筛选器实现,如: + +1. 定义 `RequestAuditFilter` 并实现 `IAsyncActionFilter` + +```cs showLineNumbers {9,45} +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Filters; +using System; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace Furion.Web.Core +{ + public class RequestAuditFilter : IAsyncActionFilter + { + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + //============== 这里是执行方法之前获取数据 ==================== + + // 获取控制器、路由信息 + var actionDescriptor = context.ActionDescriptor as ControllerActionDescriptor; + + // 获取请求的方法 + var method = actionDescriptor.MethodInfo; + + // 获取 HttpContext 和 HttpRequest 对象 + var httpContext = context.HttpContext; + var httpRequest = httpContext.Request; + + // 获取客户端 Ipv4 地址 + var remoteIPv4 = httpContext.GetRemoteIpAddressToIPv4(); + + // 获取请求的 Url 地址 + var requestUrl = httpRequest.GetRequestUrlAddress(); + + // 获取来源 Url 地址 + var refererUrl = httpRequest.GetRefererUrlAddress(); + + // 获取请求参数(写入日志,需序列化成字符串后存储) + var parameters = context.ActionArguments; + + // 获取操作人(必须授权访问才有值)"userId" 为你存储的 claims type,jwt 授权对应的是 payload 中存储的键名 + var userId = httpContext.User?.FindFirstValue("userId"); + + // 请求时间 + var requestedTime = DateTimeOffset.Now; + + + //============== 这里是执行方法之后获取数据 ==================== + var actionContext = await next(); + + // 获取返回的结果 + var returnResult = actionContext.Result; + + // 判断是否请求成功,没有异常就是请求成功 + var isRequestSucceed = actionContext.Exception == null; + + // 获取调用堆栈信息,提供更加简单明了的调用和异常堆栈 + var stackTrace = EnhancedStackTrace.Current(); + + // 这里写入日志,或存储到数据库中!!!~~~~~~~~~~~~~~~~~~~~ + } + } +} +``` + +2. 注册 `RequestAuditFilter` 筛选器 + +```cs showLineNumbers +services.AddMvcFilter(); +``` + +## 5.4.3 `LoggingMonitor` 审计日志 + +:::tip 小知识 + +`Furion` 提供了非常强大的 `LoggingMonitor` 审计日志功能,可直接使用:[LoggingMonitor 文档](logging.mdx#1811-loggingmonitor-监听日志) + +::: + +```bash showLineNumbers +┏━━━━━━━━━━━ Logging Monitor ━━━━━━━━━━━ +┣ Furion.Application.TestLoggerServices.GetPerson (Furion.Application) +┣ +┣ 控制器名称: TestLoggerServices +┣ 操作名称: GetPerson +┣ 路由信息: [area]: ; [controller]: test-logger; [action]: person +┣ 请求方式: POST +┣ 请求地址: https://localhost:44316/api/test-logger/person/11 +┣ 来源地址: https://localhost:44316/api/index.html +┣ 浏览器标识: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36 Edg/103.0.1264.62 +┣ 客户端 IP 地址: 0.0.0.1 +┣ 服务端 IP 地址: 0.0.0.1 +┣ 服务端运行环境: Development +┣ 执行耗时: 31ms +┣ ━━━━━━━━━━━━━━━ 授权信息 ━━━━━━━━━━━━━━━ +┣ JWT Token: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEsIkFjY291bnQiOiJhZG1pbiIsImlhdCI6MTY1ODcxNjc5NywibmJmIjoxNjU4NzE2Nzk3LCJleHAiOjE2NTg3MTc5OTcsImlzcyI6ImRvdG5ldGNoaW5hIiwiYXVkIjoicG93ZXJieSBGdXJpb24ifQ.VYZkwwqCwlUy3aJjuL-og62I0rkxNQ96kSjEm3VgXtg +┣ +┣ UserId (integer): 1 +┣ Account (string): admin +┣ iat (integer): 1658716797 +┣ nbf (integer): 1658716797 +┣ exp (integer): 1658717997 +┣ iss (string): dotnetchina +┣ aud (string): powerby Furion +┣ ━━━━━━━━━━━━━━━ 参数列表 ━━━━━━━━━━━━━━━ +┣ Content-Type: +┣ +┣ id (Int32): 11 +┣ ━━━━━━━━━━━━━━━ 返回信息 ━━━━━━━━━━━━━━━ +┣ 类型: Furion.Application.Persons.PersonDto +┣ 返回值: {"Id":11,"Name":null,"Age":0,"Address":null,"PhoneNumber":null,"QQ":null,"CreatedTime":"0001-01-01T00:00:00+00:00","Childrens":null,"Posts":null} +┗━━━━━━━━━━━ Logging Monitor ━━━━━━━━━━━ +```` + +## 5.4.4 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/auth-control.mdx b/handbook/docs/auth-control.mdx new file mode 100644 index 0000000000000000000000000000000000000000..3a70a823f30a6a966d552169beefbfdd396f84ff --- /dev/null +++ b/handbook/docs/auth-control.mdx @@ -0,0 +1,608 @@ +--- +id: auth-control +title: 15. 安全鉴权 +sidebar_label: 15. 安全鉴权 +--- + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **问题修复** + + -  修复 使用刷新 `Token` 也能通过鉴权检查严重安全 `Bug` 4.8.8.42 ⏱️2023.08.28 [#I7TII4](https://gitee.com/dotnetchina/Furion/issues/I7TII4) + +
+
+
+ +## 15.1 什么是鉴权 + +**鉴权实际上就是一种身份认证**。 + +由用户提供凭据,然后将其与存储在操作系统、数据库、应用或资源中的凭据进行比较。 在授权过程中,如果凭据匹配,则用户身份验证成功,可执行已向其授权的操作。 授权指判断允许用户执行的操作的过程。 +也可以将身份验证理解为进入空间(例如服务器、数据库、应用或资源)的一种方式,而授权是用户可以对该空间(服务器、数据库或应用)内的哪些对象执行哪些操作。 + +### 15.1.1 常见的鉴权方式 + +- `HTTP Basic Authentication` + +这是 `HTTP` 协议实现的基本认证方式,我们在浏览网页时,从浏览器正上方弹出的对话框要求我们输入账号密码,正是使用了这种认证方式 + +- `Session + Cookie` + +利用服务器端的 session(会话)和浏览器端的 cookie 来实现前后端的认证,由于 http 请求时是无状态的,服务器正常情况下是不知道当前请求之前有没有来过,这个时候我们如果要记录状态,就需要在服务器端创建一个会话(session),将同一个客户端的请求都维护在各自的会话中,每当请求到达服务器端的时候,先去查一下该客户端有没有在服务器端创建 session,如果有则已经认证成功了,否则就没有认证。 + +- `Token` + +客户端在首次登录以后,服务端再次接收 `HTTP` 请求的时候,就只认 `Token` 了,请求只要每次把 `Token` 带上就行了,服务器端会拦截所有的请求,然后校验 `Token` 的合法性,合法就放行,不合法就返回 401(鉴权失败) + +`Token`验证比较灵活,适用于大部分场景。常用的 `Token` 鉴权方式的解决方案是 `JWT`,`JWT` 是通过对带有相关用户信息的进行加密,加密的方式比较灵活,可以根据需求具体设计。 + +- `OAuth` + +OAuth(开放授权)是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容,为了保护用户数据的安全和隐私,第三方网站访问用户数据前都需要显式的向用户征求授权。我们常见的提供 OAuth 认证服务的厂商有支付宝、QQ 和微信。 + +OAuth 协议又有 1.0 和 2.0 两个版本。相比较 1.0 版,2.0 版整个授权验证流程更简单更安全,也是目前最主要的用户身份验证和授权方式。 + +## 15.2 如何使用 + +:::info 配置之前 + +在添加授权服务之前,请先确保 `Startup.cs` 中 `Configure` 是否添加了以下两个中间件: + +```cs showLineNumbers +app.UseAuthentication(); +app.UseAuthorization(); +``` + +::: + +### 15.2.1 添加 `Cookie` 身份验证 + +:::important 使用说明 + +如果您使用的是 `WebAPI`,则该小节可忽略,通常 `WebAPI` 使用的是 `JWT` 授权方式,而非 `Cookie`。 + +::: + +```cs showLineNumbers +// Cookies单独身份验证 +services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) + .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, b => + { + b.LoginPath = "/Home/Login"; + }); +``` + +### 15.2.2 添加 `Jwt` 身份验证 + +- 安装 `Furion.Extras.Authentication.JwtBearer` 拓展包 + +- 在 `Startup.cs` 中注册 `AddJwt` 服务,注意,**必须在 `.AddControllers()` 之前注册!!** + +```cs showLineNumbers {2,5} +// 默认授权机制,需授权的即可(方法)需贴 `[Authorize]` 特性 +services.AddJwt(); + +// 启用全局授权,这样每个接口都必须授权才能访问,无需贴 `[Authorize]` 特性,推荐!!!!!!!!!❤️ +// services.AddJwt(enableGlobalAuthorize:true); +``` + +**注:如果项目使用了 `services.AddSignalR();` 服务,那么该服务必须在 `services.AddJwt` 之后注册。** + +:::note 额外补充 + +默认 `JwtHandler` 代码: + +```cs showLineNumbers {8,14} +using Furion.Authorization; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using System.Threading.Tasks; + +namespace FurionApi.Web.Core; + +public class JwtHandler : AppAuthorizeHandler +{ + public override Task PipelineAsync(AuthorizationHandlerContext context, DefaultHttpContext httpContext) + { + // 这里写您的授权判断逻辑,授权通过返回 true,否则返回 false + + return Task.FromResult(true); + } +} +``` + +::: + +- 自定义 `Jwt` 配置(**默认无需配置**) + +```json showLineNumbers {4,6,8} +{ + "JWTSettings": { + "ValidateIssuerSigningKey": true, // 是否验证密钥,bool 类型,默认true + "IssuerSigningKey": "你的密钥", // 密钥,string 类型,必须是复杂密钥,长度大于16,.NET8+ 长度需大于 32 + "ValidateIssuer": true, // 是否验证签发方,bool 类型,默认true + "ValidIssuer": "签发方", // 签发方,string 类型 + "ValidateAudience": true, // 是否验证签收方,bool 类型,默认true + "ValidAudience": "签收方", // 签收方,string 类型 + "ValidateLifetime": true, // 是否验证过期时间,bool 类型,默认true,建议true + "ExpiredTime": 20, // 过期时间,long 类型,单位分钟,默认20分钟 + "ClockSkew": 5, // 过期时间容错值,long 类型,单位秒,默认 5秒 + "Algorithm": "HS256" // 加密算法,string 类型,默认 HS256 + } +} +``` + +:::warning 系统安全注意事项 + +`Furion` 框架为了方便开发,已经自动添加了 `Jwt` 默认配置。建议每个项目都应该单独配置 `IssuerSigningKey`,`ValidIssuer`,`ValidAudience` 这三个。否则同样用了 `Furion` 框架生成的 `Token` 可能存在相互访问各自系统的风险。 + +::: + +:::important `Algorithm` 算法支持列表 + +目前支持的`加密算法` + +- `HS256` +- `HS384` +- `HS512` +- `PS256` +- `PS384` +- `PS512` +- `RS256`:需自行实现算法 +- `RS384`:需自行实现算法 +- `RS512`:需自行实现算法 +- `ES256` +- `ES256K` +- `ES384` +- `ES512` +- `EdDSA` + +详情请查阅 [SecurityAlgorithms](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/dev/src/Microsoft.IdentityModel.Tokens/SecurityAlgorithms.cs) + +::: + +- ❤️ ❤️ 生成 `Token` + +通常我们需要在登录成功之后生成 `JWT` Token 并返回,可通过 `JWTEncryption.Encrypt` 静态方法生成,如: + +:::tip 关于 `Token` 的值 + +字典 `Dictionary` 中的值支持所有基元类型和基元类型组成的值,但应尽可能避免使用 `数组` 值。 + +::: + +```cs showLineNumbers {2,4-5} +// 生成 token +var accessToken = JWTEncryption.Encrypt(new Dictionary() + { + { "UserId", user.Id }, // 存储Id + { "Account",user.Account }, // 存储用户名 + }); +``` + +### 15.2.3 混合身份验证 + +有时候我们一个系统中需要多种混合验证方式,这时候我们需要配置一个**主验证** 方式,所以需要修改 `options.DefaultAuthenticateScheme` 和 `options.DefaultChallengeScheme` 为主验证方式。 + +如需第二种方式,只需要通过 `[Authorize(JwtBearerDefaults.AuthenticationScheme)]` 贴即可。 + +- `JWT` 和 `Cookies` 混合身份验证 + +```cs showLineNumbers {1,3-4,6} +services.AddJwt(options => +{ + options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme; // 更改默认验证为 Cookies + options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme; // 更改默认验证为 Cookies +}) +.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options => +{ + options.LoginPath = "/Home/Login"; +}); +``` + +- `JWT` 和 `Windows` 身份验证混合验证 + +```cs showLineNumbers {1,3-4,6} +services.AddJwt(options => +{ + options.DefaultAuthenticateScheme = NegotiateDefaults.AuthenticationScheme; // 更改默认验证为 Windows 身份验证 + options.DefaultChallengeScheme = NegotiateDefaults.AuthenticationScheme; // 更改默认验证为 Windows 身份验证 +}) +.AddNegotiate(); +``` + +- 应用例子 + +```cs showLineNumbers {1-2,7-8} +// 贴了 [Authorize] 则表示应用 JwtBearerDefaults.AuthenticationScheme +[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] +public class ApiServices : IDynamicApiController +{ +} + +// 不贴则应用主验证,也即是 `DefaultAuthenticateScheme` 设置的 +public class HomeController: Controller +{ +} +``` + +## 15.3 高级自定义授权 + +`Furion` 框架提供了非常灵活的高级策略鉴权和授权方式,通过该策略授权方式可以实现任何自定义授权。 + +### 15.3.1 `AppAuthorizeHandler` + +`Furion` 框架提供了 `AppAuthorizeHandler` 策略授权处理程序提供基类,只需要创建自己的 `Handler` 继承它即可。如:`JwtHandler`: + +```cs showLineNumbers {20,12} +using Furion.Authorization; +using Furion.Core; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.IdentityModel.JsonWebTokens; + +namespace Furion.Web.Core +{ + /// + /// JWT 授权自定义处理程序 + /// + public class JwtHandler : AppAuthorizeHandler + { + /// + /// 请求管道 + /// + /// + /// + /// + public override Task PipelineAsync(AuthorizationHandlerContext context, DefaultHttpContext httpContext) + { + // 此处已经自动验证 Jwt token的有效性了,无需手动验证 + + // 检查权限,如果方法是异步的就不用 Task.FromResult 包裹,直接使用 async/await 即可 + return Task.FromResult(CheckAuthorzie(httpContext)); + } + + /// + /// 检查权限 + /// + /// + /// + private static bool CheckAuthorzie(DefaultHttpContext httpContext) + { + // 获取权限特性 + var securityDefineAttribute = httpContext.GetMetadata(); + if (securityDefineAttribute == null) return true; + + return "查询数据库返回是否有权限"; + } + } +} +``` + +之后注册 `JwtHandler` 即可: + +```cs showLineNumbers +services.AddJwt(); +``` + +### 15.3.2 完全自定义授权 + +有些时候可能针对不同的平台采用不一样的授权方式,比如合作信任的第三方机构可以免授权,这时候我们只需要重写 `HandleAsync` 方法即可。如: + +```cs showLineNumbers {11-25} +using Furion.Authorization; +using Furion.Core; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using System.Threading.Tasks; + +namespace Furion.Web.Core +{ + public class JwtHandler : AppAuthorizeHandler + { + public override async Task HandleAsync(AuthorizationHandlerContext context) + { + // 常规授权(可以判断是不是第三方) + var isAuthenticated = context.User.Identity.IsAuthenticated; + + // 第三方授权自定义 + if(是第三方){ + foreach (var requirement in pendingRequirements) + { + // 授权成功 + context.Succeed(requirement); + } + } + // 授权失败 + else context.Fail(); + } + } +} +``` + +## 15.4 授权特性及全局授权 + +**默认情况下,所有的路由都是允许匿名访问的,所以如果需要对某个 `Action` 或 `Controller` 设定授权访问,只需要在 `Action` 或 `Controller` 贴 `[AppAuthorize]` 或 `[Authorize]` 特性即可。** + +如果需要对特定的 `Action` 或 `Controller` 允许匿名访问,则贴 `[AllowAnonymous]` 即可。 + +### 15.4.1 全局授权 + +```cs showLineNumbers +services.AddJwt(enableGlobalAuthorize:true); +``` + +### 15.4.2 匿名访问 + +如果需要对特定的 `Action` 或 `Controller` 允许匿名访问,则贴 `[AllowAnonymous]` 即可。 + +## 15.5 自动刷新 Token + +### 15.5.1 后端登录部分 + +当用户登录成功之后,返回 `accessToken` 字符串,之后通过 `JWTEncryption.GenerateRefreshToken()` 获取 `刷新Token`,并通过响应报文头返回,如: + +```cs showLineNumbers {9} +// token +var accessToken = JWTEncryption.Encrypt(new Dictionary() + { + { "UserId", user.Id }, // 存储Id + { "Account",user.Account }, // 存储用户名 + }); + +// 获取刷新 token +var refreshToken = JWTEncryption.GenerateRefreshToken(accessToken, 43200); // 第二个参数是刷新 token 的有效期(分钟),默认三十天 + +// 设置响应报文头 +httpContextAccessor.HttpContext.Response.Headers["access-token"] = accessToken; +httpContextAccessor.HttpContext.Response.Headers["x-access-token"] = refreshToken; +``` + +用户登录成功之后把 `accessToken` 和 `refreshToken` 一起返回给客户端存储起来。 + +### 15.5.2 后端授权 `Handler` 部分 + +```cs showLineNumbers {16-28} +using Furion.Authorization; +using Furion.Core; +using Furion.DataEncryption; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using System.Threading.Tasks; + +namespace Furion.Web.Core +{ + /// + /// JWT 授权自定义处理程序 + /// + public class JwtHandler : AppAuthorizeHandler + { + /// + /// 重写 Handler 添加自动刷新收取逻辑 + /// + /// + /// + public override async Task HandleAsync(AuthorizationHandlerContext context) + { + // 自动刷新 token + if (JWTEncryption.AutoRefreshToken(context, context.GetCurrentHttpContext())) + { + await AuthorizeHandleAsync(context); + } + else context.Fail(); // 授权失败 + } + + /// + /// 验证管道,也就是验证核心代码 + /// + /// + /// + /// + public override Task PipelineAsync(AuthorizationHandlerContext context, DefaultHttpContext httpContext) + { + // 检查权限,如果方法是异步的就不用 Task.FromResult 包裹,直接使用 async/await 即可 + return Task.FromResult(true); + } + } +} +``` + +### 15.5.3 客户端部分 + +客户端每次请求需将 `accessToken` 和 `refreshToken` 放到请求报文头中传送到服务端,格式为: + +``` +Authorization: Bearer 你的token +X-Authorization: Bearer 你的刷新token +``` + +:::caution 特别注意 + +`Authorization` 和 `X-Authorization` 都必须添加 `Bearer ` 前缀。 + +::: + +`Furion` 框架提供了 `vue/react/angular` 客户端请求参考代码:**[https://gitee.com/dotnetchina/Furion/tree/v4/clients](https://gitee.com/dotnetchina/Furion/tree/v4/clients)** + +:::tip 小建议 + +建议使用自动生成 `Vue/React/Angular` 代理方式:[5.6 Vue/React/Angular 接口代理](./clientapi.mdx) + +::: + +:::important 其他补充 + +在正常开发中,`refreshToken` 无需每次请求携带,而是 `accessToken` 即将过期之后携带即可。可以在客户端自行判断 `accessToken` 是否即将过期。 + +::: + +如果 `Token` 过期,那么 `Furion` 将自动根据有效期内的 `refreshToken` 自动生成新的 `AccessToken`,并在 **响应报文头** 中返回,如: + +``` +access-token: 新的token +x-access-token: 新的刷新token +``` + +:::note 存储新的 Token + +前端需要获取 **响应报文头** 新的 token 和刷新 token 替换之前在客户处存储旧的 token 和刷新 token。 + +::: + +## 15.6 获取 `Jwt` 存储的信息 + +```cs showLineNumbers +// 获取 `Jwt` 存储的信息 +var userId = App.User?.FindFirstValue("键"); +``` + +**注意引入 `System.Security.Claims` 命名空间** + +:::warning 获取不到 `Token` 信息说明 + +请确保 `.AddJwt` 服务已注册且启用了 `全局授权` 或该接口(方法)贴有 `[Authorize]` 特性。 + +::: + +## 15.7 前端解密 `JWT` 信息 + +通常在用户登录成功后我们会将 `JWT Token` 存储到浏览器中,这时候就需要在浏览器端解析 `token` 里面存储的信息,可以通过调用下面方法实现: + +- `TypeScript` 版本 + +```ts showLineNumbers {7-9} +/** + * 解密 JWT token 的信息 + * @param token jwt token 字符串 + * @returns object + */ +function decryptJWT(token: string): any { + token = token.replace(/_/g, "/").replace(/-/g, "+"); + var json = decodeURIComponent(escape(window.atob(token.split(".")[1]))); + return JSON.parse(json); +} +``` + +- `JavaScript` 版本 + +```js showLineNumbers {7-9} +/** + * 解密 JWT token 的信息 + * @param token jwt token 字符串 + * @returns object + */ +function decryptJWT(token) { + token = token.replace(/_/g, "/").replace(/-/g, "+"); + var json = decodeURIComponent(escape(window.atob(token.split(".")[1]))); + return JSON.parse(json); +} +``` + +这样就可以把后端放在 `token` 里面的信息解析出来了。 + +:::tip 小知识 + +可以在解密之后读取 `过期时间 exp` 来解决请求时是否需要带刷新 `Token`,比如即将过期前 `5` 分钟。 + +::: + +## 15.8 `Jwt` 身份验证过程监听 + +有时候我们希望能够自定义或者监听 `Jwt` 验证过程,比如验证失败后在 `Response` 中添加 `Headers`,或者对接第三方验证时要求提供 `apikey` 等方式,这时候就用到了自定义功能。 + +```cs showLineNumbers {1,5,9,14,19,24} +// 注册 JWT 授权 +services.AddJwt(jwtBearerConfigure: options => +{ + // 实现 JWT 身份验证过程控制 + options.Events = new JwtBearerEvents + { + // 添加额外 Token 读取处理 + // 可以在这里实现任何方式的读取 Token,然后设置给 context.Token 即可 + OnMessageReceived = context => + { + return Task.CompletedTask; + }, + // Token 验证通过处理 + OnTokenValidated = context => + { + return Task.CompletedTask; + }, + // Token 验证失败处理 + OnAuthenticationFailed = context => + { + return Task.CompletedTask; + }, + // 客户端未提供 Token 或 Token 格式不正确处理 + OnChallenge = context => + { + return Task.CompletedTask; + } + }; +}); +``` + +### 15.8.1 实现 `Url` 参数验证 `Token` + +正常情况下,`JWT` 都是通过请求头的 `Authorization` 设置,我们可以通过下列代码实现支持 `Url` 设置 `Token`,如: + +```cs showLineNumbers {5,8,13-19} +// 实现 JWT 身份验证过程控制 +services.AddJwt(jwtBearerConfigure: options => +{ + // 实现 JWT 身份验证过程控制 + options.Events = new JwtBearerEvents + { + // 添加读取 Token 的方式 + OnMessageReceived = context => + { + var httpContext = context.HttpContext; + + // 判断请求是否包含 Authorization 参数,如果有就设置给 Token + if (httpContext.Request.Query.ContainsKey("Authorization")) + { + var token = httpContext.Request.Query["Authorization"]; + + // 设置 Token + context.Token = token; + } + + return Task.CompletedTask; + } + }; +}); +``` + +这样就可以通过:`https://www.xxxx.com?Authorization=你的Token` 访问了。 + +## 15.9 关于 `Blazor + WebAPI` 混合授权 + +一些应用使用了 `Blazor` + `WebAPI` 模板后并启用全局授权,可能会遇到 `401/403` 授权失败的提示,这时只需要在启动层 `YourProject.Web.Entry` 下的 `Pages/_Host.cshtml` 顶部添加以下代码即可: + +```cs showLineNumbers +@using Microsoft.AspNetCore.Authorization +@attribute [AllowAnonymous] +``` + +## 15.10 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: + +--- + +:::note 了解更多 + +想了解更多 `鉴权授权` 知识可查阅 [ASP.NET Core - 安全和标识](https://docs.microsoft.com/zh-cn/aspnet/core/security/?view=aspnetcore-5.0) 章节。 + +::: diff --git a/handbook/docs/author.mdx b/handbook/docs/author.mdx new file mode 100644 index 0000000000000000000000000000000000000000..e9fee1ff6ab865b94ddfb89a66548bc423e32a4e --- /dev/null +++ b/handbook/docs/author.mdx @@ -0,0 +1,63 @@ +--- +id: author +title: 1.2 关于作者 +sidebar_label: 1.2 关于作者 +description: 纵你阅人何其多,再无一人恰似我。 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +:::tip 孤芳自赏 + +纵你阅人何其多,再无一人恰似我。 + +::: + +## 网名 + +- ~~新生帝~~ +- **百小僧** +- MonkSoul +- Rustln + +## 专属头像 + + + +于 2016 年 07 月 14 日,亲自设计了该头像。 + +## 个性签名 + +成在经营,败在管理,错在不学习。 + +## 奉献宣言 + +无私奉献不是天方夜谭,有时候,我们也可以做到。 + +## 开源初衷 + +开源如同人的脸,好坏一面便知,缺点可能会受到嘲讽批评,优点也会收获赞扬尊重。别担心,他们正在塑造更好的你。 + +## 兴趣爱好 + +对新技术颇感兴趣,喜欢开源事业,喜欢分享技术,喜欢纹身文化,喜欢科技产品,喜欢掌机,喜欢穿越火线和我的世界游戏。 + +常逛开源中国、博客园、知乎、IT 之家、Github 和 Gitee,偶尔也会刷抖音,逛 B 站、看动漫和泡美剧。 + +## 个人主页 + +- Gitee:[https://gitee.com/monksoul](https://gitee.com/monksoul) +- Github:[https://github.com/monksoul](https://github.com/monksoul) +- Bilibili(B 站):[https://space.bilibili.com/695987967](https://space.bilibili.com/695987967) + +## 技术能力 + +小僧不才,自 2008 年 接触 IT 行业有十余载,对互联网主流技术略懂皮毛,最熟悉的编程技术是 `Rust`,`C#` 和 `JavaScript/React`。 + +## 常用工具 + +`Vim/NeoVim`,`Visual Studio Code`,`PostgreSQL`,`Visual Studio 2022`。 + +## 领域兴趣 + +对软件工程、架构、底层、算法、嵌入式/单片机、网络编程等颇感兴趣。 diff --git a/handbook/docs/benchmark.mdx b/handbook/docs/benchmark.mdx new file mode 100644 index 0000000000000000000000000000000000000000..0af7052a636aef367ad5705b02b2d81cd1d3d279 --- /dev/null +++ b/handbook/docs/benchmark.mdx @@ -0,0 +1,133 @@ +--- +id: benchmark +title: 36.3 基准测试 +sidebar_label: 36.3 基准测试 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +## 36.3.1 基准测试 + +基准测试(benchmarking)是一种测量和评估软件性能指标的活动。你可以在某个时候通过基准测试建立一个已知的性能水平(称为基准线),当系统的软硬件环境发生变化之后再进行一次基准测试以确定那些变化对性能的影响。这是基准测试最常见的用途。其他用途包括测定某种负载水平下的性能极限、管理系统或环境的变化、发现可能导致性能问题的条件,等等。 + +## 36.3.2 基准测试特质及意义 + +### 36.3.2.1 特质 + +- **可重复性**:可进行重复性的测试,这样做有利于比较每次的测试结果,得到性能结果的长期变化趋势,为系统调优和上线前的容量规划做参考。 +- **可观测性**:通过全方位的监控(包括测试开始到结束,执行机、服务器、数据库),及时了解和分析测试过程发生了什么。 +- **可展示性**:相关人员可以直观明了的了解测试结果(web 界面、仪表盘、折线图树状图等形式)。 +- **真实性**:测试的结果反映了客户体验到的真实的情况(真实准确的业务场景+与生产一致的配置+合理正确的测试方法)。 +- **可执行性**:相关人员可以快速的进行测试验证修改调优(可定位可分析)。 + +### 36.3.2.2 意义 + +- 为容量规划确定系统和应用程序的极限; +- 为配置测试的参数和配置选项提供参考依据; +- 为验收测试确定系统是否具备自己所宣称的能力; +- 为性能基线的建立提供长期的数据统计来源以及比较基准; + +## 36.3.3 `BenchmarkDotNet` + +`BenchmarkDotNet` 是 `.NET` 平台提供的基准测试工具,`BenchmarkDotNet` 可帮助您将方法转换为基准,跟踪其性能,并共享可重复的测量实验。`BenchmarkDotNet` 可保护您免受流行的基准测试错误,并在基准设计或获得的测量中出现问题时警告您。结果以用户友好的形式呈现,突出显示了有关实验的所有重要事实。 + +### 36.3.3.1 如何使用 + +创建一个 `控制台` 应用程序,并通过 `NuGet` 安装 [BenchmarkDotNet](https://www.nuget.org/packages/BenchmarkDotNet/) 拓展包。编写测试: + +```cs showLineNumbers {3-4,22,25,33} +using System; +using System.Security.Cryptography; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Running; + +namespace MyBenchmarks +{ + public class Md5VsSha256 + { + private const int N = 10000; + private readonly byte[] data; + + private readonly SHA256 sha256 = SHA256.Create(); + private readonly MD5 md5 = MD5.Create(); + + public Md5VsSha256() + { + data = new byte[N]; + new Random(42).NextBytes(data); + } + + [Benchmark] + public byte[] Sha256() => sha256.ComputeHash(data); + + [Benchmark] + public byte[] Md5() => md5.ComputeHash(data); + } + + public class Program + { + public static void Main(string[] args) + { + var summary = BenchmarkRunner.Run(); + } + } +} +``` + +### 36.3.3.2 查看结果 + +运行控制器程序,将得到以下结果 + +``` +BenchmarkDotNet=v0.12.0, OS=Windows 10.0.17763.805 (1809/October2018Update/Redstone5) +Intel Core i7-7700K CPU 4.20GHz (Kaby Lake), 1 CPU, 8 logical and 4 physical cores + [Host] : .NET Framework 4.7.2 (4.7.3468.0), X64 RyuJIT + Net472 : .NET Framework 4.7.2 (4.7.3468.0), X64 RyuJIT + NetCoreApp30 : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), X64 RyuJIT + CoreRt30 : .NET CoreRT 1.0.28236.02 @Commit: 741d61493c560ba96e8151f9e56876d4d3828489, X64 AOT + Mono : Mono 6.4.0 (Visual Studio), X64 +``` + +| Method | Runtime | N | Mean | Error | StdDev | Ratio | +| ------ | ------------- | ----- | ---------: | --------: | --------: | ----: | +| Sha256 | .NET 4.7.2 | 1000 | 7.735 us | 0.1913 us | 0.4034 us | 1.00 | +| Sha256 | .NET Core 3.0 | 1000 | 3.989 us | 0.0796 us | 0.0745 us | 0.50 | +| Sha256 | CoreRt 3.0 | 1000 | 4.091 us | 0.0811 us | 0.1562 us | 0.53 | +| Sha256 | Mono | 1000 | 13.117 us | 0.2485 us | 0.5019 us | 1.70 | +| | | | | | | | +| Md5 | .NET 4.7.2 | 1000 | 2.872 us | 0.0552 us | 0.0737 us | 1.00 | +| Md5 | .NET Core 3.0 | 1000 | 1.848 us | 0.0348 us | 0.0326 us | 0.64 | +| Md5 | CoreRt 3.0 | 1000 | 1.817 us | 0.0359 us | 0.0427 us | 0.63 | +| Md5 | Mono | 1000 | 3.574 us | 0.0678 us | 0.0753 us | 1.24 | +| | | | | | | | +| Sha256 | .NET 4.7.2 | 10000 | 74.509 us | 1.5787 us | 4.6052 us | 1.00 | +| Sha256 | .NET Core 3.0 | 10000 | 36.049 us | 0.7151 us | 1.0025 us | 0.49 | +| Sha256 | CoreRt 3.0 | 10000 | 36.253 us | 0.7076 us | 0.7571 us | 0.49 | +| Sha256 | Mono | 10000 | 116.350 us | 2.2555 us | 3.0110 us | 1.58 | +| | | | | | | | +| Md5 | .NET 4.7.2 | 10000 | 17.308 us | 0.3361 us | 0.4250 us | 1.00 | +| Md5 | .NET Core 3.0 | 10000 | 15.726 us | 0.2064 us | 0.1930 us | 0.90 | +| Md5 | CoreRt 3.0 | 10000 | 15.627 us | 0.2631 us | 0.2461 us | 0.89 | +| Md5 | Mono | 10000 | 30.205 us | 0.5868 us | 0.6522 us | 1.74 | + +### 36.3.3.3 导出报表 + +也可以导出各种图表 + + + +## 36.3.4 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: + +--- + +:::note 了解更多 + +想了解更多 `BenchmarkDotNet` 知识可查阅 [BenchmarkDotNet 官网](https://benchmarkdotnet.org/)。 + +::: diff --git a/handbook/docs/bingfa.mdx b/handbook/docs/bingfa.mdx new file mode 100644 index 0000000000000000000000000000000000000000..10583a60c07858be3444dc5cd6ac517591f21386 --- /dev/null +++ b/handbook/docs/bingfa.mdx @@ -0,0 +1,13 @@ +--- +id: bingfa +title: 36.4 并发测试 +sidebar_label: 36.4 并发测试 +--- + +:::tip 视频教程 + +【[并发测试视频教程](https://www.bilibili.com/video/BV1eo4y1Q7sJ/)】 + +::: + +文档紧急编写中,可以先看旧文档:https://monksoul.gitbook.io/hoa/ diff --git a/handbook/docs/bs-to-cs.mdx b/handbook/docs/bs-to-cs.mdx new file mode 100644 index 0000000000000000000000000000000000000000..b83b3463aaab1287c8e54c39211a717d317f3843 --- /dev/null +++ b/handbook/docs/bs-to-cs.mdx @@ -0,0 +1,375 @@ +--- +id: bs-to-cs +title: 34.7 发布桌面程序(WinForm/WPF) +sidebar_label: 34.7 发布桌面程序(WinForm/WPF) +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.7.28 +` 版本使用。 + +::: + +`Furion` 框架灵活的架构模式使得 `Web` 和各客户端(`WinForm/WPF/Console`) 项目之间迁移变得非常简单,只需要极少数修改即可实现无缝迁移。 + +**通过这种方式可以实现类似前端的 `Electron/Tauri` 混合式开发。** + +本章节提供如何将 `Web` 项目发布(迁移)到 `WinForm/WPF` 中,同样也适合控制台应用程序。 + +## 34.7.1 迁移步骤 + +### 34.7.1.1 创建 `WinForm/WPF` 项目 + +通过 `Visual Studio` 创建 `WinForm/WPF` 项目,同样适合控制台项目。 + +### 34.7.1.2 初始化 `WinForm/WPF` 配置 + +- `WinForm` 初始化 + +```cs showLineNumbers {3,8-9,12} +namespace WinFormsApp1; + +internal static class Program +{ + [STAThread] + private static void Main() + { + Serve.RunNative(RunOptions.Default); // 默认 5000 端口,如果出现占用,推荐使用下面的方式 + // Serve.RunNative(RunOptions.Default, Serve.IdleHost.Urls); // 随机端口 + + ApplicationConfiguration.Initialize(); + Application.Run(Native.CreateInstance()); + } +} +``` + +:::tip 关于 `Web` 主机环境 + +默认情况下,通过 `WinForm` 启动 `Web` 主机环境总是为 `Production`,但在开发阶段可能会出现配置加载错误问题,这时只需要添加 `ConfigureOptions` 配置即可,如: + +```cs showLineNumbers {2,4-5} +Serve.RunNative(RunOptions.Default + .ConfigureOptions(new WebApplicationOptions + { + // Debugger.IsAttached 可判断释放为 Debug 模式 + EnvironmentName = Debugger.IsAttached ? "Development" : default + })); +``` + +::: + +- `WPF` 初始化 + +```cs showLineNumbers {5,9-10,13-17} +using System.Windows; + +namespace WpfApp1; + +public partial class App : Application +{ + public App() + { + Serve.RunNative(RunOptions.Default); // 默认 5000 端口,如果出现占用,推荐使用下面的方式 + // Serve.RunNative(RunOptions.Default, Serve.IdleHost.Urls); // 随机端口 + } + + protected override void OnStartup(StartupEventArgs e) + { + Native.CreateInstance().Show(); + base.OnStartup(e); + } +} +``` + +:::tip 关于 `Web` 主机环境 + +默认情况下,通过 `WPF` 启动 `Web` 主机环境总是为 `Production`,但在开发阶段可能会出现配置加载错误问题,这时只需要添加 `ConfigureOptions` 配置即可,如: + +```cs showLineNumbers {2,4-5} +Serve.RunNative(RunOptions.Default + .ConfigureOptions(new WebApplicationOptions + { + // Debugger.IsAttached 可判断释放为 Debug 模式 + EnvironmentName = Debugger.IsAttached ? "Development" : default + })); +``` + +::: + +### 34.7.1.3 添加 `YourProject.Web.Core` 层引用 + +```xml showLineNumbers {2} + + + +``` + +### 34.7.1.4 拷贝 `YourProject.Web.Entry` 内容 + +如果有 `YourProject.Web.Entry` 启动层包含 `*.json` 文件、`Controllers`、`Views` 、`wwwroot` 目录则拷贝到 `WinForm/WPF` 中 + + + +## 34.7.2 配置 `WinForm/WPF` 项目文件 + +双击 `WinForm/WPF` 项目进入 `.csproj` 文件编辑。 + +### 34.7.2.1 修改 `Sdk` 属性 + +将 `Sdk="Microsoft.NET.Sdk"` 修改为 `Sdk="Microsoft.NET.Sdk.Razor"` + +```xml showLineNumbers + +``` + +### 34.7.2.2 添加 `MVC/Razor` 支持 + +- `WinForm` + +```xml showLineNumbers {1,9-11} + + + + WinExe + net7.0-windows + enable + true + enable + en-US + true + true + + + +``` + +- `WPF` + +```xml showLineNumbers {1,9-11} + + + + WinExe + net7.0-windows + enable + enable + true + en-US + true + true + + + + +``` + +### 34.7.2.3 添加 `wwwroot` 静态发布配置 + +如果 `YourProject.Web.Entry` 包含 `wwwroot` 目录,则添加以下配置,否则跳过。 + +```xml showLineNumbers + + + PreserveNewest + + +``` + +### 34.7.2.4 完整配置 + +完整的配置大概如下: + +- `WinForm` + +```xml showLineNumbers {1,9-11,14-18,20-22} + + + + WinExe + net7.0-windows + enable + true + enable + en-US + true + true + + + + + PreserveNewest + + + + + + + + +``` + +- `WPF` + +```xml showLineNumbers {1,9-11,14-18,20-22} + + + + WinExe + net7.0-windows + enable + enable + true + en-US + true + true + + + + + PreserveNewest + + + + + + + + + +``` + +## 37.7.3 嵌入 `WebView2` + +我们可以在 `WinForm/WinForm` 层添加 `Microsoft.Web.WebView2` 包,然后在窗口中添加 `WebView2` 组件,实现类似前端 `Electron.js` 混合开发。 + +### 37.7.3.1 添加 `WebView2` 拓展 + +```bash showLineNumbers +dotnet add package Microsoft.Web.WebView2 +``` + +:::caution 特别注意 + +**`Microsoft.Web.WebView2 v1.0.1722.32` 版本存在很严重问题(微软错误更新),所以避免安装该版本。** + +::: + +### 37.7.3.2 添加 `WebView2` 控件并填充窗口 + +- `WinForm` + +```cs showLineNumbers {8,12-13} +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.Extensions.Hosting; + +namespace WinFormsApp1; + +public partial class Form1 : Form +{ + public Form1(IServer server) // 注入 IServer 服务,获取 Web 启动地址/端口 + { + InitializeComponent(); + + webview.Dock = DockStyle.Fill; + webview.Source = new Uri(server.GetServerAddress()); + } +} +``` + +- `WPF` + +```xml + +``` + +```cs showLineNumbers {9,12} +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.Extensions.Hosting; +using System.Windows; + +namespace WpfApp1; + +public partial class MainWindow : Window +{ + public MainWindow(IServer server) // 注入 IServer 服务,获取 Web 启动地址/端口 + { + InitializeComponent(); + webview.Source = new Uri(server.GetServerAddress()); + } +} +``` + +### 34.7.3.3 预览效果 + + + + + +
+
+ +相关技术文档: + +- [https://learn.microsoft.com/zh-cn/microsoft-edge/webview2/get-started/winforms](https://learn.microsoft.com/zh-cn/microsoft-edge/webview2/get-started/winforms) +- [https://learn.microsoft.com/zh-cn/microsoft-edge/webview2/get-started/wpf](https://learn.microsoft.com/zh-cn/microsoft-edge/webview2/get-started/wpf) + +## 34.7.4 发布 `WinForm/WPF` + +### 34.7.4.1 启动层添加 `SingleFilePublish` + +通常桌面软件都是发布为不依赖环境(独立发布),所以在启动层添加 `SingleFilePublish.cs` 文件编辑。 + +**详细教程可查看 [【34.5 单文件发布】](./singlefile.mdx)** + +```cs showLineNumbers {4,6} +using Furion; +using System.Reflection; + +namespace WinFormsApp1; + +public class SingleFilePublish : ISingleFilePublish +{ + public Assembly[] IncludeAssemblies() + { + return Array.Empty(); + } + + public string[] IncludeAssemblyNames() + { + return new[] + { + "Furion.Application", + "Furion.Core", + "Furion.EntityFramework.Core", + "Furion.Web.Core" + }; + } +} + +``` + +### 34.7.4.2 发布配置 + +发布可参考以下配置即可。 + + + +相关技术文档: + +- [Furion 示例代码](https://gitee.com/dotnetchina/Furion/tree/v4/samples) + +## 34.7.5 打包成 `exe` 或 `msi` + +可参考以下文档: + +[https://blog.csdn.net/xfy18317776108/article/details/122343091](https://blog.csdn.net/xfy18317776108/article/details/122343091) + +## 34.7.6 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/bug-report.mdx b/handbook/docs/bug-report.mdx new file mode 100644 index 0000000000000000000000000000000000000000..b4dc18074400b15d6effb5257816400cd0aa3927 --- /dev/null +++ b/handbook/docs/bug-report.mdx @@ -0,0 +1,21 @@ +--- +id: bug-report +title: 39. 安全漏洞报告 +sidebar_label: 39. 安全漏洞报告 +--- + +**报告安全漏洞的方式** + +我们非常重视用户对我们开源项目的安全性关注,并欢迎您报告任何发现的安全漏洞。为了确保我们能够迅速响应和解决安全问题,请您按照以下步骤向我们报告安全漏洞: + +1. **收集信息**:尽可能详细地记录漏洞的描述、影响范围、可能的攻击方法等信息。您可以在报告中提供相关的截图、日志文件等辅助信息。 + +2. **联系方式**:请您提供您的联系方式,以便我们与您进行进一步的沟通和反馈。您可以在报告中留下您的电子邮件地址、社交媒体账号或其他可靠的联系方式。 + +3. **发送报告**:请将您的安全漏洞报告发送至我们指定的安全邮箱 (monksoul@outlook.com) 或报告渠道 (作者微信:ibaiqian)。如果我们有提供特定的报告指南或漏洞报告表格,请您按照指示进行报告。如果没有特定的指引,您可以通过邮件发送报告给我们的安全团队。 + +4. **等待回复**:我们将尽快收到您的报告后进行评估,并在确认漏洞的准确性后与您联系。请理解我们可能需要一些时间来进行调查和修复漏洞,我们会及时地与您保持沟通并提供进展更新。 + +我们非常感谢您对我们项目安全的关注和贡献。您的报告将帮助我们改进和加强项目的安全性,并确保用户的数据和隐私得到有效的保护。如果您对报告过程有任何疑问或需要更多的指导,请随时联系我们。 + +再次感谢您对我们项目的支持和关注! \ No newline at end of file diff --git a/handbook/docs/cache.mdx b/handbook/docs/cache.mdx new file mode 100644 index 0000000000000000000000000000000000000000..194171812f700fd2b412cb4dc2e598feccd87a83 --- /dev/null +++ b/handbook/docs/cache.mdx @@ -0,0 +1,351 @@ +--- +id: cache +title: 14. 分布式缓存 +sidebar_label: 14. 分布式缓存 +--- + +## 14.1 什么是缓存 + +缓存可以减少生成内容所需的工作,从而显著提高应用程序的性能和可伸缩性。 **缓存适用于不经常更改的数据,因为生成成本很高**。 通过缓存,可比从数据源返回数据的副本速度快得多。 应该对应用进行编写和测试,使其不要永远依赖于缓存的数据。 + +## 14.2 缓存类型 + +- 内存缓存:顾名思义,就是缓存在应用部署所在服务器的内存中 +- 分布式缓存:分布式缓存是由多个应用服务器共享的缓存 +- 响应缓存:缓存服务器端 `Not Modified` 的数据 + +## 14.3 内存缓存使用 + +内存缓存是最常用的缓存方式,具有存取快,效率高特点。 + +内存缓存通过注入 `IMemoryCache` 方式注入即可。 + +:::important 备注 + +在 `Furion` 框架中,内存缓存服务已经默认注册,无需手动注册。 + +::: + +### 14.3.1 基本使用 + +如,缓存当前时间: + +```cs showLineNumbers {13,21-24} +using Furion.DynamicApiController; +using Microsoft.Extensions.Caching.Memory; +using System; + +namespace Furion.Application +{ + public class CacheServices : IDynamicApiController + { + private const string _timeCacheKey = "cache_time"; + + private readonly IMemoryCache _memoryCache; + + public CacheServices(IMemoryCache memoryCache) + { + _memoryCache = memoryCache; + } + + [ApiDescriptionSettings(KeepName = true)] + public DateTimeOffset GetOrCreate() + { + return _memoryCache.GetOrCreate(_timeCacheKey, entry => + { + return DateTimeOffset.UtcNow; + }); + } + } +} +``` + +### 14.3.2 设置缓存选项 + +内存缓存支持设置缓存时间、缓存大小、及绝对缓存过期时间等 + +```cs showLineNumbers +_memoryCache.GetOrCreate(_timeCacheKey, entry => +{ + entry.SlidingExpiration = TimeSpan.FromSeconds(3); // 滑动缓存时间 + return DateTimeOffset.UtcNow; +}); + +await _memoryCache.GetOrCreateAsync(_timeCacheKey, async entry => +{ + // 这里可以使用异步~~ +}); +``` + +:::caution 关于缓存时间 + +具有可调过期的缓存项集存在过时的风险。 如果访问的时间比滑动过期时间间隔更频繁,则该项将永不过期。 + +将弹性过期与绝对过期组合在一起,以保证项目在其绝对过期时间通过后过期。 绝对过期会将项的上限设置为可缓存项的时间,同时仍允许项在可调整过期时间间隔内未请求时提前过期。 + +如果同时指定了绝对过期和可调过期时间,则过期时间以逻辑方式运算。 如果滑动过期时间间隔 或 绝对过期时间通过,则从缓存中逐出该项。 + +如: + +```cs showLineNumbers +_memoryCache.GetOrCreate(_timeCacheKey, entry => +{ + entry.SetSlidingExpiration(TimeSpan.FromSeconds(3)); + entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(20); + return DateTime.Now; +}); +``` + +前面的代码保证数据的缓存时间不超过绝对时间。 + +::: + +### 14.3.3 手动设置缓存选项 + +除了上面的 `Func` 方式设置缓存选项,我们可以手动创建并设置,如: + +```cs showLineNumbers +var cacheEntryOptions = new MemoryCacheEntryOptions() + .SetSlidingExpiration(TimeSpan.FromSeconds(3)); + +_memoryCache.Set(_timeCacheKey, DateTimeOffset.UtcNow, cacheEntryOptions); +``` + +### 14.3.4 缓存依赖关系 + +下面的示例演示如何在依赖条目过期后使缓存条目过期。 `CancellationChangeToken` 添加到缓存的项。 当 `Cancel` 在上调用时 `CancellationTokenSource` ,将逐出两个缓存项。 + +```cs showLineNumbers +public IActionResult CreateDependentEntries() +{ + var cts = new CancellationTokenSource(); + _cache.Set(CacheKeys.DependentCTS, cts); + + using (var entry = _cache.CreateEntry(CacheKeys.Parent)) + { + // expire this entry if the dependant entry expires. + entry.Value = DateTime.Now; + entry.RegisterPostEvictionCallback(DependentEvictionCallback, this); + + _cache.Set(CacheKeys.Child, + DateTime.Now, + new CancellationChangeToken(cts.Token)); + } + + return RedirectToAction("GetDependentEntries"); +} + +public IActionResult GetDependentEntries() +{ + return View("Dependent", new DependentViewModel + { + ParentCachedTime = _cache.Get(CacheKeys.Parent), + ChildCachedTime = _cache.Get(CacheKeys.Child), + Message = _cache.Get(CacheKeys.DependentMessage) + }); +} + +public IActionResult RemoveChildEntry() +{ + _cache.Get(CacheKeys.DependentCTS).Cancel(); + return RedirectToAction("GetDependentEntries"); +} + +private static void DependentEvictionCallback(object key, object value, + EvictionReason reason, object state) +{ + var message = $"Parent entry was evicted. Reason: {reason}."; + ((HomeController)state)._cache.Set(CacheKeys.DependentMessage, message); +} +``` + +使用 `CancellationTokenSource` 允许将多个缓存条目作为一个组逐出。 `using` 在上面的代码中,在块中创建的缓存条目 `using` 将继承触发器和过期设置。 + +:::note 了解更多 + +想了解更多 `内存中的缓存` 知识可查阅 [ASP.NET Core - 内存缓存](https://docs.microsoft.com/zh-cn/aspnet/core/performance/caching/memory?view=aspnetcore-5.0) 章节。 + +::: + +## 14.4 分布式缓存 + +分布式缓存是由多个应用服务器共享的缓存,通常作为外部服务在访问它的应用服务器上维护。 分布式缓存可以提高 `ASP.NET Core` 应用程序的性能和可伸缩性,尤其是在应用程序由云服务或服务器场托管时。 + +与其他缓存方案相比,分布式缓存具有多项优势,其中缓存的数据存储在单个应用服务器上。 + +当分布式缓存数据时,数据将: + +- (一致性) 跨多个 服务器的请求 +- 存活在服务器重启和应用部署之间 +- 不使用本地内存 + +分布式缓存配置是特定于实现的。 本文介绍如何配置 `SQL Server` 和 `Redis` 分布式缓存。 第三方实现也可用,例如 GitHub 上的 [NCache](https://github.com/Alachisoft/NCache) (NCache) 。 + +**无论选择哪种实现,应用都会使用接口与缓存交互 `IDistributedCache` 。** + +### 14.4.1 使用条件 + +- 若要使用 `SQL Server` 分布式缓存,则添加 `Microsoft.Extensions.Caching.SqlServer` 包 +- 若要使用 `Redis` 分布式缓存,则添加 `Microsoft.Extensions.Caching.StackExchangeRedis` 包 +- 若要使用 `NCache` 分布式缓存,则添加 `NCache.Microsoft.Extensions.Caching.OpenSource` 包 + +### 14.4.2 `IDistributedCache` + +`IDistributedCache` 接口提供以下方法来处理分布式缓存实现中的项: + +- `Get/GetAsync`:接受字符串键,并检索缓存项作为 `byte[]` 数组(如果在缓存中找到) +- `Set/SetAsync`:使用字符串键将项 (作为 `byte[]` 数组) 添加到缓存中 +- `Refresh/RefreshAsync` :根据项的键刷新缓存中的项,重置其滑动过期超时(如果有) +- `Remove/RemoveAsync`:根据缓存项的字符串键删除缓存项 + +### 14.4.3 分布式内存缓存 + +分布式内存缓存(`AddDistributedMemoryCache`)是一个框架提供的实现 `IDistributedCache` ,它将项存储在内存中。 **分布式内存缓存不是实际的分布式缓存,缓存项由应用程序实例存储在运行应用程序的服务器上。** + +分布式内存缓存优点: + +- 用于开发和测试方案。 +- 在生产环境中使用单一服务器并且内存消耗不是问题。 实现分布式内存缓存会抽象化缓存的数据存储。 如果需要多个节点或容错,可以在将来实现真正的分布式缓存解决方案。 + +:::important 备注 + +在 `Furion` 框架中,分布式内存缓存服务已经默认注册,无需手动调用 `services.AddDistributedMemoryCache();` 注册。 + +::: + +### 14.4.4 分布式 SQL Server 缓存 + +分布式 `SQL Server` 缓存实现 (`AddDistributedSqlServerCache`) 允许分布式缓存使用 `SQL Server` 数据库作为其后备存储。 + +若要在 `SQL Server` 实例中创建 `SQL Server` 缓存的项表,可以使用 `sql-cache` 工具。 该工具将创建一个表,其中包含指定的名称和架构。 + +通过运行命令 `sql-cache create` 创建一个表,提供 `SQL Server` 实例 (Data Source) 、数据库 (Initial Catalog) 、架构 (例如) dbo 和表名称。例如 `TestCache`: + +```shell showLineNumbers +dotnet sql-cache create "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=DistCache;Integrated Security=True;" dbo TestCache +``` + +创建成功后,在 `Startup.cs` 中注册即可: + +```cs showLineNumbers +services.AddDistributedSqlServerCache(options => +{ + options.ConnectionString = + _config["DistCache_ConnectionString"]; + options.SchemaName = "dbo"; + options.TableName = "TestCache"; +}); +``` + +### 14.4.5 分布式 `Redis` 缓存 + +`Redis` 是内存中数据存储的开源数据存储,通常用作分布式缓存。在使用时通过 `services.AddStackExchangeRedisCache()` 中注册即可。 + +这里不细讲 `Redis` 相关内容,后续章节会使用基本例子演示。 + +`Redis` 基本配置: + +```cs showLineNumbers +services.AddStackExchangeRedisCache(options => +{ + // 连接字符串,这里也可以读取配置文件 + options.Configuration = "192.168.111.134,password=aW1HAyupRKmiZn3Q"; + // 键名前缀 + options.InstanceName = "furion_"; +}); +``` + +### 14.4.6 分布式 `NCache` 缓存 + +`NCache` 是在 `.NET` 和 `.Net Core` 中以本机方式开发的开源内存中分布式缓存。 `NCache` 在本地工作并配置为分布式缓存群集,适用于在 `Azure` 或其他托管平台上运行的 `ASP.NET Core` 应用。 +若要在本地计算机上安装和配置 `NCache`,请参阅 [适用于 Windows 的 NCache 入门指南](https://www.alachisoft.com/resources/docs/ncache-oss/getting-started-guide-windows/)。 + +`NCache` 基本配置: + +- 安装 `Alachisoft.NCache.OpenSource.SDK` 包 +- 在 [ncconf](https://www.alachisoft.com/resources/docs/ncache-oss/admin-guide/client-config.html) 中配置缓存群集 +- 注册 `NCache` 服务 + +```cs showLineNumbers +services.AddNCacheDistributedCache(configuration => +{ + configuration.CacheName = "demoClusteredCache"; + configuration.EnableLogs = true; + configuration.ExceptionsEnabled = true; +}); +``` + +## 14.5 分布式缓存使用 + +若要使用 `IDistributedCache` 接口,请 `IDistributedCache` 通过构造函数依赖关系注入。 + +```cs showLineNumbers {5,16,30-33} +public class IndexModel : PageModel +{ + private readonly IDistributedCache _cache; + + public IndexModel(IDistributedCache cache) + { + _cache = cache; + } + + public string CachedTimeUTC { get; set; } + + public async Task OnGetAsync() + { + CachedTimeUTC = "Cached Time Expired"; + // 获取分布式缓存 + var encodedCachedTimeUTC = await _cache.GetAsync("cachedTimeUTC"); + + if (encodedCachedTimeUTC != null) + { + CachedTimeUTC = Encoding.UTF8.GetString(encodedCachedTimeUTC); + } + } + + public async Task OnPostResetCachedTime() + { + var currentTimeUTC = DateTime.UtcNow.ToString(); + byte[] encodedCurrentTimeUTC = Encoding.UTF8.GetBytes(currentTimeUTC); + + // 设置分布式缓存 + var options = new DistributedCacheEntryOptions() + .SetSlidingExpiration(TimeSpan.FromSeconds(20)); + + await _cache.SetAsync("cachedTimeUTC", encodedCurrentTimeUTC, options); + + return RedirectToPage(); + } +} +``` + +## 14.6 分布式缓存建议 + +确定 `IDistributedCache` 最适合你的应用的实现时,请考虑以下事项: + +- 现有基础结构 +- 性能要求 +- 成本 +- 团队经验 + +缓存解决方案通常依赖于内存中的存储以快速检索缓存的数据,但是,内存是有限的资源,并且很昂贵。 仅将常用数据存储在缓存中。 + +通常,**`Redis` 缓存提供比 `SQL Server` 缓存更高的吞吐量和更低的延迟。** 但是,通常需要进行基准测试来确定缓存策略的性能特征。 + +当 `SQL Server` 用作分布式缓存后备存储时,对缓存使用同一数据库,并且应用的普通数据存储和检索会对这两种情况的性能产生负面影响。 建议使用分布式缓存后备存储的专用 `SQL Server` 实例。 + +## 14.7 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: + +--- + +:::note 了解更多 + +想了解更多 `分布式缓存` 知识可查阅 [ASP.NET Core - 分布式缓存](https://docs.microsoft.com/zh-cn/aspnet/core/performance/caching/distributed?view=aspnetcore-5.0) 章节。 + +::: diff --git a/handbook/docs/case.mdx b/handbook/docs/case.mdx new file mode 100644 index 0000000000000000000000000000000000000000..651f298fa73835a7a298d961aeca42bd0c2e8a5a --- /dev/null +++ b/handbook/docs/case.mdx @@ -0,0 +1,36 @@ +--- +id: case +title: 1.4 开源案例 +sidebar_label: 1.4 开源案例 +description: 这里有优秀的开源案例,非常期待您的开源 +--- + +:::tip 案例说明 + +以下案例均由开发者自愿提供,Furion 官方不做调查征集。若您愿意公开您的案例,可点击底部 `Edit this page` 按钮进行编辑提交。 + +::: + +越来越多的开发者使用 `Furion` 框架进行开发,以下是已知且开源的案例(**非官方仅供参考**): + +- **[Yi.Admin](https://gitee.com/ccnetcore/Yi)**: 基于 `Furion` + `SqlSugar` + `RuoYi Vue3` 一套以用户体验出发的 `.NET6 Web` 开源框架系统,集大成者,终究轮子 🔥 +- **[Easy.Admin](https://gitee.com/miss_you/easy-admin)**:基于 `Furion` + `Sqlsugar` + `Vue-Next-Admin` + `vite-plugin-ssr` 开发的博客(支持服务端渲染) 🆕 +- **[ThingsGateway](https://gitee.com/diego2098/ThingsGateway)**: 基于 `Furion` + `SqlSugar` + `BlazorServer` 的工业物联网关 🆕 +- **[SimpleAdmin](https://gitee.com/zxzyjs/SimpleAdmin)**: 基于 `Furion` + `Vue3` 开发的通用管理系统 🆕 +- **[TulingMember](https://gitee.com/a106_admin/tuling-member)**: 基于 `Furion` + `IView` 开发的极简进销管理系统 🔥 +- **[WeiXinApi](https://gitee.com/huguodong520/weixinapi)**: 基于`Furion` + `SenparcSdk` 的微信公众号开发 Demo +- **[Magic.NET](https://gitee.com/zhengguojing/magic-net)**:基于 `Furion` + `SqlSugar` 的通用权限管理平台 🔥 +- **[Abc.Mvc](https://gitee.com/co1024/AbcMvc)**: 基于 `NET7`+ `Furion` + `EFCore` + `SqlSugar` + `Pear Layui Addmin` 的 `MVC` 后台管理框架(`EFCore` 为主,`SqlSugar` 为辅) 🆕 +- **[Abc.Mvc_SqlSugar](https://gitee.com/co1024/abcmvc_sqlsugar)**: 基于 `NET7`+ `Furion`+ `SqlSugar` + `Pear Layui Admin` 的 `MVC` 后台管理框架(`SqlSugar` 为主) 🆕 +- **[BaseNet](https://gitee.com/wg_jianghai/base-net)**: 基于`Furion` + `Pear Admin Layui` 快速开发基础框架,包含前端、后台权限控制,到手即用 +- **[NiuPi](https://gitee.com/GionConnection/niupi-free)**: 基于 `Furion` + `SqlSugar` + `Vue-Next-Admin` 细粒度权限控制的快速开发框架 +- **[Admin.NET](https://gitee.com/zuohuaijun/Admin.NET)**:基于 `Furion` 的通用权限管理平台 🔥 +- **[考试君](https://gitee.com/pig0224/ExamKing)**:基于 `Furion` 的在线考试系统 +- **[园丁](https://gitee.com/hgflydream/Gardener)**:基于 `Furion` + `Blazor` 的超简单后台管理系统 +- **[Queer](https://gitee.com/songzhidan/queer)**:基于 `Furion` + `Layui` 的通用型管理系统 +- **[Pear Admin](https://gitee.com/pear-admin/pear-admin-furion)**:基于 `Furion` + `PearAdmin` 管理系统 +- **[JoyAdmin](https://gitee.com/a106_admin/joy-admin)**:基于 `Furion` + `IViewAdmin` 开发的管理系统 +- ~~**[YShop](https://gitee.com/yell-run/yshop)**:基于 `Furion` + `Vue` 开发的移动电商项目(已弃)~~ +- **[Vboot](https://gitee.com/zsvg/vboot-net)**: 基于 `Furion` + `Vben` 开发的快速开发管理平台 + +如果您使用了 `Furion` 进行项目开发,可以告诉我们,点击底部的 `Edit this page` 进行编辑添加。 diff --git a/handbook/docs/clayobj.mdx b/handbook/docs/clayobj.mdx new file mode 100644 index 0000000000000000000000000000000000000000..0a6e554ad029c350ace69dd51149215d3cb65bb7 --- /dev/null +++ b/handbook/docs/clayobj.mdx @@ -0,0 +1,499 @@ +--- +id: clayobj +title: 29. 粘土对象 +sidebar_label: 29. 粘土对象 +--- + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **新特性** + + -  新增 粘土对象 `.ConvertTo` 支持自定义值提供器 4.8.8.40 ⏱️2023.08.03 [70d5888](https://gitee.com/dotnetchina/Furion/commit/70d58888b3cec88c5c2a8458654dca1881e2a88b) + -  新增 粘土对象支持结构 `struct` 对象类型 4.8.8.7 ⏱️2023.04.30 [a0fa3aa](https://gitee.com/dotnetchina/Furion/commit/a0fa3aa7ae536e948740401b510d99cf45e251dc) + -  新增 粘土对象可反射转换成特定 `IEnumerable` 类型:`clay.ConverTo()` 4.8.8 ⏱️2023.04.13 [5d54a65](https://gitee.com/dotnetchina/Furion/commit/5d54a6579be3d710649bb199dd985f60acaf9787) + -  新增 **粘土对象可配置访问不存在 `Key` 时是抛异常还是返回 `null`** 4.8.7.40 ⏱️2023.04.10 [e994d53](https://gitee.com/dotnetchina/Furion/commit/e994d53b64a825461673f48960df1716be44f192) + -  新增 **粘土对象可转换成 `IEnumerable` 对象并实现 `Lambda/Linq` 操作** 4.8.7.19 ⏱️2023.03.22 [2b14ed9](https://gitee.com/dotnetchina/Furion/commit/2b14ed9da03699619b1fade6e053f65b77a5b0fe) + +
+ 查看变化 +
+ +```cs showLineNumbers {3-4,6-12} +dynamic clay = Clay.Parse("{\"Foo\":\"json\",\"Bar\":100,\"Nest\":{\"Foobar\":true},\"Arr\":[\"NOR\",\"XOR\"]}"); + +// 将 clay.Arr 转换成 IEnumerable +IEnumerable query = clay.Arr.AsEnumerator(); + +// 实现 Lambda/Linq 操作 +var result = query.Where(u => u.StartsWith("N")) + .Select(u => new + { + Name = u + }) + .ToList(); +``` + +
+
+ +- -  新增 粘土对象支持任何字符作为 `JSON/XML` 键 4.8.6.9 ⏱️2023.02.19 [f99aee8](https://gitee.com/dotnetchina/Furion/commit/f99aee8dafddf3cfd148922166abd3998e8eb087) [#note_16329657](https://gitee.com/dotnetchina/Furion/commit/4961e01486f604db12a8d8d71f9bd563ed7d7d48#note_16329657) + +- **问题修复** + + -  修复 粘土对象不支持枚举类型问题 4.8.8.41 ⏱️2023.08.25 [#I7VDDL](https://gitee.com/dotnetchina/Furion/issues/I7VDDL) + -  修复 `ExpandoObject.ToDictionary()` 转换异常 4.8.8.25 ⏱️2023.06.14 [#I7BY0P](https://gitee.com/dotnetchina/Furion/issues/I7BY0P) + -  修复 粘土对象转换为 `Dictionary` 类型异常 4.8.7.41 ⏱️2023.04.11 [f96baeb](https://gitee.com/dotnetchina/Furion/commit/f96baebbb06b53fc481ea7925cbbfbcb191f9c10) + -  修复 粘土对象不支持运行时动态设置携带特殊字符的 `Key` 键 4.8.7.39 ⏱️2023.04.10 [6572515](https://gitee.com/dotnetchina/Furion/commit/6572515abbd93c4572cc513da4dd5aa497d144d2) + -  修复 粘土对象遍历对象键值对因 `4.8.7.19` 版本更新导致异常 4.8.7.25 ⏱️2023.03.28 [#I6R4ZU](https://gitee.com/dotnetchina/Furion/issues/I6R4ZU) + -  修复 粘土对象不支持 `数字` 作为 `JSON/XML` 键问题 4.8.6.9 ⏱️2023.02.19 [#note_16329657](https://gitee.com/dotnetchina/Furion/commit/4961e01486f604db12a8d8d71f9bd563ed7d7d48#note_16329657) + -  修复 粘土对象不支持 `中文` 作为 `JSON/XML` 键问题 4.8.6.6 ⏱️2023.02.18 [4961e01](https://gitee.com/dotnetchina/Furion/commit/4961e01486f604db12a8d8d71f9bd563ed7d7d48) + +- **其他更改** + + -  调整 粘土对象 `number` 类型处理,若含 `.` 转 `double` 类型,否则转 `long` 类型 4.8.7.24 ⏱️2023.03.28 [e82e883](https://gitee.com/dotnetchina/Furion/commit/e82e883d54282b749390ae5e93df8c3e7acaa97e) + +
+
+
+ +:::important 版本说明 + +以下内容仅限 `Furion 2.1.12 +` 版本使用。 + +::: + +## 29.1 关于粘土对象 + +粘土对象是 `Furion` 框架推出的一种动态类型,可以模拟弱(动态)语言操作特性,使 C# 对象实现类似 `JavaScript` 一样操作对象。只需通过 `Clay` 类初始化即可。 + +**为什么起名为 “粘土” 呢?因为这个对象可以自由的添加属性,移除属性,又可以固化成任何对象,具有可拓展、可塑造的特点。** + +### 29.1.1 使用场景 + +粘土对象常用于需要动态构建对象的地方,如 `CMS` 系统的 `ViewModel`,或者运行时创建一个新的对象,或者请求第三方 `API` 情况。 + +### 29.1.2 关于性能 + +粘土性能实际上并不高效,但是性能也并不低下,只不过略输于强类型调用。什么时候使用可以看以上的【使用场景】。 + +## 29.2 `Clay` 对象 + +`Clay` 对象是继承自 `DynamicObject` 的一个特殊对象,提供了像弱(动态)语言一样操作对象的方法及索引获取方式。 + +## 29.3 如何使用 + +### 29.3.1 创建一个对象 + +```cs showLineNumbers {2,5,8} +// 创建一个空的粘土对象 +dynamic clay = new Clay(); + +// 从现有的对象创建 +dynamic clay2 = Clay.Object(new {}); + +// 从 json 字符串创建,可用于第三方 API 对接,非常有用 +dynamic clay3 = Clay.Parse(@"{""foo"":""json"", ""bar"":100, ""nest"":{ ""foobar"":true } }"); +``` + +### 29.3.2 读取/获取属性 + +```cs showLineNumbers {1,11-14} +dynamic clay = Clay.Object(new +{ + Foo = "json", + Bar = 100, + Nest = new + { + Foobar = true + } +}); + +var r1 = clay.Foo; // "json" - string类型 +var r2 = clay.Bar; // 100 - double类型 +var r3 = clay.Nest.Foobar; // true - bool类型 +var r4 = clay["Nest"]["Foobar"]; // 还可以和 JavaScript 一样通过索引器获取 +``` + +### 29.3.3 新增属性 + +```cs showLineNumbers {1,12-14} +dynamic clay = Clay.Object(new +{ + Foo = "json", + Bar = 100, + Nest = new + { + Foobar = true + } +}); + +// 新增 +clay.Arr = new string[] { "NOR", "XOR" }; // 添加一个数组 +clay.Obj1 = new City { }; // 新增一个实例对象 +clay.Obj2 = new { Foo = "abc", Bar = 100 }; // 新增一个匿名类 +``` + +### 29.3.4 更新属性值 + +```cs showLineNumbers {1,12-14} +dynamic clay = Clay.Object(new +{ + Foo = "json", + Bar = 100, + Nest = new + { + Foobar = true + } +}); + +// 更新 +clay.Foo = "Furion"; +clay["Nest"].Foobar = false; +clay.Nest["Foobar"] = true; +``` + +### 29.3.5 删除属性 + +```cs showLineNumbers {1,13-16} +dynamic clay = Clay.Object(new +{ + Foo = "json", + Bar = 100, + Nest = new + { + Foobar = true + }, + Arr = new string[] { "NOR", "XOR" } +}); + +// 删除操作 +clay.Delete("Foo"); // 通过 Delete 方法删除 +clay.Arr.Delete(0); // 支持数组 Delete 索引删除 +clay("Bar"); // 支持直接通过对象作为方法删除 +clay.Arr(1); // 支持数组作为方法删除 +``` + +### 29.3.6 判断键/索引是否存在 + +```cs showLineNumbers {1,13-18} +dynamic clay = Clay.Object(new +{ + Foo = "json", + Bar = 100, + Nest = new + { + Foobar = true + }, + Arr = new string[] { "NOR", "XOR" } +}); + +// 判断属性是否存在 +var a = clay.IsDefined("Foo"); // true +var b = clay.IsDefined("Foooo"); // false +var c = clay.Foo(); // true +var d = clay.Foooo(); // false; +var e = clay.Arr.IsDefined(0); // true +var f = clay.Arr.IsDefined(3); // false +``` + +### 29.3.7 遍历对象 + +```cs showLineNumbers {12,18,24,33,36} +dynamic clay = Clay.Object(new +{ + Foo = "json", + Bar = 100, + Nest = new + { + Foobar = true + }, + Arr = new string[] { "NOR", "XOR" } +}); + +// 遍历数组 +foreach (string item in clay.Arr) +{ + Console.WriteLine(item); // NOR, XOR +} + +// 遍历整个对象属性及值,类似 JavaScript 的 for (var p in obj) +foreach (KeyValuePair item in clay) +{ + Console.WriteLine(item.Key + ":" + item.Value); // Foo:json, Bar: 100, Nest: { "Foobar":true}, Arr:["NOR","XOR"] +} + +// 数组/集合 可使用 Lambda 方式,Furion 4.8.7.19+ 支持 +IEnumerable query = clay.Arr.AsEnumerator(); // 通过 .AsEnumerator() 转换成 IEnumerable 类型 +var result = query.Where(u => u.StartsWith("N")) + .Select(u => new + { + Name = u + }) + .ToList(); + +// 也可以通过原生方法转换成 IEnumerable 对象 +IEnumerable query = ((System.Collections.IEnumerable)clay.Arr).Cast(); // 其中 Cast 的 T 可以时任意类型,比如 Cast(); + +// 获取对象所有键或数组所有索引 +IEnumerable keys = clay.GetDynamicMemberNames(); +``` + +### 29.3.8 转换成具体对象 + +```cs showLineNumbers {5-7,11-13} +dynamic clay = new Clay(); +clay.Arr = new string[] { "Furion", "Fur" }; + +// 数组转换示例 +var a1 = clay.Arr.Deserialize(); // 通过 Deserialize 方法 +var a2 = (string[])clay.Arr; // 强制转换 +string[] a3 = clay.Arr; // 声明方式 + +// 对象转换示例 +clay.City = new City { Id = 1, Name = "中山市" }; +var c1 = clay.City.Deserialize(); // 通过 Deserialize 方法 +var c2 = (City)clay.City; // 强制转换 +City c3 = clay.City; // 声明方式 +``` + +### 29.3.9 固化粘土 + +固化粘土在很多时候和序列化很像,但是如果直接调用 `Deserialize` 或 `Deserialize` 无法返回实际类型,所以就有了固化类型的功能,如: + +```cs showLineNumbers {2,5,8} +// 返回 object +var obj = clay.Solidify(); + +// 返回 dynamic +var obj1 = clay.Solidify(); + +// 返回其他任意类型 +var obj2 = clay.Solidify(); +``` + +### 29.3.10 输出 `JSON` + +```cs showLineNumbers {1,13} +dynamic clay = Clay.Object(new +{ + Foo = "json", + Bar = 100, + Nest = new + { + Foobar = true + }, + Arr = new string[] { "NOR", "XOR" } +}); + +// 输出 JSON +var json = clay.ToString(); // "{\"Foo\":\"json\",\"Bar\":100,\"Nest\":{\"Foobar\":true},\"Arr\":[\"NOR\",\"XOR\"]}" +``` + +:::tip `Clay` 序列化成 `JSON` 键大小写控制 + +默认情况下,`Clay` 输出成 `JSON` 后将保持原样输出,如果需要实现键命名控制,则需要先转换成 `Dictionary` 然后再配置 `AddJsonOptions` 服务,如: + +```cs showLineNumbers {8-9} +public IActionResult OutputClay() +{ + dynamic clay = Clay.Object(new + { + // .... + }); + + // 转换成 dictionary + var dic = clay.ToDictionary(); + + return new JsonResult(dic); +} +``` + +配置序列化 `Dictionary` 键命名策略支持: + +```cs showLineNumbers {2,4} +services.AddControllers() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase; // 配置 Dictionary 类型序列化输出 + }); +``` + +::: + +### 29.3.11 输出 `XML` 对象 + +```cs showLineNumbers {1,13} +dynamic clay = Clay.Object(new +{ + Foo = "json", + Bar = 100, + Nest = new + { + Foobar = true + }, + Arr = new string[] { "NOR", "XOR" } +}); + +// 输出 XElement +var xml = clay.XmlElement; +``` + +### 29.3.12 关键字处理 + +```cs showLineNumbers {2-3} +dynamic clay = new Clay(); +clay.@int = 1; +clay.@event = "事件"; +``` + +### 29.3.13 转换成字典类型 + +```cs showLineNumbers {3} +dynamic clay = Clay.Object(new { name = "张三" }); +clay.name = "百小僧"; +Dictionary parms = clay.ToDictionary(); +``` + +### 29.3.14 获取不存在 `Key` 处理 + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.7.40 +` 版本使用。 + +::: + +默认情况下,如果通过粘土对象获取不存在的值会抛出 `Microsoft.CSharp.RuntimeBinder.RuntimeBinderException` 异常,如: + +```cs showLineNumbers {12} +dynamic clay = Clay.Object(new +{ + Foo = "json", + Bar = 100, + Nest = new + { + Foobar = true + } +}); + +// 获取不存在的值 +var undefined = clay["undefined"]; // 抛出 Microsoft.CSharp.RuntimeBinder.RuntimeBinderException 异常 +``` + +这时我们可以配置粘土对象的 `ThrowOnUndefined` 属性行为,**配置 `true` 时获取不存在的 `Key` 抛异常,`false` 表示返回 `null`**。如: + +```cs showLineNumbers {1,10,12,14,25-27,29,40-41,43-46} +// 方式一,初始化指定 throwOnUndefined: false,所有构造函数和静态初始化方法都有该参数 +dynamic clay = Clay.Object(new +{ + Foo = "json", + Bar = 100, + Nest = new + { + Foobar = true + } +}, throwOnUndefined: false); + +var undefined = clay.Undefined; // => null + +// 方式二,操作时指定,设置 ThrowOnUndefined 属性 +dynamic clay = Clay.Object(new +{ + Foo = "json", + Bar = 100, + Nest = new + { + Foobar = true + } +}); + +var undefined = clay.Undefined; // 抛出 Microsoft.CSharp.RuntimeBinder.RuntimeBinderExceptionnull +clay.ThrowOnUndefined = false; +var undefined = clay.Undefined; // => null + +// 方式三,只配置局部对象 +dynamic clay = Clay.Object(new +{ + Foo = "json", + Bar = 100, + Nest = new + { + Foobar = true + } +}); + +var undefined = clay.Undefined; // 抛出 Microsoft.CSharp.RuntimeBinder.RuntimeBinderException +var bar = clay.Nest.Bar; // 抛出 Microsoft.CSharp.RuntimeBinder.RuntimeBinderException + +// 设置 Nest 节点不抛异常 +clay.Nest.ThrowOnUndefined = false; +var undefined = clay.Undefined; // 抛出 Microsoft.CSharp.RuntimeBinder.RuntimeBinderException +var bar = clay.Nest.Bar; // => null +``` + +### 29.3.15 转换为特定集合 + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.8 +` 版本使用。 + +::: + +```cs showLineNumbers {2} +dynamic clay = Clay.Parse(@"[]"); +IEnumerable data = clay.ConvertTo(); +``` + +这种方式区别于 `.Solidify`,`ConvertTo` 内部通过反射创建对象并自动适配属性类型。 + +另外在 `Furion 4.8.8.40+` 版本提供了 `ConvertTo` 自定义值参数,可以针对特定属性进行额外处理,如: + +```cs showLineNumbers {1,4-7,12} +Func valueProvider = (property, oldValue) => +{ + // 例如针对属性名为 Device 的进行额外处理 + if (property.Name == "Device") + { + return JsonSerializer.Deserialize(oldValue.ToString()); + } + + return oldValue; +}; + +IEnumerable data = clay.ConvertTo(valueProvider); +``` + +## 29.4 序列化支持 + +默认情况下,`Clay` 为动态类型对象,不支持直接通过 `System.Text.Json` 和 `Newtonsoft.Json` 进行序列化和反序列化,这时只需添加以下配置即可。 + +- `System.Text.Json` 方式 + +```cs showLineNumbers {2,4} +services.AddControllersWithViews() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.Converters.AddClayConverters(); + }); +``` + +- `Newtonsoft.Json` 方式 + +```cs showLineNumbers {3} +.AddNewtonsoftJson(options => +{ + options.SerializerSettings.Converters.AddClayConverters(); +}) +``` + +## 29.5 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/clientapi.mdx b/handbook/docs/clientapi.mdx new file mode 100644 index 0000000000000000000000000000000000000000..069cdf068fe6f68219ad9315aff7a3c19c13aab2 --- /dev/null +++ b/handbook/docs/clientapi.mdx @@ -0,0 +1,424 @@ +--- +id: clientapi +title: 5.6 Vue/React/Angular 接口代理 +sidebar_label: 5.6 Vue/React/Angular 请求代理 +description: 原来 Ajax 也可以不用写的 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +:::tip 视频教程 + +[https://www.bilibili.com/video/BV1EW4y1C71D](https://www.bilibili.com/video/BV1EW4y1C71D) + +::: + +## 5.6.1 历史背景 + +在现在主流的 Web 项目开发中,越来越多的开发者选择使用 `Vue/React/Angular` 三大框架进行开发,这三大框架和传统开发模式最大的不同是前者采用前后端分离的方式,而后者统一由后端程序员编写。 + +在前后端分离的模式中,前后端程序员各司其职,后端程序负责编写接口(API),前端程序员负责编写客户端请求后端接口(API)并进行数据绑定。 + +**但这里暴露出了一个工作效率极低且易出错的问题,那就是前端程序需要将后端几百个甚至上千个接口进行一一对应编写,大多都是采用 `$.ajax` 或 `axios` 的方式。** + +一旦后端接口参数或返回值发生改变,前端程序员需要一一进行勘正,一旦出现纠正不完全就会导致系统无法响应或接收错误的用户消息从而造成不必要的维护工作和成本浪费。 + +## 5.6.2 如何解决? + +`Furion` 框架编写的所有后端接口都会生成规范化的 `swagger.json` 文件,使用该文件可以在 **[https://editor.swagger.io](https://editor.swagger.io)** 生成任何支持标准 `swagger` 的界面或客户端代码。 + +**自此,前端程序员再也无需自己手写 `$.ajax` 和 `axios` 代码,这部分代码全部自动生成,以后开发效率至少提高一半以上。** + +## 5.6.3 生成客户端请求代码 + +:::important 关于 `TypeScript` 和 `JavaScript` + +以下教程仅适用于 `Vue/React/Angular` 的 `TypeScript` 类型项目,暂不支持 `JavaScript`。 + +为了项目良好的发展和维护,建议使用 `TypeScript` 进行编写。 + +::: + +### 5.3.3.1 生成客户端代码 + +1. **打开规范化文档(Swagger)首页,并点击顶部 `/swagger/xxxx/swagger.json` 到新窗口打开。** + + + +2. **接着全选并复制全部内容** + + + +3. **打开 [https://editor.swagger.io](https://editor.swagger.io/) 官网并粘贴进去** + +:::tip 无法联网 + +`Furion` 也提供了 **[Swagger-Editor.rar](https://gitee.com/dotnetchina/Furion/blob/v4/clients/Swagger-Editor.rar)** 离线包,可直接下载解压并双击 `index.html` 即可。 + +::: + + + +4. **最后点击顶部的 `Generate Client` 选择对应的语言/框架进行生成即可。** + +--- + +### 5.6.3.2 `Vue/React` 配置 + +**点击 `Generate Client` 顶部菜单并选择 `typescript-axios` 进行下载。** + + + +**下载成功之后拷贝下图选择的目录和文件:** + + + +**接着打开你的 `Vue` 或 `React` 项目,并在 `src` 目录下创建 `api-services` 目录并将刚刚复制的目录文件放在里面。** + + + +**接下来通过 `npm` 或 `yarn` 安装 `axios` 包** + +```bash showLineNumbers +# npm 方式 +npm i axios@0.21.4 + +# yarn 方式 +yarn add axios@0.21.4 +``` + +:::important `axios` 版本说明 + +注意 `axios` 版本必须是 `0.21.4` 版本,如果安装其他版本可能会出现无法编译的情况。 + +::: + +**接着下载 `Furion` 提供的 `Vue/React` 工具库 `axios-utils.ts` 并拷贝到和 `api-services` 同级目录下:** + +[axios-utils.ts 下载地址](https://gitee.com/dotnetchina/Furion/blob/v4/clients/axios_vue_react/axios-utils.ts) + + + +:::tip `Vue3` 项目不能编译问题 + +如果在 `Vue3` 项目中无法编译通过,则需要修改根目录下的 `tsconfig.app.json` 和 `tsconfig.vite-config.json` 和 `tsconfig.vitest.json` 文件并添加下列配置即可,如: + +```json showLineNumbers {1-3} +"compilerOptions": { + "importsNotUsedAsValues": "remove", // TypeScript 5.0 - 使用 + "preserveValueImports": false, // TypeScript 5.0 - 使用 + // "verbatimModuleSyntax": false // TypeScript 5.0 + 使用 +} +``` + + + +::: + +--- + +### 5.6.3.3 `Angular` 配置 + +**点击 `Generate Client` 顶部菜单并选择 `typescript-angular` 进行下载。** + + + +**下载成功之后拷贝下图选择的目录和文件:** + + + +**接着打开你的 `Angular` 项目,并在 `src` 目录下创建 `api-services` 目录并将刚刚复制的目录文件放在里面。** + + + +**接着下载 `Furion` 提供的 `Angular` 工具库 `angular-utils.ts` 并拷贝到和 `api-services` 同级目录下:** + +[angular-utils.ts 下载地址](https://gitee.com/dotnetchina/Furion/blob/v4/clients/angular/angular-utils.ts) + + + +:::tip `Angular` 项目不能编译问题 + +如果在 `Angular` 项目中无法编译通过,则需要修改根目录下的 `api-services/encoder.ts` 文件,并在 `encodeKey` 和 `encodeValue` 前添加 `override` 即可,如: + +```ts showLineNumbers {2,6} title="api-services/encoder.ts" +export class CustomHttpUrlEncodingCodec extends HttpUrlEncodingCodec { + override encodeKey(k: string): string { + k = super.encodeKey(k); + return k.replace(/\+/gi, "%2B"); + } + override encodeValue(v: string): string { + v = super.encodeValue(v); + return v.replace(/\+/gi, "%2B"); + } +} +``` + + + +::: + +**最后在 `src/app/app.module.ts` 中注册 `ServeModule`** + +```ts showLineNumbers {7,14} title="src/app/app.module.ts" +import { NgModule } from "@angular/core"; +import { BrowserModule } from "@angular/platform-browser"; + +import { AppRoutingModule } from "./app-routing.module"; +import { AppComponent } from "./app.component"; + +import { ServeModule } from "src/angular-utils"; + +@NgModule({ + declarations: [AppComponent], + imports: [ + BrowserModule, + AppRoutingModule, + ServeModule, // 注册代理服务模块 + ], + providers: [], + bootstrap: [AppComponent], +}) +export class AppModule {} +``` + + + +## 5.6.4 初始配置 + +完成上面步骤之后还需要最后一步,那就修改服务端(后端)接口(API)地址。 + +### 5.6.4.1 `Vue/React` 配置 + +在 `Vue/React` 项目中编辑 `Furion` 框架提供的 `axios-utils.ts` 文件,并将 `serveConfig` 修改为对应的后端地址即可,如: + +```ts showLineNumbers {21-24} +/** + * 当前版本:v1.0.3 + * 使用描述:https://editor.swagger.io 代码生成 typescript-axios 辅组工具库 + * 依赖说明:适配 axios 版本:v0.21.1 + * 视频教程:https://www.bilibili.com/video/BV1EW4y1C71D + */ + +import globalAxios, { AxiosInstance } from "axios"; +import { Configuration } from "./api-services"; +import { BaseAPI, BASE_PATH } from "./api-services/base"; + +// 如果是 Angular 项目,则取消下面注释即可 +// import { environment } from './environments/environment'; + +/** + * 接口服务器配置 + */ +export const serveConfig = new Configuration({ + // 如果是 Angular 项目,则取消下面注释,并删除 process.env.NODE_ENV !== "production" + // basePath: !environment.production + basePath: + process.env.NODE_ENV !== "production" + ? "https://localhost:44342" // 开发环境服务器接口地址 + : "http://furion.baiqian.ltd", // 生产环境服务器接口地址 +}); + +// ...... +``` + + + +### 5.6.4.2 `Angular` 配置 + +如果是 `Angular` 项目则编辑 `Furion` 框架提供的 `angular-utils.ts` 文件,并将 `serveConfig` 修改为对应的后端地址即可,如: + +```ts showLineNumbers {25-27} +/** + * 当前版本:v1.0.3 + * 使用描述:https://editor.swagger.io 代码生成 typescript-angular 辅组工具库 + */ + +import { + HttpClientModule, + HttpEvent, + HttpHandler, + HttpHeaders, + HttpInterceptor, + HttpRequest, + HttpResponse, + HTTP_INTERCEPTORS, +} from "@angular/common/http"; +import { Injectable, NgModule } from "@angular/core"; +import { finalize, Observable, tap } from "rxjs"; +import { ApiModule, Configuration } from "./api-services"; +import { environment } from "./environments/environment"; + +/** + * 接口服务器配置 + */ +export const serveConfig = new Configuration({ + basePath: !environment.production + ? "https://localhost:44316" // 开发环境服务器接口地址 + : "http://furion.baiqian.ltd", // 生产环境服务器接口地址 +}); + +// ...... +``` + + + +## 5.6.5 基本使用 + +### 5.6.5.1 `Vue/React` 中使用 + +在 `Vue/React` 中使用有两种方式,一种是 `Promise`,另外一种就是 `async/await`,推荐使用后者。 + +- `Promise` 方式 + +```ts showLineNumbers {1,3} +import { getAPI } from "../axios-utils"; // 注意项目的路径 + +getAPI(SystemAPI) // SystemAPI 对应的是 Swagger 分组标签名称 + API + .apiGetXXXX() + .then((res) => { + var data = res.data.data!; + }) + .catch((err) => { + console.log(err); + }) + .finally(() => { + console.log("api request completed."); + }); +``` + +- `async/await` 方式 + +```ts showLineNumbers {1,3} +import { getAPI, feature } from "../axios-utils"; // 注意项目的路径 + +const [err, res] = await feature(getAPI(SystemAPI).apiGetXXX()); + +if (err) { + console.log(err); +} else { + var data = res.data.data!; +} + +console.log("api request completed."); +``` + +:::tip 关于 `关于文件流下载` + +对于文件流下载可能存在下载文件过大的情况,这时候需要添加 `options` 参数 `responseType: "blob"` 解决,如: + +```ts showLineNumbers {1} +getAPI(SystemAPI, { responseType: "blob" }).apiGetXXX(); +``` + +::: + +### 5.6.5.2 `Angular` 中使用 + +在 `Angular` 项目中,通过构造函数注入对应的服务即可 + +```ts showLineNumbers {2,12-13,16-28} +import { Component } from "@angular/core"; +import { PersonService } from "src/api-services"; // 注意项目的路径 + +@Component({ + selector: "app-root", + templateUrl: "./app.component.html", + styleUrls: ["./app.component.css"], +}) +export class AppComponent { + title = "angulars"; + + // 注入 PersonService + constructor(private personService: PersonService) {} + + ngOnInit(): void { + // 使用 personService + this.personService.apiPersonAllGet().subscribe({ + next: (res) => { + // 请求成功 + console.log(res); + }, + error: (err) => { + // 请求失败 + }, + complete: () => { + // 请求完成 + }, + }); + } +} +``` + +## 5.6.6 重新生成代码 + +如果后端接口(API)发生改变,只需要删除 `api-services` 下所有目录文件并重新生成复制进去即可。 + +:::tip 关于 `Angular` 项目 + +如果是 `Angular` 项目,可以保留 `api-services/encoder.ts` 文件并删除其他目录文件,新生成的目录文件无需复制 `encoder.ts`,这样可以避免每次修改 `encoder.ts` 文件。 + +::: + +## 5.6.7 `Swagger` 多分组生成 + +在很多大型系统中,为了方便对接口进行归类,往往使用了 `Swagger` 多分组功能,这样会使系统的接口散落在多个 `swagger.json` 中。 + +这个时候,需要在后端规范化文档中启用多分组配置: + +```json showLineNumbers {2,3} +{ + "SpecificationDocumentSettings": { + "EnableAllGroups": true + } +} +``` + +启用配置之后在 `Swagger` 导航栏顶部下拉分组将出现 `All Groups` 选项,这时候使用这个 `All Groups` 的 `swagger.json` 进行生成。 + +## 5.6.8 自定义生成前端方法名 + +:::important 版本说明 + +以下内容仅限 `Furion 4.1.7+` 版本使用。 + +::: + +通过我们根据 `swagger.json` 生成前端代码时,`Swagger` 会自动根据路由地址生成调用的 `api` 名称,但这样的名称往往不易读,这时候可自定义 `[OperationId]` 来配置生成的前端名称。 + +```cs showLineNumbers {5} +using Furion.SpecificationDocument; + +public class PersonDto +{ + [OperationId(nameof(TestMethod))] + public string TestMethod() + { + // ... + } +} +``` + +## 5.6.9 框架客户端工具库介绍 + +`axios-utils.ts` 和 `angular-utils.ts` 是 `Furion` 框架专门针对 `Furion` 开发的 `WebAPI` 项目编写的客户端代理库,在这个代理库中已经处理了跨域,授权,自动刷新 `token` 以及解密客户端 `JWT token` 问题。 + +同时提供了非常方便的 `feature` 方法,可将异步方法进行同步化处理。 + +## 5.6.10 无法连接外网/内网情况/离线包 + +在一些比较注重代码安全的组织或公司中,可能不能连接外网进行生成,这个时候只需要下载 [https://github.com/swagger-api/swagger-editor](https://github.com/swagger-api/swagger-editor) 代码在本地部署即可。 + +`Furion` 官网也提供了 `Swagger-Editor.rar` 离线包下载:[https://gitee.com/dotnetchina/Furion/blob/v4/clients/Swagger-Editor.rar](https://gitee.com/dotnetchina/Furion/blob/v4/clients/Swagger-Editor.rar) + +下载离线包后直接双击 `index.html` 启动即可 + + + +## 5.6.11 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/component.mdx b/handbook/docs/component.mdx new file mode 100644 index 0000000000000000000000000000000000000000..288b76451c3aa840d0e391773827945406eb4193 --- /dev/null +++ b/handbook/docs/component.mdx @@ -0,0 +1,528 @@ +--- +id: component +title: 3.2 组件化启动 +sidebar_label: 3.2 组件化启动 +description: 组件化配置依赖服务或许是最佳实践 +--- + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **其他更改** + + -  调整 组件 `Component` 模式支持 `[DependsOn]` 支持继承 4.8.8.16 ⏱️2023.05.15 [#I733RF](https://gitee.com/dotnetchina/Furion/issues/I733RF) + +
+
+
+ +## 3.2.1 历史背景 + +在 `.NET Core 2+` 之后,微软推出了 `Startup.cs` 模式,在这样的模式中,需要任何服务或者中间件处理,只需要在 `Startup.cs` 文件的两个方法(`ConfigureServices` 和 `Configure`)中配置即可。 + +**但在 `.NET6` 之后,微软不再推荐使用 `Startup.cs` 模式。** + +在这里,不阐述 `Startup.cs` 的优点,就列举几个比较明显的缺点: + +- 默认情况下必须放在启动层且主机启动时需通过 `.UseStartup<>` 进行注册,此问题在 `Furion` 已解决 `AppStartup` +- 配置服务很容易编写出又臭又长的 `service.AddXXX()` 和 `app.AddXXX()` 代码,不管是阅读性和灵活性大大减分 +- **对服务注册和中间件注册有顺序要求,不同的顺序可能产生不同的效果,甚至出现异常** +- **不能实现模块化自动装载注册,添加新的模块需要手动注册,注册又得考虑模块化之间依赖顺序问题** +- **不能对模块注册进行监视,比如加载之前,加载失败,加载之后** + +## 3.2.2 先看一个例子 + +在一个大型的 `.NET Core` 项目中,会经常看到这样的代码: + +```cs showLineNumbers {12-26,31-50} +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Furion.Web.Core; + +public sealed class FurWebCoreStartup : AppStartup +{ + public void ConfigureServices(IServiceCollection services) + { + services.AddCorsAccessor(); + services.AddControllers().AddInject(); + services.AddRemoteRequest(); + services.AddEventBus(); + services.AddAppLocalization(); + services.AddViewEngine(); + services.AddSensitiveDetection(); + services.AddVirtualFileServer(); + services.AddX(); + services.AddXX(); + services.AddXXX(); + services.AddXXXX(); + services.AddXXXXX(); + services.AddXXXXXX(); + // ..... + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + app.UseHttpsRedirection(); + app.UseRouting(); + app.UseCorsAccessor(); + app.UseAuthentication(); + app.UseAuthorization(); + app.UseInject(); + app.UseX(); + app.UseXX(); + app.UseXXX(); + app.UseXXXX(); + app.UseXXXXX(); + app.UseXXXXXX(); + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } +} +``` + +可能对于大部分 `.NET` 开发者来说貌似没有任何问题,但是仔细瞧瞧,这里充斥着大量的 `.AddXXXX()` 和 `.UseXXXX()`,**真的美观,真的好吗**?而且稍有不慎移动了它们的注册顺序可能会引发灾难,还有可能多个服务之间相互依赖,要么全部移除,要么全部保留,未来替代你开发岗位的人知道吗? + +**试问,这个问题是无解吗?** + +## 3.2.3 当然有解 + +在 `Furion 3.7.3+` 版本之后,借助 `Docker-Compose` 的设计理念,推出了全新的 `Component 组件化` 模式,通过组件化开发可以实现组件之间相互依赖,相互链接,还可以共享参数,你仅仅需要编写一个入口组件即可。 + +**先看一个例子:** + +- 创建 `EntryServiceComponent` 入口服务组件 + +```cs showLineNumbers {2,6} +// 创建入口服务组件实现 IServeComponent 接口 +public sealed class EntryServiceComponent : IServiceComponent +{ + public void Load(IServiceCollection services, ComponentContext componentContext) + { + // 做任何你想做的事情,如 service.AddYourInitService(); 如添加你的模块初始化配置 + } +} +``` + +- 通过 `AddComponent<>` 注册入口组件 + +```cs showLineNumbers {2} +// 通过 .AddComponent 注册一个入口服务组件 +Serve.Run(RunOptions.Default.AddComponent()); +``` + +**接下来,我们模拟实际项目的开发需求:** + +1. 需要添加跨域服务,创建 `CorsServiceComponent` 组件 + +```cs showLineNumbers {1,5} +public sealed class CorsServiceComponent : IServiceComponent +{ + public void Load(IServiceCollection services, ComponentContext componentContext) + { + services.AddCorsAccessor(); + } +} +``` + +2. 需要添加动态 `WebAPI` 服务,创建 `DynamicApiServiceComponent` 组件 + +```cs showLineNumbers {1,5} +public sealed class DynamicApiServiceComponent : IServiceComponent +{ + public void Load(IServiceCollection services, ComponentContext componentContext) + { + services.AddDynamicApiControllers(); + } +} +``` + +3. 需要添加 `XXX` 第三方服务,创建 `XXXServiceComponent` 组件 + +```cs showLineNumbers {1,5} +public sealed class XXXServiceComponent : IServiceComponent +{ + public void Load(IServiceCollection services, ComponentContext componentContext) + { + services.AddXXX(); + } +} +``` + +有了这么多服务组件,那怎么将它们关联起来呢,而且能够正确的处理它们的顺序呢?比如 `AddXXX()` 必须等 `AddDynamicApiControllers()` 注册才能注册,这时候只需要为 `XXXServiceComponent` 添加依赖即可,如: + +```cs showLineNumbers {1-3} +[DependsOn( + typeof(DynamicApiServiceComponent) +)] +public sealed class XXXServiceComponent : IServiceComponent +{ + // .... +} +``` + +这样表示 `XXXServiceComponent` 依赖 `DynamicApiServiceComponent` 组件,只有 `DynamicApiServiceComponent` 完成注册才会注册 `XXXServiceComponent`。 + +那么最后的 `EntryServiceComponent` 的代码将会是: + +```cs showLineNumbers {1-4} +[DependsOn( + typeof(CorsServiceComponent), + typeof(XXXServiceComponent) +)] +public sealed class EntryServiceComponent : IServiceComponent +{ + // .... +} +``` + +最后生成的调用顺序为:`AddCorsAccessor()` -> `AddDynamicApiControllers()` -> `AddXXX()` -> `AddEntry()`。 + +**看到这里,是否已找到答案:每一个项目只有一个入口组件,每个组件只做一件事,组件之间可以通过 `DependsOn` 配置依赖,组件之间还能共享上下文数据 `ComponentContext`。** + +没错,这就是 Furion 目前能够想到的最优解决方案。 + +## 3.2.4 `IComponent` + +在 `Furion 3.7.3+` 版本,新增了 `Components` 模块,该模块的根接口为 `IComponent`,含有两个派生接口 `IServiceComponent` 和 `IApplicationComponent`。 + +### 3.2.4.1 `IServiceComponent` + +`IServiceComponent` 接口简称**服务组件**对应的是 `Startup.cs` 中的 `ConfigureService`,接口签名为: + +```cs showLineNumbers {6,13} +namespace System; + +/// +/// 服务组件依赖接口 +/// +public interface IServiceComponent : IComponent +{ + /// + /// 装载服务 + /// + /// + /// 组件上下文 + void Load(IServiceCollection services, ComponentContext componentContext); +} +``` + +需要注册服务可在 `Load` 方法中注册即可。 + +### 3.2.4.2 `IApplicationComponent` + +`IApplicationComponent` 接口简称**中间件组件**对应的是 `Startup.cs` 中的 `Configure`,接口签名为: + +```cs showLineNumbers {6,14} +namespace System; + +/// +/// 应用中间件接口 +/// +public interface IApplicationComponent : IComponent +{ + /// + /// 装置中间件 + /// + /// + /// + /// 组件上下文 + void Load(IApplicationBuilder app, IWebHostEnvironment env, ComponentContext componentContext); +} +``` + +需要注册中间件可在 `Load` 方法中注册即可。 + +### 3.2.4.3 `IWebComponent` + +`IWebComponent` 接口简称** `Web` 组件**对应的是 `Program.cs` 中的 `WebApplicationBuilder`,接口签名为: + +```cs showLineNumbers {6,13} +namespace System; + +/// +/// WebApplicationBuilder 组件依赖接口 +/// +public interface IWebComponent : IComponent +{ + /// + /// 装置 Web 应用构建器 + /// + /// + /// 组件上下文 + void Load(WebApplicationBuilder builder, ComponentContext componentContext); +} +``` + +需要注册中间件可在 `Load` 方法中注册即可。 + +### 3.2.4.4 注册组件 + +`Furion` 提供了多种注册组件的方式: + +- 方式一 + +通过 `RunOptions`,`LegacyRunOptions`,`GenericRunOptions` 方式: + +```cs showLineNumbers {2-3,7} +Serve.Run(RunOptions.Default + .AddComponent() + .UseComponent()); + +// .NET6+ 还支持 AddWebComponent(); +Serve.Run(RunOptions.Default + .AddWebComponent()); +``` + +- 方式二 + +通过 `services.AddComponent` 和 `app.UseComponent` 方式 + +```cs showLineNumbers {2,5,8} +// 服务组件 +service.AddComponent(); + +// 中间件组件 +app.UseComponent(); + +// .NET6+ 还支持 AddWebComponent(); +builder.AddWebComponent(); +``` + +- 方式三 + +组件注册可以传递参数,通过最后的参数指定。 + +```cs showLineNumbers {2,5,8} +// 服务组件 +service.AddComponent(options); + +// 中间件组件 +app.UseComponent(options); + +// .NET6+ 还支持 AddWebComponent(); +builder.AddWebComponent(options); +``` + +:::tip 类型 `Type` 注册方式 + +除了提供泛型注册组件的方式,还提供了 `.AddComponent(typeof(XXXComponent))` 和 `.UseComponent(typeof(XXXComponent))` 方式。 + +::: + +## 3.2.5 组件设计原则 + +### 3.2.5.1 职责单一性 + +组件的设计理应遵循**职责单一性原则**,具有单一性又有职责明确性,通俗点说每一个组件尽可能的只做一件事,如果组件之间有依赖,通过 `[DependsOn]` 声明配置,如: + +```cs showLineNumbers {1,5} +[DependsOn( + typeof(OtherServiceComponent), + "Other.Assembly;Other.Assembly.OtherServiceComponent" +)] +public sealed class YourServiceComponent : IServiceComponent +{ + public void Load(IServiceCollection services, ComponentContext componentContext) + { + services.AddXXX(); + } +} +``` + +### 3.2.5.2 约定大于配置 + +由于组件通常包含服务和中间件两个注册,所以推荐组件类的命名统一为:`XXXComponent.cs`,然后在 `XXXComponent.cs` 中分别写 `IServiceComponent` 和 `IApplicationComponent` 组件。 + +尽可能每一个服务组件(`IServiceComponent`)以 `ServiceComponent` 结尾,每一个中间件组件(`IApplicationComponent`)以 `ApplicationComponent` 结尾。如: + +```cs showLineNumbers {3-4,9-10,15-16} title="XXXComponent.cs" +namespace Your.Components; + +// 服务组件 +public sealed class XXXServiceComponent : IServiceComponent +{ + // .... +} + +// 中间件组件 +public sealed class XXXApplicationComponent : IApplicationComponent +{ + // .... +} + +// WebApplicationBuilder 组件 +public sealed class XXXWebComponent : IWebComponent +{ + // .... +} +``` + +:::tip 小知识 + +如果没有 `IServiceComponent` 或 `IApplicationComponent`,则写其一即可。 + +::: + +## 3.2.6 `[DependsOn]` 详解 + +由于组件和组件之间存在依赖方式,甚至没有依赖关系但支持唤醒其他组件功能,所以 `Furion` 提供了 `[DependsOn]` 特性。 + +### 3.2.6.1 配置介绍 + +- `DependsOn` + - `DependComponents`:配置组件依赖关系,`Type[]` 类型,一旦配置了依赖关系,那么被依赖的组件会先于当前组件注册 + - `Links`:配置组件链接关系,`Type[]` 类型,该配置主要解决一些组件并不是从 `根组件` 进行配置,而是处于和 `根组件` 平行的情况,类似多入口组件 + +:::tip 构造函数说明 + +`DependComponents` 是 `DependsOnAttribute` 特性的默认构造函数,支持 `Type` 和 `String` 类型,如: + +```cs showLineNumbers {2-4} +[DependsOn( + typeof(XXXComponent), + typeof(XXXXComponent), + "程序集;类型完整限定名" // 会自动加载程序集中特定的组件,后续模块化开发非常方便 +)] +``` + +如需配置 `Links`,只需要这样接口: + +```cs showLineNumbers {3-6} +[DependsOn( + typeof(XXXComponent), + Links = new object[]{ + typeof(XXXComponent), + typeof(XXXXComponent) + } +)] +``` + +::: + +### 3.2.6.2 重复依赖问题 + +`Furion` 框架中已经处理了组件重复依赖问题,会自动生成好最佳的注册顺序并去除重复依赖注册问题。 + +### 3.2.6.3 循环依赖问题 + +循环依赖实际上是一种错误注册组件的方式,会导致出现内存溢出情况,早期组件化版本框架处理了循环依赖问题,也就是主动忽略或报错,但是考虑此行为本身带有潜在的安全问题,所以移除了循环依赖处理,而是选择在开发阶段抛出异常方式。 + +## 3.2.7 `ComponentContext` 详解 + +`ComponentContext` 是组件注册 `Load` 方法的最后参数,该参数提供了组件之间的一些元数据。 + +### 3.2.7.1 属性介绍 + +- `ComponentContext` + - `ComponentType`:组件类型,`Type` 类型 + - `CalledContext`:上级组件,`ComponentContext` 类型,也就是 `DependsOn` 中的组件上下文,如果没有则是前一个组件的上下文 + - `RootContext`:根组件/入口组件,`ComponentContext` 类型 + - `DependComponents`:组件依赖的所有组件列表,`Type[]` 类型 + - `LinkComponents`:组件链接的所有组件列表,`Type[]` 类型 + +### 3.2.7.2 参数配置/获取 + +在注册组件小节中,我们可以通过 `.AddComponent` 和 `.UseComponent` 最后的参数来指定组件的参数,那么如何在组件中获取你传递的参数呢? + +`ComponentContext` 提供了多种方法: + +- `GetProperty()`:获取组件的参数 +- `GetProperty(Type)`:通过类型获取组件参数 +- `GetProperty(string)`:通过指定 `key` 获取 +- `GetProperties()`:获取组件所有参数列表(包括依赖,链接等) +- `SetProperty(object)`:设置特定组件参数 +- `SetProperty(Type, object)`:设置特定类型组件的参数 +- `SetProperty(string, object)`:设置指定 `key` 的参数值 + +#### 例子说明 + +注册时传入 `EntryOption` 参数 + +```cs showLineNumbers +service.AddComponent(new EntryOption {}); +``` + +在组件内部获取: + +```cs showLineNumbers {5,7} +public sealed class EntryServiceComponent : IServiceComponent +{ + public void Load(IServiceCollection services, ComponentContext componentContext) + { + var options = componentContext.GetProperty(); + + services.AddXXXX(options); + } +} +``` + +除此之外,还可以通过 `componentContext.SetProperty(new xxxOptions{})` 来设置下游组件的参数。 + +## 3.2.8 实现 `Startup.cs` 模式 + +组件模式是非常强大且灵活的,我们也可以通过组件的模式模拟出传统的 `Startup.cs`,如: + +```cs showLineNumbers {1,6-7,11,16-21} title="StartupComponent" +// 模拟 ConfigureService +public sealed class StartupServiceComponent : IServiceComponent +{ + public void Load(IServiceCollection services, ComponentContext componentContext) + { + services.AddControllers() + .AddInject(); + } +} + +// 模拟 Configure +public sealed class StartupApplicationComponent : IApplicationComponent +{ + public void Load(IApplicationBuilder app, IWebHostEnvironment env, ComponentContext componentContext) + { + app.UseRouting(); + app.UseInject(string.Empty); + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } +} +``` + +只需要通过 `service.AddComponent()` 注册即可,如果使用 `Serve.Run()` 模式将更简单,如: + +```cs showLineNumbers {2-3} +Serve.Run(RunOptions.Default + .AddComponent() + .UseComponent()); +``` + +是不是很灵活啊~ + +## 3.2.9 最佳实践? + +在写最佳实践时是最痛苦的,因为最佳实践应该是把微软底层所有的 `service.AddXXX` 和 `app.AddXXX` 独立成一个个组件,比如 `servers.AddControllers()` 对应一个 `ControllersServiceComponent`。 + +这样做的话工作量是非常大的,但如果不这样做,组件化就无法彻底。 + +所以现阶段暂时采用自由定制组件方式,比如自己在项目中编写 `ControllersServiceComponent` 这类组件。 + +## 3.2.10 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/configuration.mdx b/handbook/docs/configuration.mdx new file mode 100644 index 0000000000000000000000000000000000000000..b7eeed59de24cdee1e84b8b418123bea5515f273 --- /dev/null +++ b/handbook/docs/configuration.mdx @@ -0,0 +1,606 @@ +--- +id: configuration +title: 4.1 配置 +sidebar_label: 4.1 配置 +description: 约定大于配置,配置大于硬编码 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **新特性** + + -  新增 配置模块 `IgnoreConfigurationFiles` 支持完整的文件通配符 4.8.8.22 ⏱️2023.05.25 [#I78ABL](https://gitee.com/dotnetchina/Furion/issues/I78ABL) + +
+
+
+ +:::caution 自定义配置特别注意 + +如:`xxxx.json`,必须在 `VS Studio` 中右键属性设置 `复制` 输出目录为 `如果较新则复制`,生成操作为:`内容`。 + +如果 `.json` 文件配置在多个项目层,也必须保证命名唯一,不然编译后出现相互覆盖的情况。 + +::: + +:::tip 中文乱码问题 + +默认情况下,`.json` 文件并未采用 `utf-8` 编码,所以如果存在中文读取后就会出现乱码情况,这时候,只需要修改 `.json` 文件编码为 `utf-8` 即可。 + +::: + +## 4.1.1 什么是配置 + +简单来说,配置将系统应用可动态调配的选项放在统一地方管理,通过不同的配置让系统做出动态调整。 + +在 `ASP.NET Core` 应用程序启动时默认加载 `启动项目` 下的 `appsettings.json` 作为应用配置。同时还支持**不同的运行环境**加载对应的配置文件,如: + +- `Development`:加载 `appsettings.Development.json` +- `Staging`:加载 `appsettings.Staging.json` +- `{Environment}`:`appsettings.{Environment}.json` + +## 4.1.2 配置的使用 + +假设我们需要在系统运行时获取**系统名称、版本号及版权信息**,这些信息可能随时变化而且需要在多个地方使用。这时就需要将这些信息配置起来。具体步骤如下: + +### 4.1.2.1 配置 `appsettings.json` 信息 + +```json showLineNumbers {2-6} +{ + "AppInfo": { + "Name": "Furion", + "Version": "1.0.0", + "Company": "Baiqian" + } +} +``` + +:::caution 特别注意 + +`appsettings.json` 复制输出目录为`如果较新则复制`,生成操作为:`内容`。 + +另外,某些 `linux` 系统不支持读取带 `注释` 的 json 文件,直接读取将会报错。需要将 `json` 内的注释全部 `删除` 才能正常读取。 + +::: + +### 4.1.2.2 读取 `appsettings.json` 信息 + +在 `Furion` 框架中,提供了两种读取方式: + +- 依赖注入 `IConfiguration` 对象读取 +- 通过 `App.Configuration[path]` 读取 + + + + +```cs showLineNumbers {11-13} +using Microsoft.AspNetCore.Mvc; + +namespace Furion.Web.Entry.Controllers +{ + [Route("api/[controller]")] + public class DefaultController : ControllerBase + { + [HttpGet] + public string Get() + { + return $@"名称:{App.Configuration["AppInfo:Name"]}, + 版本:{App.Configuration["AppInfo:Version"]}, + 公司:{App.Configuration["AppInfo:Company"]}"; + } + } +} +``` + + + + +```cs showLineNumbers {2,10-15} +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; + +namespace Furion.Web.Entry.Controllers +{ + [Route("api/[controller]")] + public class DefaultController : ControllerBase + { + [HttpGet] + public string Get([FromServices] IConfiguration configuration) + { + return $@"名称:{configuration["AppInfo:Name"]}, + 版本:{configuration["AppInfo:Version"]}, + 公司:{configuration["AppInfo:Company"]}"; + } + } +} +``` + + + + +:::tip 依赖注入的方式 + +通过依赖注入注入实例有几种方式: + +- 构造函数注入方式 + +```cs showLineNumbers {2} +private readonly IConfiguration _configuration; +public DefaultController(IConfiguration configuration) +{ + _configuration = configuration; +} +``` + +- 参数注入方式 `[FromServices]` + +```cs showLineNumbers {1} +public string Get([FromServices] IConfiguration configuration) +{ +} +``` + +- 属性注入方式 + +```cs showLineNumbers {1} +public IConfiguration Configuration { get; set; } +``` + +想了解更多关于《[ASP.NET Core - 依赖注入](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-3.1)》 知识 + +::: + +### 4.1.2.3 如何选择读取方式 + +- 在可依赖注入类中,依赖注入 `IConfiguration` 读取 +- 在静态类/非依赖注入类中,选择 `App.Configuration[path]` 读取 + +### 4.1.2.4 读取配置并转换成特定类型 + +`ASP.NET Core` 和 `Furion` 提供了多种配置类型读取并转换成特定类型,包括基础类型,对象类型,数组,集合,字典等等,如: + +```cs showLineNumbers {2,5} +// ASP.NET Core +var data = Configuration.GetSection("配置节点").Get<类型>(); + +// Furion,推荐!!! +var data = App.GetConfig<类型>("配置节点"); +``` + +## 4.1.3 `路径符` 查找节点 + +在 `ASP.NET Core` 中,配置采用 `:` 分隔符来读取分层配置数据。如上述例子中的 `AppInfo:Name`。如有更多层级数据则只需要通过 `:` 进入下一层节点即可。 + +假设我们有以下配置信息: + +```json showLineNumbers {5-12} +{ + "AppInfo": { + "Name": "Furion", + "Version": "1.0.0", + "Company": { + "Name": "Baiqian", + "Address": { + "City": "中国", + "Province": "广东省", + "Detail": "中山市东区紫马公园西门" + } + } + } +} +``` + + + + +```cs showLineNumbers +var companyName = App.Configuration["AppInfo:Name"]; // => Furion +``` + + + + +```cs showLineNumbers +var companyName = App.Configuration["AppInfo:Company:Name"]; // => Baiqian +``` + + + + +```cs showLineNumbers +var companyName = App.Configuration["AppInfo:Company:Address:Detail"]; // => 中山市东区紫马公园西门 +``` + + + + +```cs showLineNumbers +var companyName = App.Configuration["AppInfo:Tier2:Tier3:Tier4...Tiern1:Tiern3..."]; // => 中山市东区紫马公园西门 +``` + + + + +### 4.1.3.1 查找数组节点 + +有些时候我们需要或者数组特定的某些,可以通过 `App.Configuration["array:0"]` 获取,`0` 是索引数字。 + +## 4.1.4 自定义配置文件 + +:::important XML 配置说明 + +`Furion v2.8.0` 版本移除了 `.xml` 文件自动扫描配置了,改为手动添加配置。 + +::: + +大多情况下,我们的配置只需要在 `appsettings.json` 中配置即可,但一些特殊情况下,我们希望某些组件或功能拥有独立的配置,这个时候就需要用到自定义配置,`Furion` 目前支持 `.json` 和 `.xml` 两种方式配置,如: + +```json showLineNumbers {2-13} title="Furion.Web.Entry/emailsetting.json" +{ + "outlook": { + "smtp": { + "server": "smtp.office365.com", + "port": "587", + "ssl": "STARTTLS" + }, + "pop": { + "server": "outlook.office365.com", + "port": "995", + "ssl": "TLS" + } + } +} +``` + +```xml + + + MyXMLFile Value + + Title from MyXMLFile + Name from MyXMLFile + + + + Information + Warning + + + +``` + +:::important xml 配置事项 + +如果采用 `xml` 配置,那么文件名必须以 `.config.xml` 结尾(不区分大小写)。 + +::: + +:::important 特别说明 + +`Furion` 框架会在启动时自动扫描每一个项目层根目录下的 `*.json` 文件加入配置中,所以无需手工配置。 + +新增 `*.json` 文件的属性 `复制到输出目录` 设置为始终复制或较新复制,否则不会载入。另外配置文件不能出现重名,也就是保证整个项目中配置文件名字唯一。比如不能在非 `Web` 其他层定义 `appsettings.json` 文件。 + +在 `v2.16.7+` 版本版本之后,支持自定义配置扫描目录: + +```json showLineNumbers +{ + "ConfigurationScanDirectories": ["目录1名称", "目录1名称/子目录名称"] +} +``` + +::: + +同时 `Furion` 提供了非常灵活的方式支持自定义配置文件读取,如: + +### 4.1.4.1 读取 `emailsetting.json` 配置 + +读取自定义配置文件和读取 `appsettings.json` 一致,系统会自动从多个配置文件中读取输入,如: + + + + +```cs showLineNumbers +var smtpServer = App.Configuration["outlook:smtp:server"]; // => smtp.office365.com +``` + + + + +```cs showLineNumbers +var smtpServer = _configuration["outlook:smtp:server"]; // => smtp.office365.com +``` + + + + +### 4.1.4.2 排除特定配置文件 + +有些时候,我们不需要 `.json` 或 `.xml` 自动载入配置中,我们只需要在启动层 `appsettings.json` 中添加 `IgnoreConfigurationFiles` 节点即可: + +```json showLineNumbers +{ + "IgnoreConfigurationFiles": ["runtime.json"] +} +``` + +:::tip 通配符支持 + +`Furion 4.8.8.22+` 版本支持完整的通配符支持,如: + +```json showLineNumbers +{ + "IgnoreConfigurationFiles": ["seed_*.json"] +} +``` + +额外知识:[文件通配符语法](https://learn.microsoft.com/zh-cn/dotnet/core/extensions/file-globbing)。 + +::: + +## 4.1.5 不同环境读取 + +在实际应用开发中,我们可能会根据不同的环境加载不同的配置文件,如 `数据库连接字符串`。 + +这时我们只需要遵循特定命名规范 `{name}.{Environment}.json` 即可。如: + +- `appsettings.Development.json` +- `appsettings.Staging.json` +- `appsettings.Production.json` +- `emailsetting.Development.json` +- `emailsetting.Staging.json` +- `emailsetting.Production.json` + +这时,`ASP.NET Core` 会在应用启动时自动加载不同环境的配置文件。 + +## 4.1.6 配置更改通知(`热更新`) + +在 `.NET Core` 应用程序中,配置支持更改通知,也就是热更新操作。**一旦监听到 `appsetting.json` 或自定义配置文件发生变动,就会触发 `OnChange` 方法**。代码如下: + +```cs showLineNumbers {2,4-5} +var appInfoConfiguration = App.Configuration.GetSection("AppInfo"); +ChangeToken.OnChange(() => App.Configuration.GetReloadToken(), () => +{ + var name = appInfoConfiguration["Name"]; // 实时的最新值 + var version = appInfoConfiguration["Version"]; // 实时的最新值 +}); +``` + +:::important 监听对象 + +如果监听全局配置文件传入 `App.Configuration.GetReloadToken()`,如果只需要监听特定节点,传入 `App.Configuration.GetSection("AppInfo")` + +::: + +## 4.1.7 手动添加配置文件 + +:::info 获取路径说明 + +- 获取项目目录:`AppContext.BaseDirectory` +- 获取网站根目录:`Directory.GetCurrentDirectory()` + +::: + +有些时候,我们的配置文件没有放在项目的根目录下,这时候我们需要手动载入自定义配置文件,有以下几种方式: + +- 方式一:**`appsettings.json`** 中 (推荐) + +:::important 支持版本 + +在 `v2.16.7+` 版本有效 + +::: + +```json showLineNumbers {2} +{ + "ConfigurationScanDirectories": ["目录1名称", "目录1名称/子目录名称"] +} +``` + +- 方式二:**`.NET5`** 中 `Program.cs` 中配置 + +```cs showLineNumbers {10,13} +public class Program +{ + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureAppConfiguration((hostingContext, config) => + { + // 加载自定义配置 + config.AddJsonFile("MyConfig.json", optional: true, reloadOnChange: true); + }) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); +} +``` + +- 方式三:**`.NET6`** 中 `Program.cs` 中配置 + +```cs showLineNumbers {2-4} +var builder = WebApplication.CreateBuilder(args) +builder.Configuration.AddJsonFile("MyConfig.json", optional: true, reloadOnChange: true); +// 注意先添加配置再初始化 Furion +builder.Inject(); +var app = builder.Build(); +app.Run(); +``` + +如果使用 `Serve.Run()` 模式可使用下列代码配置: + +```cs showLineNumbers {2} +Serve.Run(RunOptions.Default.ConfigureConfiguration((env, configuration) => { + configuration.AddJsonFile("MyConfig.json", optional: true, reloadOnChange: true); +})); +``` + +## 4.1.8 配置的优缺点 + +- 优点 + + - 能够在系统运行时快速读取 + - 无需额外配置 + +- 缺点 + + - 存在重复读取 + - 通过硬编码字符串读取,容易出错 + - 不能设置默认值 + - 不能在运行环境中动态配置 + - 不能验证配置有效性 + - 不支持更改通知 + +## 4.1.9 配置使用场景 + +如果只需要**一次性读取**配置信息,则使用配置,否则应该使用 《[4.2 选项](options.mdx)》代替。 + +## 4.1.10 实现配置中心 + +`ASP.NET Core` 除了通过配置文件读取配置信息外,还支持自定义 `配置提供程序`,通过 `配置提供程序` 可以实现配置中心,比如通过数据库提供配置。 + +具体实现查看微软官方文档:[https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/configuration/?view=aspnetcore-5.0#custom-configuration-provider](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/configuration/?view=aspnetcore-5.0#custom-configuration-provider) + +下面给出集成 `AgileConfig` 例子: + +:::important 版本说明 + +以下内容仅限 `Furion 4.4.9 +` 版本使用。 + +::: + +**`Serve.Run` 方式:** + +```cs showLineNumbers {2,4,6,12,14} +Serve.Run(RunOptions.Default.WithArgs(args) + .ConfigureInject((builder, options) => + { + options.ConfigureAppConfiguration((_, cfb) => + { + cfb.AddAgileConfig(new AgileConfig.Client.ConfigClient(builder.Configuration), obj => + { + Console.WriteLine($"{obj}"); + }); + }); + + options.ConfigureWebServices((_, services) => + { + services.AddAgileConfig(); + }); + }) +); +``` + +**`.NET6+` 方式** + +```cs showLineNumbers {2,6,11,13} +var builder = WebApplication.CreateBuilder(args) + .Inject((builder, options) => + { + options.ConfigureAppConfiguration((_, cfb) => + { + cfb.AddAgileConfig(new AgileConfig.Client.ConfigClient(builder.Configuration), obj => + { + Console.WriteLine($"{obj}"); + }); + }); + options.ConfigureWebServices((_, services) => + { + services.AddAgileConfig(); + }); + }); +``` + +**`.NET5` 方式:** + +```cs showLineNumbers {13,15,17,22,24} +public class Program +{ + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder + .Inject((builder, options) => + { + options.ConfigureAppConfiguration((_, cfb) => + { + cfb.AddAgileConfig(new AgileConfig.Client.ConfigClient(cfb.Build()), obj => + { + Console.WriteLine($"{obj}"); + }); + }); + options.ConfigureWebServices((_, services) => + { + services.AddAgileConfig(); + }); + }) + .UseStartup(); + }); +} +``` + +## 4.1.11 重载配置 + +`Furion` 会在应用启动的时候对 `IConfiguration` 进行静态缓存,如果使用了 `App.Configuration` 静态属性且配置数据已发生变更,则调用以下方法刷新即可: + +```cs showLineNumbers +App.Configuration.Reload(); +``` + +## 4.1.12 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: + +--- + +:::note 了解更多 + +想了解更多 `配置` 知识可查阅 [ASP.NET Core - 配置](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/configuration/?view=aspnetcore-5.0) 章节。 + +::: diff --git a/handbook/docs/contribute.mdx b/handbook/docs/contribute.mdx new file mode 100644 index 0000000000000000000000000000000000000000..c98e3088ea02b29597c40ab53ebd2a5ed25cb463 --- /dev/null +++ b/handbook/docs/contribute.mdx @@ -0,0 +1,47 @@ +--- +id: contribute +title: 38. 贡献指南 +sidebar_label: 38. 贡献指南 +--- + +## 38.1 提交错误报告 + +如果您在 `Furion` 中发现了一个不存在安全问题的漏洞,请在 `Furion` 仓库中的 Issues 中搜索,以防该漏洞已被提交,如果找不到漏洞可以创建一个新的 Issues,如果发现了一个安全问题请不要将其公开,请参阅安全问题处理方式,提交错误报告时应该详尽。 + +## 38.2 安全问题处理 + +本项目中对安全问题处理的形式,项目核心人员确认编辑,该部分内容可以根据项目情况添加。 + +## 38.3 解决现有问题 + +通过查看仓库的 Issues 列表可以发现需要处理的问题信息,可以尝试解决其中的某个问题。 + +## 38.4 如何提出新功能 + +提出新功能有些项目使用 Issues 的 Feature 标签进行管理,有些则通过邮件的形式统一收集,在收集后项目内人员会进行确认开发,一般将确认开发的功能会放入下一个版本的任务列表。 + +## 38.5 如何设置开发环境并运行测试 + +如果是通过 Git 管理可以从 `git clone https://gitee.com/dotnetchina/Furion.git` 开始编写,将开发环境的配置信息,IDE 的设置等信息配置文档编写。 + +## 38.6 变更日志填写规则 + +1. 使用现在时态 +2. 第一行字数限制 +3. 提交内容的约束 + +## 38.7 编码约定 + +- 项目内编码约定文件:`.editorconfig` + +## 38.8 分支处理约定 + +- 分支处理形式,如 gitFlow + +## 38.9 合并 PR 的形式 + +在什么情况下可以合并到 master/main: + +1. 通过 CI +2. 两个及以上的维护者通过. +3. 最新版本 diff --git a/handbook/docs/cors.mdx b/handbook/docs/cors.mdx new file mode 100644 index 0000000000000000000000000000000000000000..ea621f9333c9151953edf5110fc88c4468f4d9c7 --- /dev/null +++ b/handbook/docs/cors.mdx @@ -0,0 +1,212 @@ +--- +id: cors +title: 16. CORS 跨域 +sidebar_label: 16. CORS 跨域 +--- + +## 16.1 什么是跨域 + +简单来说,当一个请求 `url` 的协议、域名、端口三者之间任意一个与当前页面 `url` 不同即为跨域。那为什么会出现跨域问题呢? + +出于浏览器的同源策略限制。同源策略(Sameoriginpolicy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说 `Web` 是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。同源策略会阻止一个域的 javascript 脚本和另外一个域的内容进行交互。所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port)。 + +## 16.2 有跨域行为示例 + +| 当前页面 url | 被请求页面 url | 是否跨域 | 原因 | +| ---------------------------- | ---------------------------------- | -------- | ----------------------------------- | +| http://www.baiqian.ltd/ | http://www.baiqian.ltd/index.html | 否 | 同源(协议、域名、端口号相同) | +| http://www.baiqian.ltd/ | https://www.baiqian.ltd/index.html | 跨域 | 协议不同(http/https) | +| http://www.baiqian.ltd/ | http://www.baidu.com/ | 跨域 | 主域名不同(baiqian.ltd/baidu.com) | +| http://furion.baiqian.ltd/ | http://fur.baiqian.ltd/ | 跨域 | 子域名不同(furion/fur) | +| http://www.baiqian.ltd:8080/ | http://www.baiqian.ltd:7001/ | 跨域 | 端口号不同(8080/7001) | + +## 16.3 什么是 CORS + +跨源资源共享 (`CORS`) : + +- 是一种 `W3C` 标准,可让服务器放宽相同的源策略。 +- 不是一项安全功能,`CORS` 放宽 `security`。 `API` 不能通过允许 `CORS` 来更安全。 有关详细信息,请参阅 [CORS 工作原理](https://docs.microsoft.com/zh-cn/aspnet/core/security/cors?view=aspnetcore-5.0#how-cors)。 +- 允许服务器明确允许一些跨源请求,同时拒绝其他请求。 +- 比早期的技术(如 `JSONP`)更安全且更灵活。 + +## 16.4 如何使用 + +### 16.4.1 添加 `CORS` 服务 + +启用跨域 `Cors` 支持首先添加 `CorsAccessor` 服务,如: + +```cs showLineNumbers {13,22} +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Furion.Web.Core +{ + [AppStartup(700)] + public sealed class FurWebCoreStartup : AppStartup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddCorsAccessor(); + + // ... + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + //... + + app.UseCorsAccessor(); + + // ... + } + } +} +``` + +:::caution 特别注意 + +`services.AddCorsAccessor();` 需在 `services.AddControllers()` 之前注册。 + +`app.UseCorsAccessor();` 需在 `app.UseRouting();` 和 `app.UseAuthentication();` 之间注册。 + +::: + +### 16.4.2 配置允许跨域域名 + +:::important 小提醒 + +默认情况下,`Furion` 允许所有域名来源访问,也就是无需配置任何来源域名,另外前端也需要设置请求参数:`withCredentials:false` + +::: + +如果需要指定特定域名,则添加以下配置即可: + +```json showLineNumbers {4} +{ + "CorsAccessorSettings": { + "PolicyName": "自定义跨域策略名", + "WithOrigins": ["http://localhost:4200", "http://furion.baiqian.ltd"] + } +} +``` + +## 16.5 `CorsAccessorSettings` 配置 + +- `CorsAccessorSettings` + - `PolicyName`:跨域策略名,`string` 类型,必填,默认 `App.Cors.Policy` + - `WithOrigins`:允许跨域的域名列表,`string[]` 类型,默认 `*` + - `WithHeaders`:请求表头,没有配置则允许所有表头,`string[]` 类型 + - **`WithExposedHeaders`:设置客户端可获取的响应标头,`string[]` 类型,默认 `["access-token", "x-access-token"]`** + - **默认情况下,若后端输出特定的响应头 `Key`,那么需将该 `Key` 配置在数组中** + - `WithMethods`:设置跨域允许请求谓词,没有配置则允许所有,`string[]` 类型 + - `AllowCredentials`:是否允许跨域请求中的凭据,`bool` 类型,默认值 `true` + - `SetPreflightMaxAge`:设置预检过期时间,`int` 类型,默认值 `24小时` + - `FixedClientToken`:是否默认配置 `WithExposedHeaders`,`bool` 类型,默认 `true` + - `SignalRSupport`:是否启用 `SignalR` 跨域支持,`bool` 类型,默认 `false` + +## 16.6 前端不能读取响应头注意事项 + +有时候,我们通过 `ajax` 或者 `axios` 第三方库无法读取响应头自定义信息,这时需要响应报文中公开特定 `Header` 才能放行,如:`Access-Control-Expose-Headers: xxxxx`,所以,需要添加以下配置: + +```cs showLineNumbers title="appsettings.json" +{ + "CorsAccessorSettings": { + "WithExposedHeaders": ["access-token","x-access-token"] + } +} +``` + +需要获取哪个头,就在 `WithExposedHeaders` 数组中配置即可。如果使用 `ajax` 可以通过 `xhr.getResponseHeader(key)` 或 `xhr.getAllResponseHeaders()` 获取配置的 `key`。 + +特别情况下不能请求,可以考虑设置 `withCredentials: false` 。 + +## 16.7 使用 `$.ajax` 前端注意事项 + +使用 `Jquery` 前端请求可以参考以下配置: + +```cs showLineNumbers {4-7} +$.ajax({ +        url: "https://localhost:5001/api/system/getdata", +        type: "GET", +        xhrFields: { +            withCredentials: false // 如果是https请求,可以试试 true +        }, +        crossDomain: true, +        success: function (res) { +            render(res); +        } +}); +``` + +:::important 特别注意 + +在本地开发阶段,请求如果出现 ` Access to XMLHttpRequest...has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header...` 错误,请确保 `ajax` 的 `url` 参数是正确的,通常**错误的做法**是: + +- 使用了 `127.0.0.1` 而不是 `localhost` 主机地址 +- 使用 `http` 而不是 `https` 主机协议 +- 使用了 `5000` 而不是 `5001` 主机端口 + +::: + +## 16.8 禁用跨域 + +有时候,我们希望某个方法不检查跨域请求,可以在 `Action` 中贴 `[DisableCors]` 特性即可。 + +## 16.9 `SignalR` 跨域问题 + +`SignalR` 实现跨域需要满足下面几个条件: + +- 允许特定的预期来源,允许任何来源是可行的,但不安全或不推荐使用 +- 必须允许使用 HTTP 方法 `GET` 和 `POST` +- 为了使基于 `cookie` 的粘滞会话正常工作,必须允许使用凭据,即使未使用身份验证,也必须启用它们。 + +官方文档说明 [https://docs.microsoft.com/zh-cn/aspnet/core/signalr/security?view=aspnetcore-6.0](https://docs.microsoft.com/zh-cn/aspnet/core/signalr/security?view=aspnetcore-6.0) + +**在 `Furion 4.1.4+` 版本已修正 `SignalR` 跨域问题,只需要启用 `SignalRSupport` 配置即可**,如: + +```json showLineNumbers {2,3} +{ + "CorsAccessorSettings": { + "SignalRSupport": true + } +} +``` + +## 16.10 静态资源跨域问题 + +有时候我们可能通过前端 `XMLHttpRequest/Ajax/Fetch` 方式加载静态资源,这时可能出现跨域问题,可以通过以下配置解决: + +```cs showLineNumbers {1,3,5-7} +app.UseStaticFiles(new StaticFileOptions +{ + OnPrepareResponse = (stf) => + { + stf.Context.Response.Headers["Access-Control-Allow-Origin"] = "*"; + stf.Context.Response.Headers["Access-Control-Allow-Headers"] = "*"; + } +}); +``` + +:::tip 小知识 + +如果已经注册了 `app.UseStaticFiles()`,则只需要传递 `new StaticFileOptions{ ... }` 参数即可,避免多次注册 `app.UseStaticFiles()`。 + +::: + +## 16.11 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: + +--- + +:::note 了解更多 + +想了解更多 `跨域请求` 知识可查阅 [ASP.NET Core - 启用跨域请求](https://docs.microsoft.com/zh-cn/aspnet/core/security/cors?view=aspnetcore-5.0) 章节。 + +::: diff --git a/handbook/docs/course.mdx b/handbook/docs/course.mdx new file mode 100644 index 0000000000000000000000000000000000000000..56331d229c22841b4a2e971408a23f9fa91297dc --- /dev/null +++ b/handbook/docs/course.mdx @@ -0,0 +1,76 @@ +--- +id: course +title: 1.7 发展大事记 +sidebar_label: 1.7 发展大事记 +description: 2020 年 09 月 01 日,Fur 正式写下第一行代码。 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +## 2020 年 + +- **2020 年 06 月 22 日**,`Fur` 在 Gitee 平台创建空仓库 [25de190](https://gitee.com/dotnetchina/Furion/commit/25de190d83027fab58e72714ca7c044206607127)。 +- **2020 年 09 月 01 日**,`Fur` 正式写下第一行代码。 +- **2020 年 10 月 01 日**,**`Fur` 获得 Gitee 最有价值开源项目 [GVP](http://furion.baiqian.ltd/img/gvp.png) 证书**。 +- **2020 年 10 月 22 日**,`Fur` 在 Gitee 平台获得 1000 stars. +- **2020 年 11 月 11 日**,`Fur` 单身节当天发布了 `1.0.0` 正式版。 +- **2020 年 11 月 18 日**,**`Fur` 改名为 `Furion`**。 [a24acd4](https://gitee.com/dotnetchina/Furion/commit/a24acd44a70bac94d2af8ab290197478aa10ef51) [97011ef](https://gitee.com/dotnetchina/Furion/commit/97011efab563d88b3d369d4a6a8c9e7ac324123f) +- **2020 年 11 月 23 日**,`Furion` Logo 由之前的 `奶牛` 更换为 `袋鼠`。 +- **2020 年 12 月 22 日**,`Furion` 在 Gitee 平台获得 2000 stars。 + +## 2021 年 + +- **2021 年 03 月 01 日**,`Furion` 捐赠项目到 [dotNET China](https://gitee.com/dotnetchina) 组织。 +- **2021 年 03 月 05 日**,`Furion` 在 Gitee 平台获得 3000 stars。 +- **2021 年 04 月 01 日**,`Furion` 所在群 `dotNET China` 突破 5000 人。 +- **2021 年 04 月 06 日**,`Furion` 在 Gitee 平台获得 4000 stars。 +- **2021 年 04 月 19 日**,`Furion` 正式发布 `2.0.0` 版本,并支持控制台应用开发。 +- **2021 年 04 月 29 日**,`Furion` 所在群 `dotNET China` 突破 6000 人。 +- **2021 年 05 月 13 日**,`Furion` 在 Gitee 平台获得 5000 stars。 +- **2021 年 06 月 01 日**,`Furion` 所在群 `dotNET China` 突破 7000 人。 +- **2021 年 06 月 22 日**,`Furion` 在 Gitee 平台获得 6000 stars。 +- **2021 年 07 月 04 日**,**`Furion` 登顶 Gitee 平台 `C#` 语言板块第一名。** +- **2021 年 07 月 16 日**,`Furion` 采用 `百小僧` 头像作为 `Logo`。 +- **2021 年 07 月 20 日**,`Furion` 将 `Apache 2.0` 开源协议修改为 `MulanPSL-2.0` (木兰宽松许可证) +- **2021 年 07 月 27 日**,`Furion` 正式支持全平台、`.NET` 全平台项目开发。 +- **2021 年 08 月 11 日**,**`Furion` 加入 [木兰开源社区](https://portal.mulanos.cn/) 重点孵化。** +- **2021 年 08 月 21 日**,**`Furion` 在 `NuGet` 平台突破 `100万` 下载量。** +- **2021 年 08 月 30 日**,`Furion` 在 Gitee 平台获得 7000 stars。 +- **2021 年 09 月 01 日**,`Furion` 诞生一周年。 +- **2021 年 11 月 09 日**,`Furion` 正式发布 `3.0.0` 版本,全新的 `.NET6` 架构。 +- **2021 年 11 月 22 日**,`Furion` 迎来了第一个赞助商。 + +## 2022 年 + +- **2022 年 05 月 20 日**,`Furion` 在 Gitee 平台获得 8000 Stars。 +- **2022 年 05 月 28 日**,`Furion` 在 `NuGet` 平台突破 `200万` 下载量。 +- **2022 年 06 月 18 日**,`Furion` 有了自己的入口函数 `Serve.Run()` 和错误页。 +- **2022 年 06 月 20 日**,`Furion` 项目贡献者突破 200 人。 +- **2022 年 07 月 25 日**,`Furion` 正式发布 `4.0.0` 版本,彻底实现大一统(`.NET5`-`.NET N`)都可以升级。 +- **2022 年 08 月 01 日**,`Furion` 将 `MulanPSL-2.0` 开源协议修改为 [MIT](https://gitee.com/dotnetchina/Furion/blob/v4/LICENSE)。 +- **2022 年 08 月 18 日**,`Furion` 在 `NuGet` 平台突破 `300万` 下载量。 +- **2022 年 09 月 01 日**,`Furion` 诞生两周年。 +- **2022 年 09 月 18 日**,**`Furion` 解散 `QQ` 群,回归最初的开源协作模式,[了解更多](https://gitee.com/dotnetchina/Furion/issues/I5RWYL)**。 +- **2022 年 10 月 29 日**,`Furion` 在 `NuGet` 平台突破 `400万` 下载量。 +- **2022 年 11 月 08 日**,`Furion` 正式适配 `.NET7` 架构。 +- **2022 年 11 月 24 日**,`Furion` 发布了全新的分布式定时任务模块 [Sundial](https://gitee.com/dotnetchina/Sundial)。 +- **2022 年 12 月 07 日**,`Furion` 在 `NuGet` 平台突破 `500万` 下载量。 +- **2022 年 12 月 29 日**,`Furion` 获得开源云联盟优秀开源项目奖项:[查看获奖](https://mp.weixin.qq.com/s/2zW-WnBbzs8rOdQ8AfwVag)。 + +## 2023 年 + +- **2023 年 02 月 04 日**,`Furion` 获得《[2022 年中国开源年度报告](https://kaiyuanshe.feishu.cn/wiki/wikcnnJ8b90pOoDRFzXngfRslkd)》 `Gitee` 指数 `Top 10` 项目:[查看报告](https://kaiyuanshe.feishu.cn/wiki/wikcnnJ8b90pOoDRFzXngfRslkd)。 +- **2023 年 02 月 06 日**,`Furion` 在 `NuGet` 平台突破 `600万` 下载量。 +- **2023 年 03 月 15 日**,`Furion` 在 `NuGet` 平台突破 `700万` 下载量。 +- **2023 年 04 月 18 日**,`Furion` 在 Gitee 平台获得 9000 Stars。 +- **2023 年 04 月 18 日**,`Furion` 在 `NuGet` 平台突破 `800万` 下载量。 +- **2023 年 06 月 07 日**,`Furion` 正式开通微信公众号 `Furion`。 +- **2023 年 06 月 08 日**,**`Furion` 成功购买下 [furion.net](https://furion.net) 域名:[查看官宣](https://mp.weixin.qq.com/s?__biz=Mzg4OTI0ODg3MA==&mid=2247483653&idx=1&sn=36d046a0369beb24a60aa9bd0499ea2e&chksm=cfef8c0cf898051a6637c1bb2643249b0b3d08017cc5ea5d0901a3d3fd127a1cc3fad1e06ace&token=162131388&lang=zh_CN#rd)。** +- **2023 年 06 月 14 日**,`Furion` 在 `NuGet` 平台突破 `900万` 下载量。 +- **2023 年 08 月 22 日**,**`Furion` 在 `NuGet` 平台突破 `1000万` 下载量。** +- **2023 年 09 月 01 日**,`Furion` 诞生三周年。 +- **2023 年 09 月 05 日**,`Furion` 申请从 [木兰开源社区](https://portal.mulanos.cn/) 毕业。 +- **2023 年 10 月 19 日**,`Furion` 在 `NuGet` 平台突破 `1100万` 下载量。 +- **2023 年 11 月 03 日**,**`Furion` 推出官方 `VIP` 服务。** +- **2023 年 11 月 15 日**,`Furion` 正式适配 `.NET8` 架构。 +- **2023 年 11 月 17 日**,`Furion` 通过 ⌈中国电子技术标准化研究院⌋ 成熟度评估。 diff --git a/handbook/docs/cron.mdx b/handbook/docs/cron.mdx new file mode 100644 index 0000000000000000000000000000000000000000..ead8a2124a204445226dc06573884e3e68f2ae1e --- /dev/null +++ b/handbook/docs/cron.mdx @@ -0,0 +1,359 @@ +--- +id: cron +title: 26.2 Cron 表达式 +sidebar_label: 26.2 Cron 表达式 +--- + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **新特性** + + -  新增 `Crontab.IsValid(...)` 静态方法,判断 `Cron` 表达式是否有效 4.8.7.17 ⏱️2023.03.20 [#I6OHO4](https://gitee.com/dotnetchina/Furion/issues/I6OHO4) + -  新增 `crontab.GetSleepTimeSpan(baseTime)` 实例方法 4.8.4.10 ⏱️2023.01.09 [#I69HM4](https://gitee.com/dotnetchina/Furion/issues/I69HM4) + -  新增 `Crontab.ParseAt(..)` 静态方法 4.8.2.6 ⏱️2022.11.30 [035cc23](https://gitee.com/dotnetchina/Furion/commit/035cc23a20045e9673a1406b94e72030f5f18375) + -  新增 `Crontab` 所有 `Macro At` 静态方法 4.8.2.6 ⏱️2022.11.30 [a15b69d](https://gitee.com/dotnetchina/Furion/commit/a15b69d0eeb4bdc8d0ec042cfec22ff0049dc89f) + -  新增 `Crontab.Workday` 表示周一至周五的 `Macro` 静态属性 4.8.2.6 ⏱️2022.11.30 [a15b69d](https://gitee.com/dotnetchina/Furion/commit/a15b69d0eeb4bdc8d0ec042cfec22ff0049dc89f) + +- **问题修复** + + -  修复 `Cron` 表达式步长解析器错误 4.8.8.25 ⏱️2023.06.14 [#I7D9XU](https://gitee.com/dotnetchina/TimeCrontab/issues/I7D9XU) + -  修复 `Cron` 表达式 `*` 符号解析器不够严谨,如:`*1111aaaaa` 也被解析为 `*` 4.8.7.17 ⏱️2023.03.20 [#I6OHO4](https://gitee.com/dotnetchina/Furion/issues/I6OHO4) + +
+
+
+ +import useBaseUrl from "@docusaurus/useBaseUrl"; + +:::warning 版本说明 + +在 `Furion 4.8.0+` 版本采用 [TimeCrontab](https://gitee.com/dotnetchina/TimeCrontab) 作为 `Cron` 表达式解析。 + +::: + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.0 +` 版本使用。 + +::: + +## 26.2.1 关于 `Cron` 表达式 + +`Cron` 表达式是一个字符串,字符串以 `5` 或 `6` 个空格隔开,分为 `6` 或 `7` 个域,每一个域代表一个含义,`Cron` 表达式通常是作为实现定时任务的基石。 + + + +## 26.2.2 快速入门 + +### 26.2.2.1 常规格式 + +**常规格式**:分 时 天 月 周 + +```cs showLineNumbers {1} +var crontab = Crontab.Parse("* * * * *"); +var nextOccurrence = crontab.GetNextOccurrence(DateTime.Now); +``` + +### 26.2.2.2 支持年份 + +**支持年份**:分 时 天 月 周 年 + +```cs showLineNumbers {1} +var crontab = Crontab.Parse("* * * * * *", CronStringFormat.WithYears); +var nextOccurrence = crontab.GetNextOccurrence(DateTime.Now); +``` + +### 26.2.2.3 支持秒数 + +**支持秒数**:秒 分 时 天 月 周 + +```cs showLineNumbers {1} +var crontab = Crontab.Parse("* * * * * *", CronStringFormat.WithSeconds); +var nextOccurrence = crontab.GetNextOccurrence(DateTime.Now); +``` + +### 26.2.2.4 支持秒和年 + +**支持秒和年**:秒 分 时 天 月 周 年 + +```cs showLineNumbers {1} +var crontab = Crontab.Parse("* * * * * * *", CronStringFormat.WithSecondsAndYears); +var nextOccurrence = crontab.GetNextOccurrence(DateTime.Now); +``` + +### 26.2.2.5 `Macro` 标识符和静态属性 + +为了方便常见的 `Cron` 表达式,如 `每天`,`每月`,`每小时` 等等,所以提供了 `Macro` 标识符和静态属性: + +```cs showLineNumbers {1,11} +// macro 字符串 +var secondly = Crontab.Parse("@secondly"); // 每秒 .0000000 +var minutely = Crontab.Parse("@minutely"); // 每分钟 00 +var hourly = Crontab.Parse("@hourly"); // 每小时 00:00 +var daily = Crontab.Parse("@daily"); // 每天 00:00:00 +var monthly = Crontab.Parse("@monthly"); // 每月 1 号 00:00:00 +var weekly = Crontab.Parse("@weekly"); // 每周日 00:00:00 +var yearly = Crontab.Parse("@yearly"); // 每年 1 月 1 号 00:00:00 +var workday = Crontab.Parse("@workday"); // 每周一至周五 00:00:00 + +// 静态属性 +var secondly = Crontab.Secondly; // 每秒 .0000000 +var minutely = Crontab.Minutely; // 每分钟 00 +var hourly = Crontab.Hourly; // 每小时 00:00 +var daily = Crontab.Daily; // 每天 00:00:00 +var monthly = Crontab.Monthly; // 每月 1 号 00:00:00 +var weekly = Crontab.Weekly; // 每周日 00:00:00 +var yearly = Crontab.Yearly; // 每年 1 月 1 号 00:00:00 +var workday = Crontab.Workday; // 每周一至周五 00:00:00 +``` + +### 26.2.2.6 `Macro At` 标识符 + +```cs showLineNumbers +// 每第 3 秒 +var crontab = Crontab.SecondlyAt(3); +// 每第 3,5,6 秒 +var crontab = Crontab.SecondlyAt(3, 5, 6); + +// 每分钟第 3 秒 +var crontab = Crontab.MinutelyAt(3); +// 每分钟第 3,5,6 秒 +var crontab = Crontab.MinutelyAt(3, 5, 6); + +// 每小时第 3 分钟 +var crontab = Crontab.HourlyAt(3); +// 每小时第 3,5,6 分钟 +var crontab = Crontab.HourlyAt(3, 5, 6); + +// 每天第 3 小时正(点) +var crontab = Crontab.DailyAt(3); +// 每天第 3,5,6 小时正(点) +var crontab = Crontab.DailyAt(3, 5, 6); + +// 每月第 3 天零点正 +var crontab = Crontab.MonthlyAt(3); +// 每月第 3,5,6 天零点正 +var crontab = Crontab.MonthlyAt(3, 5, 6); + +// 每周星期 3 零点正 +var crontab = Crontab.WeeklyAt(3); +var crontab = Crontab.WeeklyAt("WED"); // SUN(星期天),MON,TUE,WED,THU,FRI,SAT +// 每周星期 3,5,6 零点正 +var crontab = Crontab.WeeklyAt(3, 5, 6); +var crontab = Crontab.WeeklyAt("WED", "FRI", "SAT"); +// 还支持混合 +var crontab = Crontab.WeeklyAt(3, "FRI", 6); + +// 每年第 3 月 1 日零点正 +var crontab = Crontab.YearlyAt(3); +var crontab = Crontab.YearlyAt("MAR"); // JAN(一月),FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC +// 每年第 3,5,6 月 1 日零点正 +var crontab = Crontab.YearlyAt(3); +var crontab = Crontab.YearlyAt(3, 5, 6); +var crontab = Crontab.YearlyAt("MAR", "MAY", "JUN"); +// 还支持混合 +var crontab = Crontab.YearlyAt(3, "MAY", 6); +``` + +## 26.2.3 `Cron` 各字段说明 + +| 字段 | 允许值 | 允许特别符号 | 格式化 | +| ----------------------------------- | ------------------- | ---------------- | ------------------------------------------------------------------------ | +| 秒       | `0-59` | `\* , - /` | `CronStringFormat.WithSeconds` 或 `CronStringFormat.WithSecondsAndYears` | +| 分钟       | `0-59` | `\* , - /` | `ALL` | +| 小时       | `0-23` | `\* , - /` | `ALL` | +| 天       | `1-31` | `\* , - / ? L W` | `ALL` | +| 月份       | `1-12` or `JAN-DEC` | `\* , - /` | `ALL` | +| 星期       | `0-6` or `SUN-SAT` | `\* , - / ? L #` | `ALL` | +| 年份       | `0001–9999` | `\* , - /` | `CronStringFormat.WithYears` 或 `CronStringFormat.WithSecondsAndYears` | + +- `*`:表示匹配该域的任意值,假如在 `分钟` 域使用 `*`,即表示每分钟都会触发事件。 +- `?`:只能用在 `天` 和 `星期` 两个域。它也匹配域的任意值,但实际不会。因为 `天` 和 `星期` 会相互影响。例如想在 `每月的20日` 触发调度,不管 20 日到底是星期几,则只能使用如下写法:`13 13 15 20 * ?`, 其中最后一位只能用 `?`,而不能使用 `*`,如果使用 `*` 表示不管星期几都会触发,实际上并不是这样。 +- `-`:表示范围,例如在 `分钟` 域使用 `5-20`,表示从 `5分` 到`20分钟` 每分钟触发一次。 +- `/`:表示起始时间开始触发,然后每隔固定时间触发一次,例如在 `分钟` 域使用 `5/20`,则意味着 `5分钟` 触发一次,而 `25,45` 等分别触发一次。 +- `,`:表示列出枚举值。例如:在 `分钟` 域使用 `5,20`,则意味着在 `第5` 和 `第20分钟` 分别触发一次。 +- `L`:表示最后,只能出现在 `星期` 和 `月份` 域,如果在 `星期` 域使用 `5L`,意味着在 `最后的一个星期四` 触发。 +- `W`:表示有效工作日(周一到周五),只能出现在 `天` 域,系统将在离指定日期的最近的有效工作日触发事件。例如:在 `天` 使用 `5W`,如果 `5日是星期六,则将在最近的工作日:星期五,即 4日 触发`。如果 `5日是星期天,则在6日(周一)触发`;如果 `5日在星期一到星期五中的一天,则就在 5日 触发`。另外一点,`W` 的最近寻找不会跨过月份。 +- `LW`:这两个字符可以连用,表示在某个月最后一个工作日,即最后一个非周六周末的日期。 +- `#`:用于确定每个月第几个星期几,只能出现在 `星期` 域。例如在 `2#3`,表示某月的第二个星期三。 + +## 26.2.4 `CronStringFormat` 格式化 + +默认情况下,`Cron` 表达式不支持 `秒` 和 `年` 的,如有需求,可配置 `CronStringFormat` 枚举参数。 + +- `CronStringFormat` 提供以下枚举值: + - `CronStringFormat.Default`:默认格式,书写顺序:`分 时 天 月 周` + - `CronStringFormat.WithYears`:带年份格式,书写顺序:`分 时 天 月 周 年` + - `CronStringFormat.WithSeconds`:带秒格式,书写顺序:`秒 分 时 天 月 周` + - `CronStringFormat.WithSecondsAndYears`:带秒和年格式,书写顺序:`秒 分 时 天 月 周 年` + +## 26.2.5 在线生成 `Cron` 表达式 + +对于大多数开发者来说,编写 `Cron` 表达式是有难度的,所以推荐使用在线 `Cron` 表达式生成器。 + +[https://cron.qqe2.com/](https://cron.qqe2.com/) + +## 26.2.6 实现简单定时任务 + +:::tip 小建议 + +建议使用 【[26.1 调度作业](/docs/job)】 章节内容实现强大的分布式定时任务。 + +::: + +通过 `Cron` 表达式解析和 `while` 循环可以实现简单的定时任务。 + +### 26.2.6.1 `while` + `Task` 方式 + +```cs showLineNumbers {2-3,5,10-11,13,15,18} +// 阻塞方式 +var crontab = Crontab.Parse("* * * * * *", CronStringFormat.WithSeconds); +while(true) +{ + Thread.Sleep(crontab.GetSleepMilliseconds(DateTime.Now)); + Console.WriteLine(DateTime.Now.ToString("G")); +} + +// 无阻塞方式 +var crontab = Crontab.Parse("* * * * * *", CronStringFormat.WithSeconds); +Task.Factory.StartNew(async () => +{ + while (true) + { + await Task.Delay(crontab.GetSleepMilliseconds(DateTime.Now)); + Console.WriteLine(DateTime.Now.ToString("G")); + } +}, TaskCreationOptions.LongRunning); +``` + +### 26.2.6.2 `BackgroundService` 方式 + +```cs showLineNumbers {1,8,13,20-21,30} +using Furion.TimeCrontab; + +namespace WorkerService; + +public class Worker : BackgroundService +{ + private readonly ILogger _logger; + private readonly Crontab _crontab; + + public Worker(ILogger logger) + { + _logger = logger; + _crontab = Crontab.Parse("* * * * * *", CronStringFormat.WithSeconds); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + var taskFactory = new TaskFactory(System.Threading.Tasks.TaskScheduler.Current); + await taskFactory.StartNew(async () => + { + // 你的业务代码写到这里面 + + _logger.LogInformation("Worker running at: {time}", DateTime.Now); + + await Task.CompletedTask; + }, stoppingToken); + + await Task.Delay(_crontab.GetSleepTimeSpan(DateTime.Now), stoppingToken); + } + } +} +``` + +## 26.2.7 `Crontab` 对象属性和方法 + +```cs showLineNumbers {1,4,14,27} +// 实例属性 +var format = crontab.Format; // 获取当前格式化配置 + +// 静态属性 +var secondly = Crontab.Secondly; // 每秒 .0000000 +var minutely = Crontab.Minutely; // 每分钟 00 +var hourly = Crontab.Hourly; // 每小时 00:00 +var daily = Crontab.Daily; // 每天 00:00:00 +var monthly = Crontab.Monthly; // 每月 1 号 00:00:00 +var weekly = Crontab.Weekly; // 每周日 00:00:00 +var yearly = Crontab.Yearly; // 每年 1 月 1 号 00:00:00 +var workday = Crontab.Workday; // 每周一至周五 00:00:00 + +// 实例方法 +// 获取下一个执行时间 +var nextOccurrence = crontab.GetNextOccurrence(起始时间); +var nextOccurrence = crontab.GetNewDbContext(起始时间, 结束时间); +// 获取特定时间所有执行执行时间 +var nextOccurrences = crontab.GetNextOccurrences(起始时间, 结束时间); +// 获取当前时间和下一个发生时间相差毫秒数 +var sleepMilliseconds = crontab.GetSleepMilliseconds(起始时间); +// 获取当前时间和下一个发生时间相差时间戳 +var sleepTimeSpan = crontab.GetSleepTimeSpan(起始时间); // Furion 4.8.4.10+ 版本有效 +// 将 crontab 对象转换成 cron 表达式 +var expression = crontab.ToString(); + +// 静态方法 +// 解析表达式 +var crontab = Crontab.Parse("表达式", CronStringFormat.Default); // 转换表达式为 Crontab 对象 +var crontab = Crontab.TryParse("表达式", CronStringFormat.Default); // 转换表达式为 Crontab 对象 +var crontab = Crontab.ParseAt("Macro 符号", 1, 3, 5); // 创建 Macro At Crontab 对象 +var isValid = Crontab.IsValid("表达式", CronStringFormat.Default); // 判断表达式是否有效,Furion 4.8.7.17+ 支持 + +// 每第 3 秒 +var crontab = Crontab.SecondlyAt(3); +// 每第 3,5,6 秒 +var crontab = Crontab.SecondlyAt(3, 5, 6); + +// 每分钟第 3 秒 +var crontab = Crontab.MinutelyAt(3); +// 每分钟第 3,5,6 秒 +var crontab = Crontab.MinutelyAt(3, 5, 6); + +// 每小时第 3 分钟 +var crontab = Crontab.HourlyAt(3); +// 每小时第 3,5,6 分钟 +var crontab = Crontab.HourlyAt(3, 5, 6); + +// 每天第 3 小时正(点) +var crontab = Crontab.DailyAt(3); +// 每天第 3,5,6 小时正(点) +var crontab = Crontab.DailyAt(3, 5, 6); + +// 每月第 3 天零点正 +var crontab = Crontab.MonthlyAt(3); +// 每月第 3,5,6 天零点正 +var crontab = Crontab.MonthlyAt(3, 5, 6); + +// 每周星期 3 零点正 +var crontab = Crontab.WeeklyAt(3); +var crontab = Crontab.WeeklyAt("WED"); // SUN(星期天),MON,TUE,WED,THU,FRI,SAT +// 每周星期 3,5,6 零点正 +var crontab = Crontab.WeeklyAt(3, 5, 6); +var crontab = Crontab.WeeklyAt("WED", "FRI", "SAT"); +// 还支持混合 +var crontab = Crontab.WeeklyAt(3, "FRI", 6); + +// 每年第 3 月 1 日零点正 +var crontab = Crontab.YearlyAt(3); +var crontab = Crontab.YearlyAt("MAR"); // JAN(一月),FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC +// 每年第 3,5,6 月 1 日零点正 +var crontab = Crontab.YearlyAt(3); +var crontab = Crontab.YearlyAt(3, 5, 6); +var crontab = Crontab.YearlyAt("MAR", "MAY", "JUN"); +// 还支持混合 +var crontab = Crontab.YearlyAt(3, "MAY", 6); +``` + +## 26.2.8 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/dapper.mdx b/handbook/docs/dapper.mdx new file mode 100644 index 0000000000000000000000000000000000000000..4751272c73d102fd935d6920dfee1afe3a7116e8 --- /dev/null +++ b/handbook/docs/dapper.mdx @@ -0,0 +1,203 @@ +--- +id: dapper +title: 10.2 Dapper 集成 +sidebar_label: 10.2 Dapper 集成 +--- + +:::warning 温馨提醒 + +在 `Furion` 包中默认集成了 `EFCore`,**如果不使用 `EFCore`,可安装纯净版 `Furion.Pure` 代替 `Furion`**。 + +::: + +## 10.2.1 关于 Dapper + +`Dapper` 是 .NET/C# 平台非常优秀的 `微型 ORM` 框架,主要是为 `ADO.NET` 操作对象提供拓展能力,推崇原生 `sql` 操作法。 + +`Dapper` 官方仓库地址:[https://github.com/StackExchange/Dapper](https://github.com/StackExchange/Dapper) + +## 10.2.2 如何集成 + +在 `Furion` 框架中,已经推出 `Dapper` 拓展包 [Furion.Extras.DatabaseAccessor.Dapper](https://www.nuget.org/packages/Furion.Extras.DatabaseAccessor.Dapper)。 + +### 10.2.2.1 注册 `Dapper` 服务 + +使用非常简单,只需要在 `Startup.cs` 中添加 `services.AddDapper(connStr, SqlProvider)` 即可。如: + +```cs showLineNumbers +services.AddDapper("Data Source=./Furion.db", SqlProvider.Sqlite); + +// 更多配置,仅 v3.4.3+ 版本有效 +servers.AddDapper("Data Source=./Furion.db", SqlProvider.Sqlite, () => { + DefaultTypeMap.MatchNamesWithUnderscores = true; +}) +``` + +### 10.2.2.2 安装对应的数据库提供器 + +- `SqlServer`:`Microsoft.Data.SqlClient` +- `Sqlite`:`Microsoft.Data.Sqlite` +- `MySql`:`MySql.Data` +- `Npgsql`:`Npgsql` +- `Oracle`:`Oracle.ManagedDataAccess.Core` +- `Firebird`:`FirebirdSql.Data.FirebirdClient` + +:::important 安装拓展包位置 + +在 `Furion` 框架中,推荐将拓展包 `Furion.Extras.DatabaseAccessor.Dapper` 安装到 `Furion.Core` 层中。 + +::: + +## 10.2.3 基本使用 + +在使用之前,我们可以通过构造函数注入 `IDapperRepository` 或 `IDapperRepository` 接口,如: + +- 非泛型版本 + +```cs showLineNumbers +private readonly IDapperRepository _dapperRepository; +public PersonService(IDapperRepository dapperRepository) +{ + _dapperRepository = dapperRepository; +} +``` + +- 泛型版本 + +```cs showLineNumbers +private readonly IDapperRepository _personRepository; +public PersonService(IDapperRepository personRepository) +{ + _personRepository = personRepository; +} +``` + +### 10.2.3.1 `sql` 操作 + +```cs showLineNumbers +var data = _dapperRepository.Query("select * from person"); +var data = await _dapperRepository.QueryAsync("select * from person"); + +var data = _dapperRepository.Query("select * from person"); + +var guid = Guid.NewGuid(); +var dog = _dapperRepository.Query("select Age = @Age, Id = @Id", new { Age = (int?)null, Id = guid }); +``` + +```cs showLineNumbers +var count = _dapperRepository.Execute(@"insert MyTable(colA, colB) values (@a, @b)", + new[] { new { a=1, b=1 }, new { a=2, b=2 }, new { a=3, b=3 } } + ); + +var user = _dapperRepository.Query("spGetUser", new {Id = 1}, + commandType: CommandType.StoredProcedure).SingleOrDefault(); +``` + +用法和官方一致,此处不再举更多例子。 + +### 10.2.3.2 `` 操作 + +`Furion` 框架提供了 `IDapperRepository` 和 `IDapperRepository` 两个操作仓储,后者继承前者。使用如下: + +```cs showLineNumbers +var person = personRepository.Get(1); +var persons = personRepository.GetAll(); + +var effects = personRepository.Insert(person); +var effects = personRepository.Update(person); +var effects = personRepository.Delete(person); + +var effects = personRepository.Insert(persons); // 插入多个 +var effects = personRepository.Update(persons); // 更新多个 +var effects = personRepository.Delete(persons); // 删除多个 + +var effects = await personRepository.InsertAsync(person); +``` + +## 10.2.4 高级使用 + +`IDapperRepository` 和 `IDapperRepository` 仓储提供了 `Context` 和 `DynamicContext` 属性,该属性返回 `IDbConnection` 对象。 + +拿到该对象后,我们就可以操作 `Dapper` 提供的所有操作了,如: + +### 10.2.4.1 查询一对一 + +```cs showLineNumbers +var sql = +@"select * from #Posts p +left join #Users u on u.Id = p.OwnerId +Order by p.Id"; + +var data = dapperRepository.Context.Query(sql, (post, user) => { post.Owner = user; return post;}); +var post = data.First(); +``` + +### 10.2.4.2 查询多个结果 + +```cs showLineNumbers +var sql = +@" +select * from Customers where CustomerId = @id +select * from Orders where CustomerId = @id +select * from Returns where CustomerId = @id"; + +using (var multi = dapperRepository.Context.QueryMultiple(sql, new {id=selectedId})) +{ + var customer = multi.Read().Single(); + var orders = multi.Read().ToList(); + var returns = multi.Read().ToList(); + // ... +} +``` + +### 10.2.4.3 更多操作 + +```cs showLineNumbers +var shapes = new List(); +using (var reader = dapperRepository.Context.ExecuteReader("select * from Shapes")) +{ + var circleParser = reader.GetRowParser(typeof(Circle)); + var squareParser = reader.GetRowParser(typeof(Square)); + var triangleParser = reader.GetRowParser(typeof(Triangle)); + + var typeColumnIndex = reader.GetOrdinal("Type"); + + while (reader.Read()) + { + IShape shape; + var type = (ShapeType)reader.GetInt32(typeColumnIndex); + switch (type) + { + case ShapeType.Circle: + shape = circleParser(reader); + break; + case ShapeType.Square: + shape = squareParser(reader); + break; + case ShapeType.Triangle: + shape = triangleParser(reader); + break; + default: + throw new NotImplementedException(); + } + + shapes.Add(shape); + } +} +``` + +## 10.2.5 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: + +--- + +:::note 了解更多 + +想了解更多 `Dapper` 知识可查阅 [Dapper 官网](https://github.com/StackExchange/Dapper)。 + +::: diff --git a/handbook/docs/data-validation.mdx b/handbook/docs/data-validation.mdx new file mode 100644 index 0000000000000000000000000000000000000000..57cb67fd863cfc5ea53ca6255b21bdaf657431be --- /dev/null +++ b/handbook/docs/data-validation.mdx @@ -0,0 +1,905 @@ +--- +id: data-validation +title: 8. 数据校验 +sidebar_label: 8. 数据校验 +--- + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **新特性** + + -  新增 验证特性 `[DataValidation]` 支持 `[Display]` 和 `[DisplayName]` 特性设置 `{0}` 4.8.8.42 ⏱️2023.09.01 [#I7XB3T](https://gitee.com/dotnetchina/Furion/issues/I7XB3T) + -  新增 `ValidationTypes` 更多常见验证格式(`手机机身码类型`,`统一社会信用代码`,`GUID/UUID`,`base64`) 4.8.3.6 ⏱️2022.12.13 [3680d7a](https://gitee.com/dotnetchina/Furion/commit/3680d7a7a53515dfb78625f2c6a393a788025685) + +- **问题修复** + + -  修复 数据验证 `ValiationTypes.GUID_OR_UUID` 不支持大写问题 4.8.7.14 ⏱️2023.03.16 [#I6NP22](https://gitee.com/dotnetchina/Furion/issues/I6NP22) + +
+
+
+ +import useBaseUrl from "@docusaurus/useBaseUrl"; +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +## 8.1 关于数据校验 + +数据校验字面上的意思就是对使用者提交过来的数据进行合法性验证。在一套完善的应用系统中,数据有效性校验是必不可少的业务处理第一道关卡。 + +## 8.2 数据校验的好处 + +- 过滤不安全数据,提高系统的安全性 +- 减少不必要的业务异常处理,提高系统的响应速度 +- 大大提高系统稳定性 +- 大数据并发时起着一定的缓冲作用 + +## 8.3 数据校验方式 + +- 传统方式,在业务代码之前手动验证 +- `Mvc` 特性方式,`Mvc` 内置的 `DataAnnotations` 方式 +- **推荐方式**,`Furion` 框架内置的 `DataValidation` 验证 +- 其他方式,使用第三方验证库,如 `FluentValidation` + +### 8.3.1 传统方式 + +在很多老项目中,我们经常看到这样的代码: + +```cs showLineNumbers {4,9,14} +public bool Insert(Person person) +{ + // 验证参数 + if(string.IsNullOrEmty(person.Name)) + { + throw new System.Exception("名字不能为空"); + } + + if(person.Age < 18) + { + throw new System.Exception("年龄不能小于 18 岁"); + } + + if(!person.Password.Equals(person.ConfirmPassword) + { + throw new System.Exception("两次密码不一致"); + } + + // 业务代码 + _repository.Insert(person.Adapt()); + + // ... +} +``` + +从上面的代码看起来,似乎没有什么不妥,但是从一个程序可维护性来说,这是一个糟糕的代码,因为该业务代码中**包含了太多与业务无关的数据验证**。 + +试想一下,如果这个 `Person` 有 几十个参数都需要验证呢?可想而知,这是一个庞大的业务代码。 + +再者,如果其他地方也需要用到这个 `Person` 类验证呢?那代码好比老鼠啃过的面包屑一样,到处都是。 + +如此得知,这样的方式是极其不推荐的,**不但污染了业务代码,也破坏了业务职责单一性原理,也让验证逻辑无法实现通用,后续维护难度大大升级**。 + +### 8.3.2 `Mvc` 特性方式 + +在 `ASP.NET Core` 中,微软为我们提供了全新的 `特性` 验证方式,可通过对对象贴特性实现数据验证。这种方式有效的将数据校验和业务代码剥离开来,而且容易使用和拓展。 + +- **在模型中验证** + +```cs showLineNumbers {1,7-8,11-12} +using System.ComponentModel.DataAnnotations; + +namespace Hoa.Application.Authorization.Dtos +{ + public class SignInInput + { + [Required] // 必填验证 + [MinLength(4)] // 最小长度验证 + public string Account { get; set; } + + [Required] // 必填验证 + [MaxLength(32)] // 最大长度验证 + public string Password { get; set; } + } +} +``` + +- **在参数中验证** + +```cs showLineNumbers {2-3,8-9,12-13} +public void CheckMethodParameterValid( + [Required] // 必填验证 + [MinLength(4)] // 最小长度验证 + string name, + + int age, + + [Required] // 必填验证 + [RegularExpression("[a-zA-Z0-9_]{8,30}") // 正则表达式验证 + string password, + + [Required] // 必填验证 + [RegularExpression("[a-zA-Z0-9_]{8,30}") // 正则表达式验证 + string confirmPassword +) +{ + // TODO +} +``` + +:::note 小提醒 + +如果函数的参数大于或等于 3 个,建议抽离出模型类,也就是不建议上面的方式。 + +::: + +- **`Mvc` 内置特性** + + - `[ValidateNever]`:指示熟悉或参数从验证中排除 + - `[CreditCard]`:信用卡格式验证 + - `[Compare]`:验证两个属性值是否匹配 + - `[EmailAddress]`:验证电子邮箱 + - `[Phone]`:验证电话号码 + - `[Range]`:验证指定范围 + - `[RegularExpression]`:验证属性值是否匹配正则表达式 + - `[Required]`:验证不为 null + - `[StringLength]`:验证字符串长度 + - `[URL]`:验证是否有效的 `URL` 格式 + - `[Remote]`:调用远程服务地址进行客户端验证 + +:::tip `Mvc` 内置特性 + +想了解 `Mvc` 内置特性列表可查看官方文档 [ASP.NET Core - 模型验证](https://docs.microsoft.com/zh-cn/aspnet/core/mvc/models/validation?view=aspnetcore-6.0) + +::: + +- **自定义特性验证** + +```cs showLineNumbers {1,13-24} +public class ClassicMovieAttribute : ValidationAttribute +{ + public ClassicMovieAttribute(int year) + { + Year = year; + } + + public int Year { get; } + + public string GetErrorMessage() => + $"Classic movies must have a release year no later than {Year}."; + + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + var movie = (Movie)validationContext.ObjectInstance; + var releaseYear = ((DateTime)value).Year; + + if (movie.Genre == Genre.Classic && releaseYear > Year) + { + return new ValidationResult(GetErrorMessage()); + } + + return ValidationResult.Success; + } +} +``` + +- **`IValidatableObject` 复杂验证** + +```cs showLineNumbers {1,3,10-21} +using System.Collections.Generic; + +public class DtoModel : IValidatableObject +{ + [Required] + [StringLength(100)] + public string Title { get; set; } + + // 你的验证逻辑 + public IEnumerable Validate(ValidationContext validationContext) + { + // 还可以解析服务 + var service = validationContext.GetService(typeof(类型)); + + if (你的逻辑代码) + { + yield return new ValidationResult( + "错误消息" + ,new[] { nameof(Title) } // 验证失败的属性 + ); + } + } +} +``` + +`Mvc` 特性方式极大的将业务逻辑和验证进行了剥离和解耦,而且还能实现自定义复杂验证。 + +**但是 `Mvc` 特性验证方式有几个明显的缺点**: + +- 只能在 `控制器` 中的 `Action`(动作方法)中使用 +- **无法在任意类、任意方法中使用** +- 内置的验证类型非常有限,且不易拓展 +- 不支持验证消息后期配置 + +所以,`Furion` 提供了新的验证引擎 `DataValidation`,在完全兼容 `Mvc` 内置验证的同时提供了大量常见验证、复杂验证、自定义验证等能力。 + +## 8.4 `DataValidation` 验证 🤗 + +`DataValidation` 是 `Furion` 框架提供了全新的验证方式,完全兼容 `Mvc` 内置验证,并且赋予了超能。 + +### 8.4.1 `DataValidation` 优点 + +- **完全兼容 `Mvc` 内置验证引擎** +- **内置常见验证类型及可自定义验证类型功能** +- 提供全局对象拓展验证方式 +- 支持验证消息后期配置,支持实时更新 +- 支持在任何类,任何方法、任何位置实现手动验证、特性方式验证等 +- 支持设置验证结果模型 + +## 8.5 `DataValidation` 使用 + +:::tip 小提示 + +`.AddDataValidation()` 默认已经集成在 `AddInject()` 中了,**无需再次注册**。也就是 `8.5.1` 章节可不配置。 + +::: + +### 8.5.1 注册验证服务 + +```cs showLineNumbers {11} title="Furion.Web.Core\FurWebCoreStartup.cs" +using Microsoft.Extensions.DependencyInjection; + +namespace Furion.Web.Core +{ + [AppStartup(800)] + public sealed class FurWebCoreStartup : AppStartup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers() + .AddDataValidation(); + } + } +} +``` + +:::important 特别注意 + +`.AddDataValidation()` 需在 `services.AddControllers()` 之后注册。 + +::: + +### 8.5.2 兼容 `Mvc` 特性验证 + + + + +```cs showLineNumbers {1,7,10} +using System.ComponentModel.DataAnnotations; + +namespace Furion.Application +{ + public class TestDto + { + [Range(10, 20, ErrorMessage = "Id 只能在 10-20 区间取值")] + public int Id { get; set; } + + [Required(ErrorMessage = "必填"), MinLength(3, ErrorMessage = "字符串长度不能少于3位")] + public string Name { get; set; } + } +} +``` + + + + +```cs showLineNumbers {12,22} +using Furion.DynamicApiController; + +namespace Furion.Application +{ + public class FurionAppService : IDynamicApiController + { + /// + /// 值类型验证 + /// + /// + /// + public int Get(int id) + { + return id; + } + + /// + /// 对象类型验证 + /// + /// + /// + public TestDto Post(TestDto testDto) + { + return testDto; + } + } +} +``` + + + + +如下图所示: + + + +### 8.5.3 兼容 `Mvc` 复杂验证 + +```cs showLineNumbers {2,6,14-24} +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Furion.Application +{ + public class TestDto : IValidatableObject + { + [Range(10, 20, ErrorMessage = "Id 只能在 10-20 区间取值")] + public int Id { get; set; } + + [Required(ErrorMessage = "必填"), MinLength(3, ErrorMessage = "字符串长度不能少于3位")] + public string Name { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) + { + // 还可以解析服务 + var service = validationContext.GetService(typeof(类型)); + + if (Name.StartsWith("Furion")) + { + yield return new ValidationResult( + "不能以 Furion 开头" + , new[] { nameof(Name) } + ); + } + } + } +} +``` + +如下图所示: + + + +## 8.6 手动验证 + +### 8.6.1 验证模型 + +```cs showLineNumbers {1,11} +using Furion.DataValidation; +using Furion.DynamicApiController; + +namespace Furion.Application +{ + public class FurionAppService : IDynamicApiController + { + [NonValidation] // 跳过全局验证 + public DataValidationResult Post(TestDto testDto) + { + return testDto.TryValidate(); + } + } +} +``` + +如下图所示: + + + +:::note + +支持 **`Mvc`** 内置的特性验证、属性验证及复杂的 **`IValidatableObject`** 验证。 + +::: + +### 8.6.2 `TryValidate` 和 `Validate` + +`Furion` 提供了 `TryValidate()` 和 `Validate()` 两个验证拓展方法,唯一的区别就是后者验证失败将自动抛出异常消息。 + +### 8.6.3 `ValidationTypes` 常见验证 + +`Furion` 内置了很多常用类型的数据验证,包括: + +- `Numeric`:数值类型 +- `PositiveNumber`:正数类型 +- `NegativeNumber`:负数类型 +- `Integer`:整数类型 +- `Money`:金钱类型 +- `Date`:日期类型 +- `Time`:时间类型 +- `IDCard`:身份证类型 +- `PostCode`:邮编类型 +- `PhoneNumber`:手机号类型 +- `Telephone`:固话类型 +- `PhoneOrTelNumber`:手机或固话类型 +- `EmailAddress`:邮件地址类型 +- `Url`:网址类型 +- `Color`:颜色值类型 +- `Chinese`:中文类型 +- `IPv4`:IPv4 地址类型 +- `IPv6`:IPv6 地址类型 +- `Age`:年龄类型 +- `ChineseName`:中文名类型 +- `EnglishName`:英文名类型 +- `Capital`:纯大写英文类型 +- `Lowercase`:纯小写英文类型 +- `Ascii`:Ascii 类型 +- `Md5`:Md5 字符串类型 +- `Zip`:压缩包格式类型 +- `Image`:图片格式类型 +- `Document`:文档格式类型 +- `MP3`:Mp3 格式类型 +- `Flash`:Flash 格式类型 +- `Video`:视频文件格式类型 +- `Html`:`Html` 标签格式类型 +- `IMEI`:手机机身码类型 +- `SocialCreditCode`:统一社会信用代码类型 +- `GUID_OR_UUID`:`GUID` 或 `UUID` 类型 +- `Base64`:`base64` 格式类型 + +**使用示例** + + + + +```cs showLineNumbers +// 验证中文 +"我叫 MonK".TryValidate(ValidationTypes.Chinese); // => false + +// 验证数值 +2.TryValidate(ValidationTypes.Numeric); // => true + +// 验证整数 +true.TryValidate(ValidationTypes.Integer); // => false + +// 验证邮箱 +"monksoul@outlook.com".TryValidate(ValidationTypes.EmailAddress); // => true + +// 验证负数 +2.0m.TryValidate(ValidationTypes.NegativeNumber); // => false + +// 自定义正则表达式验证 +"Furion".TryValidate("/^Furion$"); // => true +``` + + + + +```cs showLineNumbers +// 验证数值类型且是整数 +"20".TryValidate(ValidationTypes.Numeric, ValidationTypes.Integer); // => true + +// 验证时日期或时间格式 +"2020-05-20".TryValidate(ValidationPattern.AtLeastOne, ValidationTypes.Date, ValidationTypes.Time); // => true +"23:45:20".TryValidate(ValidationPattern.AtLeastOne, ValidationTypes.Date, ValidationTypes.Time); // => true + +``` + + + + +:::tip 小知识 + +可通过设置 `TryValidate([ValidationPattern], params object[] validationTypes)` 方法的 `ValidationPattern` 参数配置验证逻辑,如:**`同时成立`** 或 **`只要一个成立`** 即可验证通过 + +::: + +### 8.6.4 `[DataValidation]` 特性 + +`Furion` 还提供了 `[DataValidation]` 特性方便在模型参数中使用 `ValidationTypes` 常见验证或自定义验证。 + +```cs showLineNumbers {1,7,10,14,17,20} +using Furion.DataValidation; + +namespace Furion.Application +{ + public class TestDto + { + [DataValidation(ValidationTypes.Integer)] + public int Id { get; set; } + + [DataValidation(ValidationTypes.Numeric, ValidationTypes.Integer)] + public int Cost { get; set; } + + [DataValidation(ValidationPattern.AtLeastOne, ValidationTypes.Chinese, ValidationTypes.Date)] + public string Name { get; set; } + + // 可以和Mvc特性共存 + [Required, DataValidation(ValidationTypes.Age)] + public int Age { get; set; } + + [DataValidation(ValidationTypes.IDCard, ErrorMessage = "自定义身份证提示消息")] + public string IDCard { get; set; } + } +} +``` + +**`[DataValidation]` 特性具备 `ValidationAttribute` 特性的所有配置以外还提供了以下配置:** + +- `ValidationTypes`:验证类型,`Enum[]` 类型, +- `ValidationPattern`:验证逻辑,`ValidationPattern` 类型,可选 `AllOfThem(全部验证通过)` 和 `AtleastOne(至少一个验证通过)` +- `AllowNullValue`:是否允许空值,`bool` 类型,默认 `false` +- `AllowEmptyStrings`:是否允许空字符串,`bool` 类型,默认 `false` + +在 `Furion 4.8.8.42+` 版本,`[DataValidation]` 的 `ErrorMessage` 支持 `[Display]` 和 `[DisplayName]` 特性指定格式化 `{0}` 信息,如: + +```cs showLineNumbers {1-3} +[Display(Name = "手机号码")] +// [DisplayName("手机号码")] +[DataValidation(ValidationTypes.PhoneNumber, ErrorMessage = "不是一个正确的{0}")] +public string PhoneNumber { get; set; } +``` + +### 8.6.5 `[ModelBinder]` 特性 + +默认情况下,验证失败信息会根据属性名进行序列化,但是如果属性序列化自定义了 `[JsonPropertyName]` 特性,那么验证失败的消息就不匹配了,这时我们需要添加 `[ModelBinder(Name = "序列化对应名字")]` 进行纠正。如下图所示: + +```cs showLineNumbers {1} +[JsonPropertyName("phone_number"), ModelBinder(Name = "phone_number")] +public string PhoneNumber { get; set; } +``` + +## 8.7 `[NonValidation]` 跳过验证 + +`Furion` 框架提供了对象模型跳过验证特性 `[NonValidation]`,支持在 `控制器`,`动作方法`,`类` 中使用。 + +一旦贴了此特性,那么将不会执行验证操作。 + +:::note + +**`[NonValidation]`** 只对对象类型有效,值类型无效。 + +::: + +## 8.8 高级自定义操作 + +### 8.8.1 自定义 `ValidationTypes` 类型 + +除了 `Furion` 内置的验证类型以外,`Furion` 还提供了非常灵活的自定义验证类型机制。 + +实现自定义验证类型必须遵循以下配置: + +- 验证类型必须是公开且是 `Enum` 枚举类型 +- 枚举类型必须贴有 `[ValidationType]` 特性 +- 枚举中每一项必须贴有 `[ValidationItemMetadata]` 特性 + +**如**: + +```cs showLineNumbers {1,6,12,18} +using Furion.DataValidation; +using System.Text.RegularExpressions; + +namespace Furion.Application +{ + [ValidationType] + public enum MyValidationTypes + { + /// + /// 强密码类型 + /// + [ValidationItemMetadata(@"^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,10}$", "必须须包含大小写字母和数字的组合,不能使用特殊字符,长度在8-10之间")] + StrongPassword, + + /// + /// 以 Furion 字符串开头,忽略大小写 + /// + [ValidationItemMetadata(@"^(furion).*", "默认提示:必须以Fur字符串开头,忽略大小写", RegexOptions.IgnoreCase)] + StartWithFurString + } +} +``` + +:::tip 小知识 + +`Any-Rule` 是国人记录的正则表达式大全,如需查找正则表达式可查阅 [https://any86.github.io/any-rule/](https://any86.github.io/any-rule/) + +::: + +**使用** + +- 手动使用 + +```cs showLineNumbers +"q1w2e3".TryValidate(MyValidationTypes.StrongPassword); // => false + +"furos".TryValidate(MyValidationTypes.StartWithFurString); // => true +``` + +- `[DataValidation]` 中使用 + +```cs showLineNumbers +[DataValidation(MyValidationTypes.StrongPassword)] +public string Password { get; set; } +``` + +- 多个自定义类型混用 + +```cs showLineNumbers +"Q1w2e3r4t5!*".TryValidate(MyValidationTypes.StrongPassword, ValidationTypes.EmailAddress); // => true +``` + +:::caution 特别注意 + +自定义的验证类型也要保证名称全局唯一,也就是多个验证类型不能出现一样的名字。 + +::: + +### 8.8.2 自定义 `ValidationTypes` 失败消息 + +`Furion` 内置的 `ValidationTypes` 已有默认的失败消息: + +- `Numeric`:**The value is not a numeric type.** +- `PositiveNumber`:**The value is not a positive number type.** +- `NegativeNumber`:**The value is not a negative number type.** +- `Integer`:**The value is not a integer type.** +- `Money`:**The value is not a money type.** +- `Date`:**The value is not a date type.** +- `Time`:**The value is not a time type.** +- `IDCard`:**The value is not a idcard type.** +- `PostCode`:**The value is not a postcode type.** +- `PhoneNumber`:**The value is not a phone number type.** +- `Telephone`:**The value is not a telephone type.** +- `PhoneOrTelNumber`:**The value is not a phone number or telephone type.** +- `EmailAddress`:**The value is not a email address type.** +- `Url`:**The value is not a url address type.** +- `Color`:**The value is not a color type.** +- `Chinese`:**The value is not a chinese type.** +- `IPv4`:**The value is not a IPv4 type.** +- `IPv6`:**The value is not a IPv6 type.** +- `Age`:**The value is not a age type.** +- `ChineseName`:**The value is not a chinese name type.** +- `EnglishName`:**The value is not a english name type.** +- `Capital`:**The value is not a capital type.** +- `Lowercase`:**The value is not a lowercase type.** +- `Ascii`:**The value is not a ascii type.** +- `Md5`:**The value is not a md5 type.** +- `Zip`:**The value is not a zip type.** +- `Image`:**The value is not a image type.** +- `Document`:**The value is not a document type.** +- `MP3`:**The value is not a mp3 type.** +- `Flash`:**The value is not a flash type.** +- `Video`:**The value is not a video type.** +- `Html`:**The value is not a html type.** +- `IMEI`:**The value is not a IMEI type.** +- `SocialCreditCode`:**The value is not a social credit code type.** +- `GUID_OR_UUID`:**The value is not a GUID or UUID type.** +- `Base64`:**The value is not a base64 type.** + +我们可以通过创建继承 `IValidationMessageTypeProvider` 验证消息提供器类型,或通过 `appsettings.json` 配置。 + +- **`[ValidationMessageType]`** 方式 + +```cs showLineNumbers {5,9,12,15,19,22} +using Furion.DataValidation; + +namespace Furion.Application +{ + [ValidationMessageType] + public enum MyValidationMessageType + { + [ValidationMessage("必须是数值类型")] + Numeric, + + [ValidationMessage("必须是正数")] + PositiveNumber, + + // 修改自定义类型验证失败消息 + [ValidationMessage("密码太简单了")] + StrongPassword, + + [ValidationMessage("必须以 Furion 开头")] + StartWithFurString + } +} +``` + +:::tip 小知识 + +除了贴 `[ValidationMessageType]` 特性外,`Furion` 框架还提供了 `IValidationMessageTypeProvider` 方式查找验证消息类型,如下图所示: + +```cs showLineNumbers {1,6,8-12} +using Furion.DataValidation; +using System; + +namespace Furion.Application +{ + public class MyValidationTypeMessageProvider : IValidationMessageTypeProvider + { + public Type[] Definitions => new[] + { + typeof(MyValidationMessageType), + typeof(MyValidationMessageType2) + }; + } +} +``` + +注册验证消息提供器 + +```cs showLineNumbers {11} title="Furion.Web.Core\FurWebCoreStartup.cs" +using Microsoft.Extensions.DependencyInjection; + +namespace Furion.Web.Core +{ + [AppStartup(800)] + public sealed class FurWebCoreStartup : AppStartup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers() + .AddDataValidation(); + } + } +} +``` + +::: + +如下图所示: + + + +- **`appsettings.json` 方式** + +```json showLineNumbers {2-9} title="Furion.Web.Entry/appsettings.json" +{ + "ValidationTypeMessageSettings": { + "Definitions": [ + ["Numeric", "必须是数值类型"], + + ["StrongPassword", "密码太简单了!!!"] + ] + } +} +``` + +:::important + +`appsettings.json` 中相同的 `Key` 会覆盖 `IValidationMessageTypeProvider` 提供相同 `Key` 的值。 + +::: + +#### 错误消息查找优先级 + +`DefaultErrorMessage` -> `IValidationMessageTypeProvider` -> `appsettings.json` (**低 -> 高**) + +## 8.9 模型验证范围 + +`Furion` 提供多种模型验证范围设置: + +- 全局验证(默认) +- `[NonValidation]` 跳过验证 +- `[TypeFilter(typeof(DataValidationFilter))]` 局部验证 +- `[ApiController]` 控制器范围验证 + +### 8.9.1 全局验证 + +默认情况下,通过 `.AddDataValidation()` 注册数据验证服务已经启用了全局验证,如若不想启用全局验证,则传入 `false` 即可,如:`.AddDataValidation(false)`。 + +### 8.9.2 `[NonValidation]` 跳过验证 + +可通过 `[NonValidation]` 贴在 `控制器`,`动作方法`,`类` 中跳过全局验证或不需要验证 + +### 8.9.3 `[TypeFilter(typeof(DataValidationFilter))]` 局部验证 + +我们也可以无需注册 `.AddDataValidation()` 服务,直接在 `动作方法` 上贴 `[TypeFilter(typeof(DataValidationFilter))]` 可启用局部验证。如: + +```cs showLineNumbers {1,3,9} +using Furion.DataValidation; +using Furion.DynamicApiController; +using Microsoft.AspNetCore.Mvc; + +namespace Furion.Application +{ + public class FurionAppService : IDynamicApiController + { + [TypeFilter(typeof(DataValidationFilter))] + public TestDto Post(TestDto testDto) + { + return testDto; + } + } +} +``` + +### 8.9.4 `[ApiController]` 控制器范围验证 + +`[ApiController]` 是 `Mvc` 提供的控制器范围(含所有动作方法)的验证。 + +```cs showLineNumbers {1,5} +using Microsoft.AspNetCore.Mvc; + +namespace Furion.Web.Entry.Controllers +{ + [ApiController] + public class MvcController : Controller + { + public IActionResult Index() + { + return View(); + } + } +} +``` + +## 8.10 `MiniProfiler` 查看 + +如下图所示: + + + +## 8.11 多语言支持 + +参见 [【全球化和本地化(多语言)】](./local-language) 章节 + +## 8.12 集成 `FluentValidation` 第三方校验 + +`Furion` 内置的验证已经可以满足绝大多数校验情况,但是对于 `场景` 验证目前暂未支持。这里推荐集成 [`FluentValidation`](https://github.com/FluentValidation/FluentValidation) 第三方校验组件。 + +### 8.12.1 安装 `FluentValidation.AspNetCore` 拓展包 + +```shell showLineNumbers +dotnet add package FluentValidation.AspNetCore +``` + +### 8.12.2 在 `Startup.cs` 中注册 + +```cs showLineNumbers {2} +services.AddControllers() + .AddFluentValidation(fv => { + fv.RegisterValidatorsFromAssemblies(App.Assemblies); + }); +``` + +### 8.12.3 使用例子 + +```cs showLineNumbers +public class Person { + public int Id { get; set; } + public string Name { get; set; } + public string Email { get; set; } + public int Age { get; set; } +} + +public class PersonValidator : AbstractValidator { + public PersonValidator() { + RuleFor(x => x.Id).NotNull(); + RuleFor(x => x.Name).Length(0, 10); + RuleFor(x => x.Email).EmailAddress(); + RuleFor(x => x.Age).InclusiveBetween(18, 60); + } +} +``` + +**在控制器中使用无需手动调用 `ModelState.IsValid` 进行判断,`Furion` 会自动执行该操作。** + +如需了解更多 `FluentValidation` 知识可查阅官方文档:[https://fluentvalidation.net/](https://fluentvalidation.net/) + +## 8.13 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/dbcontext-Interceptor.mdx b/handbook/docs/dbcontext-Interceptor.mdx new file mode 100644 index 0000000000000000000000000000000000000000..152bbbeeccc129a77a23ef79f4ac64cd42b175eb --- /dev/null +++ b/handbook/docs/dbcontext-Interceptor.mdx @@ -0,0 +1,320 @@ +--- +id: dbcontext-Interceptor +title: 9.25 数据库操作拦截器 +sidebar_label: 9.25 数据库操作拦截器 +--- + +## 9.25.1 数据库拦截器 + +`Furion` 框架提供四种数据库操作拦截器,可以通过拦截器动态修改数据库连接字符串,动态修改 sql,动态更改参数等操作。 + +`Furion` 支持这四种拦截器: + +- `DbConnectionInterceptor`:数据库连接拦截器 +- `DbCommandInterceptor`:数据库执行 `Sql` 拦截器 +- `SaveChangesInterceptor`:提交到数据库拦截器 +- 在数据库上下文中重写 `SavedChangesEvent` 相关事件 + +## 9.25.2 支持拦截类型 + +### 9.25.2.1 `DbConnectionInterceptor` + +```cs showLineNumbers +using Microsoft.EntityFrameworkCore.Diagnostics; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; + +namespace Furion.DatabaseAccessor.Interceptors +{ + public class SqlConnectionInterceptor : DbConnectionInterceptor + { + // 数据库连接之前 + public override InterceptionResult ConnectionOpening(DbConnection connection, ConnectionEventData eventData, InterceptionResult result) + { + return base.ConnectionOpening(connection, eventData, result); + } + + // 数据库连接之前(异步) + public override ValueTask ConnectionOpeningAsync(DbConnection connection, ConnectionEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default) + { + return base.ConnectionOpeningAsync(connection, eventData, result, cancellationToken); + } + + // 数据库连接成功 + public override void ConnectionOpened(DbConnection connection, ConnectionEndEventData eventData) + { + base.ConnectionOpened(connection, eventData); + } + + // 数据库连接成功(异步) + public override Task ConnectionOpenedAsync(DbConnection connection, ConnectionEndEventData eventData, CancellationToken cancellationToken = default) + { + return base.ConnectionOpenedAsync(connection, eventData, cancellationToken); + } + + // 数据库连接关闭之前 + public override InterceptionResult ConnectionClosing(DbConnection connection, ConnectionEventData eventData, InterceptionResult result) + { + return base.ConnectionClosing(connection, eventData, result); + } + + // 数据库连接关闭之前(异步) + public override ValueTask ConnectionClosingAsync(DbConnection connection, ConnectionEventData eventData, InterceptionResult result) + { + return base.ConnectionClosingAsync(connection, eventData, result); + } + + // 数据库连接关闭成功 + public override void ConnectionClosed(DbConnection connection, ConnectionEndEventData eventData) + { + base.ConnectionClosed(connection, eventData); + } + + // 数据库连接关闭成功(异步) + public override Task ConnectionClosedAsync(DbConnection connection, ConnectionEndEventData eventData) + { + return base.ConnectionClosedAsync(connection, eventData); + } + + // 数据库连接失败 + public override void ConnectionFailed(DbConnection connection, ConnectionErrorEventData eventData) + { + base.ConnectionFailed(connection, eventData); + } + + // 数据库连接失败(异步) + public override Task ConnectionFailedAsync(DbConnection connection, ConnectionErrorEventData eventData, CancellationToken cancellationToken = default) + { + return base.ConnectionFailedAsync(connection, eventData, cancellationToken); + } + } +} +``` + +### 9.25.2.2 `DbCommandInterceptor` + +```cs showLineNumbers +using Microsoft.EntityFrameworkCore.Diagnostics; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; + +namespace Furion.DatabaseAccessor +{ + internal sealed class SqlCommandProfilerInterceptor : DbCommandInterceptor + { + // 创建命令对象之前 + public override InterceptionResult CommandCreating(CommandCorrelatedEventData eventData, InterceptionResult result) + { + return base.CommandCreating(eventData, result); + } + + // 创建命令对象之后 + public override DbCommand CommandCreated(CommandEndEventData eventData, DbCommand result) + { + return base.CommandCreated(eventData, result); + } + + // 创建命令对象失败 + public override void CommandFailed(DbCommand command, CommandErrorEventData eventData) + { + base.CommandFailed(command, eventData); + } + + // 创建命令对象失败(异步) + public override Task CommandFailedAsync(DbCommand command, CommandErrorEventData eventData, CancellationToken cancellationToken = default) + { + return base.CommandFailedAsync(command, eventData, cancellationToken); + } + + // 读取数据之前 + public override InterceptionResult ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult result) + { + return base.ReaderExecuting(command, eventData, result); + } + + // 读取数据之前(异步) + public override ValueTask> ReaderExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default) + { + return base.ReaderExecutingAsync(command, eventData, result, cancellationToken); + } + + // 读取数据之后 + public override DbDataReader ReaderExecuted(DbCommand command, CommandExecutedEventData eventData, DbDataReader result) + { + return base.ReaderExecuted(command, eventData, result); + } + + // 读取数据之后(异步) + public override ValueTask ReaderExecutedAsync(DbCommand command, CommandExecutedEventData eventData, DbDataReader result, CancellationToken cancellationToken = default) + { + return base.ReaderExecutedAsync(command, eventData, result, cancellationToken); + } + + // DataReader 对象释放之前 + public override InterceptionResult DataReaderDisposing(DbCommand command, DataReaderDisposingEventData eventData, InterceptionResult result) + { + return base.DataReaderDisposing(command, eventData, result); + } + + // 无查询执行 sql 之前 + public override InterceptionResult NonQueryExecuting(DbCommand command, CommandEventData eventData, InterceptionResult result) + { + return base.NonQueryExecuting(command, eventData, result); + } + + // 无查询执行 sql 之前(异步) + public override ValueTask> NonQueryExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default) + { + return base.NonQueryExecutingAsync(command, eventData, result, cancellationToken); + } + + // 无查询执行 sql 之后 + public override int NonQueryExecuted(DbCommand command, CommandExecutedEventData eventData, int result) + { + return base.NonQueryExecuted(command, eventData, result); + } + + // 无查询执行 sql 之后(异步) + public override ValueTask NonQueryExecutedAsync(DbCommand command, CommandExecutedEventData eventData, int result, CancellationToken cancellationToken = default) + { + return base.NonQueryExecutedAsync(command, eventData, result, cancellationToken); + } + + // 执行 sql 返回单行单列之前 + public override InterceptionResult ScalarExecuting(DbCommand command, CommandEventData eventData, InterceptionResult result) + { + return base.ScalarExecuting(command, eventData, result); + } + + // 执行 sql 返回单行单列之前(异步) + public override ValueTask> ScalarExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default) + { + return base.ScalarExecutingAsync(command, eventData, result, cancellationToken); + } + + // 执行 sql 返回单行单列之后 + public override object ScalarExecuted(DbCommand command, CommandExecutedEventData eventData, object result) + { + return base.ScalarExecuted(command, eventData, result); + } + + // 执行 sql 返回单行单列之后(异步) + public override ValueTask ScalarExecutedAsync(DbCommand command, CommandExecutedEventData eventData, object result, CancellationToken cancellationToken = default) + { + return base.ScalarExecutedAsync(command, eventData, result, cancellationToken); + } + } +} +``` + +### 9.25.2.3 `SaveChangesInterceptor` + +```cs showLineNumbers +using Microsoft.EntityFrameworkCore.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace Furion.DatabaseAccessor +{ + public class DbContextSaveChangesInterceptor : SaveChangesInterceptor + { + // 提交到数据库之前 + public override InterceptionResult SavingChanges(DbContextEventData eventData, InterceptionResult result) + { + return base.SavingChanges(eventData, result); + } + + // 提交到数据库之前(异步) + public override ValueTask> SavingChangesAsync(DbContextEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default) + { + return base.SavingChangesAsync(eventData, result, cancellationToken); + } + + // 提交到数据库之后 + public override int SavedChanges(SaveChangesCompletedEventData eventData, int result) + { + return base.SavedChanges(eventData, result); + } + + // 提交到数据库之后(异步) + public override ValueTask SavedChangesAsync(SaveChangesCompletedEventData eventData, int result, CancellationToken cancellationToken = default) + { + return base.SavedChangesAsync(eventData, result, cancellationToken); + } + + // 提交数据库失败 + public override void SaveChangesFailed(DbContextErrorEventData eventData) + { + base.SaveChangesFailed(eventData); + } + + // 提交数据库失败(异步) + public override Task SaveChangesFailedAsync(DbContextErrorEventData eventData, CancellationToken cancellationToken = default) + { + return base.SaveChangesFailedAsync(eventData, cancellationToken); + } + } +} +``` + +### 9.25.2.4 `SavedChangesEvent` 拦截 + +`Furion` 框架中为所有 `AppDbContext` 子类都提供了三个可重写的方法,这三个方法分别由三个事件触发: + +- `提交更改之前 SavingChanges 事件`:触发 `void SavingChangesEvent(DbContextEventData eventData, InterceptionResult result)` 方法 +- `提交更改之后 SavedChanges 事件`:触发 `void SavedChangesEvent(SaveChangesCompletedEventData eventData, int result)` 方法 +- `提交更改失败 SaveChangesFailed 事件`:触发 `void SaveChangesFailedEvent(DbContextErrorEventData eventData)` 方法 + +通过这三个事件我们可以**在数据库做增、删、改时候做拦截,比如设置创建时间、更新时间或其他默认操作**。 + +如自动添加租户 Id: + +```cs showLineNumbers +protected override void SavingChangesEvent(DbContextEventData eventData, InterceptionResult result) +{ + // 获取当前事件对应上下文 + var dbContext = eventData.Context; + + // 获取所有新增和更新的实体 + var entities = dbContext.ChangeTracker.Entries() + .Where(u => u.State == EntityState.Added || u.State == EntityState.Modified); + + foreach (var entity in entities) + { + switch (entity.State) + { + // 自动设置租户Id + case EntityState.Added: + entity.Property(nameof(Entity.TenantId)).CurrentValue = GetTenantId(); + break; + // 排除租户Id + case EntityState.Modified: + entity.Property(nameof(Entity.TenantId)).IsModified = false; + break; + } + } +} +``` + +## 9.25.3 注册自定义筛选器 + +定义好过滤器之后,我们需要在数据库上下文中注册: + +```cs showLineNumbers +// services.AddDb 也是一样用法 +services.AddDbPool(interceptors: new IInterceptor[] { + new YourSqlConnectionProfilerInterceptor(), + new YourDbContextSaveChangesInterceptor(), + new YourSqlCommandProfilerInterceptor() +}); +``` + +## 9.25.4 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/dbcontext-add-or-update.mdx b/handbook/docs/dbcontext-add-or-update.mdx new file mode 100644 index 0000000000000000000000000000000000000000..99bc729d061ec2c86439d6417f7bdd14b73c8dfe --- /dev/null +++ b/handbook/docs/dbcontext-add-or-update.mdx @@ -0,0 +1,283 @@ +--- +id: dbcontext-add-or-update +title: 9.8 新增或更新操作 +sidebar_label: 9.8 新增或更新操作 +--- + +:::warning 功能移除声明 + +以下内容在 `Furion 2.5.1 +` 版本中已移除。此操作让很多不了解 `EFCore` 的开发者产生了很大的误解,不知何时新增或何时更新,故移除此功能。 + +::: + +## 9.8.1 新增或更新(不立即提交) + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +repository.InsertOrUpdate(user); + +// 示例二 +user.InsertOrUpdate(); + +// ==== 异步操作 ==== + +// 示例一 +await repository.InsertOrUpdateAsync(user); + +// 示例二 +await user.InsertOrUpdateAsync(); +``` + +## 9.8.2 新增或更新(立即提交) + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +repository.InsertOrUpdateNow(user); + +// 示例二 +user.InsertOrUpdateNow(); + +// ==== 异步操作 ==== + +// 示例一 +await repository.InsertOrUpdateNowAsync(user); + +// 示例二 +await user.InsertOrUpdateNowAsync(); +``` + +## 9.8.3 新增或更新部分列(不立即提交) + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +repository.InsertOrUpdateInclude(user, u=>u.Name, u=>u.Age); + +// 示例二 +repository.InsertOrUpdateInclude(user, "Age", "Name"); + +// 示例三 +repository.InsertOrUpdateInclude(user, new[] { u=>u.Name, u=>u.Age}); + +// 示例四 +repository.InsertOrUpdateInclude(user, new[] {"Age", "Name"}); + +// 示例五 +user.InsertOrUpdateInclude(u=>u.Name, u=>u.Age); + +// 示例六 +user.InsertOrUpdateInclude("Age", "Name"); + +// 示例七 +user.InsertOrUpdateInclude(new[] { u=>u.Name, u=>u.Age}); + +// 示例八 +user.InsertOrUpdateInclude(new[] {"Age", "Name"}); + +// ==== 异步操作 ==== + +// 示例一 +await repository.InsertOrUpdateIncludeAsync(user, u=>u.Name, u=>u.Age); + +// 示例二 +await repository.InsertOrUpdateIncludeAsync(user, "Age", "Name"); + +// 示例三 +await repository.InsertOrUpdateIncludeAsync(user, new[] { u=>u.Name, u=>u.Age}); + +// 示例四 +await repository.InsertOrUpdateIncludeAsync(user, new[] {"Age", "Name"}); + +// 示例五 +await user.InsertOrUpdateIncludeAsync(u=>u.Name, u=>u.Age); + +// 示例六 +await user.InsertOrUpdateIncludeAsync("Age", "Name"); + +// 示例七 +await user.InsertOrUpdateIncludeAsync(new[] { u=>u.Name, u=>u.Age}); + +// 示例八 +await user.InsertOrUpdateIncludeAsync(new[] {"Age", "Name"}); +``` + +## 9.8.4 新增或更新部分列(立即提交) + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +repository.InsertOrUpdateIncludeNow(user, u=>u.Name, u=>u.Age); + +// 示例二 +repository.InsertOrUpdateIncludeNow(user, "Age", "Name"); + +// 示例三 +repository.InsertOrUpdateIncludeNow(user, new[] { u=>u.Name, u=>u.Age}); + +// 示例四 +repository.InsertOrUpdateIncludeNow(user, new[] {"Age", "Name"}); + +// 示例五 +user.InsertOrUpdateIncludeNow(u=>u.Name, u=>u.Age); + +// 示例六 +user.InsertOrUpdateIncludeNow("Age", "Name"); + +// 示例七 +user.InsertOrUpdateIncludeNow(new[] { u=>u.Name, u=>u.Age}); + +// 示例八 +user.InsertOrUpdateIncludeNow(new[] {"Age", "Name"}); + +// ==== 异步操作 ==== + +// 示例一 +await repository.InsertOrUpdateIncludeNowAsync(user, u=>u.Name, u=>u.Age); + +// 示例二 +await repository.InsertOrUpdateIncludeNowAsync(user, "Age", "Name"); + +// 示例三 +await repository.InsertOrUpdateIncludeNowAsync(user, new[] { u=>u.Name, u=>u.Age}); + +// 示例四 +await repository.InsertOrUpdateIncludeNowAsync(user, new[] {"Age", "Name"}); + +// 示例五 +await user.InsertOrUpdateIncludeNowAsync(u=>u.Name, u=>u.Age); + +// 示例六 +await user.InsertOrUpdateIncludeNowAsync("Age", "Name"); + +// 示例七 +await user.InsertOrUpdateIncludeNowAsync(new[] { u=>u.Name, u=>u.Age}); + +// 示例八 +await user.InsertOrUpdateIncludeNowAsync(new[] {"Age", "Name"}); +``` + +## 9.8.5 新增或更新排除特定列(不立即提交) + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +repository.InsertOrUpdateExclude(user, u=>u.Name, u=>u.Age); + +// 示例二 +repository.InsertOrUpdateExclude(user, "Age", "Name"); + +// 示例三 +repository.InsertOrUpdateExclude(user, new[] { u=>u.Name, u=>u.Age}); + +// 示例四 +repository.InsertOrUpdateExclude(user, new[] {"Age", "Name"}); + +// 示例五 +user.InsertOrUpdateExclude(u=>u.Name, u=>u.Age); + +// 示例六 +user.InsertOrUpdateExclude("Age", "Name"); + +// 示例七 +user.InsertOrUpdateExclude(new[] { u=>u.Name, u=>u.Age}); + +// 示例八 +user.InsertOrUpdateExclude(new[] {"Age", "Name"}); + +// ==== 异步操作 ==== + +// 示例一 +await repository.InsertOrUpdateExcludeAsync(user, u=>u.Name, u=>u.Age); + +// 示例二 +await repository.InsertOrUpdateExcludeAsync(user, "Age", "Name"); + +// 示例三 +await repository.InsertOrUpdateExcludeAsync(user, new[] { u=>u.Name, u=>u.Age}); + +// 示例四 +await repository.InsertOrUpdateExcludeAsync(user, new[] {"Age", "Name"}); + +// 示例五 +await user.InsertOrUpdateExcludeAsync(u=>u.Name, u=>u.Age); + +// 示例六 +await user.InsertOrUpdateExcludeAsync("Age", "Name"); + +// 示例七 +await user.InsertOrUpdateExcludeAsync(new[] { u=>u.Name, u=>u.Age}); + +// 示例八 +await user.InsertOrUpdateExcludeAsync(new[] {"Age", "Name"}); +``` + +## 9.8.6 新增或更新排除特定列(立即提交) + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +repository.InsertOrUpdateExcludeNow(user, u=>u.Name, u=>u.Age); + +// 示例二 +repository.InsertOrUpdateExcludeNow(user, "Age", "Name"); + +// 示例三 +repository.InsertOrUpdateExcludeNow(user, new[] { u=>u.Name, u=>u.Age}); + +// 示例四 +repository.InsertOrUpdateExcludeNow(user, new[] {"Age", "Name"}); + +// 示例五 +user.InsertOrUpdateExcludeNow(u=>u.Name, u=>u.Age); + +// 示例六 +user.InsertOrUpdateExcludeNow("Age", "Name"); + +// 示例七 +user.InsertOrUpdateExcludeNow(new[] { u=>u.Name, u=>u.Age}); + +// 示例八 +user.InsertOrUpdateExcludeNow(new[] {"Age", "Name"}); + +// ==== 异步操作 ==== + +// 示例一 +await repository.InsertOrUpdateExcludeNowAsync(user, u=>u.Name, u=>u.Age); + +// 示例二 +await repository.InsertOrUpdateExcludeNowAsync(user, "Age", "Name"); + +// 示例三 +await repository.InsertOrUpdateExcludeNowAsync(user, new[] { u=>u.Name, u=>u.Age}); + +// 示例四 +await repository.InsertOrUpdateExcludeNowAsync(user, new[] {"Age", "Name"}); + +// 示例五 +await user.InsertOrUpdateExcludeNowAsync(u=>u.Name, u=>u.Age); + +// 示例六 +await user.InsertOrUpdateExcludeNowAsync("Age", "Name"); + +// 示例七 +await user.InsertOrUpdateExcludeNowAsync(new[] { u=>u.Name, u=>u.Age}); + +// 示例八 +await user.InsertOrUpdateExcludeNowAsync(new[] {"Age", "Name"}); +``` + +## 9.8.7 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/dbcontext-add.mdx b/handbook/docs/dbcontext-add.mdx new file mode 100644 index 0000000000000000000000000000000000000000..eaa1478c15ed76e2505600c500a9e45ddc227ed2 --- /dev/null +++ b/handbook/docs/dbcontext-add.mdx @@ -0,0 +1,191 @@ +--- +id: dbcontext-add +title: 9.6 新增操作 +sidebar_label: 9.6 新增操作 +--- + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **问题修复** + + -  修复 实体拓展方式操作数据库出现空异常问题 4.8.5 ⏱️2023.01.28 [#I6AXU6](https://gitee.com/dotnetchina/Furion/issues/I6AXU6) + +
+
+
+ +`Furion` 框架提供非常多的语法糖进行数据库操作。 + +## 9.6.1 新增一条,无返回值 + +```cs showLineNumbers +var user = new User { Name = "百小僧", Age = 27 }; + +// ==== 同步操作 ==== + +// 示例一 +repository.Insert(user); + +// 示例二 +user.Insert(); + +// 示例三 +repository.Entities.Add(user); + +// 示例四 +repository.ChangeEntityState(user, EntityState.Added); + +// ==== 异步操作 ==== + +// 示例一 +await repository.InsertAsync(user); + +// 示例二 +await user.InsertAsync(); + +// 示例三 +await repository.Entities.AddAsync(user); + +``` + +## 9.6.2 新增一条,返回最新数据 + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +var newEntity = repository.InsertNow(user); + +// 示例三 +var newEntity = user.InsertNow(); + +// ==== 异步操作 ==== + +// 示例二 +var newEntity = await repository.InsertNowAsync(user); // 有三个重载 + +// 示例四 +var newEntity = await user.InsertNowAsync(); // 有三个重载 +``` + +## 9.6.3 新增多条(不立即提交) + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +repository.Insert(user, user2); + +// 示例二 +repository.Insert(new List { user, user2 }); + +// 示例三 +repository.Insert(new[] {user, user2 }); + +// ==== 异步操作 ==== + +// 示例一 +await repository.InsertAsync(user, user2); + +// 示例二 +await repository.InsertAsync(new List { user, user2 }); + +// 示例三 +await repository.InsertAsync(new[] {user, user2 }); +``` + +## 9.6.4 新增多条(立即提交) + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +repository.InsertNow(user, user2); + +// 示例二 +repository.InsertNow(new List { user, user2 }); + +// 示例三 +repository.InsertNow(new[] {user, user2 }); + +// ==== 异步操作 ==== + +// 示例一 +await repository.InsertNowAsync(user, user2); + +// 示例二 +await repository.InsertNowAsync(new List { user, user2 }); + +// 示例三 +await repository.InsertNowAsync(new[] {user, user2 }); +``` + +:::tip 小知识 + +所有带 `Now` 结尾的表示立即提交到数据库,也就是立即调用 `SaveChanges` 或 `SaveChangesAsync`。 + +::: + +## 9.6.5 忽略空值新增 + +默认情况下,`EFCore` 新增会插入全部列(除实体跟踪方式以外),有些时候我们希望 `Null` 值无需插入,这是我们只需要在更新时候配置 `ignoreNullValues` 参数即可,如: + +```cs showLineNumbers +repository.Insert(entity, ignoreNullValues: true); +``` + +注意:`EFCore` 还是会对 `NULL` 值列生成 `SQL` 语句。 + +也可以全局配置,在 `AppDbContext` 的派生类的构造函数中启用即可: + +```cs showLineNumbers {11} +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore; + +namespace Furion.EntityFramework.Core +{ + [AppDbContext("Sqlite3ConnectionString", DbProvider.Sqlite)] + public class DefaultDbContext : AppDbContext + { + public DefaultDbContext(DbContextOptions options) : base(options) + { + InsertOrUpdateIgnoreNullValues = true; + } + } +} +``` + +## 9.6.6 表带触发器异常解决 + +在某些情况下,数据库表存在触发器,这时候可能会出现下列异常: + +```txt showLineNumbers +Microsoft.EntityFrameworkCore.DbUpdateException: + Could not save changes because the target table has database triggers. + Please configure your entity type accordingly, + see https://aka.ms/efcore-docs-sqlserver-save-changes-and-triggers for more information. +``` + +这时我们只需要添加 `HasTrigger` 即可,如: + +```cs showLineNumbers {3} +public void Configure(EntityTypeBuilder entityBuilder, DbContext dbContext, Type dbContextLocator) +{ + entityBuilder.ToTable(tb => tb.HasTrigger("TriggerName")); // 标记数据库表存在触发器,触发器名称可随意 +} +``` + +相关 Issue [https://gitee.com/dotnetchina/Furion/issues/I5S4EC](https://gitee.com/dotnetchina/Furion/issues/I5S4EC) + +## 9.6.7 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/dbcontext-audit.mdx b/handbook/docs/dbcontext-audit.mdx new file mode 100644 index 0000000000000000000000000000000000000000..7c17b7ad78cab760c9d4baa60dfcc66750bd6030 --- /dev/null +++ b/handbook/docs/dbcontext-audit.mdx @@ -0,0 +1,239 @@ +--- +id: dbcontext-audit +title: 9.23 审计日志 +sidebar_label: 9.23 审计日志 (Audit) +--- + +## 9.23.1 审计日志 + +在一个企业应用系统中,用户对系统所有的操作包括请求、数据库操作等等都应该记录起来,那么这些日志我们称为操作日志,也可以说审计日志。 + +**通常来说,我们审计日志更多指的是数据库的操作记录**。 + +审计日志一般会记录以下三个操作: + +- `新增操作`:记录某某人在某某时间对哪个表新增了什么数据 +- `更新操作`:记录某某人在某某时间对哪个表的哪些数据做了更改,记录更改前的值和更改后的值 +- `删除操作`:记录某某人在某某时间对哪个表删除了什么数据 + +## 9.23.2 关于 `SaveChanges` 事件 + +`Furion` 框架中为所有 `AppDbContext` 子类都提供了三个可重写的方法,这三个方法分别由三个事件触发: + +- `提交更改之前 SavingChanges 事件`:触发 `void SavingChangesEvent(DbContextEventData eventData, InterceptionResult result)` 方法 +- `提交更改之后 SavedChanges 事件`:触发 `void SavedChangesEvent(SaveChangesCompletedEventData eventData, int result)` 方法 +- `提交更改失败 SaveChangesFailed 事件`:触发 `void SaveChangesFailedEvent(DbContextErrorEventData eventData)` 方法 + +通过这三个事件我们就可以捕获所有更改的实体然后保存到数据库审计日志中。 + +## 9.23.3 如何实现 + +### 9.23.3.1 数据库审计日志 + +我们只需要在 `AppDbContext` 子类中重写 `SavingChanges` 事件对应方法即可: + +```cs showLineNumbers {23} +using Furion.DatabaseAccessor; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using System; +using System.Linq; +using System.Security.AccessControl; + +namespace Furion.EntityFramework.Core +{ + [AppDbContext("Sqlite3ConnectionString")] + public class FurionDbContext : AppDbContext + { + public FurionDbContext(DbContextOptions options) : base(options) + { + } + + /// + /// 重写保存之前事件 + /// + /// + /// + protected override void SavingChangesEvent(DbContextEventData eventData, InterceptionResult result) + { + // 获取当前事件对应上下文 + var dbContext = eventData.Context; + + // 强制重新检查一边实体更改信息 + // dbContext.ChangeTracker.DetectChanges(); + + // 获取所有更改,删除,新增的实体,但排除审计实体(避免死循环) + var entities = dbContext.ChangeTracker.Entries() + .Where(u => u.Entity.GetType() != typeof(Audit) && (u.State == EntityState.Modified || u.State == EntityState.Deleted || u.State == EntityState.Added)) + .ToList(); + + // 通过请求中获取当前操作人 + var userId = App.GetService().HttpContext.Items["UserId"]; + + // 获取所有已更改的实体 + foreach (var entity in entities) + { + // 获取实体类型 + var entityType = entity.Entity.GetType(); + + // 获取所有实体有效属性,排除 [NotMapper] 属性 + var props = entity.OriginalValues.Properties; + + // 获取实体当前(现在)的值 + var currentValues = entity.CurrentValues; + + // 获取数据库中实体的值 + var databaseValues = entity.GetDatabaseValues(); + + // 遍历所有属性 + foreach (var prop in props) + { + // 获取属性名 + var propName = prop.Name; + + // 获取现在的实体值 + var newValue = currentValues[propName]; + + object oldValue = null; + // 如果是新增数据,则 databaseValues 为空,所以需要判断一下 + if (databaseValues != null) + { + oldValue = databaseValues[propName]; + } + + // 插入审计日志表,Audit 是你自定义的实体 + dbContext.Set().Add(new Audit + { + Table = entityType.Name, // 表名 + Column = propName, // 更新的列 + NewValue = newValue, // 新值 + OldValue = oldValue, // 旧值 + CreatedTime = DateTime.Now, // 操作时间 + UserId = userId, // 操作人 + Operate = entity.State.ToString() // 操作方式:新增、更新、删除 + }); + } + } + } + } +} +``` + +:::tip 小知识 + +如果对性能有所要求,那么建议审计日志通过 `日志组件` 写入数据库,如通过 `Nlog、Log4Net` 这些等: + +```cs showLineNumbers +// 插入审计日志表 +dbContext.Set().Add(new Audit +{ + Table = entityType.Name, // 表名 + Column = propName, // 更新的列 + newValue = newValue, // 新值 + OldValue = oldValue, // 旧值 + CreatedTime = DateTime.Now, // 操作时间 + UserId = userId, // 操作人 + Operate = entity.State.ToString() // 操作方式:新增、更新、删除 +}); +``` + +替换为: + +```cs showLineNumbers +logger.Information(JsonConvert.SerializeObject(new Audit +{ + Table = entityType.Name, // 表名 + Column = propName, // 更新的列 + newValue = newValue, // 新值 + OldValue = oldValue, // 旧值 + CreatedTime = DateTime.Now, // 操作时间 + UserId = userId, // 操作人 + Operate = entity.State.ToString() // 操作方式:新增、更新、删除 +})); +``` + +::: + +通过上面的例子,我们就可以对数据库所有的新增、更新、删除进行监控了。 + +### 9.23.3.2 执行 `sql` 审计日志 + +主要通过 `DbCommandInterceptor` 拦截实现,具体使用可查看 [数据库拦截器 - DbCommandInterceptor](./dbcontext-Interceptor#92422-dbcommandinterceptor),如: + +```cs showLineNumbers {11} +using Microsoft.EntityFrameworkCore.Diagnostics; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; + +namespace Furion.Web.Core +{ + /// + /// 执行 sql 审计 + /// + public sealed class SqlCommandAuditInterceptor : DbCommandInterceptor + { + public override InterceptionResult NonQueryExecuting(DbCommand command, CommandEventData eventData, InterceptionResult result) + { + // 获取执行的 sql 语句 + var sql = command.CommandText; + + // 获取执行的 sql 类型,是 sql 语句,还是存储过程,还是其他 + var type = command.CommandType; + + // 获取 sql 传递的命令参数 + var parameters = command.Parameters; + + // 写日志~~~~ + + return base.NonQueryExecuting(command, eventData, result); + } + + public override ValueTask> NonQueryExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default) + { + // 获取执行的 sql 语句 + var sql = command.CommandText; + + // 获取执行的 sql 类型,是 sql 语句,还是存储过程,还是其他 + var type = command.CommandType; + + // 获取 sql 传递的命令参数 + var parameters = command.Parameters; + + // 写日志~~~~ + + return base.NonQueryExecutingAsync(command, eventData, result, cancellationToken); + } + + // 其他 override + } +} +``` + +- 注册审计日志 + +只需要在注册数据库上下文中指定 `interceptors` 参数即可 + +```cs showLineNumbers +// services.AddDb 一样 +services.AddDbPool(interceptors: new IInterceptor[] { + new SqlCommandAuditInterceptor() +}); +``` + +### 9.23.3.3 请求审计日志 + +:::tip 关于请求审计日志 + +如需实现请求审计日志可查阅 【[5.4 请求审计日志章节](./audit.mdx)】 + +::: + +## 9.23.4 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/dbcontext-batch.mdx b/handbook/docs/dbcontext-batch.mdx new file mode 100644 index 0000000000000000000000000000000000000000..70a158cb0f0105581fc37947260a8694a69f27e5 --- /dev/null +++ b/handbook/docs/dbcontext-batch.mdx @@ -0,0 +1,164 @@ +--- +id: dbcontext-batch +title: 9.10 批量操作 +sidebar_label: 9.10 批量操作 +--- + +## 9.10.1 关于批量操作 + +`Furion` 框架中,默认只提供小数据(100 条 以内)批量数据操作,如果需要更大的数据批量处理,推荐使用第三方包 [Zack.EFCore.Batch](https://github.com/yangzhongke/Zack.EFCore.Batch),支持和 `Furion` 无缝衔接。 + +## 9.10.2 `Zack.EFCore.Batch` 使用 + +:::important `EFCore` 拦截器说明 + +使用该第三方拓展库可能不会触发 `EFCore` 的拦截器,如 `DbCommandInterceptor`,`SaveChangesInterceptor`。 + +::: + +### 9.10.2.1 安装对应的数据库 `NuGet` 包 + +- `MSSQL`:`Zack.EFCore.Batch.MSSQL` +- `MySql`:`Zack.EFCore.Batch.MySQL.Pomelo` +- `Npgsql`:`Zack.EFCore.Batch.Npgsql` +- `Oracle`:`Zack.EFCore.Batch.Oracle` +- `Sqlite`:`Zack.EFCore.Batch.Sqlite` + +### 9.10.2.2 注册并配置服务 + +```cs showLineNumbers {3,5} +services.AddDatabaseAccessor(options => +{ + options.AddDbPool(providerName: default, optionBuilder: (services, opt) => // 如果是 v3.7.11 之前,使用 opt => + { + opt.UseBatchEF_Sqlite(); // SQlite 数据库包 + }); +}); +``` + +### 9.10.2.3 基本使用 + +```cs showLineNumbers +// 批量更新 +await repository.Context.BatchUpdate() + .Set(b => b.Price, b => b.Price + 3) + .Set(b => b.Title, b => s) + .Set(b => b.AuthorName,b=>b.Title.Substring(3,2)+b.AuthorName.ToUpper()) + .Set(b => b.PubTime, b => DateTime.Now) + .Where(b => b.Id > n || b.AuthorName.StartsWith("Zack")) + .ExecuteAsync(); + +// 批量删除 +await repository.Context.DeleteRangeAsync(b => b.Price > n || b.AuthorName == "zack yang"); +``` + +## 9.10.3 `EFCore.BulkExtensions` 使用 + +通过 `NuGet` 安装 `EFCore.BulkExtensions` 包即可。 + +### 9.10.3.1 常见批量操作 + +```cs showLineNumbers +// 批量插入 +repository.Context.BulkInsert(entitiesList); +repository.Context.BulkInsertAsync(entitiesList); + +// 批量更新 +repository.Context.BulkUpdate(entitiesList); +repository.Context.BulkUpdateAsync(entitiesList); + +// 批量删除 +repository.Context.BulkDelete(entitiesList); +repository.Context.BulkDeleteAsync(entitiesList); + +// 批量插入或更新 +repository.Context.BulkInsertOrUpdate(entitiesList); +repository.Context.BulkInsertOrUpdateAsync(entitiesList); + +// 批量插入或更新或删除 +repository.Context.BulkInsertOrUpdateOrDelete(entitiesList); +repository.Context.BulkInsertOrUpdateOrDeleteAsync(entitiesList); + +// 批量读取多个实体 +repository.Context.BulkRead(entitiesList); +repository.Context.BulkReadAsync(entitiesList); + +// 批量清空表(慎用!!!!!) +repository.Context.Truncate(); +repository.Context.TruncateAsync(); +``` + +### 9.10.3.2 查询后批量操作 + +```cs showLineNumbers +// 根据条件批量删除 +repository.Where(a => a.ItemId > 500).BatchDelete(); +await repository.Where(a => a.ItemId > 500).BatchDeleteAsync(); + +// 根据条件批量更新 +repository.Where(a => a.ItemId <= 500).BatchUpdate(a => new Item { Quantity = a.Quantity + 100 }); +repository.Where(a => a.ItemId <= 500).BatchUpdate(new Item { Description = "Updated" }); +await repository.Where(a => a.ItemId <= 500).BatchUpdateAsync(new Item { Description = "Updated" }); + +// 批量更新指定列 +var updateColumns = new List { nameof(Item.Quantity) }; +var q = repository.Where(a => a.ItemId <= 500); +int affected = q.BatchUpdate(new Item { Description = "Updated" }, updateColumns); +``` + +### 9.10.3.3 批量操作性能 + +| Operations\Rows | 100,000 EF | 100,000 EFBulk | 1,000,000 EFBulk | +| ----------------- | -----------: | ---------------: | -----------------: | +| Insert | 38.98 s | 2.10 s | 17.99 s | +| Update | 109.25 s | 3.96 s | 31.45 s | +| Delete | 7.26 s | 2.04 s | 12.18 s | +| ----------------- | ------------ | ---------------- | ------------------ | +| **Together** | 70.70 s | 5.88 s | 56.84 s | + +## 9.10.4 `EFCore 7` 内置批量操作 + +:::caution 非微软支持数据库适配问题 + +如果使用的是非微软支持的数据库,如 `Oracle`,`MySQL`,`PostgreSQL` 等,可能适配 `EFCore 7` 不及时会出现找不到方法异常。 + +::: + +[https://docs.microsoft.com/zh-cn/ef/core/what-is-new/ef-core-7.0/whatsnew#executeupdate-and-executedelete-bulk-updates](https://docs.microsoft.com/zh-cn/ef/core/what-is-new/ef-core-7.0/whatsnew#executeupdate-and-executedelete-bulk-updates) + +```cs showLineNumbers {2,7} +// 批量删除 +await repository.Entities.ExecuteDeleteAsync(); +await repository.Entities.Where(t => t.Text.Contains(".NET")).ExecuteDeleteAsync(); +await repository.Entities.Where(t => t.Posts.All(e => e.PublishedOn.Year < 2022)).ExecuteDeleteAsync(); + +// 批量更新 +await repository.Entities.ExecuteUpdateAsync( + s => s.SetProperty(b => b.Name, b => b.Name + " *Featured!*")); + +await repository.Entities + .Where(p => p.PublishedOn.Year < 2022) + .ExecuteUpdateAsync(s => s + .SetProperty(b => b.Title, b => b.Title + " (" + b.PublishedOn.Year + ")") + .SetProperty(b => b.Content, b => b.Content + " ( This content was published in " + b.PublishedOn.Year + ")")); + +await repository.Entities + .Where(t => t.Posts.All(e => e.PublishedOn.Year < 2022)) + .ExecuteUpdateAsync(s => s.SetProperty(t => t.Text, t => t.Text + " (old)")); +``` + +## 9.10.5 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: + +--- + +:::note 了解更多 + +想了解更多 `EFCore.BulkExtensions` 知识可查阅 [EFCore.BulkExtensions 开源仓库](https://github.com/borisdj/EFCore.BulkExtensions)。 + +::: diff --git a/handbook/docs/dbcontext-code-first.mdx b/handbook/docs/dbcontext-code-first.mdx new file mode 100644 index 0000000000000000000000000000000000000000..a24f229068b2f12e54546f993d0183abf506ecef --- /dev/null +++ b/handbook/docs/dbcontext-code-first.mdx @@ -0,0 +1,344 @@ +--- +id: dbcontext-code-first +title: 9.21 模型生成数据库 +sidebar_label: 9.21 模型生成数据库 (Code First) +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +:::tip 开始之前 + +`Fur.Web.Entry` 层需要安装 `Microsoft.EntityFrameworkCore.Tools` 包。 + +::: + +## 9.21.1 数据库开发方式 + +`Furion` 提供两种主要方法来 **保持实体模型和数据库架构同步**。 + +至于我们应该选用哪个方法,请确定你是希望以实体模型为准还是以数据库为准: + +- 如果希望 **以实体模型为准**,请使用正向工程(Code First)。 对实体模型进行更改时,此方法会以增量方式将相应架构更改应用到数据库,以使数据库保持与实体模型兼容。 + +- 如果希望 **以数据库架构为准**,请使用反向工程(Database First)。 使用此方法,可通过将数据库架构反向工程到实体模型来生成相应的实体类型。 + +本章节是 **正向工程(Code First)** 的相关内容。 + +## 9.21.2 操作指南 + +:::important 特别注意 + +`Furion` 框架默认数据迁移的程序集为:`Furion.Database.Migrations`,所以如果您改了程序集名称或通过 `NuGet` 方式安装的 `Furion` 包,则需要配置迁移程序集名称: + +```cs showLineNumbers {4} +services.AddDatabaseAccessor(options => +{ + options.AddDbPool(DbProvider.Sqlite); +}, "存放迁移文件的项目名称"); +``` + +另外,如果应用中配置了多个数据库上下文,那么所有的 `迁移命令` 都需要指定 `-Context 数据库上下文名称` 参数。如: + +```shell showLineNumbers +Add-Migration v1.0.0 -Context FurionDbContext +``` + +::: + +### 9.21.2.1 创建实体模型 `Person` + +```cs showLineNumbers {1,6,8} +using Furion.DatabaseAccessor; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Furion.Core +{ + public class Person : Entity + { + /// + /// 构造函数 + /// + public Person() + { + CreatedTime = DateTime.Now; + IsDeleted = false; + } + + /// + /// 姓名 + /// + [MaxLength(32)] + public string Name { get; set; } + + /// + /// 年龄 + /// + public int Age { get; set; } + + /// + /// 住址 + /// + public string Address { get; set; } + } +} +``` + +:::important 实体约定 + +所有数据库实体必须直接或间接继承 `IEntity` 接口。 + +::: + +### 9.21.2.2 打开 `程序包管理控制台` + + + +### 9.21.2.3 切换默认项目 + +将 `程序包管理控制台` 默认项目设置为 `Furion.Database.Migrations` + + + +### 9.21.2.4 创建模型版本 + +```shell showLineNumbers +Add-Migration v1.0.0 +``` + +:::note 特别说明 + +**v1.0.0** 是此处数据库更改的版本号,可以写任何字符串,但推荐写版本号,每次 **+1**。 + +::: + +最终命令如下: + +```shell showLineNumbers +PM> Add-Migration v1.0.0 +Build started... +Build succeeded. +Microsoft.EntityFrameworkCore.Model.Validation[10400] + Sensitive data logging is enabled. Log entries and exception messages may include sensitive application data, this mode should only be enabled during development. +Microsoft.EntityFrameworkCore.Infrastructure[10403] + Entity Framework Core 5.0.0-rc.1.20451.13 initialized 'FurionDbContext' using provider 'Microsoft.EntityFrameworkCore.Sqlite' with options: SensitiveDataLoggingEnabled DetailedErrorsEnabled MaxPoolSize=100 MigrationsAssembly=Furion.Database.Migrations +To undo this action, use Remove-Migration. +PM> +``` + +生成成功后,`Furion.Database.Migrations` 项目下会新增 `Migrations` 文件夹(如果没有),同时本次的架构生成文件,如: + + + +### 9.21.2.5 更新到数据库 + +```shell showLineNumbers +Update-Database +``` + +执行该命令后,数据库就会自动根据模型生成对应的表。 + +:::tip 小知识 + +如果 `Update-Database` 后面带字符串参数,则会自动还原数据库到指定版本,如: + +```shell showLineNumbers +Update-Database v0.0.3 +``` + +将数据库还原到 `v0.0.3` 版本 + +::: + +## 9.21.3 更新模型 + +如果模型改变了,重复上面操作即可,如: + +```shell showLineNumbers +Add-Migration v1.0.1 +``` + +```shell showLineNumbers +Update-Database +``` + +## 9.21.4 导出 `Sql` + +有些时候,我们没有直接更新数据库的权限,或者怕出问题,我们都会先生成 `Sql` 看看,这时候只需要通过 `Script-Migration` 导出即可,如: + +```shell showLineNumbers +Script-Migration +``` + + + +## 9.21.5 `VS Code/Rider/任何IDE/操作系统` 方式 + +### 9.21.5.1 安装 `dotnet ef` + +```shell showLineNumbers +dotnet tool install --global dotnet-ef --version 5.0.0-rc.2.20475.6 +``` + +### 9.21.5.2 `cd` 目录 + +通过 `VS Code` 打开 `.sln` 所在的目录,如:`framework`。 + +之后进入 `Furion.Database.Migrations` 目录 + +```shell showLineNumbers +cd Furion.Database.Migrations +``` + +### 9.21.5.3 执行命令 + +```shell showLineNumbers +dotnet ef migrations add v1.0.0 -s "../Furion.Web.Entry" +``` + +```shell showLineNumbers +dotnet ef database update -s "../Furion.Web.Entry" +``` + +## 9.21.6 应用启动时自动生成数据库 + +`Furion` 框架建议大家使用命令方式操作数据库,**完全不推荐自动化生成数据库**,但是有些特殊情况下,有这个必要,故将此功能写出: + +### 9.21.6.1 对已经生成 `Migrations` 文件情况 + +如果已经生成 `Migrations` 文件,那么可以直接在 `Startup.cs` 代码中实现程序启动时自动执行 `update-database` 命令,如: + +```cs showLineNumbers {1,4,6,8-9} +public void Configure(IApplicationBuilder app, IHostingEnvironment env) +{ + // 判断开发环境!!!必须!!!! + if (env.IsDevelopment()) + { + Scoped.Create((_, scope) => + { + var context = scope.ServiceProvider.GetRequiredService(); + context.Database.Migrate(); + }); + } + + // 其他代码 +} +``` + +### 9.21.6.2 如果没有生成过 `Migrations` 文件情况 + +```cs showLineNumbers {1,4,6,8-9} +public void Configure(IApplicationBuilder app, IHostingEnvironment env) +{ + // 判断开发环境!!!必须!!!! + if (env.IsDevelopment()) + { + Scoped.Create((_, scope) => + { + var context = scope.ServiceProvider.GetRequiredService(); + context.Database.EnsureCreated(); + }); + } + + // 其他代码 +} +``` + +**如果需要在创建数据库之前先删除旧的,可先调用 `context.Database.EnsureDeleted();` 代码。慎重!!!!!!!!!!!!** + +## 9.21.7 `MySql.EntityFrameworkCore` 在 `.NET 6.0.8+` 问题 + +在 `.NET 6.0.8+` 版本,微软底层修改了 `IDesignTimeServices` 逻辑导致 `MySql.EntityFrameworkCore` 版本没有及时更新导致一下错误: + +```bash showLineNumbers {51} +PM> Add-Migration v0.0.1 +Build started... +Build succeeded. +Microsoft.EntityFrameworkCore.Model.Validation[10400] + Sensitive data logging is enabled. Log entries and exception messages may include sensitive application data; this mode should only be enabled during development. +Microsoft.EntityFrameworkCore.Infrastructure[10403] + Entity Framework Core 6.0.8 initialized 'DefaultDbContext' using provider 'MySql.EntityFrameworkCore:6.0.4+MySQL8.0.30' with options: SensitiveDataLoggingEnabled DetailedErrorsEnabled MaxPoolSize=100 MigrationsAssembly=Furion.TestMS.Database.Migrations +System.InvalidOperationException: Unable to resolve service for type 'Microsoft.EntityFrameworkCore.Storage.TypeMappingSourceDependencies' while attempting to activate 'MySql.EntityFrameworkCore.Storage.Internal.MySQLTypeMappingSource'. + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateArgumentCallSites(Type implementationType, CallSiteChain callSiteChain, ParameterInfo[] parameters, Boolean throwIfCallSiteNotFound) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateConstructorCallSite(ResultCache lifetime, Type serviceType, Type implementationType, CallSiteChain callSiteChain) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact(ServiceDescriptor descriptor, Type serviceType, CallSiteChain callSiteChain, Int32 slot) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateCallSite(Type serviceType, CallSiteChain callSiteChain) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.GetCallSite(Type serviceType, CallSiteChain callSiteChain) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateArgumentCallSites(Type implementationType, CallSiteChain callSiteChain, ParameterInfo[] parameters, Boolean throwIfCallSiteNotFound) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateConstructorCallSite(ResultCache lifetime, Type serviceType, Type implementationType, CallSiteChain callSiteChain) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact(ServiceDescriptor descriptor, Type serviceType, CallSiteChain callSiteChain, Int32 slot) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateCallSite(Type serviceType, CallSiteChain callSiteChain) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.GetCallSite(Type serviceType, CallSiteChain callSiteChain) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateArgumentCallSites(Type implementationType, CallSiteChain callSiteChain, ParameterInfo[] parameters, Boolean throwIfCallSiteNotFound) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateConstructorCallSite(ResultCache lifetime, Type serviceType, Type implementationType, CallSiteChain callSiteChain) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact(ServiceDescriptor descriptor, Type serviceType, CallSiteChain callSiteChain, Int32 slot) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateEnumerable(Type serviceType, CallSiteChain callSiteChain) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateCallSite(Type serviceType, CallSiteChain callSiteChain) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.GetCallSite(Type serviceType, CallSiteChain callSiteChain) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateArgumentCallSites(Type implementationType, CallSiteChain callSiteChain, ParameterInfo[] parameters, Boolean throwIfCallSiteNotFound) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateConstructorCallSite(ResultCache lifetime, Type serviceType, Type implementationType, CallSiteChain callSiteChain) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact(ServiceDescriptor descriptor, Type serviceType, CallSiteChain callSiteChain, Int32 slot) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateCallSite(Type serviceType, CallSiteChain callSiteChain) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.GetCallSite(Type serviceType, CallSiteChain callSiteChain) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateArgumentCallSites(Type implementationType, CallSiteChain callSiteChain, ParameterInfo[] parameters, Boolean throwIfCallSiteNotFound) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateConstructorCallSite(ResultCache lifetime, Type serviceType, Type implementationType, CallSiteChain callSiteChain) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact(ServiceDescriptor descriptor, Type serviceType, CallSiteChain callSiteChain, Int32 slot) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateCallSite(Type serviceType, CallSiteChain callSiteChain) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.GetCallSite(Type serviceType, CallSiteChain callSiteChain) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateArgumentCallSites(Type implementationType, CallSiteChain callSiteChain, ParameterInfo[] parameters, Boolean throwIfCallSiteNotFound) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateConstructorCallSite(ResultCache lifetime, Type serviceType, Type implementationType, CallSiteChain callSiteChain) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact(ServiceDescriptor descriptor, Type serviceType, CallSiteChain callSiteChain, Int32 slot) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateCallSite(Type serviceType, CallSiteChain callSiteChain) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.GetCallSite(Type serviceType, CallSiteChain callSiteChain) + at Microsoft.Extensions.DependencyInjection.ServiceProvider.CreateServiceAccessor(Type serviceType) + at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory) + at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope) + at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType) + at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType) + at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider) + at Microsoft.EntityFrameworkCore.Design.Internal.MigrationsOperations.AddMigration(String name, String outputDir, String contextType, String namespace) + at Microsoft.EntityFrameworkCore.Design.OperationExecutor.AddMigrationImpl(String name, String outputDir, String contextType, String namespace) + at Microsoft.EntityFrameworkCore.Design.OperationExecutor.AddMigration.<>c__DisplayClass0_0.<.ctor>b__0() + at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.<>c__DisplayClass3_0`1.b__0() + at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.Execute(Action action) +Unable to resolve service for type 'Microsoft.EntityFrameworkCore.Storage.TypeMappingSourceDependencies' while attempting to activate 'MySql.EntityFrameworkCore.Storage.Internal.MySQLTypeMappingSource'. +PM> +``` + +解决办法也很简单,只需要在启动层添加 `MysqlEntityFrameworkDesignTimeServices.cs` 并写入以下内容即可: + +```cs showLineNumbers {4,6} +using Microsoft.EntityFrameworkCore.Design; +using MySql.EntityFrameworkCore.Extensions; + +namespace YourProject.Web.Entry; + +public class MysqlEntityFrameworkDesignTimeServices : IDesignTimeServices +{ + public void ConfigureDesignTimeServices(IServiceCollection serviceCollection) + { + serviceCollection.AddEntityFrameworkMySQL(); + new EntityFrameworkRelationalDesignServicesBuilder(serviceCollection) + .TryAddCoreServices(); + } +} +``` + +相关 `Issue` 讨论:[https://gitee.com/dotnetchina/Furion/issues/I5O5ER](https://gitee.com/dotnetchina/Furion/issues/I5O5ER) + +## 9.21.8 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: + +--- + +:::note 了解更多 + +想了解更多 `正向工厂` 知识可查阅 [EF Core - 管理数据库架构](https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli) 章节。 + +::: diff --git a/handbook/docs/dbcontext-db-first.mdx b/handbook/docs/dbcontext-db-first.mdx new file mode 100644 index 0000000000000000000000000000000000000000..9965b8ea9700a11ce6cc6213f9c28155eef71445 --- /dev/null +++ b/handbook/docs/dbcontext-db-first.mdx @@ -0,0 +1,414 @@ +--- +id: dbcontext-db-first +title: 9.20 数据库生成模型 +sidebar_label: 9.20 数据库生成模型 (Db First) +--- + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **问题修复** + + -  修复 **`cli.ps1` 脚本不支持 `EFCore 7.0` 问题** 4.8.1.12 ⏱️2022.12.07 [!676](https://gitee.com/dotnetchina/Furion/pulls/676) + +
+
+
+ +import useBaseUrl from "@docusaurus/useBaseUrl"; + +:::tip 视频教程 + +[https://www.bilibili.com/video/BV1Rt4y1W7oJ](https://www.bilibili.com/video/BV1Rt4y1W7oJ) + +::: + +:::tip 关于脚本 + +在阅读下面文档之前,必须把 `Furion` 源码文件夹下的 `tools/cli.ps1` 文件拷贝到本地中(**不能直接复制源码自行创建,会有编码异常问题**)。而且 `Fur.Web.Entry` 层需要安装 `Microsoft.EntityFrameworkCore.Tools` 包。 + +::: + +:::important 系统提示禁止运行脚本解决 + +如果出现 `cli.ps1` 无法运行的情况,如提示:`“因为在此系统上禁止运行脚本”`,只需要打开系统管理员 `CMD/Powershell` 执行:`set-ExecutionPolicy RemoteSigned` 命令并根据操作提示输入 `A` 即可。 + +之后重启 `Visual Studio` 工具。 + +::: + +## 9.20.1 数据库开发方式 + +`Furion` 提供两种主要方法来 **保持实体模型和数据库架构同步**。 + +至于我们应该选用哪个方法,请确定你是希望以实体模型为准还是以数据库为准: + +- 如果希望 **以实体模型为准**,请使用正向工程(Code First)。 对实体模型进行更改时,此方法会以增量方式将相应架构更改应用到数据库,以使数据库保持与实体模型兼容。 + +- 如果希望 **以数据库架构为准**,请使用反向工程(Database First)。 使用此方法,可通过将数据库架构反向工程到实体模型来生成相应的实体类型。 + +本章节是 **反向工程(Database First)** 的相关内容。 + +## 9.20.2 操作指南 + +:::important 操作之前注意事项 + +目前 `Furion Tools` 生成工具**默认不支持任何数据库**生成,所以如需生成特定数据库的代码,只需要在 `Furion.EntityFrameworkCore.Core` 安装对应的数据库包即可: + +各个数据库的包可查阅:[多数据库操作-数据库提供器对应包](dbcontext-multi-database) + +另外,只有 `SqlServer` 数据库支持可视化 `GUI` 操作,其他的只能命令行操作。 + +::: + +### 9.20.2.1 打开 `程序包管理控制台` + +注意:开始之前先把 `Furion.Web.Entry` 设为启动项目。 + + + +### 9.20.2.2 切换默认项目 + +将 `程序包管理控制台` 默认项目设置为 `Furion.Core`,如果您是其他名字,则切换对应即可。 + + + +### 9.20.2.3 输入 `cli.ps1` 命令 + +```shell showLineNumbers +PM> Show-Command ../tools/cli.ps1 +``` + + + +:::important 全命令方式(推荐)❤️ + +除了采用 `Show-Command` 方式以外,还可以直接执行命令,如: + +```shell showLineNumbers +&"./tools/cli.ps1" -DbProvider "Microsoft.EntityFrameworkCore.SqlServer" -CoreProject "XXX.Core" -EntryProject "XXX.Web.Entry" -ConnectionName "Default" +``` + +::: + +:::note 小提示 + +如果使用的是 `SqlServer` 数据库,则默认不需要指定 `-DbProvider` 参数。 + +::: + +如果不清楚当前运行环境的路径,可以输入 `pwd` 查看。 + +:::important 关于数据库命名 + +如果需要保持和数据库一模一样的命名,则使用 `-UseDatabaseNames` 参数指定,如: + +```cs showLineNumbers + &"../tools/cli.ps1" -UseDatabaseNames +``` + +::: + + + +:::important 等待输入 + +执行上面命令后,此时 `Cli` 有一个等待输入提示: + +```shell showLineNumbers +Furion Tools v1.0.0 请键入操作类型:[G] 界面操作,[任意字符] 命令行操作 +Furion Tools v1.0.0 您的输入是: +``` + +**输入大写 `G` 进入界面操作模式,其他任意字符进入命令行操作模式。** + +::: + +:::caution 注意事项 + +目前只有 `Sql Server` 数据库才支持 `GUI 界面操作模式`,其他数据库请使用命令行模式。 + +::: + +## 9.20.3 界面操作模式 + +### 9.20.3.1 启动界面操作 + +当我们输入 `G` 时,将打开 `GUI` 界面操作模式,如: + + + +这时,`Furion Tools` 会自动查找所有数据库配置连接字符串的 `.json` 文件: + +:::important 注意事项 + +数据库连接字符串配置项需写到 `json` 配置文件中,且根节点需要写为 `ConnectionStrings` 。 + +::: + +```json showLineNumbers {12-13} +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "Microsoft.EntityFrameworkCore": "Information" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DbConnectionString": "Server=localhost;Database=Furion;User=sa;Password=000000;MultipleActiveResultSets=True;", + "Sqlite3ConnectionString": "Data Source=./Furion.db" + } +} +``` + +### 9.20.3.2 加载数据库表 + +选择连接字符串之后,点击 `加载数据库表和视图` 按钮加载。 + + + +### 9.20.3.3 选择表或视图生成 + +加载表完成后,可以选择您要生成的表或视图,**支持多选** + + + +点击底部按钮 `立即生成` + +### 9.20.3.4 选择保存目录 + +点击 `立即生成` 按钮后,会弹出实体保存选择目录资源管理器,默认实体只能保存在 `Furion.Core` 层: + + + +点击确定后就可完成所有生成操作。 + +### 9.20.3.5 生成最终实体代码 + + + +最终脚本如下: + +```shell showLineNumbers +PM> &"../tools/cli.ps1" +// ----------------------------------------------------------------------------- +// ______ _______ _ +// | ____| |__ __| | | +// | |__ _ _ _ __ | | ___ ___ | |___ +// | __| | | | '__| | |/ _ \ / _ \| / __| +// | | | |_| | | | | (_) | (_) | \__ \ +// |_| \__,_|_| |_|\___/ \___/|_|___/ +// +// ----------------------------------------------------------------------------- +Furion Tools v1.0.0 启动中...... +Furion Tools v1.0.0 启动成功! +Furion Tools v1.0.0 请键入操作类型:[G] 界面操作,[任意字符] 命令行操作 +Furion Tools v1.0.0 您的输入是: G +Furion Tools v1.0.0 正在加载数据库表和视图...... +Furion Tools v1.0.0 加载成功! +Furion Tools v1.0.0 正在编译解决方案代码...... +Build started... +Build succeeded. +For foreign key FK_PersonDetail_Person_PersonId on table dbo.PersonDetail, unable to model the end of the foreign key on principal table dbo.Person. This is usually because the principal table was not included in the selection set. +Furion Tools v1.0.0 编译成功! +Furion Tools v1.0.0 开始生成实体文件...... +Furion Tools v1.0.0 正在生成 City.cs 实体代码...... +Furion Tools v1.0.0 成功生成 City.cs 实体代码 +// ----------------------------------------------------------------------------- +// 以下代码由 Furion Tools v1.0.0 生成 +// ----------------------------------------------------------------------------- + +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using System; +using System.Collections.Generic; + +#nullable disable + +namespace Furion.Core +{ + public partial class City : IEntity, IEntityTypeBuilder + { + + public City() + { + InverseParent = new HashSet(); + } + + public int Id { get; set; } + public string Name { get; set; } + public int? ParentId { get; set; } + public DateTime CreatedTime { get; set; } + public DateTime? UpdatedTime { get; set; } + public bool IsDeleted { get; set; } + + public virtual City Parent { get; set; } + public virtual ICollection InverseParent { get; set; } + + public void Configure(EntityTypeBuilder entityBuilder, DbContext dbContext, Type dbContextLocator) + { + entityBuilder.HasIndex(e => e.ParentId, "IX_City_ParentId"); + + entityBuilder.HasOne(d => d.Parent) + .WithMany(p => p.InverseParent) + .HasForeignKey(d => d.ParentId); + + } + + } +} + +Furion Tools v1.0.0 正在生成 PersonDetail.cs 实体代码...... +Furion Tools v1.0.0 成功生成 PersonDetail.cs 实体代码 +// ----------------------------------------------------------------------------- +// 以下代码由 Furion Tools v1.0.0 生成 +// ----------------------------------------------------------------------------- + +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using System; +using System.Collections.Generic; + +#nullable disable + +namespace Furion.Core +{ + public partial class PersonDetail : IEntity, IEntityTypeBuilder + { + + public int Id { get; set; } + public string PhoneNumber { get; set; } + public string Qq { get; set; } + public int PersonId { get; set; } + + public void Configure(EntityTypeBuilder entityBuilder, DbContext dbContext, Type dbContextLocator) + { + entityBuilder.HasIndex(e => e.PersonId, "IX_PersonDetail_PersonId") + .IsUnique(); + + entityBuilder.Property(e => e.Qq).HasColumnName("QQ"); + + } + + } +} + +Furion Tools v1.0.0 正在生成 Post.cs 实体代码...... +Furion Tools v1.0.0 成功生成 Post.cs 实体代码 +// ----------------------------------------------------------------------------- +// 以下代码由 Furion Tools v1.0.0 生成 +// ----------------------------------------------------------------------------- + +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using System; +using System.Collections.Generic; + +#nullable disable + +namespace Furion.Core +{ + public partial class Post : IEntity + { + + public int Id { get; set; } + public string Name { get; set; } + public DateTime CreatedTime { get; set; } + public DateTime? UpdatedTime { get; set; } + public bool IsDeleted { get; set; } + + } +} + +Furion Tools v1.0.0 正在生成 VPerson.cs 实体代码...... +Furion Tools v1.0.0 成功生成 VPerson.cs 实体代码 +// ----------------------------------------------------------------------------- +// 以下代码由 Furion Tools v1.0.0 生成 +// ----------------------------------------------------------------------------- + +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using System; +using System.Collections.Generic; + +#nullable disable + +namespace Furion.Core +{ + public partial class VPerson : IEntity, IEntityTypeBuilder + { + + public int Id { get; set; } + public string Name { get; set; } + public int Age { get; set; } + public string Address { get; set; } + + public void Configure(EntityTypeBuilder entityBuilder, DbContext dbContext, Type dbContextLocator) + { + entityBuilder.HasNoKey(); + + entityBuilder.ToView("V_Person"); + + entityBuilder.Property(e => e.Id).ValueGeneratedOnAdd(); + + entityBuilder.Property(e => e.Name).HasMaxLength(32); + + } + + } +} + +Furion Tools v1.0.0 全部实体生成成功! +PM> +``` + +## 9.20.4 命令参数配置 + +`Furion Tools Cli` 支持多个参数配置,使用方法只需要在命令后面添加即可,如: + +```shell showLineNumbers + &"../tools/cli.ps1" -Context 数据库上下文名 -ConnectionName 连接字符串Key +``` + +支持参数如下: + +- `-Tables`:配置要生成的数据库表,数组类型,如果为空,则生成数据库所有表和视图。如:`-Tables Person,PersonDetails` +- `-Context`:配置数据库上下文,默认 `FurionDbContext`,如果有多个数据库上下文,则此参数必须配置 +- `-ConnectionName`:配置数据库连接字符串,对应 `appsetting.json` 中的 `ConnectionStrings` 定义的 `Key` +- `-OutputDir`:生成实体代码输出目录,默认为:`./Furion.Core/Entities/` +- `-DbProvider`:数据库提供器,默认是 `Microsoft.EntityFrameworkCore.SqlServer`,其他数据库请指定对应程序集 + - `SqlServer`:`Microsoft.EntityFrameworkCore.SqlServer` + - `Sqlite`:`Microsoft.EntityFrameworkCore.Sqlite` + - `Cosmos`:`Microsoft.EntityFrameworkCore.Cosmos` + - `InMemoryDatabase`:`Microsoft.EntityFrameworkCore.InMemory` + - `MySql`:`Pomelo.EntityFrameworkCore.MySql` 或 `MySql.EntityFrameworkCore` + - `PostgreSQL`:`Npgsql.EntityFrameworkCore.PostgreSQL` + - `Oracle`:`Oracle.EntityFrameworkCore` + - `Dm`:`Microsoft.EntityFrameworkCore.Dm` +- `-EntryProject`:Web 启用项目层名,默认 `Furion.Web.Entry` +- `-CoreProject`:实体项目层名,默认 `Furion.Core` +- `-DbContextLocators`:多数据库上下文定位器,默认 `MasterDbContextLocator`,支持多个,如:`MasterDbContextLocator,MySqlDbContextLocator` +- `-Product`:解决方案默认前缀,如 `Furion` +- `-UseDatabaseNames`:是否保持生成和数据库、表一致的名称 +- `-Namespace`:指定实体命名空间 + +## 9.20.5 常见问题 + +### 9.20.5.1 数据库未定义 `HasCharset` 或 `IsClustered` 类似问题 + +通常情况下,数据库包应安装在 `YourProject.EntityFramework.Core` 层,但如遇到出现 `未定义` 等类似上述问题可将数据库包安装在 `YourProject.Core` 层。 + +## 9.20.6 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/dbcontext-delete.mdx b/handbook/docs/dbcontext-delete.mdx new file mode 100644 index 0000000000000000000000000000000000000000..6c68b4972c6bf279d6c5cc33debe12e71ea3c9ee --- /dev/null +++ b/handbook/docs/dbcontext-delete.mdx @@ -0,0 +1,233 @@ +--- +id: dbcontext-delete +title: 9.9 删除操作 +sidebar_label: 9.9 删除操作 +--- + +:::warning 功能移除声明 + +以下内容包含 `Exists` 单词的在 `Furion 2.6.0 +` 版本中已移除。 + +::: + +## 9.9.1 删除一个实体(不立即提交) + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +repository.Delete(user); + +// 示例二 +user.Delete(); + +// 示例三 +repository.ChangeEntityState(user, EntityState.Deleted); + +// 示例四 +repository.Entities.Remove(user); + +// ==== 异步操作 ==== + +// 示例一 +await repository.DeleteAsync(user); + +// 示例二 +await user.DeleteAsync(); +``` + +## 9.9.2 删除一个实体(立即提交) + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +repository.DeleteNow(user); + +// 示例二 +user.DeleteNow(); + +// ==== 异步操作 ==== + +// 示例一 +await repository.DeleteNowAsync(user); + +// 示例二 +await user.DeleteNowAsync(); +``` + +## 9.9.3 根据主键删除记录(不立即提交) + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +repository.Delete(1); + +// 示例二 +user.Delete(1); + +// ==== 异步操作 ==== + +// 示例一 +await repository.DeleteAsync(1); + +// 示例二 +await user.DeleteAsync(1); +``` + +## 9.9.4 根据主键删除记录(立即提交) + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +repository.DeleteNow(1); + +// 示例二 +user.DeleteNow(1); + +// ==== 异步操作 ==== + +// 示例一 +await repository.DeleteNowAsync(1); + +// 示例二 +await user.DeleteNowAsync(1); +``` + +## 9.9.5 数据存在才根据主键删除(不立即提交) + +:::caution 功能移除声明 + +以下内容已在 `Furion 2.6.0 +` 版本移除。 + +::: + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +repository.DeleteExists(1); + +// 示例二 +user.DeleteExists(1); + +// ==== 异步操作 ==== + +// 示例一 +await repository.DeleteExistsAsync(1); + +// 示例二 +await user.DeleteExistsAsync(1); +``` + +## 9.9.6 数据存在才根据主键删除(立即提交) + +:::caution 功能移除声明 + +以下内容已在 `Furion 2.6.0 +` 版本移除。 + +::: + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +repository.DeleteExistsNow(1); + +// 示例二 +user.DeleteExistsNow(1); + +// ==== 异步操作 ==== + +// 示例一 +await repository.DeleteExistsNowAsync(1); + +// 示例二 +await user.DeleteExistsNowAsync(1); +``` + +:::tip 小知识 + +所有带 `Now` 结尾的表示立即提交到数据库,也就是立即调用 `SaveChanges` 或 `SaveChangesAsync`。 + +::: + +## 9.9.7 假删除/软删除 + +:::warning 功能移除声明 + +以下内容在 `Furion 2.10 +` 版本中已移除。 + +::: + +`Furion` 框架中可以通过 `[FakeDelete]` 特性标记假删除特性,如: + +### 9.9.7.1 添加 `[FakeDelete]` 标记 + +```cs showLineNumbers {3} +public class Model: IEntity +{ + [FakeDelete(true)] // 设置假删除的值 + public bool IsDeleted { get; set; } +} +``` + +:::note [FakeDelete] + +**[FakeDelete]** 属性拥有带一个参数的构造函数,这个参数是假删除的值 + +::: + +### 9.9.7.2 假删除使用 + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +repository.FakeDelete(entity); + +// 示例二 +repository.FakeDeleteNow(entity); + +// 示例三 +repository.FakeDelete(1); + +// 示例四 +repository.FakeDeleteNow(1); + +// 示例五 +entity.FakeDelete(); + +// 示例六 +repository.UpdateInclude(user, u => u.IsDeleted); + +// ==== 异步操作 ==== + +// 示例一 +await repository.FakeDeleteAsync(entity); + +// 示例二 +await repository.FakeDeleteNowAsync(entity); + +// 示例三 +await repository.FakeDeleteAsync(1); + +// 示例四 +await repository.FakeDeleteNowAsync(1); + +// 示例五 +await entity.FakeDeleteAsync(); + +// 示例六 +await repository.UpdateIncludeAsync(user, u => u.IsDeleted); +``` + +## 9.9.8 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/dbcontext-entitytrigger.mdx b/handbook/docs/dbcontext-entitytrigger.mdx new file mode 100644 index 0000000000000000000000000000000000000000..1813339ba16ec7da44eed37a0b7360948e39c00b --- /dev/null +++ b/handbook/docs/dbcontext-entitytrigger.mdx @@ -0,0 +1,163 @@ +--- +id: dbcontext-entitytrigger +title: 9.26 实体数据监听器 +sidebar_label: 9.26 实体数据监听器 +--- + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **问题修复** + + -  修复 `EFCore` 实体监听器 `IEntityChangedListener` 问题 4.8.1.7 ⏱️2022.11.26 [#I61CTI](https://gitee.com/dotnetchina/Furion/issues/I61CTI) + +
+
+
+ +## 9.26.1 实体数据监听器 + +在最新的 `Furion` 的 `1.1.6+` 版本中,新增了 `IEntityChangedListener` 实体数据监听接口,可以监听 `EFCore` 任何实体表 `增删改` 操作。 + +## 9.26.2 有何作用 + +- 类似数据库 `触发器` 功能,可实现 `增删改` 监听 +- 可以实现特殊操作,比如刷新缓存,记录日志等 + +## 9.26.3 如何使用 + +在 `Furion` 框架中,默认不启用实体数据监听器,如想启用,只需要在 `数据库上下文` 构造函数中启用即可: + +### 9.26.3.1 启用数据监听 + +```cs showLineNumbers {11} +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore; + +namespace Furion.EntityFramework.Core +{ + [AppDbContext("Sqlite3ConnectionString")] + public class DefaultDbContext : AppDbContext + { + public DefaultDbContext(DbContextOptions options) : base(options) + { + EnabledEntityChangedListener = true; + } + } +} +``` + +### 9.26.3.2 监听特定实体数据 + +:::caution 特别注意 + +如果实体没有直接或间接继承 `Entity` 或者 `EntityBase`,那么需要在实体中添加 `__State__` 非公开属性,如: + +```cs +internal EntityState __State__ { get; set; } +``` + +::: + +```cs showLineNumbers {9,30-41} +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; +using System; +using System.Collections.Generic; + +namespace Furion.Core +{ + public class Post : Entity, IEntityChangedListener + { + /// + /// 构造函数 + /// + public Post() + { + CreatedTime = DateTimeOffset.UtcNow; + IsDeleted = false; + } + + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// Person 集合 + /// + public ICollection Persons { get; set; } + + /// + /// 实体更改后触发 + /// + /// 新数据 + /// 旧数据 + /// 数据库上下文 + /// 数据库上下文定位器 + /// 实体状态 + public void OnChanged(Post entity, Post oldEntity, DbContext dbContext, Type dbContextLocator, EntityState state) + { + // 刷新缓存 + App.GetService().Set("Key", "Value"); + } + } +} +``` + +## 9.26.4 `IEntityChangedListener` 定义 + +```cs showLineNumbers {25} +/// +/// 实体数据改变监听依赖接口 +/// +/// +public interface IEntityChangedListener + where TEntity : class, IPrivateEntity, new() +{ + /// + /// 监听数据改变之前(仅支持EFCore操作) + /// + /// + /// + /// + /// + void OnChanging(TEntity entity, DbContext dbContext, Type dbContextLocator, EntityState state) { } + + /// + /// 监听数据改变之后(仅支持EFCore操作) + /// + /// 新值 + /// 旧值 + /// + /// + /// + void OnChanged(TEntity newEntity, TEntity oldEntity, DbContext dbContext, Type dbContextLocator, EntityState state); + + /// + /// 监听数据改变失败(仅支持EFCore操作) + /// + /// + /// + /// + /// + void OnChangeFailed(TEntity entity, DbContext dbContext, Type dbContextLocator, EntityState state) { } +} +``` + +## 9.26.5 `[SuppressChangedListener]` 跳过监听 + +默认情况下,`Furion` 框架会对所有新增、更新、编辑的实体进行监听,有些时候我们无需监听特定实体,只需要在实体上贴 `[SuppressChangedListener]` 特性即可。 + +## 9.26.6 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/dbcontext-filter.mdx b/handbook/docs/dbcontext-filter.mdx new file mode 100644 index 0000000000000000000000000000000000000000..05ecf28274e18de99c23d26bb71bc2bedf08cdf7 --- /dev/null +++ b/handbook/docs/dbcontext-filter.mdx @@ -0,0 +1,100 @@ +--- +id: dbcontext-filter +title: 9.24 实体/全局查询筛选器 +sidebar_label: 9.24 实体/全局查询筛选器 +--- + +## 9.24.1 查询筛选器 + +通常,我们系统中有一些维护字段,如 `IsDeleted` 字段,这个字段用来标识用户已经删除的数据,那么我们需要每次查询数据的时候带上这个字段,避免查询出不该出现的数据。 + +`Furion` 提供非常灵活方便的全局查询筛选器,能够应用到每一次查询中。 + +## 9.24.2 多种筛选器配置 + +### 9.24.2.1 单表筛选器 + +单表筛选器就是只针对特定实体进行筛选操作,使用简单,只需要在继承 `IEntityTypeBuilder` 接口并实现即可,如: + +```cs showLineNumbers {8,24} +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using System; + +namespace Furion.Core +{ + public class Person : Entity, IEntityTypeBuilder + { + public Person() + { + CreatedTime = DateTime.Now; + IsDeleted = false; + } + + public string Name { get; set; } + + public int Age { get; set; } + + public string Address { get; set; } + + public void Configure(EntityTypeBuilder entityBuilder, DbContext dbContext, Type dbContextLocator) + { + entityBuilder.HasQueryFilter(u => !u.IsDeleted); + } + } +} +``` + +### 9.24.2.2 全局筛选器 + +全局筛选器可以配置所有实体应用筛选器中,无需一个一个去配置。使用方法稍微有些复杂,需要动态构建 `Lambda` 表达式。 + +实现全局筛选器依赖于 `IModelBuilderFilter` 接口,该接口提供两个方法: + +- `OnCreating`:实体构建之前 +- `OnCreated`:实体构建之后 + +通过实现这两个方法即可配置全局过滤器,如: + +```cs showLineNumbers {10,18-19,21,24} +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using System; +using System.Linq.Expressions; + +namespace Furion.EntityFramework.Core +{ + [AppDbContext("Sqlite3ConnectionString")] + public class FurionDbContext : AppDbContext, IModelBuilderFilter + { + public FurionDbContext(DbContextOptions options) : base(options) + { + } + + public void OnCreating(ModelBuilder modelBuilder, EntityTypeBuilder entityBuilder, DbContext dbContext, Type dbContextLocator) + { + // 设置软删除表达式 + var fakeDeleteQueryFilterExpression = FakeDeleteQueryFilterExpression(entityBuilder, dbContext); + if (fakeDeleteQueryFilterExpression == null) return; + + entityBuilder.HasQueryFilter(fakeDeleteQueryFilterExpression); + } + } +} +``` + +:::note 小建议 + +如果对动态构建 `LambdaExpression` 不熟悉的朋友,可以使用 `System.Linq.Dynamic.Core` 包 [https://github.com/zzzprojects/System.Linq.Dynamic.Core](https://github.com/zzzprojects/System.Linq.Dynamic.Core) + +::: + +## 9.24.3 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/dbcontext-function.mdx b/handbook/docs/dbcontext-function.mdx new file mode 100644 index 0000000000000000000000000000000000000000..b209624a103940565511d0011299a8671fa1ff8b --- /dev/null +++ b/handbook/docs/dbcontext-function.mdx @@ -0,0 +1,327 @@ +--- +id: dbcontext-function +title: 9.15 函数操作 +sidebar_label: 9.15 函数操作 +--- + +:::important 温馨提示 + +推荐使用 《[9.18 Sql 高级代理](dbcontext-sql-proxy.mdx)》代替本章节功能。`Sql 高级代理` 能够提供更容易且更易维护的方式。 + +::: + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +## 9.15.1 数据库函数 + +引用百度百科: + +> 数据库函数是指当需要分析数据清单中的数值是否符合特定条件时,使用数据库工作表函数。 + +简单来说,数据库函数就是用于子计算的函数。其计算的结果可以用于构建 `sql` 语句。 + +### 9.15.1.1 支持标量函数的数据库 + +| SqlServer | Sqlite | Cosmos | InMemoryDatabase | MySql | PostgreSQL | Oracle | Firebird | Dm | +| --------- | ------ | ------ | ---------------- | ----- | ---------- | ------ | -------- | --- | +| ✔ | | ✔ | | ✔ | ✔ | ✔ | | | + +### 9.15.1.2 支持表值函数的数据库 + +| SqlServer | Sqlite | Cosmos | InMemoryDatabase | MySql | PostgreSQL | Oracle | Firebird | Dm | +| --------- | ------ | ------ | ---------------- | ----- | ---------- | ------ | -------- | --- | +| ✔ | | ✔ | | | ✔ | ✔ | | | + +## 9.15.2 数据库函数类型 + +在关系型数据库中,数据库函数有这两种类型: + +- `标量函数`:只能返回单个值 +- `表值函数`:只能返回一个结果集 + +## 9.15.3 函数的使用 + +### 9.15.3.1 标量函数返回 `object` + +```cs showLineNumbers +// ISqlRepository 方法 +var value = _sqlRepository.SqlFunctionScalar("func_GetValue"); + +// ISqlDispatchProxy 方式 +var value = _sqlExecuteProxy.GetValue(); // 推荐方式 + +// 实体仓储方式 +var value = _personRepository.SqlFunctionScalar("func_GetValue"); + +// IRepository 非泛型方式 +var value = _repository.Sql().SqlFunctionScalar("func_GetValue"); + +// 变态懒人方式,直接通过函数名执行 +var value = "func_GetValue".SqlFunctionScalar(); +``` + +:::note 关于异步 + +`Furion` 框架每一个数据库操作都支持异步方式,由于篇幅有限,就不列举异步方式了。 + +::: + +### 9.15.3.2 标量函数返回 `T` + +```cs showLineNumbers +// ISqlRepository 方法 +var value = _sqlRepository.SqlFunctionScalar("func_GetValue"); + +// ISqlDispatchProxy 方式 +var value = _sqlExecuteProxy.GetValue(); // 推荐方式 + +// 实体仓储方式 +var value = _personRepository.SqlFunctionScalar("func_GetValue"); + +// IRepository 非泛型方式 +var value = _repository.Sql().SqlFunctionScalar("func_GetValue"); + +// 变态懒人方式,直接通过函数名执行 +var value = "func_GetValue".SqlFunctionScalar(); +``` + +:::note 关于异步 + +`Furion` 框架每一个数据库操作都支持异步方式,由于篇幅有限,就不列举异步方式了。 + +::: + +### 9.15.3.3 表值函数返回 `DataTable` + +```cs showLineNumbers +// ISqlRepository 方法 +var value = _sqlRepository.SqlFunctionQuery("func_GetTable"); + +// ISqlDispatchProxy 方式 +var value = _sqlExecuteProxy.GetTable(); // 推荐方式 + +// 实体仓储方式 +var value = _personRepository.SqlFunctionQuery("func_GetTable"); + +// IRepository 非泛型方式 +var value = _repository.Sql().SqlFunctionQuery("func_GetTable"); + +// 变态懒人方式,直接通过函数名执行 +var value = "func_GetTable".SqlFunctionQuery(); +``` + +:::note 关于异步 + +`Furion` 框架每一个数据库操作都支持异步方式,由于篇幅有限,就不列举异步方式了。 + +::: + +### 9.15.3.4 表值函数返回 `List` + +```cs showLineNumbers +// ISqlRepository 方法 +var value = _sqlRepository.SqlFunctionQuery("func_GetTable"); + +// ISqlDispatchProxy 方式 +var value = _sqlExecuteProxy.GetTable(); // 推荐方式 + +// 实体仓储方式 +var value = _personRepository.SqlFunctionQuery("func_GetTable"); + +// IRepository 非泛型方式 +var value = _repository.Sql().SqlFunctionQuery("func_GetTable"); + +// 变态懒人方式,直接通过函数名执行 +var value = "func_GetTable".SqlFunctionQuery(); +``` + +:::note 关于异步 + +`Furion` 框架每一个数据库操作都支持异步方式,由于篇幅有限,就不列举异步方式了。 + +::: + +## 9.15.4 在 `Linq` 中使用 `标量函数` + +`Furion` 框架提供非常灵活的在 `Linq` 中使用标量函数的方法。如果像使用这样的方式,需要满足以下两个条件: + +- 标量函数必须定义在**公开静态类**中,且自己也是**公开静态方法** +- 该**公开静态方法**必须贴有 `[QueryableFunction]` 特性 + +示例如下: + +### 9.15.4.1 创建标量函数 + +```sql showLineNumbers +CREATE FUNCTION FN_GetId +( + @id INT +) +RETURNS INT +AS +BEGIN + RETURN @id + 1; +END; +``` + +### 9.15.4.2 创建静态类和静态方法 + +创建静态类,如 `QueryFunctions`,将该 `标量函数` 放在静态类中: + +```cs showLineNumbers {1, 7, 10-11} +using Furion.DatabaseAccessor; +using System; + +namespace Furion.Application +{ + // 必须是公开静态的 + public static class QueryFunctions + { + // 必须是静态方法 + [QueryableFunction("FN_GetId", "dbo")] // 配置标量函数 + public static int GetId(int id) => throw new NotSupportedException(); + } +} +``` + +### 9.15.4.3 在 `Linq` 中使用 + +```cs showLineNumbers +_personRepository.Where(u => u.Id > QueryFunctions.GetId(1)).ToList(); +``` + +```sql showLineNumbers +SELECT [p].[Id], [p].[Address], [p].[Age], [p].[CreatedTime], [p].[IsDeleted], [p].[Name], [p].[UpdatedTime] +FROM [Person] AS [p] +WHERE [p].[Id] > [dbo].[FN_GetId](1) // 💥 注意这里 +``` + + + +## 9.15.5 在 `Linq` 中使用 `表值函数` + +`EF Core 5.0` 版本支持在 `Linq` 中操作 `表值函数`,操作有点类似 `视图操作` + +示例如下: + +### 9.15.5.1 创建表值函数 + +```sql showLineNumbers +CREATE FUNCTION dbo.GetPersons +( + @id INT +) +RETURNS TABLE +AS +RETURN +( + SELECT Id, + Name, + Age, + Address + FROM dbo.Person + WHERE Id > @id +); +``` + +### 9.15.5.2 创建表值函数模型 + +```cs showLineNumbers +namespace Furion.Core +{ + public class F_Person + { + /// + /// 主键Id + /// + public int Id { get; set; } + + /// + /// 姓名 + /// + public string Name { get; set; } + + /// + /// 年龄 + /// + public int Age { get; set; } + + /// + /// 住址 + /// + public string Address { get; set; } + } +} +``` + +### 9.15.5.3 表值函数配置 + +在 `DbContext` 类中定义方法: + +```cs showLineNumbers {3,10,20-21} +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore; +using System.Linq; + +namespace Furion.EntityFramework.Core +{ + [AppDbContext("Sqlite3ConnectionString")] + public class FurionDbContext : AppDbContext + { + public IQueryable GetPersons(int id) => FromExpression(() => GetPersons(id)); + + public FurionDbContext(DbContextOptions options) : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(typeof(F_Person)).HasNoKey(); + modelBuilder.HasDbFunction(() => GetPersons(default)); + } + } +} +``` + +### 9.15.5.4 在 `Linq` 中使用 + +```cs showLineNumbers +IQueryable query = _repository.DynamicDbContext.GetPersons(1); +var result = query.Where(u => u.Name.Equals("Furion")).ToList(); +``` + +最终生成 `Sql` + +```sql showLineNumbers +SELECT [g].Id, [g].Name, [g].Age, [g].Address +FROM dbo.GetPersons(1) AS [g] +WHERE [g].Name == N'Furion'; +``` + +## 9.15.6 在 `EF Core` 内置函数 + +`EF Core` 为我们提供了很多常用的内置函数,可以在 `Lambda` 条件中使用,主要是通过 EF.Functions 调用,如: + +```cs showLineNumbers +_repository.Where(u => EF.Functions.DateDiffHour(u.CreatedDt, DateTime.Now) > 8).FirstOrDefault(); +``` + +这个语句使用了 EF.Functions.DateDiffHour 最终生成的 Sql 如下: + +```sql showLineNumbers +SELECT TOP(1) [a].* +FROM [dbo].[TEST] AS [a] +WHERE DATEDIFF(HOUR, [a].[CREATED_DT], GETDATE()) > 8 +``` + +`EF Core` 内置函数就不一一列出了,可以通过 `EF.Functions` 查看更多,如果不能满足自己的需求,那么可以自定义 `Linq` 标量函数 + +## 9.15.7 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/dbcontext-high-query.mdx b/handbook/docs/dbcontext-high-query.mdx new file mode 100644 index 0000000000000000000000000000000000000000..ff190828f4dffac53930acbc469274f6559d0436 --- /dev/null +++ b/handbook/docs/dbcontext-high-query.mdx @@ -0,0 +1,620 @@ +--- +id: dbcontext-hight-query +title: 9.12 高级查询操作 +sidebar_label: 9.12 高级查询操作 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +## 9.12.1 关联数据模型 + + + + +```cs showLineNumbers {38,43,48} +using Furion.DatabaseAccessor; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Furion.Core +{ + public class Person : Entity + { + /// + /// 构造函数 + /// + public Person() + { + CreatedTime = DateTime.Now; + } + + /// + /// 姓名 + /// + [MaxLength(32)] + public string Name { get; set; } + + /// + /// 年龄 + /// + public int Age { get; set; } + + /// + /// 住址 + /// + public string Address { get; set; } + + /// + /// 从表 + /// + public PersonDetail PersonDetail { get; set; } + + /// + /// 一对多 + /// + public ICollection Childrens { get; set; } + + /// + /// 多对多 + /// + public ICollection Posts { get; set; } + } +} +``` + + + + +```cs showLineNumbers {25} +using Furion.DatabaseAccessor; + +namespace Furion.Core +{ + public class PersonDetail : EntityBase + { + /// + /// 电话号码 + /// + public string PhoneNumber { get; set; } + + /// + /// QQ 号码 + /// + public string QQ { get; set; } + + /// + /// 外键 + /// + public int PersonId { get; set; } + + /// + /// 主表 + /// + public Person Person { get; set; } + } +} +``` + + + + +```cs showLineNumbers {35} +using Furion.DatabaseAccessor; +using System; + +namespace Furion.Core +{ + public class Children : Entity + { + /// + /// 构造函数 + /// + public Children() + { + CreatedTime = DateTime.Now; + + } + + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 性别 + /// + public Gender Gender { get; set; } + + /// + /// 外键 + /// + public int PersonId { get; set; } + + /// + /// 主表 + /// + public Person Person { get; set; } + } +} +``` + + + + + +```cs showLineNumbers {26} +using Furion.DatabaseAccessor; +using System; +using System.Collections.Generic; + +namespace Furion.Core +{ + public class Post : Entity + { + /// + /// 构造函数 + /// + public Post() + { + CreatedTime = DateTime.Now; + + } + + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// Person 集合 + /// + public ICollection Persons { get; set; } + } +} +``` + + + + +## 9.12.2 一对一查询 + +```cs showLineNumbers +// 示例一 +var person = repository.Include(u => u.Detail); + +// 示例二 +var person = repository.Include(u => u.Detail) + .Include(u => u.Post); + +// 示例三 +var person = repository.Include(u => u.Detail) + .ThenInclude(d => d.Review) + .Include(u => u.Post); + +// 示例四 +var person = repository.Include(u => u.Detail.Where(d => d.Id > 10).OrderBy(d => d.Name)) + .ThenInclude(d => d.Review) + .Include(u => u.Post); + +// 示例五 +var person = repository.Include(!string.IsNullOrEmpty(keyword), u => u.Detail); + +// 示例六 +var person = repository.Include(!string.IsNullOrEmpty(keyword), u => u.Detail) + .Include(age > 18, u => u.Detail.Where(d => d.Id > 10).OrderBy(d => d.Name)) + .ThenInclude(d => d.Review) + .Include(u => u.Post); +``` + +## 9.12.3 一对多查询 + +```cs showLineNumbers +// 示例一 +var person = repository.Include(u => u.Childrens); + +// 参考 一对一 例子 +``` + +:::important 特别说明 + +`一对一` 和 `一对多` 查询方法一样,唯一的区别是:`一对多` 采用 `ICollection` 定义属性。 + +::: + +## 9.12.4 多对多查询 + +```cs showLineNumbers +// 示例一 +var person = repository.Include(u => u.Posts); + +// 参考 一对一 例子 +``` + +:::important 特别说明 + +`一对一` 和 `多对多` 查询方法一样,唯一的区别是:`多对多` 采用 `ICollection` 定义属性。 + +::: + +## 9.12.5 联表查询 + +### 9.12.5.1 内连接 `Inner Join` + +```cs showLineNumbers +var query = from p in _personRepository.AsQueryable() + join d in _personDetailRepository.AsQueryable() on p.Id equals d.PersonId + select new PersonDto + { + PhoneNumber = p.PersonDetail.PhoneNumber, + Address = p.Address, + Age = p.Age, + Name = p.Name, + Id = p.Id, + QQ = p.PersonDetail.QQ + }; +``` + +### 9.12.5.2 左连接 `Left Join` + +```cs showLineNumbers {2,3} +var query = from p in _personRepository.AsQueryable() + join d in _personDetailRepository.AsQueryable() on p.Id equals d.PersonId into results + from d in results.DefaultIfEmpty() + select new PersonDto + { + PhoneNumber = p.PersonDetail.PhoneNumber, + Address = p.Address, + Age = p.Age, + Name = p.Name, + Id = p.Id, + QQ = p.PersonDetail.QQ + }; +``` + +:::note 小提示 + +**`Left Join`** 和 **`Inner Join`** 不同的是,**`Left Join`** 会先将结果 **`into`** 到新的结果集然后再查询,并调用 **`DefaultIfEmpty()`** 方法。 + +::: + +### 9.12.5.3 右连接 `Right Join` + +`Right Join` 只需要将 `Left Join` 主从表位置更换即可。 + +## 9.12.6 分组查询 + +```cs showLineNumbers +// 示例一 +var query = repository.AsQueryable().GroupBy(x => new { x.Column1, x.Column2 }); + +// 示例二 +var query = from student in repository.AsQueryable() + group student by repository2.AsQueryable() into dateGroup + select new ResultData() + { + Key = dateGroup.Key, + Value = dateGroup.Count() + }; + +// 示例三 +var query = from a in repository.AsQueryable() + join b in repository2.AsQueryable() on a.Id equals b.Aid + join c in repository3.AsQueryable() on c.id equals b.Bid + group a by new { a.Age, b.Sex } into g + select new { + Peo = g.Key, + Count = g.Count() + }; +``` + +## 9.12.7 合并结果集 + +```cs showLineNumbers +var query = repository.AsQueryable(u => u.Id > 10) + .Union( + repository2.AsQueryable(u => u.Id <= 10) + ); +``` + +## 9.12.8 查询排序 + +### 9.12.8.1 正序 + +```cs showLineNumbers +// 示例一 +var query = repository.AsQueryable() + .OrderBy(u => u.Id); + +// 示例二 +var query =repository.AsQueryable() + .OrderBy(u => u.Id) + .ThenBy(u => u.Name); +``` + +### 9.12.8.2 倒序 + +```cs showLineNumbers +// 示例一 +var query = repository.AsQueryable() + .OrderByDescending(u => u.Id); + +// 示例二 +var query =repository.AsQueryable() + .OrderByDescending(u => u.Id) + .ThenByDescending(u => u.Name); +``` + +### 9.12.8.3 混合倒序 + +```cs showLineNumbers +// 示例一 +var query = repository.AsQueryable() + .OrderBy(u => u.Id) + .OrderByDescending(u => u.Name) + .ThenBy(u => u.Age); +``` + +## 9.12.9 递归查询 + + + + +```cs showLineNumbers {12,36,41,49-55} +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using System; +using System.Collections.Generic; + +namespace Furion.Core +{ + /// + /// 城市 + /// + public class City : Entity, IEntityTypeBuilder, IEntitySeedData + { + /// + /// 构造函数 + /// + public City() + { + CreatedTime = DateTime.Now; + + } + + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 上级Id + /// + public int? ParentId { get; set; } + + /// + /// 上级 + /// + public virtual City Parent { get; set; } + + /// + /// 子集 + /// + public virtual ICollection Childrens { get; set; } + + /// + /// 配置实体关系 + /// + /// + /// + /// + public void Configure(EntityTypeBuilder entityBuilder, DbContext dbContext, Type dbContextLocator) + { + entityBuilder + .HasMany(x => x.Childrens) + .WithOne(x => x.Parent) + .HasForeignKey(x => x.ParentId) + .OnDelete(DeleteBehavior.ClientSetNull); // 必须设置这一行 + } + + /// + /// 种子数据 + /// + /// + /// + /// + public IEnumerable HasData(DbContext dbContext, Type dbContextLocator) + { + return new List + { + new City { Id=1,CreatedTime =DateTime.Parse("2020-08-20 15:30:20"),IsDeleted=false,Name="中国" }, + new City { Id=2,CreatedTime =DateTime.Parse("2020-08-20 15:30:20"),IsDeleted=false,Name="广东省",ParentId=1 }, + new City { Id=3,CreatedTime =DateTime.Parse("2020-08-20 15:30:20"),IsDeleted=false,Name="中山市",ParentId=2 }, + new City { Id=4,CreatedTime =DateTime.Parse("2020-08-20 15:30:20"),IsDeleted=false,Name="珠海市",ParentId=2 }, + new City { Id=5,CreatedTime =DateTime.Parse("2020-08-20 15:30:20"),IsDeleted=false,Name="浙江省",ParentId=1 }, + }; + } + } +} +``` + + + + +```cs showLineNumbers +using System.Collections.Generic; + +namespace Furion.Application.Persons +{ + public class CityDto + { + /// + /// 主键 + /// + public int Id { get; set; } + + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 子集 + /// + public ICollection Childrens { get; set; } + } +} +``` + + + + +```cs showLineNumbers +var cities = await repository.AsQueryable() + .Include(u => u.Childrens) + .Where(u => u.Id == 1) + .ToListAsync(); + +var dtos = cities.Adapt>(); +``` + +## 9.12.10 动态 `Sql` 查询 + +`Furion` 默认不支持 动态 `Sql` 查询功能,不过可以通过第三方实现: + +在 `Furion` 项目层安装 `System.Linq.Dynamic.Core` 包 [https://github.com/zzzprojects/System.Linq.Dynamic.Core](https://github.com/zzzprojects/System.Linq.Dynamic.Core) + +### 9.12.10.1 动态 `Sql` + +```cs showLineNumbers +// 示例一 +var query = repository.AsQueryable() + .Where("City == @0 and Orders.Count >= @1", "China", 10) + .OrderBy("CompanyName") + .Select("new(CompanyName as Name, Phone)"); + +// 示例二 +var list = repository.AsQueryable() + .Where("Name.Contains(@0)","Furion") + .ToList(); + +// 示例三,支持 ? 语法 +var customers = repository.AsQueryable() + .Include(c => c.Location) + .Where(c => c.Location?.Name == "test") // 注意 Location?.Name + .ToList(); +``` + +### 9.12.10.2 动态 `Lambda` + +```cs showLineNumbers +// 示例一 +var x = Expression.Parameter(typeof(int), "x"); +var y = Expression.Parameter(typeof(int), "y"); +var e = DynamicExpressionParser + .ParseLambda(new ParameterExpression[] { x, y }, null, "(x + y) * 2"); + +// 示例二 +var e = DynamicExpressionParser.ParseLambda( + typeof(Customer), typeof(bool), + "City = @0 and Orders.Count >= @1", + "London", 10); +``` + +## 9.12.11 时态查询 + +:::warning 功能移除声明 + +以下内容在 `Furion 2.13 +` 版本中已移除。 + +::: + +`Furion` 框架还提供了时态查询功能,可以查询特定时间的数据,如: + +```cs showLineNumbers +var result = rep.Entities + .AsTemporalOf(DateTime.UtcNow.AddDays(-1)) + .Include(i=> i.Company) + .FirstOrDefault(i => i.Name == "Furion"); +``` + +另外提供了多个时态查询方法 + +- AsTemporalAll() +- AsTemporalAsOf(date) +- AsTemporalFrom(startDate, endDate) +- AsTemporalBetween(startDate, endDate) +- AsTemporalContained(startDate, endDate) + +## 9.12.12 性能优化 + +默认情况下,`EF Core` 会跟踪所有实体,也就是任何数据改变都会引起数据检查,所以如果只做查询操作,建议关闭实体跟踪功能。 + +`Furion` 框架提供了以下高性能实体集合: + +- `DetachedEntities`:脱轨/不追踪实体 +- `AsQueryable(false)`:不追踪实体 +- `Entities.AsNoTracking()`:手动关闭实体追踪 + +在 `EF Core` 中,复杂查询总是会生成一个 `sql`,也就是 `AsSingleQuery()`,我们也可以设置为 `AsSplitQuery()` 切割成多个查询。 + +## 9.12.13 分表查询小例子 + +```cs showLineNumbers +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using System; +using System.Collections.Generic; + +namespace Furion.Core +{ + public class Person : Entity, IEntityTypeBuilder + { + public string Name { get; set; } + + /// + /// 配置实体关系 + /// + /// + /// + /// + public void Configure(EntityTypeBuilder entityBuilder, DbContext dbContext, Type dbContextLocator) + { + entityBuilder.ToSqlQuery( + @"select * from dbo.person.2020-09-19 + union all + select * from dbo.person.2020-09-20"); + } + } +} +``` + +```cs showLineNumbers +var posts = repository.Where(u => u.Id > 10).ToList(); +``` + +## 9.12.14 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/dbcontext-locator.mdx b/handbook/docs/dbcontext-locator.mdx new file mode 100644 index 0000000000000000000000000000000000000000..b12fd27a03bd4129d86ca22b952a8f5a9adb9e9f --- /dev/null +++ b/handbook/docs/dbcontext-locator.mdx @@ -0,0 +1,131 @@ +--- +id: dbcontext-locator +title: 9.3 数据库上下文定位器 +sidebar_label: 9.3 数据库上下文定位器 +--- + +:::important 小提醒 + +只要数据库上下文注册绑定了数据库上下文定位器,那么所有的**仓储、实体、种子、配置、视图、函数**等数据库相关的**类、接口、方法**都需要指定数据库上下文定位器,默认数据库上下文定位器除外。 + +如果改变了和数据库实体相关的所有配置接口的定位器,还需执行 `Add-Migration` 和 `Update-Database` 命令。 + +::: + +## 9.3.1 数据库上下文定位器 + +在了解数据库上下文定位器之前,我们先了解什么是 `定位器`,`定位器` 就是给物体安装特殊配置,使其能够被实时追踪和定位。 + +那为什么需要 `定位器`? + +由于 `EF Core` 本身支持多个数据库上下文操作,但是通过 `依赖注入` 的方式默认只初始化一个数据库上下文,也就是如果我们想要操作多个数据库上下文,那么 `构造函数` 注入方式就会变得复杂。 + +所以,`Furion` 实现了一套 `定位器` 功能,通过这个 `定位器` ,我们就能够通过 `依赖注入` 等多个方式定位到数据库上下文并初始化。 + +## 9.3.2 数据库上下文定位器作用 + +- 能够实现构造函数初始化多个数据库上下文 +- 能够避免业务层直接引用 `DbContext` +- 能够实现动态切换数据库、读写分离、主从库等复杂操作 + +## 9.3.3 如何定义数据库上下文定位器 + +定义数据库上下文定位器只需遵循三个原则即可: + +- 必须是公开 `class` 类型同时具备无参构造函数 +- 该类型必须继承 `IDbContextLocator` 接口 +- 数据库上下文定位器和数据库上下文必须是一对一关系,也就是不能同时被多个数据库上下文使用 + +数据库上下文定位器定义代码如下: + +```cs showLineNumbers {1,5} +using Furion.DatabaseAccessor; + +namespace Furion.Core +{ + public sealed class FurionDbContextLocator : IDbContextLocator + { + } +} +``` + +## 9.3.4 默认数据库上下文定位器 + +在 `Furion` 框架中已经提供了 `MasterDbContextLocator` 默认数据库上下文定位器,所以默认数据库上下文只需继承 `AppDbContext` 即可。 + +如:只有一个数据库上下文定义: + +```cs showLineNumbers +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore; + +namespace Furion.EntityFramework.Core +{ + [AppDbContext("Sqlite3ConnectionString", DbProvider.Sqlite)] + public class DefaultDbContext : AppDbContext // 无需指定定位器 + { + public DefaultDbContext(DbContextOptions options) : base(options) + { + } + } +} +``` + +:::important 关于多数据库定位器 + +默认数据库的定位器默认为 `MasterDbContextLocator`,所以无需显示指定定位器,**但从第二个数据库开始,都必须指定数据库定位器**。如: + +- 注册上下文: + +```cs showLineNumbers +options.AddDbPool(); +``` + +- 定义上下文 + +```cs showLineNumbers +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore; + +namespace Furion.EntityFramework.Core +{ + [AppDbContext("Sqlite3ConnectionString", DbProvider.Sqlite)] + public class OtherDbContext : AppDbContext // 需指定定位器 + { + public OtherDbContext(DbContextOptions options) : base(options) + { + } + } +} +``` + +另外,`Entity/IEntity` 和 `IRepository` 等都需要指定定位器,如:`IEntity`,`IRepository` 操作。 + +::: + +## 9.3.5 数据库上下文定位器支持对象 + +目前数据库上下文支持以下多个对象: + +- `AppDbContext`:数据上下文 +- `IRepository`:实体仓储 +- `ISqlRepository`: Sql 操作仓储 +- `IDbRepository`: 特定数据库操作仓储 +- `IMSRepository`: 读写分离仓储 +- `Func`:依赖注入获取数据库上下文 +- `Entity` :实体配置 +- `EntityBase`:实体配置 +- `EntityNotKey`:无键实体配置 +- `IEntity`:默认实体配置 +- `IEntitySeedData`:种子数据配置 +- `IEntityTypeBuilder`:实体类型构建器 +- `IModelBuilderFilter`:模型构建筛选器 +- `[QueryableFunction(DbContextLocators=Type[])]`:查询函数 + +## 9.3.6 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/dbcontext-multi-database.mdx b/handbook/docs/dbcontext-multi-database.mdx new file mode 100644 index 0000000000000000000000000000000000000000..77e6455798edcd478e80bdccdc2e44a300b98aef --- /dev/null +++ b/handbook/docs/dbcontext-multi-database.mdx @@ -0,0 +1,256 @@ +--- +id: dbcontext-multi-database +title: 9.19 多种数据库操作 +sidebar_label: 9.19 多种数据库操作 ✨ +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +:::warning 连接字符串配置注意事项 + +如果连接字符串是配置在自定义的 `.json` 文件中,那么必须在 `Visual Studio` 中配置 `.json` 右键属性,设置 `复制` 输出目录为 `如果较新则复制`,生成操作为 `内容`。 + +否则就会提示找不到配置或连接字符串的错误。 + +::: + +## 9.19.1 `Furion` 支持数据库提供器 + +| SqlServer | Sqlite | Cosmos | InMemoryDatabase | MySql | PostgreSQL | Oracle | Firebird | Dm | +| --------- | ------ | ------ | ---------------- | ----- | ---------- | ------ | -------- | --- | +| ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | + +### 9.19.1.1 数据库提供器对应包 + +- `SqlServer`:`Microsoft.EntityFrameworkCore.SqlServer` (支持 SqlServer 2005 +) +- `Sqlite`:`Microsoft.EntityFrameworkCore.Sqlite` +- `Cosmos`:`Microsoft.EntityFrameworkCore.Cosmos` +- `InMemoryDatabase`:`Microsoft.EntityFrameworkCore.InMemory` +- `MySql` + - `Pomelo.EntityFrameworkCore.MySql`:(支持 MySql 5.x +) + - `MySql.EntityFrameworkCore`:支持 (MySql 8.x +) +- `PostgreSQL`:`Npgsql.EntityFrameworkCore.PostgreSQL` +- `Oracle`:`Oracle.EntityFrameworkCore` (支持 Oracle 10 +) +- `Firebird`:`FirebirdSql.EntityFrameworkCore.Firebird` +- `Dm`:`Microsoft.EntityFrameworkCore.Dm` + +:::tip 小知识 + +这些数据库包应该安装在 `Furion.EntityFramework.Core` 层。特殊情况需安装在 `Furion.Core` 层中,如 `Mysql` `HasCharset()` 配置。 + +::: + +## 9.19.2 多数据库服务注册 + +```cs showLineNumbers +// 注册 SqlServer +options.AddDbPool(DbProvider.SqlServer); +options.AddDbPool($"{DbProvider.SqlServer}@2005"); // 支持 2005 数据库 +options.AddDbPool($"{DbProvider.SqlServer}@2008"); // 支持 2008 数据库 + +// 注册 Sqlite +options.AddDbPool(DbProvider.Sqlite); + +// 注册 Cosmos +options.AddDbPool(DbProvider.Cosmos); + +// 注册 InMemoryDatabase +options.AddDbPool(DbProvider.InMemoryDatabase); + +// 注册 MySql +options.AddDbPool(DbProvider.MySql); +options.AddDbPool($"{DbProvider.MySql}@8.0.22"); // 可以指定版本号 +options.AddDbPool(DbProvider.MySqlOfficial); // 使用 MySql 官方包(MySql.EntityFrameworkCore) + +// 注册 PostgreSQL +options.AddDbPool(DbProvider.Npgsql); + +// 注册 Oracle +options.AddDbPool(DbProvider.Oracle); +options.AddDbPool($"{DbProvider.Oracle}@11"); // 支持 Oracle 11 版本 + +// 注册 Firebird +options.AddDbPool(DbProvider.Firebird); + +// 注册 Dm +options.AddDbPool(DbProvider.Dm); +``` + +:::caution 新版 SqlServer/MySQL/Oracle 注意 + +`SqlServer` 兼容 `2005-2008` 写法: + +```cs showLineNumbers +services.AddDatabaseAccessor(options => +{ + options.AddDbPool($"{DbProvider.SqlServer}@2005"); // 支持 2005 数据库 + options.AddDbPool($"{DbProvider.SqlServer}@2008"); // 支持 2008 数据库 +}); +``` + +`MySQL` 兼容旧版本(带版本号)写法: + +```cs showLineNumbers +services.AddDatabaseAccessor(options => +{ + options.AddDbPool($"{DbProvider.MySql}@8.0.22"); +}); +``` + +如果使用了 `MySql.EntityFrameworkCore` 包,则需改为以下注册: + +```cs showLineNumbers +services.AddDatabaseAccessor(options => +{ + options.AddDbPool(DbProvider.MySqlOfficial); +}); +``` + +`Oracle` 兼容 `11` 版本 + +```cs showLineNumbers {3-4,6-10} +services.AddDatabaseAccessor(options => +{ + // 正常这样配置即可 + options.AddDbPool($"{DbProvider.Oracle}@11"); + + // 如果依然出现 ORA-00933: SQL 错误,可以尝试以下配置: + options.AddDbPool($"{DbProvider.Oracle}@11", optionBuilder: (ses, opt) => + { + opt.UseOracle(b => b.UseOracleSQLCompatibility("11")); + }); +}); +``` + +::: + +## 9.19.3 各类数据库连接字符串配置示例 + +- `Sqlite`:`Data Source=./Furion.db` +- `MySql`:`Data Source=localhost;Database=Furion;User ID=root;Password=000000;pooling=true;port=3306;sslmode=none;CharSet=utf8;` +- `SqlServer`:`Server=localhost;Database=Furion;User=sa;Password=000000;MultipleActiveResultSets=True;Encrypt=True;TrustServerCertificate=True;` +- `Oracle`:`User Id=orcl;Password=orcl;Data Source=(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=127.0.0.1)(PORT=1521)))(CONNECT_DATA=(SERVICE_NAME=orcl)))` +- `PostgreSQL`:`PORT=5432;DATABASE=postgres;HOST=127.0.0.1;PASSWORD=postgres;USER ID=postgres;` + +## 9.19.4 多数据库使用方式 + +`Furion` 通过独创的 `数据库上下文定位器` 实现多数据库灵活操作切换。只需要为每一种数据库绑定唯一的数据库上下文定位器即可。 + +以下是 `Furion` 支持多数据库操作的实例: + +### 9.19.4.1 实体仓储方式 + +```cs showLineNumbers +// 切换到 MSSQL 操作 Person表 +var mssqlRepository = personRepository.Change(); + +// 切换到 MySql 操作 Person表 +var mysqlRepository = personRepository.Change(); + +// 切换到 Sqlite 操作 Person表 +var sqliteRepository = personRepository.Change(); + +``` + +### 9.19.4.2 非泛型仓储方式 + +```cs showLineNumbers +// 切换到 MSSQL 操作 Person表 +var mssqlRepository = repository.Change(); + +// 切换到 MySql 操作 Person表 +var mysqlRepository = repository.Change(); + +// 切换到 Sqlite 操作 Person表 +var sqliteRepository = repository.Change(); +``` + +### 9.19.4.3 `Sql` 仓储方式 + +```cs showLineNumbers +// 切换到 MSSQL 操作 Person表 +var mssqlRepository = sqlRepository.Change(); + +// 切换到 MySql 操作 Person表 +var mysqlRepository = sqlRepository.Change(); + +// 切换到 Sqlite 操作 Person表 +var sqliteRepository = sqlRepository.Change(); +``` + +### 9.19.4.4 实体定义方式 + +```cs showLineNumbers + +// 支持一个数据库 +public class Person: IEntity +{ + // .... +} + +// 支持多个数据库 +public class Person: IEntity +{ + // .... +} +``` + +:::tip 小知识 + +所有的 `实体依赖接口或抽象类` 都支持泛型方式 指定 数据库上下文定位器,最多支持 `8` 个。 + +::: + +### 9.19.4.5 Linq 函数方式 + +```cs showLineNumbers +public static class QueryFunctions +{ + [QueryableFunction("FN_GetId", "dbo", typeof(MySqlDbContextLocator), typeof(SqliteDbContextLocator))] + public static int GetId(int id) => throw new NotSupportedException(); +} +``` + +## 9.19.5 `SqlServer` 低版本支持动态配置 + +正常情况下,只需要在注册的时候指定 `@2008` 数据库即可,有些时候我们可能需要在 `DbContext` 的 `OnConfigure` 中配置,这个时候就需要添加以下代码: + +```cs showLineNumbers {4} +protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) +{ + optionsBuilder.UseSqlServer(DbProvider.GetConnectionString()) + .ReplaceService(); + base.OnConfiguring(optionsBuilder); +} +``` + +:::note 小知识 + +`DbProvider.GetConnectionString()` 是获取对应上下文配置的数据库链接字符串。 + +::: + +## 9.19.6 `MySql` 时区问题/少 8 小时问题 + +可查阅相关 `Issue`:[https://gitee.com/dotnetchina/Furion/issues/I3RSCO](https://gitee.com/dotnetchina/Furion/issues/I3RSCO) + +## 9.19.7 `snake_case` 风格表名和字段 + +默认情况下 `EFCore` 将使用和模型定义一致的方式定义表和字段,但可以通过 [https://github.com/efcore/EFCore.NamingConventions](https://github.com/efcore/EFCore.NamingConventions) 拓展插件改变此行为,如下图所示: + +```cs showLineNumbers {2} + builder.UseNpgsql() + .UseLowerCaseNamingConvention(); +``` + +其他资料:[https://www.npgsql.org/efcore/modeling/table-column-naming.html](https://www.npgsql.org/efcore/modeling/table-column-naming.html) + +## 9.19.8 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/dbcontext-proc.mdx b/handbook/docs/dbcontext-proc.mdx new file mode 100644 index 0000000000000000000000000000000000000000..f4fd728ddca19ac2222fe06fbd0522693d6a374b --- /dev/null +++ b/handbook/docs/dbcontext-proc.mdx @@ -0,0 +1,585 @@ +--- +id: dbcontext-proc +title: 9.14 存储过程操作 +sidebar_label: 9.14 存储过程操作 +--- + +:::important 温馨提示 + +推荐使用 《[9.18 Sql 高级代理](dbcontext-sql-proxy.mdx)》代替本章节功能。`Sql 高级代理` 能够提供更容易且更易维护的方式。 + +::: + +import useBaseUrl from "@docusaurus/useBaseUrl"; +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +## 9.14.1 关于存储过程 + +引用百度百科: + +> 存储过程(Stored Procedure)是在大型数据库系统中,一组为了完成特定功能的 SQL 语句集,它存储在数据库中,一次编译后永久有效,用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。 +> +> 存储过程是数据库中的一个重要对象。在数据量特别庞大的情况下利用存储过程能达到倍速的效率提升。 + +简单来说,存储过程就是关系型数据库中(`Sqlite` 除外)中编写逻辑的函数/方法,通过这种方式,可以将 `sql` 编译缓存起来,大大提高存储过程的执行效率。 + +**这里不讨论存储过程的优缺点。** + +### 9.14.2.0 支持存储过程的数据库 + +| SqlServer | Sqlite | Cosmos | InMemoryDatabase | MySql | PostgreSQL | Oracle | Firebird | Dm | +| --------- | ------ | ------ | ---------------- | ----- | ---------- | ------ | -------- | --- | +| ✔ | | ✔ | | ✔ | ✔ | ✔ | ✔ | ✔ | + +## 9.14.2 存储过程使用 + +`Furion` 框架中提供了多种存储过程操作方式: + +- `ISqlRepository`:`Sql` 操作仓储,可通过 `依赖注入` +- `ISqlDispatchProxy`:`Sql` 代理方式(高级用法,推荐) +- 通过任意实体仓储操作:`personRepository.SqlProcedureQuery(procName)` +- 通过字符串拓展方法:`procName.SqlProcedureQuery()` +- 通过 `repository.Sql().SqlProcedureQuery()` 方式 + +### 9.14.2.1 初始化方式 + + + + +```cs showLineNumbers {1,9-12} +using Furion.DatabaseAccessor; +using Furion.DynamicApiController; + +namespace Furion.Application +{ + [DynamicApiController] + public class SqlService + { + private ISqlRepository _sqlRepository; + public SqlService(ISqlRepository sqlRepository) + { + _sqlRepository = sqlRepository; + } + } +} +``` + + + + +```cs showLineNumbers {2,7,9-10} +using Furion.Core; +using Furion.DatabaseAccessor; +using System.Collections.Generic; + +namespace Furion.Application +{ + public interface ISqlExecuteProxy : ISqlDispatchProxy + { + [SqlProcedure("proc_GetPersons")] + List GetPersons(string keyword); + } +} +``` + +```cs showLineNumbers {9-12} +using Furion.DatabaseAccessor; +using Furion.DynamicApiController; + +namespace Furion.Application +{ + [DynamicApiController] + public class SqlService + { + private ISqlExecuteProxy _sqlExecuteProxy; + public SqlService(ISqlExecuteProxy sqlExecuteProxy) + { + _sqlExecuteProxy = sqlExecuteProxy; + } + } +} +``` + + + + +```cs showLineNumbers +var persons = personRepository.SqlProcedureQuery("proc_GetPersons"); +``` + + + + +```cs showLineNumbers +var persons = "proc_GetPersons".SqlProcedureQuery(); +``` + + + + + +### 9.14.2.2 返回 `DataTable` + +```cs showLineNumbers +// ISqlRepository 方法 +var dataTable = _sqlRepository.SqlProcedureQuery("proc_GetPersons"); + +// ISqlDispatchProxy 方式 +var dataTable = _sqlExecuteProxy.GetPersons(); // 推荐方式 + +// 实体仓储方式 +var dataTable = _personRepository.SqlProcedureQuery("proc_GetPersons"); + +// IRepository 非泛型方式 +var dataTable = _repository.Sql().SqlProcedureQuery("proc_GetPersons"); + +// 变态懒人方式,直接通过存储过程名执行 +var dataTable = "proc_GetPersons".SqlProcedureQuery(); +``` + +:::note 关于异步 + +`Furion` 框架每一个数据库操作都支持异步方式,由于篇幅有限,就不列举异步方式了。 + +::: + +### 9.14.2.3 返回 `DataSet` + +```cs showLineNumbers +// ISqlRepository 方法 +var dataSet = _sqlRepository.SqlProcedureQueries("proc_GetPersons"); + +// ISqlDispatchProxy 方式 +var dataSet = _sqlExecuteProxy.GetPersons(); // 推荐方式 + +// 实体仓储方式 +var dataSet = _personRepository.SqlProcedureQueries("proc_GetPersons"); + +// IRepository 非泛型方式 +var dataSet = _repository.Sql().SqlProcedureQueries("proc_GetPersons"); + +// 变态懒人方式,直接通过存储过程名执行 +var dataSet = "proc_GetPersons".SqlProcedureQueries(); +``` + +:::note 关于异步 + +`Furion` 框架每一个数据库操作都支持异步方式,由于篇幅有限,就不列举异步方式了。 + +::: + +### 9.14.2.4 返回 `List` + +```cs showLineNumbers +// ISqlRepository 方法 +var list = _sqlRepository.SqlProcedureQuery("proc_GetPersons"); + +// ISqlDispatchProxy 方式 +var list = _sqlExecuteProxy.GetPersons(); // 推荐方式 + +// 实体仓储方式 +var list = _personRepository.SqlProcedureQuery("proc_GetPersons"); + +// IRepository 非泛型方式 +var list = _repository.Sql().SqlProcedureQuery("proc_GetPersons"); + +// 变态懒人方式,直接通过存储过程名执行 +var list = "proc_GetPersons".SqlProcedureQuery(); +``` + +:::note 关于异步 + +`Furion` 框架每一个数据库操作都支持异步方式,由于篇幅有限,就不列举异步方式了。 + +::: + +### 9.14.2.5 返回 `Tuple` + +`Furion` 框架大大利用了 `Tuple` 的特性,将返回多个结果集转成 `Tuple` 类型 + +```cs showLineNumbers +// ISqlRepository 方法 + +// 返回一个结果集 +var list1 = _sqlRepository.SqlProcedureQueries("proc_GetPersons"); + +// 返回两个结果集 +var (list1, list2) = _sqlRepository.SqlProcedureQueries("proc_GetPersons"); + +// 返回三个结果集 +var (list1, list2, list3) = _sqlRepository.SqlProcedureQueries("proc_GetPersons"); + +// 返回四个结果集 +var (list1, list2, list3, list4) = _sqlRepository.SqlProcedureQueries("proc_GetPersons"); + +// 返回五个结果集 +var (list1, list2, list3, list4, list5) = _sqlRepository.SqlProcedureQueries("proc_GetPersons"); + +// 返回六个结果集 +var (list1, list2, list3, list4, list5, list6) = _sqlRepository.SqlProcedureQueries("proc_GetPersons"); + +// 返回七个结果集 +var (list1, list2, list3, list4, list5, list6, list7) = _sqlRepository.SqlProcedureQueries("proc_GetPersons"); + +// 返回八个结果集 +var (list1, list2, list3, list4, list5, list6, list7, list8) = _sqlRepository.SqlProcedureQueries("proc_GetPersons"); + +// ================================== + +// ISqlDispatchProxy 方式,推荐方式,自动处理多个结果集 + +// 返回一个结果集 +var list1 = _sqlRepository.GetDatas(); + +// 返回两个结果集 +var (list1, list2) = _sqlRepository.GetDatas(); + +// 返回三个结果集 +var (list1, list2, list3) = _sqlRepository.GetDatas(); + +// 返回四个结果集 +var (list1, list2, list3, list4) = _sqlRepository.GetDatas(); + +// 返回五个结果集 +var (list1, list2, list3, list4, list5) = _sqlRepository.GetDatas(); + +// 返回六个结果集 +var (list1, list2, list3, list4, list5, list6) = _sqlRepository.GetDatas(); + +// 返回七个结果集 +var (list1, list2, list3, list4, list5, list6, list7) = _sqlRepository.GetDatas(); + +// 返回八个结果集 +var (list1, list2, list3, list4, list5, list6, list7, list8) = _sqlRepository.GetDatas(); + +// ================================== + +// 实体仓储方式 + +// 返回一个结果集 +var list1 = _personRepository.SqlProcedureQueries("proc_GetPersons"); + +// 返回两个结果集 +var (list1, list2) = _personRepository.SqlProcedureQueries("proc_GetPersons"); + +// 返回三个结果集 +var (list1, list2, list3) = _personRepository.SqlProcedureQueries("proc_GetPersons"); + +// 返回四个结果集 +var (list1, list2, list3, list4) = _personRepository.SqlProcedureQueries("proc_GetPersons"); + +// 返回五个结果集 +var (list1, list2, list3, list4, list5) = _personRepository.SqlProcedureQueries("proc_GetPersons"); + +// 返回六个结果集 +var (list1, list2, list3, list4, list5, list6) = _personRepository.SqlProcedureQueries("proc_GetPersons"); + +// 返回七个结果集 +var (list1, list2, list3, list4, list5, list6, list7) = _personRepository.SqlProcedureQueries("proc_GetPersons"); + +// 返回八个结果集 +var (list1, list2, list3, list4, list5, list6, list7, list8) = _personRepository.SqlProcedureQueries("proc_GetPersons"); + +// ================================== + +// IRepository 非泛型方式 + +// 返回一个结果集 +var list1 = _repository.Sql().SqlProcedureQueries("proc_GetPersons"); + +// 返回两个结果集 +var (list1, list2) = _repository.Sql().SqlProcedureQueries("proc_GetPersons"); + +// 返回三个结果集 +var (list1, list2, list3) = _repository.Sql().SqlProcedureQueries("proc_GetPersons"); + +// 返回四个结果集 +var (list1, list2, list3, list4) = _repository.Sql().SqlProcedureQueries("proc_GetPersons"); + +// 返回五个结果集 +var (list1, list2, list3, list4, list5) = _repository.Sql().SqlProcedureQueries("proc_GetPersons"); + +// 返回六个结果集 +var (list1, list2, list3, list4, list5, list6) = _repository.Sql().SqlProcedureQueries("proc_GetPersons"); + +// 返回七个结果集 +var (list1, list2, list3, list4, list5, list6, list7) = _repository.Sql().SqlProcedureQueries("proc_GetPersons"); + +// 返回八个结果集 +var (list1, list2, list3, list4, list5, list6, list7, list8) = _repository.Sql().SqlProcedureQueries("proc_GetPersons"); + +// ================================== + +// 变态懒人方式,直接通过存储过程名执行 + +// 返回一个结果集 +var list1 = "proc_GetPersons".SqlProcedureQueries(); + +// 返回两个结果集 +var (list1, list2) = "proc_GetPersons".SqlProcedureQueries(); + +// 返回三个结果集 +var (list1, list2, list3) = "proc_GetPersons".SqlProcedureQueries(); + +// 返回四个结果集 +var (list1, list2, list3, list4) = "proc_GetPersons".SqlProcedureQueries(); + +// 返回五个结果集 +var (list1, list2, list3, list4, list5) = "proc_GetPersons".SqlProcedureQueries(); + +// 返回六个结果集 +var (list1, list2, list3, list4, list5, list6) = "proc_GetPersons".SqlProcedureQueries(); + +// 返回七个结果集 +var (list1, list2, list3, list4, list5, list6, list7) = "proc_GetPersons".SqlProcedureQueries(); + +// 返回八个结果集 +var (list1, list2, list3, list4, list5, list6, list7, list8) = "proc_GetPersons".SqlProcedureQueries(); +``` + +:::note 关于异步 + +`Furion` 框架每一个数据库操作都支持异步方式,由于篇幅有限,就不列举异步方式了。 + +::: + +### 9.14.2.6 返回 `单行单列 object` + +```cs showLineNumbers +// ISqlRepository 方法 +var value = _sqlRepository.SqlProcedureScalar("proc_GetName"); + +// ISqlDispatchProxy 方式 +var value = _sqlExecuteProxy.GetName(); // 推荐方式 + +// 实体仓储方式 +var value = _personRepository.SqlProcedureScalar("proc_GetName"); + +// IRepository 非泛型方式 +var value = _repository.Sql().SqlProcedureScalar("proc_GetName"); + +// 变态懒人方式,直接通过存储过程名执行 +var value = "proc_GetName".SqlProcedureScalar(); +``` + +:::note 关于异步 + +`Furion` 框架每一个数据库操作都支持异步方式,由于篇幅有限,就不列举异步方式了。 + +::: + +### 9.14.2.7 返回 `单行单列 ` + +```cs showLineNumbers +// ISqlRepository 方法 +var value = _sqlRepository.SqlProcedureScalar("proc_GetName"); + +// ISqlDispatchProxy 方式 +var value = _sqlExecuteProxy.GetName(); // 推荐方式 + +// 实体仓储方式 +var value = _personRepository.SqlProcedureScalar("proc_GetName"); + +// IRepository 非泛型方式 +var value = _repository.Sql().SqlProcedureScalar("proc_GetName"); + +// 变态懒人方式,直接通过存储过程名执行 +var value = "proc_GetName".SqlProcedureScalar(); +``` + +:::note 关于异步 + +`Furion` 框架每一个数据库操作都支持异步方式,由于篇幅有限,就不列举异步方式了。 + +::: + +### 9.14.2.8 执行但无返回 + +```cs showLineNumbers +// ISqlRepository 方法 +_sqlRepository.SqlProcedureNonQuery("proc_UpdateData"); + +// ISqlDispatchProxy 方式 +_sqlExecuteProxy.UpdateData(); // 推荐方式 + +// 实体仓储方式 +_personRepository.SqlProcedureNonQuery("proc_UpdateData"); + +// IRepository 非泛型方式 +_repository.Sql().SqlProcedureNonQuery("proc_UpdateData"); + +// 变态懒人方式,直接通过存储过程名执行 +"proc_UpdateData".SqlProcedureNonQuery(); +``` + +:::note 关于异步 + +`Furion` 框架每一个数据库操作都支持异步方式,由于篇幅有限,就不列举异步方式了。 + +::: + +## 9.14.3 执行复杂存储过程 + +在存储过程中,有一种例子非常复杂,那就是既有 `INPUT` 参数,又有 `OUTPUT` 参数,还有 `RETURN` 参数,同时还输出 结果集 💥,如: + +```sql showLineNumbers {3,4,10-12,15-17,22} +CREATE PROC PROC_Output + @Id INT, // 输入参数 + @Name NVARCHAR(32) OUTPUT, // 输出参数,还带长度 + @Age INT OUTPUT // 输出参数 +AS +BEGIN + SET @Name = 'Furion Output'; + + // 输出结果集 + SELECT * + FROM dbo.Test + WHERE Id > @Id; + + // 输出结果集 + SELECT TOP 10 + * + FROM dbo.Test; + + SET @Age = 27; + + // 带 RETURN 返回 + RETURN 10; +END; +``` + +### 9.14.3.1 创建参数模型 + +```cs showLineNumbers {1,10,13,16} +using Furion.DatabaseAccessor; +using System.Data; + +namespace Furion.Application +{ + public class ProcOutputModel + { + public int Id { get; set; } // 输入参数 + + [DbParameter(ParameterDirection.Output, Size = 32)] + public string Name { get; set; } // 输出参数 + + [DbParameter(ParameterDirection.Output)] + public int Age { get; set; } // 输出参数 + + [DbParameter(ParameterDirection.ReturnValue)] + public int ReturnValue { get; set; } // 返回值 + } +} +``` + +### 9.14.3.2 执行复杂存储过程 + +- ** `DataSet` ** 方式 + +```cs showLineNumbers +// ISqlRepository 方法 +ProcedureOutputResult result = _sqlRepository.SqlProcedureOutput("proc_Complex", new ProcOutputModel{}); + +// ISqlDispatchProxy 方式 +ProcedureOutputResult result = _sqlExecuteProxy.Complex(new ProcOutputModel{}); // 推荐方式 + +// 实体仓储方式 +ProcedureOutputResult result = _personRepository.SqlProcedureOutput("proc_Complex", new ProcOutputModel{}); + +// IRepository 非泛型方式 +ProcedureOutputResult result = _repository.Sql().SqlProcedureOutput("proc_Complex", new ProcOutputModel{}); + +// 变态懒人方式,直接通过存储过程名执行 +ProcedureOutputResult result = "proc_Complex".SqlProcedureOutput(new ProcOutputModel{}); +``` + +```cs showLineNumbers +// 获取 OUTPUT 参数值 +var outputs = result.OutputValues; + +// 获取 RETURN 返回值 +var reval = result.ReturnValue; + +// 获取返回结果集 +var dataSet = result.Result; +``` + +- `Tuple` 方式 + +```cs showLineNumbers +// ISqlRepository 方法 +ProcedureOutputResult<(List, List)> result = _sqlRepository.SqlProcedureOutput<(List, List)>("proc_Complex", new ProcOutputModel{}); + +// ISqlDispatchProxy 方式 +ProcedureOutputResult<(List, List)> result = _sqlExecuteProxy.Complex(new ProcOutputModel{}); // 推荐方式 + +// 实体仓储方式 +ProcedureOutputResult<(List, List)> result = _personRepository.SqlProcedureOutput<(List, List)>("proc_Complex", new ProcOutputModel{}); + +// IRepository 非泛型方式 +ProcedureOutputResult<(List, List)> result = _repository.Sql().SqlProcedureOutput<(List, List)>("proc_Complex", new ProcOutputModel{}); + +// 变态懒人方式,直接通过存储过程名执行 +ProcedureOutputResult<(List, List)> result = "proc_Complex".SqlProcedureOutput<(List, List)>(new ProcOutputModel{}); +``` + +```cs showLineNumbers +// 获取 OUTPUT 参数值 +var outputs = result.OutputValues; + +// 获取 RETURN 返回值 +var reval = result.ReturnValue; + +// 获取返回结果集 +var (list1,list2) = result.Result; +``` + +:::note 关于异步 + +`Furion` 框架每一个数据库操作都支持异步方式,由于篇幅有限,就不列举异步方式了。 + +::: + +## 9.14.3 关于 `[DbParameter]` + +`[DbParameter]` 特性是用来标注 `Sql`,`函数`,`存储过程` 参数的,可配置属性: + +- `Direction`:设置参数方向,`ParameterDirection` 枚举类型,默认 `ParameterDirection.Input` +- `DbType`:设置参数类型,`DbType` 枚举类型,无默认 +- `Size`:设置参数长度的,`int` 类型 + +其中 `Direction` 属性是默认构造函数参数。 + +## 9.14.4 关于 `ProcedureOutputResult` + +`ProcedureOutputResult` 和 `ProcedureOutputResult` 是复杂存储过程执行返回模型类,有以下属性: + +- `OutputValues`:多个输出值,`ProcedureOutputValue` 类型 +- `ReturnValue`:返回值,`object` 类型 +- `Result`:结果集,非泛型版本是 `DataSet`类型,否则是 泛型类型 + +## 9.14.5 存储过程参数 + +所有 `sql` 参数都支持四种方式: + +- `DbParameter[]`:数组类型 +- `new {}`:匿名类型 +- `new Class{}`:强类型类型(支持复杂存储过程参数) +- `Dictionary` 类型 + +## 9.14.6 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/dbcontext-query.mdx b/handbook/docs/dbcontext-query.mdx new file mode 100644 index 0000000000000000000000000000000000000000..72d1fd0aa66f1c979e667baae84f89eef202067a --- /dev/null +++ b/handbook/docs/dbcontext-query.mdx @@ -0,0 +1,448 @@ +--- +id: dbcontext-query +title: 9.11 查询操作 +sidebar_label: 9.11 查询操作 +--- + +## 9.11.1 根据主键查询一条 + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +var person = repository.Find(1); + +// 示例二 +var person = repository.FindOrDefault(1); + +// 示例三 +var person = repository.Entities.Find(1); + +// ==== 异步操作 ==== + +// 示例一 +var person = await repository.FindAsync(1); + +// 示例二 +var person = await repository.FindOrDefaultAsync(1); + +// 示例三 +var person = await repository.Entities.FindAsync(1); + +``` + +:::tip 小提示 + +可以支持多个键查询,如主键、联合键。`repository.Find(1, "百小僧")`; + +::: + +## 9.11.2 根据条件查询一条 + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +var person = repository.Single(u => u.Name == "Furion"); + +// 示例二 +var person = repository.SingleOrDefault(u => u.Name == "Furion"); + +// 示例三 +var person = repository.First(u => u.Name == "Furion"); + +// 示例四 +var person = repository.FirstOrDefault(u => u.Name == "Furion"); + +// 示例五 +var person = repository.Last(u => u.Name == "Furion"); + +// 示例六 +var person = repository.LastOrDefault(u => u.Name == "Furion"); + +// 示例七 +var person = repository.Entities.Single(u => u.Name == "Furion"); + +// 示例八 +var person = repository.Entities.First(u => u.Name == "Furion"); + +// 示例九 +var person = repository.Entities.FirstOrDefault(u => u.Name == "Furion"); + +// 示例十 +var person = repository.Entities.Last(u => u.Name == "Furion"); + +// 示例十一 +var person = repository.Entities.LastOrDefault(u => u.Name == "Furion"); + +// ==== 异步操作 ==== + +// 示例一 +var person = await repository.SingleAsync(u => u.Name == "Furion"); + +// 示例二 +var person = await repository.SingleOrDefaultAsync(u => u.Name == "Furion"); + +// 示例三 +var person = await repository.FirstAsync(u => u.Name == "Furion"); + +// 示例四 +var person = await repository.FirstOrDefaultAsync(u => u.Name == "Furion"); + +// 示例五 +var person = await repository.LastAsync(u => u.Name == "Furion"); + +// 示例六 +var person = await repository.LastOrDefaultAsync(u => u.Name == "Furion"); + +// 示例七 +var person = await repository.Entities.SingleAsync(u => u.Name == "Furion"); + +// 示例八 +var person = await repository.Entities.FirstAsync(u => u.Name == "Furion"); + +// 示例九 +var person = await repository.Entities.FirstOrDefaultAsync(u => u.Name == "Furion"); + +// 示例十 +var person = await repository.Entities.LastAsync(u => u.Name == "Furion"); + +// 示例十一 +var person = await repository.Entities.LastOrDefaultAsync(u => u.Name == "Furion"); +``` + +## 9.11.3 查询所有数据 + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +var persons = repository.Entities; + +// 示例二 +var persons = repository.DetachedEntities; + +// 示例三 +var persons = repository.AsQueryable(); + +// 示例四 +var persons = repository.AsEnumerable(); + +// 示例五 +var persons = await repository.AsQueryable().ToListAsync(); +``` + +## 9.11.4 根据条件查询所有数据 + +```cs showLineNumbers +// 示例一 +var persons = repository.Where(u => u.Id > 10); + +// 示例二 +var persons = repository.Where(u => u.Id > 10 && u.Name.Equals("Furion")); + +// 示例三 (多个 where 里是 "并且",Id>10 and Name == "Furion") +var persons = repository.Where(u => u.Id > 10).Where(u => u.Name.Equals("Furion")); + +// 示例四 (判断 name 是否有值,如果 name 是空的则不会执行 u => u.Id > 10 && u.Name.Equals("Furion"),如果 name 有值就会执行 u => u.Id > 10 && u.Name.Equals("Furion")) +var persons = repository.Where(!string.IsNullOrEmpty(name), u => u.Id > 10 && u.Name.Equals("Furion")); + +// 示例五 +var persons = repository.Where(!string.IsNullOrEmpty(name), u => u.Id > 10) + .Where(age > 18, u => u.Name.Contains("百小僧")) + .Where(u => u.Age > 18); + +// 示例六 (在一个 where 里用逗号分隔是 "或者",Name=="Furion" or Name == "百小僧" or Name == "MonkSoul") +var persons = repository.Where(u => u.Name == "Furion", + u => u.Name == "百小僧", + u => u.Name == "MonkSoul"); + +// 示例七 +var persons = repository.Where(u => u.Id > 10).Where(u => u.Name.Equals("Furion")) + .Where(age > 18, u => u.Name.Contains("百小僧")) + .Where(u => u.Name == "Furion", + u => u.Name == "百小僧", + u => u.Name == "MonkSoul"); + +// 示例八 +var persons = repository.Where((age > 18, u => u.Name == "Furion"), + (!string.IsNullOrEmpty(name), u => u.Id > 10)); + +// 示例九 +var persons = repository.Where(u => u.Id > 10).Where(u => u.Name.Equals("Furion")) + .Where(age > 18, u => u.Name.Contains("百小僧")) + .Where(u => u.Name == "Furion", + u => u.Name == "百小僧", + u => u.Name == "MonkSoul") + .Where((age > 18, u => u.Name == "Furion"), + (!string.IsNullOrEmpty(name), u => u.Id > 10)); + +// 示例十 +var persons = repository.Entities.Where(u => u.Id > 10) + .Where(age > 18, u => u.Name.Contains("百小僧")); + +// 示例十一 +var persons = repository.DetachedEntities.Where(u => u.Id > 20); + +// 示例十二 +var persons = repository.AsQueryable(u => u.Id > 20).Where(u => u.Name == "Furion"); +``` + +## 9.11.5 分页查询 + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +var persons = repository.Where(u => u.Id > 10).ToPagedList(); + +// 示例二 +var persons = repository.Where(u => u.Id > 10).ToPagedList(1, 10); + +// ==== 异步操作 ==== + +// 示例一 +var persons = await repository.Where(u => u.Id > 10).ToPagedListAsync(); + +// 示例二 +var persons = await repository.Where(u => u.Id > 10).ToPagedListAsync(1, 10); +``` + +## 9.11.6 其他查询 + +### 9.11.6.1 查看记录是否存在 + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +var isExists = repository.Any(); + +// 示例二 +var isExists = repository.Any(u => u.Id > 10); + +// 示例三 +var isExists = repository.Entities.Any(); + +// 示例四 +var isExists = repository.DetachedEntities.Any(u => u.Id > 10); + +// 示例五 +var isExists = repository.Where(u => u.Id > 10).Any(); + +// ==== 异步操作 ==== + +// 示例一 +var isExists = await repository.AnyAsync(); + +// 示例二 +var isExists = await repository.AnyAsync(u => u.Id > 10); + +// 示例三 +var isExists = await repository.Entities.AnyAsync(); + +// 示例四 +var isExists = await repository.DetachedEntities.AnyAsync(u => u.Id > 10); + +// 示例五 +var isExists = await repository.Where(u => u.Id > 10).AnyAsync(); +``` + +### 9.11.6.2 查询记录数 + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +var count = repository.Count(); + +// 示例二 +var count = repository.Count(u => u.Id > 10); + +// 示例三 +var count = repository.Entities.Count(u => u.Id > 10); + +// 示例四 +var count = repository.Entities.DetachedEntities.Count(); + +// 示例五 +var count = repository.Where(u => u.Id > 10).Count(); + +// ==== 异步操作 ==== + +// 示例一 +var count = await repository.CountAsync(); + +// 示例二 +var count = await repository.CountAsync(u => u.Id > 10); + +// 示例三 +var count = await repository.Entities.CountAsync(u => u.Id > 10); + +// 示例四 +var count = await repository.Entities.DetachedEntities.CountAsync(); + +// 示例五 +var count = await repository.Where(u => u.Id > 10).CountAsync(); +``` + +### 9.11.6.3 查询最大值 + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +var entity = repository.Max(); + +// 示例二 +var value = repository.Max(u => u.Id); + +// 示例三 +var entity = repository.Entities.Max(); + +// 示例四 +var value = repository.Entities.DetachedEntities.Max(u => u.Age); + +// 示例五 +var value = repository.Where(u => u.Id > 10).Max(u => u.Age); + +// ==== 异步操作 ==== + +// 示例一 +var entity = await repository.MaxAsync(); + +// 示例二 +var value = await repository.MaxAsync(u => u.Id); + +// 示例三 +var entity = await repository.Entities.MaxAsync(); + +// 示例四 +var value = await repository.Entities.DetachedEntities.MaxAsync(u => u.Age); + +// 示例五 +var value = await repository.Where(u => u.Id > 10).MaxAsync(u => u.Age); +``` + +### 9.11.6.4 查询最小值 + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +var entity = repository.Min(); + +// 示例二 +var value = repository.Min(u => u.Id); + +// 示例三 +var entity = repository.Entities.Min(); + +// 示例四 +var value = repository.Entities.DetachedEntities.Min(u => u.Age); + +// 示例五 +var value = repository.Where(u => u.Id > 10).Min(u => u.Age); + +// ==== 异步操作 ==== + +// 示例一 +var entity = await repository.MinAsync(); + +// 示例二 +var value = await repository.MinAsync(u => u.Id); + +// 示例三 +var entity = await repository.Entities.MinAsync(); + +// 示例四 +var value = await repository.Entities.DetachedEntities.MinAsync(u => u.Age); + +// 示例五 +var value = await repository.Where(u => u.Id > 10).MinAsync(u => u.Age); +``` + +### 9.11.6.5 求和查询 + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +var sum = repository.Entities.Sum(u => u.Cost); + +// 示例二 +var sum = repository.AsQueryable().Sum(u => u.Cost); + +// 示例三 +var sum = repository.DetachedEntities.Sum(u => u.Cost); + +// ==== 异步操作 ==== + +// 示例一 +var sum = await repository.Entities.SumAsync(u => u.Cost); + +// 示例二 +var sum = await repository.AsQueryable().SumAsync(u => u.Cost); + +// 示例三 +var sum = await repository.DetachedEntities.SumAsync(u => u.Cost); +``` + +### 9.11.6.6 求平均值查询 + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +var sum = repository.Entities.Average(u => u.Cost); + +// 示例二 +var sum = repository.AsQueryable().Average(u => u.Cost); + +// 示例三 +var sum = repository.DetachedEntities.Average(u => u.Cost); + +// ==== 异步操作 ==== + +// 示例一 +var sum = await repository.Entities.AverageAsync(u => u.Cost); + +// 示例二 +var sum = await repository.AsQueryable().AverageAsync(u => u.Cost); + +// 示例三 +var sum = await repository.DetachedEntities.AverageAsync(u => u.Cost); +``` + +### 9.11.6.7 时间查询 + +```cs showLineNumbers +var starDate = DateTime.Parse("2020-09-10"); +var endDate = DateTime.Parse("2020-09-10"); +var query = repository.Where(u => u.CreatedDt >= starDate && u.CreatedDt <= endDate); +``` + +### 9.11.6.8 模糊查询 + +```cs showLineNumbers +// 示例一 +repository.Where(u => u.Name.StartsWith("Furion")); + +// 示例二 +_testRepository.Where(u => u.Name.EndsWith("Furion")); + +// 示例三 +_testRepository.Where(u => u.Name.Contains("Furion")); +``` + +### 9.11.6.9 `Case When` + +数据库中的 `Case When` 实际上对应的是我们程序中的 `三元表达式` ,也就是使用 `三元表达式` 即可自动生成 `Case When` 语句。 + +## 9.11.7 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/dbcontext-read-write.mdx b/handbook/docs/dbcontext-read-write.mdx new file mode 100644 index 0000000000000000000000000000000000000000..af24d034b9ee141cbfa7a6534a64b1372d1f91c7 --- /dev/null +++ b/handbook/docs/dbcontext-read-write.mdx @@ -0,0 +1,460 @@ +--- +id: dbcontext-read-write +title: 9.28 读写分离/主从复制 +sidebar_label: 9.28 读写分离/主从复制 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +## 9.28.1 读写分离 + +其实就是将数据库分为了主从库,一个主库用于写数据,多个从库完成读数据的操作,主从库之间通过某种机制进行数据的同步,是一种常见的数据库架构。 + + + +### 9.28.1.1 解决了什么问题 + +大多数互联网业务,往往读多写少,这时候,数据库的读会首先成为数据库的瓶颈,这时,如果我们希望能够线性的提升数据库的读性能,消除读写锁冲突从而提升数据库的写性能,那么就可以使用“分组架构”(读写分离架构)。 + +用一句话概括,读写分离是用来解决数据库的读性能瓶颈的。 + +### 9.28.1.2 注意事项 + +- 数据库连接池要进行区分,哪些是读连接池,哪个是写连接池,研发的难度会增加; +- 为了保证高可用,读连接池要能够实现故障自动转移; +- 主从的一致性问题需要考虑。 + +## 9.28.2 如何实现 + +`Furion` 在数据库模块设计之初,就考虑了读写分离这种情况,所以从底层就支持动态切换数据库上下文及读写操作方法约束。 + +读写分离操作主要使用 `IMSRepository` 仓储,该仓储已经为开发者提供方便的操作调用。当然也可以不使用该仓储。 + +下面就给大家演示如何读写多库读写操作。 + +### 9.28.2.1 创建 `主库` 数据库上下文 + +```cs showLineNumbers +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore; + +namespace Furion.EntityFramework.Core +{ + /// + /// 主库数据库上下文 + /// + [AppDbContext("MasterConnectionString")] + public class MasterDbContext : AppDbContext + { + public MasterDbContext(DbContextOptions options) : base(options) + { + } + } +} +``` + +**数据库连接字符串:** + +```json showLineNumbers +{ + "ConnectionStrings": { + "MasterConnectionString": "Server=localhost;Database=Furion;User=sa;Password=000000;MultipleActiveResultSets=True;" + } +} +``` + +### 9.28.2.2 创建 `从库` 数据库上下文 + +```cs showLineNumbers {11} +using Furion.Core; +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore; + +namespace Furion.EntityFramework.Core +{ + /// + /// 从库数据库上下文 + /// + [AppDbContext("SlaveConnectionString")] + public class SlaveDbContext : AppDbContext + { + public SlaveDbContext(DbContextOptions options) : base(options) + { + } + } +} +``` + +:::important 特别注意 + +多数据库操作除了默认数据库无需自定义 **数据库上下文定位器**,其他数据库都需要有数据库上下文定位器。如 `SlaveDbContextLocator` + +::: + +**从库数据库上下文定位器:** + +```cs showLineNumbers {8} +using Furion.DatabaseAccessor; + +namespace Furion.Core +{ + /// + /// 从库数据库上下文定位器 + /// + public class SlaveDbContextLocator : IDbContextLocator + { + } +} +``` + +**数据库连接字符串:** + +```json showLineNumbers +{ + "ConnectionStrings": { + "SlaveConnectionString": "Server=localhost;Database=FurSlave;User=sa;Password=000000;MultipleActiveResultSets=True;" + } +} +``` + +### 9.28.2.3 注册 `主从库` 数据库上下文 + +```cs showLineNumbers {13-14} +using Furion.Core; +using Microsoft.Extensions.DependencyInjection; + +namespace Furion.EntityFramework.Core +{ + [AppStartup(600)] + public sealed class FurEntityFrameworkCoreStartup : AppStartup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddDatabaseAccessor(options => + { + services.AddDbPool(); + services.AddDbPool(); + }); + } + } +} +``` + +### 9.28.2.4 创建 `Person` 实体 + +由于 `主从库` 具有相同的数据库结构,所以实体也必须声明 `主从库`: + +```cs showLineNumbers {7} +using Furion.DatabaseAccessor; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Furion.Core +{ + public class Person : IEntity + { + /// + /// 主键Id + /// + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 年龄 + /// + public int Age { get; set; } + } +} +``` + +### 9.28.2.5 将 `Person` 转换成数据库表 + +**创建主库数据库表:** + +```shell showLineNumbers +Add-Migration v0.0.1 -Context MasterDbContext +``` + +```shell showLineNumbers +Update-Database -Context MasterDbContext +``` + +**创建从库数据库表:** + +```shell showLineNumbers +Add-Migration v0.0.1 -Context SlaveDbContext +``` + +```shell showLineNumbers +Update-Database -Context SlaveDbContext +``` + + + + + +### 9.28.2.6 固定主从库使用例子 + +```cs showLineNumbers {13,19,31,40} +using Furion.Core; +using Furion.DatabaseAccessor; +using Furion.DynamicApiController; +using System.Collections.Generic; + +namespace Furion.Application +{ + public class PersonService : IDynamicApiController + { + /// + /// 可调配仓储(读写分离) + /// + private readonly IMSRepository _msRepository; + + /// + /// 构造函数初始化 + /// + /// + public PersonService(IMSRepository msRepository) + { + _msRepository = msRepository; + } + + /// + /// 新增走主库 + /// + /// + /// + public void Insert(Person person) + { + _msRepository.Master().Insert(person); + } + + /// + /// 查询走从库 + /// + /// + public List Get() + { + return _msRepository.Slave1().AsEnumerable().ToList(); + } + } +} +``` + +### 9.28.2.7 `随机`或 `自定义`返回从库 ✨ + +在 `Furion 2.4.1 + ` 版本新增了 `IMSRepository` 和 `IMSRepository` 仓储类型,可以获取随机仓储或自定义仓储。使用例子如下: + +- 配置 主库 `[AppDbContext]` 特性的 `SlaveDbContextLocators` 属性,可通过构造函数最后参数传入,如: + +```cs showLineNumbers {6} +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore; + +namespace Furion.EntityFramework.Core +{ + [AppDbContext("Sqlite3ConnectionString", DbProvider.Sqlite, typeof(从库定位器1), typeof(从库定位器2), typeof(从库定位器3))] + public class MasterDbContext : AppDbContext + { + public MasterDbContext(DbContextOptions options) : base(options) + { + } + } +} +``` + +- 使用 `IMSRepository` 或 `IMSRepository` + +```cs showLineNumbers {13,19,31,40,49-53} +using Furion.Core; +using Furion.DatabaseAccessor; +using Furion.DynamicApiController; +using System.Collections.Generic; + +namespace Furion.Application +{ + public class PersonService : IDynamicApiController + { + /// + /// 可调配仓储(读写分离) + /// + private readonly IMSRepository _msRepository; // 不指定定位器,默认是 MasterDbContextLocator + + /// + /// 构造函数初始化 + /// + /// + public PersonService(IMSRepository msRepository) + { + _msRepository = msRepository; + } + + /// + /// 新增走主库 + /// + /// + /// + public void Insert(Person person) + { + _msRepository.Master().Insert(person); + } + + /// + /// 随机从库 + /// + /// + public List Get() + { + return _msRepository.Slave().AsEnumerable().ToList(); + } + + /// + /// 自定义从库 + /// + /// + public List Get() + { + return _msRepository.Slave(() => { + // 这里写你的逻辑返回从库定位器 + + return 你的从库定位器; + }).AsEnumerable().ToList(); + } + } +} +``` + +:::note 特别说明 + +`IMSRepository` 不带泛型默认指的是 `IMSRepository`,如需泛型版本,则使用 `IMSRepository<定位器>` + +::: + +## 9.28.3 主从复制 + +主从复制:是一种数据备份的方案。 + +简单来说,是使用两个或两个以上相同的数据库,将一个数据库当做主数据库,而另一个数据库当做从数据库。**在主数据库中进行相应操作时,从数据库记录下所有主数据库的操作,使其二者一模一样。** + +## 9.28.4 主从复制几种方式 + +### 9.28.4.1 同步复制 + +所谓的同步复制,意思是 `Master` 的变化,必须等待 `Slave-1,Slave-2,...,Slave-n` 完成后才能返回。 +这样,显然不可取,比如,在 `Web` 前端页面上,用户增加了条记录,需要等待很长时间。 + +### 9.28.4.2 异步复制 + +如同 AJAX 请求一样。`Master` 只需要完成自己的数据库操作即可。至于 `Slaves` 是否收到二进制日志,是否完成操作,不用关心。**(推荐方式)** + +### 9.28.4.3 半同步复制 + +`Master` 只保证 `Slaves` 中的一个操作成功,就返回,其他 `Slave` 不管。 + +下面将使用 `SqlServer` 简单配置主从复制功能。 + +## 9.28.5 `SqlServer` 主库配置 + +### 9.28.5.1 添加 `本地发布` + + + +### 9.28.5.2 选择 `分发服务器` + + + +### 9.28.5.3 启用 `代理` + + + +### 9.28.5.4 发布数据库 + + + +### 9.28.5.5 快照发布 + + + +具体选择何种发布类型,视具体业务场景而定。 + +### 9.28.5.6 选择发布项目 + + + +### 9.28.5.7 配置分发计划 + + + + + +### 9.28.5.8 配置安全设置 + + + + + +### 9.28.5.9 完成配置 + + + + + +## 9.28.6 `SqlServer` 从库配置 + +### 9.28.6.1 添加 `本地订阅` + + + +### 9.28.6.2 选择 `分发服务器` + + + +### 9.28.6.3 选择 `分发代理位置` + + + +### 9.28.6.4 选择 `订阅数据库` + + + +### 9.28.6.5 选择 `分发安全设置` + + + + + +### 9.28.6.6 选择 `同步计划` + + + +### 9.28.6.7 完成订阅 + + + +## 9.28.7 分发定义监视 + + + +## 9.28.8 查看主从复制结果 + + + + + +:::important 特别特性 + +主从复制有一定迟延性,所以系统设计要有一定“容忍性"。 + +::: + +## 9.28.9 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 diff --git a/handbook/docs/dbcontext-repository.mdx b/handbook/docs/dbcontext-repository.mdx new file mode 100644 index 0000000000000000000000000000000000000000..d241acf735269ae6efa31a2ae0c9cb7bcc688747 --- /dev/null +++ b/handbook/docs/dbcontext-repository.mdx @@ -0,0 +1,450 @@ +--- +id: dbcontext-repository +title: 9.5 仓储模式 +sidebar_label: 9.5 仓储模式 (Repository) +--- + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **新特性** + + -  新增 `IRepositoryFactory` 仓储功能,解决在 `Blazor` 中使用 `EFCore` 问题 4.9.1.1 ⏱️2023.11.16 [4285ec0](https://gitee.com/dotnetchina/Furion/commit/4285ec0b8debc2d71c7f978126cb3dc394a8ad30) [文档说明](https://learn.microsoft.com/zh-cn/aspnet/core/blazor/blazor-ef-core?view=aspnetcore-7.0) + +
+
+
+ +## 9.5.1 什么是仓储 + +> 在领域层和数据映射层的中介,使用类似集合的接口来存取领域对象,实际上,仓储被用于领域对象在数据库上的操作(实体 Entity 和值对象 Value types)。一般来说,我们针对不同的实体(或聚合根 Aggregate Root)会创建相对应的仓储。 + +简单来说,仓储就是数据存取操作的载体,但不限定于数据库。 + +## 9.5.2 内置仓储 + +`Furion` 框架内置了一个数据库操作的仓储,方便大家拓展和集成: + +:::info 关于依赖注入说明 + +目前能够被依赖注入解析服务的仓储有: + +- `IRepository` +- `IRepository` +- `IRepository` +- `ISqlRepository` +- `ISqlRepository` +- `IMSRepository` +- `IMSRepository` +- `IMSRepository` +- `IDbRepository` + +还有两个私有仓储,也是所有仓储的基类(用于高级自定义开发) + +- `IPrivateRepository`:所有实体仓储的基类 +- `IPrivateSqlRepository`:所有数据库操作的基类 + +除此之后的所有仓储只能通过 `rep.Constraint()` 进行约束创建,如,只读仓储: + +```cs showLineNumbers +var readRepository = rep.Constraint>(); +``` + +::: + +### 9.5.2.1 非泛型超级仓储 + +- `IRepository`:默认非泛型仓储接口,支持切换到任何仓储 +- `EFCoreRepository`:默认非泛型仓储实现 + +### 9.5.2.2 泛型实体仓储 + +- `IRepository`:默认数据库实体仓储接口 +- `EFCoreRepository`:默认数据库实体仓储实现 + +### 9.5.2.3 泛型多数据库实体仓储 + +- `IRepository`:任意数据库的实体仓储接口 +- `EFCoreRepository`:任意数据库的实体仓储实现 + +### 9.5.2.4 `Sql` 操作仓储 + +- `ISqlRepository`:默认数据库 `Sql` 操作仓储接口 +- `SqlRepository`:默认数据库 `Sql` 操作仓储实现 + +### 9.5.2.5 多数据库 `Sql` 操作仓储 + +- `ISqlRepository`:任意数据库的 `Sql` 操作仓储接口 +- `SqlRepository`:任意数据库的 `Sql` 操作仓储实现 + +### 9.5.2.6 只读实体仓储(支持多库) + +- `IReadableRepository`:默认数据库只读实体仓储接口 +- `IReadableRepository`:多数据库只读实体仓储实现 + +### 9.5.2.7 只写实体仓储(支持多库) + +- `IWritableRepository`:默认数据库只写实体仓储接口 +- `IWritableRepository`:多数据库只写实体仓储实现 + +### 9.5.2.8 只允许新增实体仓储(支持多库) + +- `IInsertableRepository`:默认数据库只允许新增的实体仓储接口 +- `IInsertableRepository`:多数据库只允许新增的实体仓储实现 + +### 9.5.2.9 只允许更新实体仓储(支持多库) + +- `IUpdateableRepository`:默认数据库只允许更新的实体仓储接口 +- `IUpdateableRepository`:多数据库只允许更新的实体仓储实现 + +### 9.5.2.10 只允许删除实体仓储(支持多库) + +- `IDeletableRepository`:默认数据库只允许删除的实体仓储接口 +- `IDeletableRepository`:多数据库只允许删除的实体仓储实现 + +### 9.5.2.11 只允许拓展操作实体仓储(支持多库) + +:::warning 功能移除声明 + +该功能在 `Furion 2.5.1 +` 版本中已移除。此操作让很多不了解 `EFCore` 的开发者产生了很大的误解,不知何时新增或何时更新,故移除此功能。 + +::: + +- `IOperableRepository`:默认数据库只允许拓展操作实体仓储接口 +- `IOperableRepository`:多数据库只允许拓展操作实体仓储实现 + +### 9.5.2.12 只允许 `Sql` 查询仓储(支持多库) + +- `ISqlReaderRepository`:默认数据库只允许 `Sql` 查询仓储接口 +- `ISqlReaderRepository`:多数据库只允许 `Sql` 查询仓储实现 + +### 9.5.2.13 只允许 `Sql` 非查询仓储(支持多库) + +- `ISqlExecutableRepository`:默认数据库只允许 `Sql` 非查询仓储接口 +- `ISqlExecutableRepository`:多数据库只允许 `Sql` 非查询仓储实现 + +### 9.5.2.14 读写分离仓储 + +- `IMSRepository`:最多支持 **一主 7 从** 仓储 + +### 9.5.2.15 定位器仓储 + +- `IDbRepository`:初始化特定数据库仓储 + +### 9.5.2.15 仓储工厂 + +- `IRepositoryFactory`:创建新的未释放的 `IRepository` 仓储,`Furion 4.9.1.1+` 支持 +- `IRepositoryFactory`:创建新的未释放的 `IRepository` 仓储,`Furion 4.9.1.1+` 支持 + +## 9.5.3 仓储使用 + +`Furion` 提供了非常多的方式创建仓储,目的是为了让大家可以在不同的场景中使用。 + +### 9.5.3.1 构造函数注入 + +```cs showLineNumbers {2} +private readonly IRepository _personRepository; +public FurionService(IRepository personRepository) +{ + _personRepository = personRepository; +} +``` + +### 9.5.3.2 方法参数注入 + +```cs showLineNumbers {1} +public async Task> GetAll([FromServices] IRepository repository, string keyword) +{ + var persons = await repository.AsQueryable().ToListAsync(); + return persons.Adapt>(); +} +``` + +### 9.5.3.3 `Db.GetRepository` 获取 + +```cs showLineNumbers +// 非泛型仓储 +var repository = Db.GetRepository(); + +// 泛型仓储 +var repository = Db.GetRepository(); + +// Sql 仓储 +var sqlRepository = Db.GetSqlRepository(); +``` + +:::important 特别说明 + +不管采用哪种方式,`Furion` 都保证了仓储一次请求唯一性。同时 `Db.GetRepository()` 方式支持任何静态类中使用。 + +::: + +## 9.5.4 仓储高级用法 + +### 9.5.4.1 动态切换实体仓储 + +```cs showLineNumbers +var userRepository = personRepository.Change(); +var userRepository = personRepository.Change(); // 还可以基于定位器切换 +``` + +### 9.5.4.2 动态切换仓储类型 + +比如,读写分离/主从库仓储: + +```cs showLineNumbers +// 只读仓储 +var readRepository = personRepository.Constraint>(); + +// 只写仓储 +var writeRepository = personRepository.Constraint>(); +``` + +:::tip 小知识 + +`.Constraint` 支持切换任何仓储类型。 + +::: + +### 9.5.4.3 获取 `Sql` 操作仓储 + +```cs showLineNumbers +var sqlRepository = repository.Sql(); +``` + +## 9.5.5 多数据库操作 + +`Furion` 通过 `DbContextLocator` 数据库上下文定位器实现多种数据库操作,可以随意切换数据库 + +### 9.5.5.1 动态切换多个数据库 + +#### 动态切换数据库 + +```cs showLineNumbers +// 切换到 MSSQL 操作 Person表 +var mssqlRepository = repository.Change(); + +// 切换到 MySql 操作 Person表 +var mysqlRepository = repository.Change(); + +// 切换到 Sqlite 操作 Person表 +var sqliteRepository = repository.Change(); + +// 其他更多数据库一样的操作 +``` + +#### 另外任何仓储或实体配置都支持多个数据库同时操作 + +仓储方式 + +```cs showLineNumbers +IRepository mssqlRepository + +ISqlRepository mssqlRepository; +``` + +动态 `sql` 方式 + +```cs showLineNumbers +"select * from person".Change().SqlQuery(); +``` + +实体配置方式 + +```cs showLineNumbers +public class User:Entity +{ +} +``` + +`Sql` 代理方式 + +```cs showLineNumbers +[SqlFunction("funcName", DbContextLocator = typeof(MySqlDbContextLocator))] +int GetAge(int id); +``` + +`Linq` 中方式 + +```cs showLineNumbers +[QueryableFunction("funcName","dbo", DbContextLocator = typeof(MySqlDbContextLocator))] +string GetName()=> throw Oops.Oh("不支持该数据库操作"); +``` + +## 9.5.6 在后台任务中使用 + +由于 `DbContext` 默认注册为 `Scoped` 生存周期,所以在后台任务中使用 `IServiceScopeFactory` 获取所有服务,如: + +```cs showLineNumbers +public class JobService : BackgroundService +{ + // 日志对象 + private readonly ILogger _logger; + + // 服务工厂 + private readonly IServiceScopeFactory _scopeFactory; + public JobService(ILogger logger + , IServiceScopeFactory scopeFactory) + { + _logger = logger; + _scopeFactory = scopeFactory; + } + + protected override Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("写日志~~"); + + using (var scope = _scopeFactory.CreateScope()) + { + var services = scope.ServiceProvider; + + // 获取数据库上下文 + var dbContext = Db.GetDbContext(services); + + // 获取仓储 + var respository = Db.GetRepository(services); + + // 解析其他服务 + var otherService = services.GetService(); + } + + return Task.CompletedTask; + } +} +``` + +:::important 数据库操作注意 + +如果作用域中对**数据库有任何变更操作**,需手动调用 `SaveChanges` 或带 `Now` 结尾的方法。也可以使用 `Scoped.CreateUow(handler)` 代替。 + +::: + +## 9.5.7 自定义仓储 + +有些时候我们需要自定义仓储,拓展现有的仓储功能,可参考以下代码(含定位器仓储和默认仓储实现) + +```cs showLineNumbers +/// +/// 自定义仓储接口 +/// +/// +/// +public interface IMyRepository : IPrivateRepository + where TEntity : class, IPrivateEntity, new() + where TDbContextLocator : class, IDbContextLocator +{ + /// + /// 自定义方法 + /// + void MyMethod(); +} + +/// +/// 自定义仓储实现类 +/// +/// +/// +public class MyRepository : PrivateRepository, IMyRepository, IScoped + where TEntity : class, IPrivateEntity, new() + where TDbContextLocator : class, IDbContextLocator +{ + /// + /// 实现基类构造函数 + /// + /// + public MyRepository(IServiceProvider serviceProvider) + : base(typeof(TDbContextLocator), serviceProvider) + { + } + + /// + /// 自定义方法 + /// + public void MyMethod() + { + throw new System.NotImplementedException(); + } +} + +/// +/// 默认数据库自定义仓储接口 +/// +/// +public interface IMyRepository : IMyRepository + where TEntity : class, IPrivateEntity, new() +{ +} + +/// +/// 默认数据库自定义仓储实现 +/// +/// +public class MyRepository : MyRepository, IMyRepository, IScoped + where TEntity : class, IPrivateEntity, new() +{ + public MyRepository(IServiceProvider serviceProvider) : base(serviceProvider) + { + } +} +``` + +## 9.5.8 仓储工厂 `IRepositoryFactory` + +:::important 版本说明 + +以下内容仅限 `Furion 4.9.1.1 +` 版本使用。 + +::: + +有时候我们希望可以通过类型 `new` 的方式创建一个 `IRepository` 实例,然后通过 `using` 实现手动释放,如: + +```cs showLineNumbers {5,12,15} +public class YourAppServices : IDynamicApiController +{ + private readonly IRepositoryFactory _repositoryFactory; + + public YourAppServices(IRepositoryFactory repositoryFactory) + { + _repositoryFactory = repositoryFactory; + } + + public void Create() + { + using var repository = _repositoryFactory.CreateRepository(); + var persons = repository.DetachedEntities.ToList(); + + using (var repository2 = _repositoryFactory.CreateRepository()) + { + var persons2 = repository.DetachedEntities.ToList(); + } + + var persons3 = repository.DetachedEntities.ToList(); + } +} +``` + +这样的功能非常适用于多线程或者 `Blazor` 项目中([相关文档](https://learn.microsoft.com/zh-cn/aspnet/core/blazor/blazor-ef-core?view=aspnetcore-7.0)),如: + +```cs showLineNumbers {1,5-6} +@inject IRepositoryFactory Factory; // 可以注入多个 + +private async Task GetListAsync() +{ + // 别忘记 using + using var repository = Factory.CreateRepository(); // 也可以通过 using var dbContext = Db.CreateDbContext() 获取新的 DbContext + + var list = await respository.DetachedEntities.ToListAsync(); +} +``` + +## 9.5.9 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/dbcontext-seed-data.mdx b/handbook/docs/dbcontext-seed-data.mdx new file mode 100644 index 0000000000000000000000000000000000000000..ea86c840c3b8ff81ff127071b79a4775ec098796 --- /dev/null +++ b/handbook/docs/dbcontext-seed-data.mdx @@ -0,0 +1,127 @@ +--- +id: dbcontext-seed-data +title: 9.22 实体种子数据 +sidebar_label: 9.22 实体种子数据 +--- + +:::important 特别提醒 + +一旦定义了种子数据或改变了种子数据,需要重新执行 `Add-Migration` 和 `Update-Database` 命令。 + +::: + +## 9.22.1 什么是种子数据 + +在 `Furion` 框架中,种子数据通常指的是通过程序为数据库预先设置一些初始化数据,比如我们的数据字典表,我们可能希望在系统构建初期就自动将一些规范化数据保存到数据库中。 + +如性别:男/女,地区,行业信息等等。 + +## 9.22.2 如何配置 + +`Furion` 提供非常灵活方便的 `IEntitySeedData` 依赖接口可以快速的构建种子数据,支持任何无参构造函数对象类中使用。如我们需要为 `Person` 表插入初始化数据: + +### 9.22.2.1 在实体定义中使用 + +```cs showLineNumbers {7,16-23} +using Furion.DatabaseAccessor; +using System; +using System.Collections.Generic; + +namespace Furion.Core +{ + public class Person : EntityBase, IEntitySeedData + { + public string Name { get; set; } + + public int Age { get; set; } + + public string Address { get; set; } + + // 配置种子数据 + public IEnumerable HasData(DbContext dbContext, Type dbContextLocator) + { + return new List + { + new Person { Id = 1, Name = "百小僧", Address = "广东省中山市" }, + new Person { Id = 2, Name = "新生帝", Address = "广东省珠海市" } + }; + } + } +} +``` + +:::important 特别注意 + +`主键` 值必须手动插入,因为会自动关闭主键或自增标识检查。 + +::: + +### 9.22.2.2 在任意对象类中使用 + +```cs showLineNumbers {6,9-16} +using Furion.DatabaseAccessor; +using System.Collections.Generic; + +namespace Furion.Application +{ + public class PersonSeedData : IEntitySeedData + { + // 配置种子数据 + public IEnumerable HasData(DbContext dbContext, Type dbContextLocator) + { + return new List + { + new Person { Id = 1, Name = "百小僧", Address = "广东省中山市" }, + new Person { Id = 2, Name = "新生帝", Address = "广东省珠海市" } + }; + } + } +} +``` + +## 9.22.3 导航属性 + +通常我们的实体有 `一对多`,`多对多`等外键关系,那么**我们需要单独为每一个实体添加数据种子,而不是直接写在主表中。** + +## 9.22.4 多个数据库种子数据 + +`Furion` 提供泛型的方式支持多个数据库种子数据设定,如: + +```cs showLineNumbers {6,9-16} +using Furion.DatabaseAccessor; +using System.Collections.Generic; + +namespace Furion.Application +{ + public class PersonSeedData : IEntitySeedData + { + // 配置种子数据 + public IEnumerable HasData(DbContext dbContext, Type dbContextLocator) + { + return new List + { + new Person { Id = 1, Name = "百小僧", Address = "广东省中山市" }, + new Person { Id = 2, Name = "新生帝", Address = "广东省珠海市" } + }; + } + } +} +``` + +上面的例子表示同时为 `MySqlDbContext` 和 `SqliteDbContext` 创建种子数据。 + +## 9.22.5 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: + +--- + +:::note 了解更多 + +想了解更多 `数据种子` 知识可查阅 [EF Core - 数据种子设定](https://docs.microsoft.com/zh-cn/ef/core/modeling/data-seeding) 章节。 + +::: diff --git a/handbook/docs/dbcontext-sql-proxy.mdx b/handbook/docs/dbcontext-sql-proxy.mdx new file mode 100644 index 0000000000000000000000000000000000000000..dacd269eb6af2c93de0f04a5894f87b7baab980a --- /dev/null +++ b/handbook/docs/dbcontext-sql-proxy.mdx @@ -0,0 +1,752 @@ +--- +id: dbcontext-sql-proxy +title: 9.18 Sql 高级代理 +sidebar_label: 9.18 Sql 高级代理 +--- + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **新特性** + + -  新增 **`Sql` 高级拦截支持返回 `IEnumerable` 和 `T[]` 类型值** 4.8.7.5 ⏱️2023.03.07 [f2ca2d3](https://gitee.com/dotnetchina/Furion/commit/f2ca2d303ea06febcc3a50df56ed03895e43c639) + +
+ 查看变化 +
+ +过去版本如果**返回对象类型**只支持 `List`,`T` 和 `Tuple<>`,现已支持 `IEnumerable`、`T[]` 和 `Tuple<>` 混合体。 + +```cs showLineNumbers {4,7,16} +public interface ISql : ISqlDispatchProxy +{ + [SqlExecute("select * from person")] + Person[] GetPersons(); + + [SqlExecute("select * from person")] + IEnumerable GetPersons2(); + + // 更复杂的组合 + [SqlExecute(@" +select * from person where id = 1; +select * from person; +select * from person where id > 0; +select * from person where id > 0; +")] + (Person, List, Person[], IEnumerable) GetPersons(); +} +``` + +
+
+ +
+
+
+ +## 9.18.1 关于 `Sql` 代理 + +`Sql` 代理是 `Furion` 框架中对 `Sql` 操作一个非常重要的概念,通过这种方式可以大大提高 `Sql` 书写效率,而且后期极易维护。 + +`Sql` 代理属于 `Furion` 框架中一个高级功能。 + +## 9.18.2 了解 `ISqlDispatchProxy` + +`ISqlDispatchProxy` 接口是 `Furion` 实现**被代理接口**的唯一依赖,任何公开的接口一旦集成了 `ISqlDispatchProxy` 接口,那么这个接口就是**被托管拦截**的 `Sql` 操作接口。 + +简单定义一个 **Sql 代理接口** + +```cs showLineNumbers {1,5} +using Furion.DatabaseAccessor; + +namespace Furion.Application +{ + public interface ISql : ISqlDispatchProxy + { + } +} +``` + +一旦这个接口继承了 `ISqlDispatchProxy`,那么它就会**动态创建接口实例,而且支持依赖注入/控制反转获取实例**。 + +## 9.18.3 开始领略 `Sql` 代理 + +下面我将通过多个例子来演示 `Sql` 代理的用法,为什么推荐这种方式操作 `Sql`。 + +支持各种方式获取实例: + +### 9.18.3.1 构造函数方式 + +```cs showLineNumbers {1-2} +private readonly ISql _sql; +public FurionService(ISql sql) +{ + _sql = sql; +} +``` + +### 9.18.3.2 方法参数注入 + +```cs showLineNumbers {1} +public async Task> GetAll([FromServices] ISql, string keyword) +{ +} +``` + +### 9.18.3.3 `Db.GetSqlDispatchProxy()` + +```cs showLineNumbers +var sql = Db.GetSqlDispatchProxy(); +``` + +## 9.18.4 `Sql` 操作 + +### 9.18.4.1 返回 `DataTable` + +```cs showLineNumbers {8,12,16,20} +using Furion.DatabaseAccessor; + +namespace Furion.Application +{ + public interface ISql : ISqlDispatchProxy + { + // 执行sql并传入参数,基元类型 + [SqlExecute("select * from person where id >@id and name like @name")] + DataTable GetPerson(int id, string name); + + // 执行sql并传入参数,对象类型 + [SqlExecute("select * from person where id >@id and name like @name")] + DataTable GetPerson(MyParam paras); + + // 执行存储过程 sql,支持设置参数类型 + [SqlExecute("exec PROP_NAME @id", CommandType = CommandType.StoredProcedure)] + DataTable GetPerson(int id); + + // 支持多数据库操作 + [SqlExecute("select * from person"), SqlDbContextLocator(typeof(MySqlDbContextLocator))] + DataTable GetPerson(); + + // 异步方式 + [SqlExecute("select * from person"), SqlDbContextLocator(typeof(MySqlDbContextLocator))] + Task GetPersonAsync(); + } +} +``` + +:::important 关于参数 + +`Sql` 代理参数查找规则: + +如果方法的参数是 `基元类型`(或 `string`、`值类型`),则自动将这些类型组合成 `Dictionary` 作为 `Sql` 参数。命令参数可使用方法同名参数加 `@` 符号。 + +如果方法的参数是 `类类型`,那么自动遍历该类公开实例属性生成 `DbParameter[]` 数组,每一个属性名都将是命令参数,**大部分数据库是不区分大小写,个别数据库除外**,如 `Sqlite`,如: + +```cs showLineNumbers +public class MyModel +{ + public int Id {get;set;} + public string Name {get; set;} +} +``` + +那么 `sql` 语句可以直接使用属性名作为参数: + +```sql showLineNumbers +select * from person where id > @id and name = @name; +``` + +::: + +### 9.18.4.2 返回 `List` + +```cs showLineNumbers {8,12,16,20} +using Furion.DatabaseAccessor; + +namespace Furion.Application +{ + public interface ISql : ISqlDispatchProxy + { + // 执行sql并传入参数,基元类型 + [SqlExecute("select * from person where id >@id and name like @name")] + List GetPerson(int id, string name); + + // 执行sql并传入参数,对象类型 + [SqlExecute("select * from person where id >@id and name like @name")] + List GetPerson(MyParam paras); + + // 执行存储过程 sql,支持设置参数类型 + [SqlExecute("exec PROP_NAME @id", CommandType = CommandType.StoredProcedure)] + List GetPerson(int id); + + // 支持多数据库操作 + [SqlExecute("select * from person"), SqlDbContextLocator(typeof(MySqlDbContextLocator)] + List GetPerson(); + + // 异步方式 + [SqlExecute("select * from person"), SqlDbContextLocator(typeof(MySqlDbContextLocator)] + Task> GetPersonAsync(); + } +} +``` + +### 9.18.4.3 返回 `DataSet` + +```cs showLineNumbers {8-10,14-16,20-22,26-28,32-35} +using Furion.DatabaseAccessor; + +namespace Furion.Application +{ + public interface ISql : ISqlDispatchProxy + { + // 执行sql并传入参数,基元类型 + [SqlExecute(@" + select * from person where id >@id and name like @name; + select top 10 * from student where Id >@id;")] + DataSet GetData(int id, string name); + + // 执行sql并传入参数,对象类型 + [SqlExecute(@" + select * from person where id >@id and name like @name; + select top 10 * from student where Id >@id;")] + DataSet GetData(MyParam paras); + + // 执行存储过程 sql,支持设置参数类型 + [SqlExecute(@" + exec PROP_NAME @id; + select * from person;", CommandType = CommandType.StoredProcedure)] + DataSet GetData(int id); + + // 支持多数据库操作 + [SqlExecute(@" + select * from person; + select * from student;"), SqlDbContextLocator(typeof(MySqlDbContextLocator)] + DataSet GetData(); + + // 异步方式 + [SqlExecute(@" + select * from person; + select * from student; + select 1;"), SqlDbContextLocator(typeof(MySqlDbContextLocator)] + Task GetDataAsync()); + } +} +``` + +### 9.18.4.4 返回 `Tuple` + +```cs showLineNumbers {8-10,14-16,20-22,26-28,32-35,38-42} +using Furion.DatabaseAccessor; + +namespace Furion.Application +{ + public interface ISql : ISqlDispatchProxy + { + // 执行sql并传入参数,基元类型 + [SqlExecute(@" + select * from person where id >@id and name like @name; + select top 10 * from student where Id >@id;")] + (List,List) GetData(int id, string name); + + // 执行sql并传入参数,对象类型 + [SqlExecute(@" + select * from person where id >@id and name like @name; + select top 10 * from student where Id >@id;")] + (List,List) GetData(MyParam paras); + + // 执行存储过程 sql,支持设置参数类型 + [SqlExecute(@" + exec PROP_NAME @id; + select * from person;", CommandType = CommandType.StoredProcedure)] + (List,List) GetData(int id); + + // 支持多数据库操作 + [SqlExecute(@" + select * from person; + select * from student;"), SqlDbContextLocator(typeof(MySqlDbContextLocator)] + (List,List) GetData(); + + // 异步方式 + [SqlExecute(@" + select * from person; + select * from student; + select 1;"), SqlDbContextLocator(typeof(MySqlDbContextLocator)] + Task<(List,List,List)> GetDataAsync(); + + // 自 v3.7.3+ 版本支持返回单个类类型参数 + [SqlExecute(@" + select * from person where id =@id; + select * from person")] + (Person, List) GetData(int id); // 注意返回值是 `(Person, List)` 组合 +} +``` + +### 9.18.4.5 返回 `单行单列` + +```cs showLineNumbers {7,10,13} +using Furion.DatabaseAccessor; + +namespace Furion.Application +{ + public interface ISql : ISqlDispatchProxy + { + [SqlExecute("select Name from person where id = @id")] + string GetValue(int id); + + [SqlExecute("select age from person where id = @id")] + int GetValue(int id); + + [SqlExecute("select Name from person where id = @id")] + Task GetValueAsync(int id); + } +} +``` + +### 9.18.4.6 无返回值 + +```cs showLineNumbers {7,10,13} +using Furion.DatabaseAccessor; + +namespace Furion.Application +{ + public interface ISql : ISqlDispatchProxy + { + [SqlExecute("insert into person(Name,Age) values(@name,@age)")] + void Insert(MyParam dto); + + [SqlExecute("delete from person where id = @id")] + void Delete(int id); + + [SqlExecute("update person set name=@name where id=@id")] + void Update(int id, string name); + } +} +``` + +### 9.18.4.7 返回单个类类型参数 + +:::important 版本说明 + +以下内容仅限 `Furion 3.7.1 +` 版本使用。 + +::: + +```cs showLineNumbers {3-5} +public interface ISql : ISqlDispatchProxy +{ + // 自 v3.7.3+ 版本支持返回单个类类型参数 + [SqlExecute("select * from person where id=@id")] + Person GetPerson(int id); +} +``` + +### 9.18.4.8 返回受影响行数 + +:::important 版本说明 + +以下内容仅限 `Furion 4.4.5 +` 版本使用。 + +::: + +需要在 `[SqlExcuete]` 特性中标记 `RowEffects = true` 且返回值是 `int` 或者 `Task`。 + +```cs showLineNumbers {4,8} +public interface ISql : ISqlDispatchProxy +{ + // 同步 + [SqlExecute("update person set age = 30 where id = {id}", RowEffects = true)] + int Update(int id); + + // 异步 + [SqlExecute("update person set age = 30 where id = {id}", RowEffects = true)] + Task UpdateAsync(int id); +} +``` + +### 9.18.4.9 返回 `IEnumerable` 或 `Array` 类型 + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.7.5 +` 版本使用。 + +::: + +```cs showLineNumbers {4,7,16} +public interface ISql : ISqlDispatchProxy +{ + [SqlExecute("select * from person")] + Person[] GetPersons(); + + [SqlExecute("select * from person")] + IEnumerable GetPersons2(); + + // 更复杂的组合 + [SqlExecute(@" +select * from person where id = 1; +select * from person; +select * from person where id > 0; +select * from person where id > 0; +")] + (Person, List, Person[], IEnumerable) GetPersons(); +} +``` + +## 9.18.5 `存储过程` 操作 + +### 9.18.5.1 返回 `DataTable` + +```cs showLineNumbers {7,10,13} +using Furion.DatabaseAccessor; + +namespace Furion.Application +{ + public interface ISql : ISqlDispatchProxy + { + [SqlProcedure("PROC_Name")] + DataTable GetPersons(MyParam dto); + + [SqlProcedure("PROC_Name")] + DataTable GetPersons(int id); + + [SqlProcedure("PROC_Name")] + DataTable GetPersons(int id, string name); + } +} +``` + +### 9.18.5.2 返回 `List` + +```cs showLineNumbers {7,10,13} +using Furion.DatabaseAccessor; + +namespace Furion.Application +{ + public interface ISql : ISqlDispatchProxy + { + [SqlProcedure("PROC_Name")] + List GetPersons(MyParam dto); + + [SqlProcedure("PROC_Name")] + List GetPersons(int id); + + [SqlProcedure("PROC_Name")] + List GetPersons(int id, string name); + } +} +``` + +### 9.18.5.3 返回 `DataSet` + +```cs showLineNumbers {7,10,13} +using Furion.DatabaseAccessor; + +namespace Furion.Application +{ + public interface ISql : ISqlDispatchProxy + { + [SqlProcedure("PROC_Name")] + DataSet GetData(MyParam dto); + + [SqlProcedure("PROC_Name")] + DataSet GetData(int id); + + [SqlProcedure("PROC_Name")] + DataSet GetData(int id, string name); + } +} +``` + +### 9.18.5.4 返回 `Tuple(T1,...T8)` + +```cs showLineNumbers {7,10,13,16-18} +using Furion.DatabaseAccessor; + +namespace Furion.Application +{ + public interface ISql : ISqlDispatchProxy + { + [SqlProcedure("PROC_Name")] + (List, List) GetData(MyParam dto); + + [SqlProcedure("PROC_Name")] + (List, List) GetData(int id); + + [SqlProcedure("PROC_Name")] + (List, List, Person, int) GetData(int id, string name); + + // 自 v3.7.3+ 版本支持返回单个类类型参数 + [SqlProcedure(@"PROC_Name)] + (Person, List) GetData(int id); // 注意返回值是 `(Person, List)` 组合 + } +} +``` + +### 9.18.5.5 返回 `单行单列` + +```cs showLineNumbers {7,10,13} +using Furion.DatabaseAccessor; + +namespace Furion.Application +{ + public interface ISql : ISqlDispatchProxy + { + [SqlProcedure("PROC_Name")] + object GetValue(MyParam dto); + + [SqlProcedure("PROC_Name")] + string GetValue(int id); + + [SqlProcedure("PROC_Name")] + int GetValue(int id, string name); + } +} +``` + +### 9.18.5.6 无返回值 + +```cs showLineNumbers {7,10,13} +using Furion.DatabaseAccessor; + +namespace Furion.Application +{ + public interface ISql : ISqlDispatchProxy + { + [SqlProcedure("PROC_Name")] + void GetValue(MyParam dto); + + [SqlProcedure("PROC_Name")] + void GetValue(int id); + + [SqlProcedure("PROC_Name")] + void GetValue(int id, string name); + } +} +``` + +### 9.18.5.7 带 `OUTPUT/RETURN` 返回 + +```cs showLineNumbers {7,10,13} +using Furion.DatabaseAccessor; + +namespace Furion.Application +{ + public interface ISql : ISqlDispatchProxy + { + [SqlProcedure("PROC_Name")] + ProcedureOutputResult GetOutput(ProcOutputModel pams); + + [SqlProcedure("PROC_Name")] + ProcedureOutputResult GetOutput(ProcOutputModel pams); + + [SqlProcedure("PROC_Name")] + ProcedureOutputResult<(List, List)> GetOutput(ProcOutputModel pams); + } +} +``` + +### 9.18.5.8 返回单个类类型参数 + +:::important 版本说明 + +以下内容仅限 `Furion 3.7.1 +` 版本使用。 + +::: + +```cs showLineNumbers {3-5} +public interface ISql : ISqlDispatchProxy +{ + // 自 v3.7.3+ 版本支持返回单个类类型参数 + [SqlProcedure("PROC_Name")] + Person GetPerson(int id); +} +``` + +### 9.18.5.9 返回 `IEnumerable` 或 `Array` 类型 + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.7.5 +` 版本使用。 + +::: + +```cs showLineNumbers {4,7,11} +public interface ISql : ISqlDispatchProxy +{ + [SqlProcedure("PROC_Name")] + Person[] GetPersons(); + + [SqlProcedure("PROC_Name")] + IEnumerable GetPersons2(); + + // 更复杂的组合 + [SqlProcedure("PROC_Name")] + (Person, List, Person[], IEnumerable) GetPersons(); +} +``` + +## 9.18.6 `函数` 操作 + +```cs showLineNumbers {7,10} +using Furion.DatabaseAccessor; + +namespace Furion.Application +{ + public interface ISql : ISqlDispatchProxy + { + [SqlFunction("FN_Name")] // 标量函数 + string GetValue(MyParam dto); + + [SqlProcedure("FN_Name")] // 表值函数 + List GetPersons(int id); + } +} +``` + +:::important 补充说明 + +`Sql` 代理会自动判断返回值然后自动执行特定函数类型。 + +::: + +## 9.18.7 `Sql` 模板替换 + +在最新的 `1.18.3` 版本中提供了模板替换功能,如: + +```cs showLineNumbers +[SqlExecute("select * from person where id > {id} and name like {name} and age > {user.Age}")] +List GetPerson(int id, string name, User user); +``` + +:::important 两者区别 + +模板字符串有别于命令参数替换,模板字符串采用 `{ }` 方式,运行时直接替换为实际的内容, `@` 而是转换成 `DbParameter` 参数。 + +::: + +## 9.18.8 切换数据库 + +`Sql` 代理方式的支持三种切换数据库的方式: + +### 9.18.8.1 单个方法方式 + +主要通过在方法上贴 `[SqlDbContextLocator]` 特性 + +```cs showLineNumbers {1} +[SqlExecute("select * from person"), SqlDbContextLocator(typeof(MySqlDbContextLocator)] +List GetPerson(); +``` + +### 9.18.8.2 接口方式 + +在接口中贴 `[SqlDbContextLocator]` 特性,此方式下,接口所有方法将采用指定的数据库执行。 + +```cs showLineNumbers {1} +[SqlDbContextLocator(typeof(MySqlDbContextLocator)] +public interface ISql : ISqlDispatchProxy +{ + [SqlFunction("FN_Name")] // 标量函数 + string GetValue(MyParam dto); + + [SqlProcedure("FN_Name")] // 表值函数 + List GetPersons(int id); +} +``` + +### 9.18.8.3 运行时 `.Change` 方法切换 + +除了以上两种 `静态` 配置方式,`Furion` 框架还提供 `动态` 方式,如: + +```cs showLineNumbers {2,6} +// 将 sql 代理数据库切换成特定数据库 +_sql.Change(); +_sql.GetPerson(); + +// 多次切换 +_sql.Change(); +_sql.GetPerson(); + +// 还支持重置数据库上下文定位器为初始状态 +_sql.ResetIt(); +_sql.GetPerson(); +``` + +:::important 关于优先级问题 + +`.Change<>` 优先级大于 `方法贴 [SqlDbContextLocator]` 大于 `接口贴 [SqlDbContextLocator]`。 + +默认情况下,不指定 `DbContextLocator` 属性,则为 `MasterDbContextLocator`。 + +::: + +## 9.18.9 `Sql` 代理拦截 + +在 `Furion v2.13 +` 版本新增了 `Sql` 代理拦截功能,可以篡改特定方法或所有代理方法实际执行的参数,如 `sql语句、参数、执行对象等等`。 + +**若在 `Sql` 代理中实现拦截功能,必须满足两个条件**: + +- 方法必须是 `static` 静态方法且返回值为 `void` 且只有一个 `SqlProxyMethod` 参数 +- 方法必须贴 `[Interceptor]` 特性 + +如: + +```cs showLineNumbers {9,13-17,20-24,26-30,32-36} +public interface ISql : ISqlDispatchProxy +{ + [SqlFunction("FN_Name")] + string GetValue(MyParam dto); + + [SqlProcedure("FN_Name")] + List GetPersons(int id); + + [SqlExecute("select name from person", InterceptorId = "GetPersonsByName")] // 通过 InterceptorId 解决方法名重载问题 + Task> GetPersons(); + + // 只拦截 GetValue 方法 + [Interceptor(nameof(GetValue))] + static void 拦截1(SqlProxyMethod method) + { + method.FinalSql += " where id > 1"; // 篡改最终执行 sql + } + + // 拦截 GetValue 和 GetPersons 方法 + [Interceptor(nameof(GetValue), nameof(GetPersons))] + static void 拦截2(SqlProxyMethod method) + { + method.FinalSql += " where id > 1"; // 篡改最终执行 sql + } + + [Interceptor("GetPersonsByName")] // 对应上面的 InterceptorId 配置 + static void 解决方法名重载拦截(SqlProxyMethod method) + { + // 。。。 + } + + [Interceptor] + static void 全局拦截(SqlProxyMethod method) + { + // 这里会拦截所有的方法 + } +} +``` + +## 9.18.10 设置超时时间 + +```cs showLineNumbers +[Timeout(1000)] +public interface ISql : ISqlDispatchProxy +{ + [SqlFunction("FN_Name"), Timeout(500)] // 单位秒 + string GetValue(MyParam dto); +} +``` + +## 9.18.11 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/dbcontext-sql-template.mdx b/handbook/docs/dbcontext-sql-template.mdx new file mode 100644 index 0000000000000000000000000000000000000000..f3d2dc0ae645da6d865adbd642b0c9f36bc66014 --- /dev/null +++ b/handbook/docs/dbcontext-sql-template.mdx @@ -0,0 +1,117 @@ +--- +id: dbcontext-sql-template +title: 9.17 Sql 模板 +sidebar_label: 9.17 Sql 模板 +--- + +## 9.17.1 `Sql` 模板 + +通常我们程序中执行数据库的 `sql` 语句都写在了程序集中,随程序一起编译,后续需要修改,则重新编译代码。 + +所以,`Furion` 推出一种 `Sql` 模板的方式,程序执行 `Sql` 时,只需要使用特殊标记即可:`#(模板Key)`,这些真实的 `Sql` 配置在 `.json` 或 `.xml` 配置文件中。 + +如: + +- **Json** 方式 + +```json showLineNumbers +{ + "Select.User": "select * from User where id > @id" +} +``` + +- **Xml** 方式 + +```xml + + + select * from User where id > @id + +``` + +## 9.17.2 `Sql` 模板优缺点 + +### 9.17.2.1 优点 + +- 支持 `Sql` 动态配置,可在程序运行时动态调配 `Sql` 语句 +- 支持程序 `Sql` 语句加密 + +### 9.17.2.2 缺点 + +- 需增加对应配置文件 +- 不容易调试 `Sql` 代码 + +## 9.17.3 如何使用 + +在执行 `Sql` 的时候,只需要填写指定模板即可。 + +### 9.17.3.1 常规使用 + +```cs showLineNumbers +// 仓储方式 +var users = repository.SqlQuery("#(Select.User)", new { id = 1}); + +// 懒人方式 +var users = "#(Select.User)".SqlQuery(new { id = 1}); + +// Sql 代理方式 +[SqlExecute("#(Select.User)")] +List GetUser(int id); +``` + +### 9.17.3.2 高级嵌套 + +```cs showLineNumbers +var users = repository.SqlQuery( +@"select * from user u +left join #(User.Detail) d on u.Id = d.UserId +where id > @id"); +``` + +## 9.17.4 `Sql` 模板配置 + +### 9.17.4.1 普通模式 + +```json showLineNumbers +{ + "Select.User": "select * from User" +} +``` + +### 9.17.4.2 更多配置 + +:::warning 功能移除声明 + +以下内容在 `Furion 2.13 +` 版本中已移除。 + +::: + +```json showLineNumbers +{ + "Select.User": { + "Sql": "select * from User where id > @id and Name = @name", + "Params": [ + { + "Name": " Id", + "Value": "1", + "DbType": "Int16", + "Size": 10 + }, + { + "Name": " Name", + "Value": "百小僧", + "DbType": "String", + "Size": 10 + } + ] + } +} +``` + +## 9.17.5 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/dbcontext-sql.mdx b/handbook/docs/dbcontext-sql.mdx new file mode 100644 index 0000000000000000000000000000000000000000..5ab156e6a0e498ed99d63bd54595b82a9010f34f --- /dev/null +++ b/handbook/docs/dbcontext-sql.mdx @@ -0,0 +1,791 @@ +--- +id: dbcontext-sql +title: 9.16 Sql 操作 +sidebar_label: 9.16 Sql 操作 +--- + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **问题修复** + + -  修复 使用达梦数据库执行 `sql` 不能自动修复命令参数前缀 4.8.7.18 ⏱️2023.03.21 [#I6OK4T](https://gitee.com/dotnetchina/Furion/issues/I6OK4T) + +
+
+
+ +:::important 温馨提示 + +推荐使用 《[9.18 Sql 高级代理](dbcontext-sql-proxy.mdx)》代替本章节功能。`Sql 高级代理` 能够提供更容易且更易维护的方式。 + +::: + +:::note 例子说明 + +本章节例子均以 `SqlServer` 数据库写的例子,命令参数统一用 `@` 符号,但不同数据库的参数前缀有所不同,如:`sql server` 采用 `@`,`Oracle/DM` 采用 `:`,my sql 采用 `?`。 + +::: + +## 9.16.1 关于 `Sql` + +`Furion` 框架提供非常多且灵活的 `sql` 操作方法,且性能不输于 `dapper`,同时接近 `ADO.NET` 原生操作。 + +## 9.16.2 懒人无敌 `Sql` 🐮 + +### 9.16.2.1 返回 `DataTable` + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +var dataTable = "select * from person".SqlQuery(); + +// 示例二 +var dataTable = "select top 10 * from person where id > @id".SqlQuery(new {id = 10}); + +// 示例三 +var dataTable = "select Id, Name, Age from person where name like @name".SqlQuery(new Dictionary{ {"name", "%Furion%"} }); + +// 示例四 +var dataTable = "select * from person where name=@name limit 1,10".SqlQuery(new []{ new MySqlParameter("name","Furion") }); + +// 示例五 +var dataTable = "select * from person where id>@id and name like @name".SqlQuery(new YourModel { Id = 1, Name = "%Furion%" }); + +// 示例六 +var dataTable = "exec PROC_GetPerson @id".SqlQuery(new {id = 10}); + +// 示例七 +var dataTable = "select * from FN_GetPersons(@id)".SqlQuery(new {id = 10}); + +// 示例八 +var dataTable = @" +select * from person p +left join personDetail pd on p.Id == pd.pid +where p.Id > @id;".SqlQuery(new {id = 10}); + +// ==== 异步操作 ==== + +// 示例一 +var dataTable = await "select * from person".SqlQueryAsync(); + +// 示例二 +var dataTable = await "select top 10 * from person where id > @id".SqlQueryAsync(new {id = 10}); + +// 示例三 +var dataTable = await "select Id, Name, Age from person where name like @name".SqlQueryAsync(new Dictionary{ {"name", "%Furion%"} }); + +// 示例四 +var dataTable = await "select * from person where name=@name limit 1,10".SqlQueryAsync(new []{ new MySqlParameter("name","Furion") }); + +// 示例五 +var dataTable = await "select * from person where id>@id and name like @name".SqlQueryAsync(new YourModel { Id = 1, Name = "%Furion%" }); + +// 示例六 +var dataTable = await "exec PROC_GetPerson @id".SqlQueryAsync(new {id = 10}); + +// 示例七 +var dataTable = await "select * from FN_GetPersons(@id)".SqlQueryAsync(new {id = 10}); + +// 示例八 +var dataTable = await @" +select * from person p +left join personDetail pd on p.Id == pd.pid +where p.Id > @id;".SqlQueryAsync(new {id = 10}); +``` + +### 9.16.2.2 返回 `List` + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +var persons = "select * from person".SqlQuery(); + +// 示例二 +var persons = "select top 10 * from person where id > @id".SqlQuery(new {id = 10}); + +// 示例三 +var persons = "select Id, Name, Age from person where name like @name".SqlQuery(new Dictionary{ {"name", "%Furion%"} }); + +// 示例四 +var persons = "select * from person where name=@name limit 1,10".SqlQuery(new []{ new MySqlParameter("name","Furion") }); + +// 示例五 +var persons = "select * from person where id>@id and name like @name".SqlQuery(new YourModel { Id = 1, Name = "%Furion%" }); + +// 示例六 +var persons = "exec PROC_GetPerson @id".SqlQuery(new {id = 10}); + +// 示例七 +var persons = "select * from FN_GetPersons(@id)".SqlQuery(new {id = 10}); + +// 示例八 +var persons = @" +select * from person p +left join personDetail pd on p.Id == pd.pid +where p.Id > @id;".SqlQuery(new {id = 10}); + +// ==== 异步操作 ==== + +// 示例一 +var persons = await "select * from person".SqlQueryAsync(); + +// 示例二 +var persons = await "select top 10 * from person where id > @id".SqlQueryAsync(new {id = 10}); + +// 示例三 +var persons = await "select Id, Name, Age from person where name like @name".SqlQueryAsync(new Dictionary{ {"name", "%Furion%"} }); + +// 示例四 +var persons = await "select * from person where name=@name limit 1,10".SqlQueryAsync(new []{ new MySqlParameter("name","Furion") }); + +// 示例五 +var persons = await "select * from person where id>@id and name like @name".SqlQueryAsync(new YourModel { Id = 1, Name = "%Furion%" }); + +// 示例六 +var persons = await "exec PROC_GetPerson @id".SqlQueryAsync(new {id = 10}); + +// 示例七 +var persons = await "select * from FN_GetPersons(@id)".SqlQueryAsync(new {id = 10}); + +// 示例八 +var persons = await @" +select * from person p +left join personDetail pd on p.Id == pd.pid +where p.Id > @id;".SqlQueryAsync(new {id = 10}); +``` + +### 9.16.2.3 返回 `DataSet` + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +var dataSet = @" +select * from person; +select * from student;".SqlQueries(); + +// 示例二 +var dataSet = @" +select * from person where Id > @id; +select * from student where Name like @name;".SqlQueries(new {id = 1, name = "%furion%"}); + +// 示例三 +var dataSet = @" +select * from person; +exec PROC_GetStudents(@id); +select 'Furion'; +select * from FN_GetPerson(@id);".SqlQueries(new {id = 1}); + +// ==== 异步操作 ==== + +// 示例一 +var dataSet = await @" +select * from person; +select * from student;".SqlQueriesAsync(); + +// 示例二 +var dataSet = await @" +select * from person where Id > @id; +select * from student where Name like @name;".SqlQueriesAsync(new {id = 1, name = "%furion%"}); + +// 示例三 +var dataSet = await @" +select * from person; +exec PROC_GetStudents(@id); +select 'Furion'; +select * from FN_GetPerson(@id);".SqlQueriesAsync(new {id = 1}); +``` + +### 9.16.2.4 返回 `Tuple` + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +var (persons, students) = @" +select * from person; +select * from student;".SqlQueries(); + +// 示例二 +var (persons, students) = @" +select * from person where Id > @id; +select * from student where Name like @name;".SqlQueries(new {id = 1, name = "%furion%"}); + +// 示例三 +var (persons, students, string, PersonDto) = @" +select * from person; +exec PROC_GetStudents(@id); +select 'Furion'; +select * from FN_GetPerson(@id);".SqlQueries(new {id = 1}); + +// ==== 异步操作 ==== + +// 示例一 +var (persons, students) = await @" +select * from person; +select * from student;".SqlQueriesAsync(); + +// 示例二 +var (persons, students) = await @" +select * from person where Id > @id; +select * from student where Name like @name;".SqlQueriesAsync(new {id = 1, name = "%furion%"}); + +// 示例三 +var (persons, students, string, PersonDto) = await @" +select * from person; +exec PROC_GetStudents(@id); +select 'Furion'; +select * from FN_GetPerson(@id);".SqlQueriesAsync(new {id = 1}); +``` + +### 9.16.2.5 返回 单行单列 + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +var value = "select Name from person where id = @id".SqlScalar(new {id = 1}); + +// 示例二 +var value = "select Name from person where id = @id".SqlScalar(new {id = 1}); + +// 示例三 +var value = "select Age from person where id = @id".SqlScalar(new {id = 1}); + +// ==== 异步操作 ==== + +// 示例一 +var value = await "select Name from person where id = @id".SqlScalarAsync(new {id = 1}); + +// 示例二 +var value = await "select Name from person where id = @id".SqlScalarAsync(new {id = 1}); + +// 示例三 +var value = await "select Age from person where id = @id".SqlScalarAsync(new {id = 1}); +``` + +### 9.16.2.6 返回 受影响行数 + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +var rowEffects = "insert into person(Name,Age,Address) values(@name,@age,@address)".SqlNonQuery(person); + +// 示例二 +var rowEffects = @" +insert into person(Name,Age,Address) values(@name,@age,@address); +insert into person(Name,Age,Address) values(@name,@age,@address);".SqlNonQuery(persons); + +// 示例三 +var rowEffects = "update person set name=@name where id=@id".SqlNonQuery(new {id=1, name="百小僧"}); + +// 示例四 +var rowEffects = "delete from person where @id > 10".SqlNonQuery(new {id=1}); + +// ==== 异步操作 ==== + +// 示例一 +var rowEffects = await "insert into person(Name,Age,Address) values(@name,@age,@address)".SqlNonQueryAsync(person); + +// 示例二 +var rowEffects = @" +insert into person(Name,Age,Address) values(@name,@age,@address); +insert into person(Name,Age,Address) values(@name,@age,@address);".SqlNonQueryAsync(persons); + +// 示例三 +var rowEffects = await "update person set name=@name where id=@id".SqlNonQueryAsync(new {id=1, name="百小僧"}); + +// 示例四 +var rowEffects = await "delete from person where @id > 10".SqlNonQueryAsync(new {id=1}); +``` + +## 9.16.3 懒人无敌 `存储过程` 🐮 + +### 9.16.3.1 返回 `DataTable` + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +var dataTable = "PROC_Name".SqlProcedureQuery(); + +// 示例二 +var dataTable = "PROC_Name".SqlProcedureQuery(new {id = 1}); + +// 示例三 +var dataTable = "PROC_Name".SqlProcedureQuery(new {id = 1, age = 27}); + +// ==== 异步操作 ==== + +// 示例一 +var dataTable = await "PROC_Name".SqlProcedureQueryAsync(); + +// 示例二 +var dataTable = await "PROC_Name".SqlProcedureQueryAsync(new {id = 1}); + +// 示例三 +var dataTable = await "PROC_Name".SqlProcedureQueryAsync(new {id = 1, age = 27}); +``` + +### 9.16.3.2 返回 `List` + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +var persons = "PROC_Name".SqlProcedureQuery(); + +// 示例二 +var persons = "PROC_Name".SqlProcedureQuery(new {id = 1}); + +// 示例三 +var persons = "PROC_Name".SqlProcedureQuery(new {id = 1, age = 27}); + +// ==== 异步操作 ==== + +// 示例一 +var persons = await "PROC_Name".SqlProcedureQueryAsync(); + +// 示例二 +var persons = await "PROC_Name".SqlProcedureQueryAsync(new {id = 1}); + +// 示例三 +var persons = await "PROC_Name".SqlProcedureQueryAsync(new {id = 1, age = 27}); +``` + +### 9.16.3.3 返回 `DataSet` + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +var dataSet = "PROC_Name".SqlProcedureQueries(); + +// 示例二 +var dataSet = "PROC_Name".SqlProcedureQueries(new {id = 1}); + +// 示例三 +var dataSet = "PROC_Name".SqlProcedureQueries(new {id = 1, age = 27}); + +// ==== 异步操作 ==== + +// 示例一 +var dataSet = await "PROC_Name".SqlProcedureQueriesAsync(); + +// 示例二 +var dataSet = await "PROC_Name".SqlProcedureQueriesAsync(new {id = 1}); + +// 示例三 +var dataSet = await "PROC_Name".SqlProcedureQueriesAsync(new {id = 1, age = 27}); +``` + +### 9.16.3.4 返回 `Tuple` + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +var persons = "PROC_Name".SqlProcedureQueries(); + +// 示例二 +var (persons,students) = "PROC_Name".SqlProcedureQueries(new {id = 1}); + +// 示例三 +var (persons,students,string) = "PROC_Name".SqlProcedureQueries(new {id = 1, age = 27}); + +// 示例四 +var (persons,students,personDetail,string) = "PROC_Name".SqlProcedureQueries(new {id = 1, age = 27}); + +// ==== 异步操作 ==== + +// 示例一 +var persons = await "PROC_Name".SqlProcedureQueriesAsync(); + +// 示例二 +var (persons,students) = await "PROC_Name".SqlProcedureQueriesAsync(new {id = 1}); + +// 示例三 +var (persons,students,string) = await "PROC_Name".SqlProcedureQueriesAsync(new {id = 1, age = 27}); + +// 示例四 +var (persons,students,personDetail,string) = await "PROC_Name".SqlProcedureQueriesAsync(new {id = 1, age = 27}); +``` + +### 9.16.3.5 返回 单行单列 + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +var value = "PROC_Name".SqlProcedureScalar(new {id = 1}); + +// 示例二 +var value = "PROC_Name".SqlProcedureScalar(new {id = 1, name = "新生帝", address ="广东省中山市"}); + +// 示例三 +var value = "PROC_Name".SqlProcedureScalar(new {id = 1, address ="广东省中山市"}); + +// ==== 异步操作 ==== + +// 示例一 +var value = await "PROC_Name".SqlProcedureScalarAsync(new {id = 1}); + +// 示例二 +var value = await "PROC_Name".SqlProcedureScalarAsync(new {id = 1, name = "新生帝", address ="广东省中山市"}); + +// 示例三 +var value = await "PROC_Name".SqlProcedureScalarAsync(new {id = 1, address ="广东省中山市"}); +``` + +### 9.16.3.6 返回 受影响行数 + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +var rowEffects = "PROC_Name".SqlProcedureNonQuery(person); + +// 示例二 +var rowEffects = "PROC_Name".SqlProcedureNonQuery(new {id = 1, name = "新生帝", address ="广东省中山市"}); + +// 示例三 +var rowEffects = "PROC_Name".SqlProcedureNonQuery(new {id=1, name="百小僧"}); + +// 示例四 +var rowEffects = "PROC_Name".SqlProcedureNonQuery(new {id=1}); + +// ==== 异步操作 ==== + +// 示例一 +var rowEffects = await "PROC_Name".SqlProcedureNonQueryAsync(person); + +// 示例二 +var rowEffects = await "PROC_Name".SqlProcedureNonQueryAsync(new {id = 1, name = "新生帝", address ="广东省中山市"}); + +// 示例三 +var rowEffects = await "PROC_Name".SqlProcedureNonQueryAsync(new {id=1, name="百小僧"}); + +// 示例四 +var rowEffects = await "PROC_Name".SqlProcedureNonQueryAsync(new {id=1}); +``` + +### 9.16.3.7 带 `OUTPUT/RETURN` 返回 + +```sql showLineNumbers {3,4,10-12,15-17,22} +CREATE PROC PROC_Output + @Id INT, // 输入参数 + @Name NVARCHAR(32) OUTPUT, // 输出参数,还带长度 + @Age INT OUTPUT // 输出参数 +AS +BEGIN + SET @Name = 'Furion Output'; + + // 输出结果集 + SELECT * + FROM dbo.Test + WHERE Id > @Id; + + // 输出结果集 + SELECT TOP 10 + * + FROM dbo.Test; + + SET @Age = 27; + + // 带 RETURN 返回 + RETURN 10; +END; +``` + +```cs showLineNumbers {1,10,13,16} +using Furion.DatabaseAccessor; +using System.Data; + +namespace Furion.Application +{ + public class ProcOutputModel + { + public int Id { get; set; } // 输入参数 + + [DbParameter(ParameterDirection.Output, Size = 32)] + public string Name { get; set; } // 输出参数 + + [DbParameter(ParameterDirection.Output)] + public int Age { get; set; } // 输出参数 + + [DbParameter(ParameterDirection.ReturnValue)] + public int ReturnValue { get; set; } // 返回值 + } +} +``` + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +ProcedureOutputResult result = "PROC_Name".SqlProcedureOutput(new ProcOutputModel{ Id=1}); + +// 示例二 +ProcedureOutputResult result = "PROC_Name".SqlProcedureOutput(new ProcOutputModel{ Id=1}); + +// 示例三 +ProcedureOutputResult<(List, List)> result = "PROC_Name".SqlProcedureOutput<(List, List)>(new ProcOutputModel{ Id=1}); + +// ==== 异步操作 ==== +// 示例一 +ProcedureOutputResult result = await "PROC_Name".SqlProcedureOutputAsync(new ProcOutputModel{ Id=1}); + +// 示例二 +ProcedureOutputResult result = await "PROC_Name".SqlProcedureOutputAsync(new ProcOutputModel{ Id=1}); + +// 示例三 +ProcedureOutputResult<(List, List)> result = await "PROC_Name".SqlProcedureOutputAsync<(List, List)>(new ProcOutputModel{ Id=1}); +``` + +## 9.16.4 懒人无敌 `函数` 🐮 + +### 9.16.4.1 `标量函数` + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +var value = "FN_Name".SqlFunctionScalar(); + +// 示例二 +var value = "FN_Name".SqlFunctionScalar(new {id = 1}); + +// 示例三 +var value = "FN_Name".SqlFunctionScalar(); + +// 示例四 +var value = "FN_Name".SqlFunctionScalar(new {id = 1}); + +// ==== 异步操作 ==== + +// 示例一 +var value = await "FN_Name".SqlFunctionScalarAsync(); + +// 示例二 +var value = await "FN_Name".SqlFunctionScalarAsync(new {id = 1}); + +// 示例三 +var value = await "FN_Name".SqlFunctionScalarAsync(); + +// 示例四 +var value = await "FN_Name".SqlFunctionScalarAsync(new {id = 1}); +``` + +### 9.16.4.2 `表值函数` + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +var dataTable = "FN_Name".SqlFunctionQuery(); + +// 示例二 +var dataTable = "FN_Name".SqlFunctionQuery(new {id = 1}); + +// 示例三 +var persons = "FN_Name".SqlFunctionQuery(); + +// 示例四 +var persons = "FN_Name".SqlFunctionQuery(new {id = 1}); + +// ==== 异步操作 ==== + +// 示例一 +var dataTable = await "FN_Name".SqlFunctionQueryAsync(); + +// 示例二 +var dataTable = await "FN_Name".SqlFunctionQueryAsync(new {id = 1}); + +// 示例三 +var persons = await "FN_Name".SqlFunctionQueryAsync(); + +// 示例四 +var persons = await "FN_Name".SqlFunctionQueryAsync(new {id = 1}); +``` + +## 9.16.5 设置超时时间 + +```cs showLineNumbers +var data = "select * from table".SetCommandTimeout(100).SqlQuery(); // 单位秒 +``` + +## 9.16.6 `ISqlRepository` 操作 + +`ISqlRepository` 仓储是专门处理 `Sql` 操作的,无需实体方式,所有接口和 `懒人无敌` 方式一样: + +```cs showLineNumbers +// 示例一 +var dataTable = sqlRepository.SqlQuery("select * from person"); + +// 示例二 +var dataTable = sqlRepository.SqlQuery("select * from person where id > @id", new { id = 10}); + +// 示例四 +var persons = sqlRepository.SqlQuery("select * from person"); + +// 示例五 +var persons = sqlRepository.SqlQuery("select * from person where id > @id", new { id = 10}); + +// 不再举例子。。。 +``` + +:::note 补充说明 + +不管是哪种方式操作 `Sql` ,方法名参数都是一致的,如: + +- `SqlQuery` +- `SqlQueryAsync` +- `SqlQueries` +- `SqlQueriesAsync` +- `SqlNonQuery` +- `SqlNonQueryAsync` +- `SqlScalar` +- `SqlScalarAsync` +- `SqlProcedureQuery` +- `SqlProcedureQueryAsync` +- `SqlProcedureQueries` +- `SqlProcedureQueriesAsync` +- `SqlProcedureScalar` +- `SqlProcedureScalarAsync` +- `SqlProcedureNonQuery` +- `SqlProcedureNonQueryAsync` +- `SqlProcedureOutput` +- `SqlProcedureOutputAsync` +- `SqlFunctionScalar` +- `SqlFunctionScalarAsync` +- `SqlFunctionQuery` +- `SqlFunctionQuery` + +::: + +## 9.16.7 `IRepository` 操作 + +`IRepository` 也能操作 `sql`,调用方法也是和上面一致的,如: + +```cs showLineNumbers +var dataTable = repository.Sql().SqlQuery("select * from person"); +``` + +:::note 特别说明 + +由于篇幅有限,不再列举所有例子。 + +::: + +## 9.16.8 `IRepository` 操作 + +`IRepository` 也能操作 `sql`,调用方法也是和上面一致的,如: + +```cs showLineNumbers +var dataTable = personRepository.SqlQuery("select * from person"); +``` + +:::note 特别说明 + +由于篇幅有限,不再列举所有例子。 + +::: + +## 9.16.9 关于 `Sql` 参数 + +所有 `sql`、`存储过程`,`函数` 参数都支持四种方式: + +- `DbParameter[]`:数组类型 +- `new {}`:匿名类型 +- `new Class{}`:强类型类型(支持复杂存储过程参数) +- `Dictionary` 类型 + +:::tip 小知识 + +建议除了复杂的存储过程(带 `OUTPUT/RETURN`)的以外,所有参数建议使用 `new {}` 匿名类型,如果需要动态参数,则可以使用 `Dictionary` 类型。 + +::: + +:::important 参数大小写问题 + +由于不同数据库对查询参数大小写问题处理不一致,所以**建议所有查询参数和参数名或属性名完全一致**。 + +::: + +## 9.16.10 多数据库 `Sql` 操作 💯 💛 + +`Furion` 框架拥有非常灵活的多数据库操作方式,只需通过多**数据库上下文定位器**即可动态切换数据库。 + +### 9.16.10.1 懒人无敌 🐮 方式 + +```cs showLineNumbers +var dataTable = "select * from person".Change().SqlQuery(); + +var persons = "select * from person whre id > @id".Change().SqlQuery(); +``` + +:::important 补充说明 + +懒人方式 只需要通过 `Change` 方式即可动态切换数据库。 + +::: + +### 9.16.10.2 `ISqlRepository` 方式 + +只需要通过 `ISqlRepository` 注入或通过 `sqlRepository.Change()` 切换。 + +### 9.16.10.3 `IRepository` 方式 + +只需要通过 `repository.Change()` 获取即可。 + +### 9.16.10.4 `IRepository` 方式 + +只需要通过 `IRepository` 注入或通过 `personRepository.Change()` 切换。 + +## 9.16.11 切换数据库 + +在 `Furion` 框架中,不管是懒人模式还是仓储模式都是通过 `.Change` 方式切换数据库,如: + +```cs showLineNumbers +// 懒人模式 +var data = "select * from table".Change().SqlQuery(); + +// 仓储方式 +var data = req.Change().SqlQuery("select * from table"); +``` + +## 9.16.12 多线程共享作用域 + +默认情况下,所有的 `字符串` 和 `实体` 拓展都有自己独立维护的 `ServiceProvider` 作用域。 + +在 `Web` 请求中,默认是 `HttpContext.RequestServices`,但在 `非 Web`,如多线程操作,后台任务,事件总线等场景下会自动创建新的作用域,实际上这是非常不必要的内存开销。 + +这时,我们只需要通过 `.SetXXXScoped(service)` 共享当前服务提供器作用域即可,如: + +```cs showLineNumbers +Scoped.Create((fac, scope) => { + "select * from table".SetContextScoped(scope.ServiceProvider).SqlQuery(); +}); +``` + +## 9.16.13 静态 `Default` 方式构建 + +```cs showLineNumbers +SqlExecutePart.Default.SetSqlString("select * from person").SqlQuery(); +``` + +## 9.16.14 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/dbcontext-start.mdx b/handbook/docs/dbcontext-start.mdx new file mode 100644 index 0000000000000000000000000000000000000000..799dce418caf0e9c12341d7a006d9c001d63267d --- /dev/null +++ b/handbook/docs/dbcontext-start.mdx @@ -0,0 +1,207 @@ +--- +id: dbcontext-start +title: 9.1 入门简要 +sidebar_label: 9.1 入门简要 ✨ +--- + +## 9.1.1 入门准备 + +:::note 入门条件 + +对 `EF/EFCore` 有一定基础了解,还未接触的可先看 [【EFCore 基础】](https://docs.microsoft.com/zh-cn/ef/core/) + +::: + +### 9.1.1.1 安装对应数据库包 + +:::tip + +在本章节所在的类别中,所有数据库操作功能均需要依赖 `EntityFramework Core` 框架,但 `Furion` 框架底层并未集成 `EntityFramework Core` 包,而是采用动态加载程序集方式自动载入。 + +::: + +**所以,如需使用本大类数据库功能,需安装对应的 `EntityFramework Core` 数据库包:** + +- `SqlServer`:`Microsoft.EntityFrameworkCore.SqlServer` (支持 SqlServer 2005 +) +- `Sqlite`:`Microsoft.EntityFrameworkCore.Sqlite` +- `Cosmos`:`Microsoft.EntityFrameworkCore.Cosmos` +- `InMemoryDatabase`:`Microsoft.EntityFrameworkCore.InMemory` +- `MySql` + - `Pomelo.EntityFrameworkCore.MySql`:(支持 MySql 5.x +) + - `MySql.EntityFrameworkCore`:支持 (MySql 8.x +) +- `PostgreSQL`:`Npgsql.EntityFrameworkCore.PostgreSQL` +- `Oracle`:`Oracle.EntityFrameworkCore` (支持 Oracle 10 +) +- `Firebird`:`FirebirdSql.EntityFrameworkCore.Firebird` +- `Dm`:`Microsoft.EntityFrameworkCore.Dm` + +:::tip 小知识 + +这些数据库包应该安装在 `YourProject.EntityFramework.Core` 层。特殊情况需安装在 `YourProject.Core` 层中,如 `Mysql` `HasCharset()` 配置。 + +::: + +### 9.1.1.2 创建数据库上下文 + +```cs showLineNumbers {7} +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore; + +namespace Furion.EntityFramework.Core +{ + [AppDbContext("Sqlite3ConnectionString", DbProvider.Sqlite)] + public class DefaultDbContext : AppDbContext + { + public DefaultDbContext(DbContextOptions options) : base(options) + { + } + } +} +``` + +:::important 特别注意 + +每一个数据库上下文都需继承 `AppDbContext` 基类,默认数据库的 `TDbContextLocator` 为 `MasterDbContextLocator`,无需显式编写。 + +非默认数据库则需显式继承并申明,如:`AppDbContext`。 + +::: + +想了解更多可查阅 【[数据库上下文定位器](./dbcontext-locator)】 章节。 + +### 9.1.1.3 配置连接字符串 + +`Furion` 框架提供多种方式配置数据库连接字符串: + +- `appsettings.json` 中配置 + +```json showLineNumbers {2-3} +{ + "ConnectionStrings": { + "Sqlite3ConnectionString": "Data Source=./Furion.db" + } +} +``` + +- 自定义 `.json` 文件配置 + +```json showLineNumbers {2-3} +{ + "ConnectionStrings": { + "Sqlite3ConnectionString": "Data Source=./Furion.db" + } +} +``` + +该方式和在 `appsettings.json` 的区别是自定义的 `.json` 文件不会自动添加到项目中,**须在 `Visual Studio` 中配置 `.json` 右键属性,设置 `复制` 输出目录为 `如果较新则复制`,生成操作为:`内容`。** + +- `[AppDbContext]` 特性配置 + +```cs showLineNumbers {1} +[AppDbContext("Data Source=./Furion.db", DbProvider.Sqlite)] +public class DefaultDbContext : AppDbContext +{ +} +``` + +- 在注册上下文时配置 + +```cs showLineNumbers {3-4} +services.AddDatabaseAccessor(options => +{ + // options.AddDb(connectionMetadata: "配置Key或连接字符串"); + options.AddDbPool(connectionMetadata: "配置Key或连接字符串"); +}); +``` + +- 在 `DbContext` 中配置 + +```cs showLineNumbers {12-14} +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore; + +namespace Furion.EntityFramework.Core +{ + public class DefaultDbContext : AppDbContext + { + public DefaultDbContext(DbContextOptions options) : base(options) + { + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseSqlite("连接字符串"); + base.OnConfiguring(optionsBuilder); + } + } +} +``` + +### 9.1.1.4 各类数据库连接字符串配置示例 + +- `Sqlite`:`Data Source=./Furion.db` +- `MySql`:`Data Source=localhost;Database=Furion;User ID=root;Password=000000;pooling=true;port=3306;sslmode=none;CharSet=utf8;` +- `SqlServer`:`Server=localhost;Database=Furion;User=sa;Password=000000;MultipleActiveResultSets=True;Encrypt=True;TrustServerCertificate=True;` +- `Oracle`:`User Id=orcl;Password=orcl;Data Source=(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=127.0.0.1)(PORT=1521)))(CONNECT_DATA=(SERVICE_NAME=orcl)))` +- `PostgreSQL`:`PORT=5432;DATABASE=postgres;HOST=127.0.0.1;PASSWORD=postgres;USER ID=postgres;` + +### 9.1.1.5 注册数据库上下文 + +```cs showLineNumbers {12-19} title="Furion.EntityFramework.Core\Startup.cs" +using Furion.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; + +namespace Furion.EntityFramework.Core +{ + [AppStartup(600)] + public sealed class FurEntityFrameworkCoreStartup : AppStartup + { + public void ConfigureServices(IServiceCollection services) + { + // 配置数据库上下文,支持N个数据库 + services.AddDatabaseAccessor(options => + { + // 配置默认数据库 + options.AddDbPool(); + + // 配置多个数据库,多个数据库必须指定数据库上下文定位器 + // options.AddDbPool(); + }); + } + } +} +``` + +:::tip 了解更多数据库数据库注册方式 + +如需了解各种数据库及版本注册方式可查阅 【[9.19 多数据库注册章节](dbcontext-multi-database.mdx)】 + +::: + +### 9.1.1.6 `Code First` 说明 + +:::important 特别注意 + +`Furion` 框架默认数据迁移的程序集为:`Furion.Database.Migrations`,所以如果您改了程序集名称或通过 `NuGet` 方式安装的 `Furion` 包,则需要配置迁移程序集名称: + +```cs showLineNumbers {4} +services.AddDatabaseAccessor(options => +{ + options.AddDbPool(DbProvider.Sqlite); +}, "存放迁移文件的项目名称"); +``` + +另外,如果应用中配置了多个数据库上下文,那么所有的 `迁移命令` 都需要指定 `-Context 数据库上下文名称` 参数。如: + +```shell showLineNumbers +Add-Migration v1.0.0 -Context DefaultDbContext +``` + +::: + +## 9.1.2 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/dbcontext-update.mdx b/handbook/docs/dbcontext-update.mdx new file mode 100644 index 0000000000000000000000000000000000000000..e5913c7ceb92ca10a846a11b43387606c2fa2785 --- /dev/null +++ b/handbook/docs/dbcontext-update.mdx @@ -0,0 +1,498 @@ +--- +id: dbcontext-update +title: 9.7 更新操作 +sidebar_label: 9.7 更新操作 +--- + +:::warning 功能移除声明 + +以下内容包含 `Exists` 单词的在 `Furion 2.6.0 +` 版本中已移除。 + +::: + +## 9.7.1 更新全部列(不立即提交) + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +repository.Update(user); + +// 示例二 +user.Update(); + +// 示例三 +repository.ChangeEntityState(user, EntityState.Modified); + +// 示例四 +repository.Entities.Update(user); + +// ==== 异步操作 ==== + +// 示例一 +await repository.UpdateAsync(user); + +// 示例二 +await user.UpdateAsync(); +``` + +## 9.7.2 更新全部列(立即提交) + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +repository.UpdateNow(user); + +// 示例二 +user.UpdateNow(); + +// ==== 异步操作 ==== + +// 示例一 +await repository.UpdateNowAsync(user); + +// 示例二 +await user.UpdateNowAsync(); +``` + +## 9.7.3 更新部分列(不立即提交) + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +repository.UpdateInclude(user, new[] {"Age", "Name"}); + +// 示例二 +repository.UpdateInclude(user, new[] {nameof(User.Name), nameof(User.Age)}); + +// 示例三 +repository.UpdateInclude(user, new[] {nameof(User.Name), nameof(User.Age)}, true); // 忽略空值 + +// 示例四 +user.UpdateInclude(new[] {"Age", "Name"}); + +// 示例五 +user.UpdateInclude(new[] {nameof(User.Name), nameof(User.Age)}); + +// 示例六 +user.UpdateInclude(new[] {nameof(User.Name), nameof(User.Age)}, true); // 忽略空值 + + +// ==== 异步操作 ==== + +// 示例一 +await repository.UpdateIncludeAsync(user, new[] {"Age", "Name"}); + +// 示例二 +await repository.UpdateIncludeAsync(user, new[] {nameof(User.Name), nameof(User.Age)}); + +// 示例三 +await repository.UpdateIncludeAsync(user, new[] {nameof(User.Name), nameof(User.Age)}, true); // 忽略空值 + +// 示例四 +await user.UpdateIncludeAsync(new[] {"Age", "Name"}); + +// 示例五 +await user.UpdateIncludeAsync(new[] {nameof(User.Name), nameof(User.Age)}); + +// 示例六 +await user.UpdateIncludeAsync(new[] {nameof(User.Name), nameof(User.Age)}, true); // 忽略空值 +``` + +## 9.7.4 更新部分列(立即提交) + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +repository.UpdateIncludeNow(user, new[] {"Age", "Name"}); + +// 示例二 +repository.UpdateIncludeNow(user, new[] {nameof(User.Name), nameof(User.Age)}); + +// 示例三 +repository.UpdateIncludeNow(user, new[] {nameof(User.Name), nameof(User.Age)}, true); // 忽略空值 + +// 示例四 +user.UpdateIncludeNow(new[] {"Age", "Name"}); + +// 示例五 +user.UpdateIncludeNow(new[] {nameof(User.Name), nameof(User.Age)}); + +// 示例六 +user.UpdateInclude(new[] {nameof(User.Name), nameof(User.Age)}, true); // 忽略空值 + + +// ==== 异步操作 ==== + +// 示例一 +await repository.UpdateIncludeNowAsync(user, new[] {"Age", "Name"}); + +// 示例二 +await repository.UpdateIncludeNowAsync(user, new[] {nameof(User.Name), nameof(User.Age)}); + +// 示例三 +await repository.UpdateIncludeNowAsync(user, new[] {nameof(User.Name), nameof(User.Age)}, true); // 忽略空值 + +// 示例四 +await user.UpdateIncludeNowAsync(new[] {"Age", "Name"}); + +// 示例五 +await user.UpdateIncludeNowAsync(new[] {nameof(User.Name), nameof(User.Age)}); + +// 示例六 +await user.UpdateIncludeNowAsync(new[] {nameof(User.Name), nameof(User.Age)}, true); // 忽略空值 +``` + +## 9.7.5 排除特定列更新(不立即提交) + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +repository.UpdateExclude(user, new[] {"Age", "Name"}); + +// 示例二 +repository.UpdateExclude(user, new[] {nameof(User.Name), nameof(User.Age)}); + +// 示例三 +repository.UpdateExclude(user, new[] {nameof(User.Name), nameof(User.Age)}, true); // 忽略空值 + +// 示例四 +user.UpdateExclude(new[] {"Age", "Name"}); + +// 示例五 +user.UpdateExclude(new[] {nameof(User.Name), nameof(User.Age)}); + +// 示例六 +user.UpdateExclude(new[] {nameof(User.Name), nameof(User.Age)}, true); // 忽略空值 + + +// ==== 异步操作 ==== + +// 示例一 +await repository.UpdateExcludeAsync(user, new[] {"Age", "Name"}); + +// 示例二 +await repository.UpdateExcludeAsync(user, new[] {nameof(User.Name), nameof(User.Age)}); + +// 示例三 +await repository.UpdateExcludeAsync(user, new[] {nameof(User.Name), nameof(User.Age)}, true); // 忽略空值 + +// 示例四 +await user.UpdateExcludeAsync(new[] {"Age", "Name"}); + +// 示例五 +await user.UpdateExcludeAsync(new[] {nameof(User.Name), nameof(User.Age)}); + +// 示例六 +await user.UpdateExcludeAsync(new[] {nameof(User.Name), nameof(User.Age)}, true); // 忽略空值 +``` + +## 9.7.6 排除特定列更新(立即提交) + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +repository.UpdateExcludeNow(user, new[] {"Age", "Name"}); + +// 示例二 +repository.UpdateExcludeNow(user, new[] {nameof(User.Name), nameof(User.Age)}); + +// 示例三 +repository.UpdateExcludeNow(user, new[] {nameof(User.Name), nameof(User.Age)}, true); // 忽略空值 + +// 示例四 +user.UpdateExcludeNow(new[] {"Age", "Name"}); + +// 示例五 +user.UpdateExcludeNow(new[] {nameof(User.Name), nameof(User.Age)}); + +// 示例六 +user.UpdateExcludeNow(new[] {nameof(User.Name), nameof(User.Age)}, true); // 忽略空值 + + +// ==== 异步操作 ==== + +// 示例一 +await repository.UpdateExcludeNowAsync(user, new[] {"Age", "Name"}); + +// 示例二 +await repository.UpdateExcludeNowAsync(user, new[] {nameof(User.Name), nameof(User.Age)}); + +// 示例三 +await repository.UpdateExcludeNowAsync(user, new[] {nameof(User.Name), nameof(User.Age)}, true); // 忽略空值 + +// 示例四 +await user.UpdateExcludeNowAsync(new[] {"Age", "Name"}); + +// 示例五 +await user.UpdateExcludeNowAsync(new[] {nameof(User.Name), nameof(User.Age)}); + +// 示例六 +await user.UpdateExcludeNowAsync(new[] {nameof(User.Name), nameof(User.Age)}, true); // 忽略空值 +``` + +## 9.7.7 数据存在才更新所有列(不立即提交) + +:::caution 功能移除声明 + +以下内容已在 `Furion 2.6.0 +` 版本移除。 + +::: + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +repository.UpdateExists(user); + +// ==== 异步操作 ==== + +// 示例一 +await repository.UpdateExistsAsync(user); +``` + +## 9.7.8 数据存在才更新所有列(立即提交) + +:::caution 功能移除声明 + +以下内容已在 `Furion 2.6.0 +` 版本移除。 + +::: + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +repository.UpdateExistsNow(user); + +// ==== 异步操作 ==== + +// 示例一 +await repository.UpdateExistsNowAsync(user); +``` + +## 9.7.9 数据存在才更新部分列(不立即提交) + +:::caution 功能移除声明 + +以下内容已在 `Furion 2.6.0 +` 版本移除。 + +::: + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +repository.UpdateIncludeExists(user, new[] {"Age", "Name"}); + +// 示例二 +repository.UpdateIncludeExists(user, new[] {nameof(User.Name), nameof(User.Age)}); + +// 示例三 +repository.UpdateIncludeExists(user, new[] {nameof(User.Name), nameof(User.Age)}, true); // 忽略空值 + +// ==== 异步操作 ==== + +// 示例一 +await repository.UpdateIncludeExistsAsync(user, new[] {"Age", "Name"}); + +// 示例二 +await repository.UpdateIncludeExistsAsync(user, new[] {nameof(User.Name), nameof(User.Age)}); + +// 示例三 +await repository.UpdateIncludeExistsAsync(user, new[] {nameof(User.Name), nameof(User.Age)}, true); // 忽略空值 +``` + +## 9.7.10 数据存在才更新部分列(立即提交) + +:::caution 功能移除声明 + +以下内容已在 `Furion 2.6.0 +` 版本移除。 + +::: + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +repository.UpdateIncludeExistsNow(user, new[] {"Age", "Name"}); + +// 示例二 +repository.UpdateIncludeExistsNow(user, new[] {nameof(User.Name), nameof(User.Age)}); + +// 示例三 +repository.UpdateIncludeExistsNow(user, new[] {nameof(User.Name), nameof(User.Age)}, true); // 忽略空值 + +// ==== 异步操作 ==== + +// 示例一 +await repository.UpdateIncludeExistsNowAsync(user, new[] {"Age", "Name"}); + +// 示例二 +await repository.UpdateIncludeExistsNowAsync(user, new[] {nameof(User.Name), nameof(User.Age)}); + +// 示例三 +await repository.UpdateIncludeExistsNowAsync(user, new[] {nameof(User.Name), nameof(User.Age)}, true); // 忽略空值 +``` + +## 9.7.11 数据存在才排除特定部分列更新(不立即提交) + +:::caution 功能移除声明 + +以下内容已在 `Furion 2.6.0 +` 版本移除。 + +::: + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +repository.UpdateExcludeExists(user, new[] {"Age", "Name"}); + +// 示例二 +repository.UpdateExcludeExists(user, new[] {nameof(User.Name), nameof(User.Age)}); + +// 示例三 +repository.UpdateExcludeExists(user, new[] {nameof(User.Name), nameof(User.Age)}, true); // 忽略空值 + +// ==== 异步操作 ==== + +// 示例一 +await repository.UpdateExcludeExistsAsync(user, new[] {"Age", "Name"}); + +// 示例二 +await repository.UpdateExcludeExistsAsync(user, new[] {nameof(User.Name), nameof(User.Age)}); + +// 示例三 +await repository.UpdateExcludeExistsAsync(user, new[] {nameof(User.Name), nameof(User.Age)}, true); // 忽略空值 +``` + +## 9.7.12 数据存在才排除特定部分列更新(立即提交) + +:::caution 功能移除声明 + +以下内容已在 `Furion 2.6.0 +` 版本移除。 + +::: + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +repository.UpdateExcludeExistsNow(user, new[] {"Age", "Name"}); + +// 示例二 +repository.UpdateExcludeExistsNow(user, new[] {nameof(User.Name), nameof(User.Age)}); + +// 示例三 +repository.UpdateExcludeExistsNow(user, new[] {nameof(User.Name), nameof(User.Age)}, true); // 忽略空值 + +// ==== 异步操作 ==== + +// 示例一 +await repository.UpdateExcludeExistsNowAsync(user, new[] {"Age", "Name"}); + +// 示例二 +await repository.UpdateExcludeExistsNowAsync(user, new[] {nameof(User.Name), nameof(User.Age)}); + +// 示例三 +await repository.UpdateExcludeExistsNowAsync(user, new[] {nameof(User.Name), nameof(User.Age)}, true); // 忽略空值 +``` + +## 9.7.13 更新多条记录(不立即提交) + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +repository.Update(user, user2); + +// 示例二 +repository.Update(new List { user, user2 }); + +// 示例三 +repository.Update(new[] {user, user2 }); + +// ==== 异步操作 ==== + +// 示例一 +await repository.UpdateAsync(user, user2); + +// 示例二 +await repository.UpdateAsync(new List { user, user2 }); + +// 示例三 +await repository.UpdateAsync(new[] {user, user2 }); +``` + +## 9.7.14 更新多条记录(立即提交) + +```cs showLineNumbers +// ==== 同步操作 ==== + +// 示例一 +repository.UpdateNow(user, user2); + +// 示例二 +repository.UpdateNow(new List { user, user2 }); + +// 示例三 +repository.UpdateNow(new[] {user, user2 }); + +// ==== 异步操作 ==== + +// 示例一 +await repository.UpdateNowAsync(user, user2); + +// 示例二 +await repository.UpdateNowAsync(new List { user, user2 }); + +// 示例三 +await repository.UpdateNowAsync(new[] {user, user2 }); +``` + +:::tip 小知识 + +所有带 `Now` 结尾的表示立即提交到数据库,也就是立即调用 `SaveChanges` 或 `SaveChangesAsync`。 + +::: + +## 9.7.15 忽略空值更新 + +默认情况下,`EFCore` 更新会更新全部列(除实体跟踪方式以外),有些时候我们希望 `Null` 值无需更新,这是我们只需要在更新时候配置 `ignoreNullValues` 参数即可,如: + +```cs showLineNumbers +repository.Update(entity, ignoreNullValues: true); +``` + +也可以全局配置,在 `AppDbContext` 的派生类的构造函数中启用即可: + +```cs showLineNumbers {11} +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore; + +namespace Furion.EntityFramework.Core +{ + [AppDbContext("Sqlite3ConnectionString", DbProvider.Sqlite)] + public class DefaultDbContext : AppDbContext + { + public DefaultDbContext(DbContextOptions options) : base(options) + { + InsertOrUpdateIgnoreNullValues = true; + } + } +} +``` + +## 9.7.16 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/dbcontext-view.mdx b/handbook/docs/dbcontext-view.mdx new file mode 100644 index 0000000000000000000000000000000000000000..dd529c3df6404dc18e9cb6908c473a04dd010b87 --- /dev/null +++ b/handbook/docs/dbcontext-view.mdx @@ -0,0 +1,123 @@ +--- +id: dbcontext-view +title: 9.13 视图操作 +sidebar_label: 9.13 视图操作 +--- + +## 9.13.1 关于视图 + +视图是数据库中非常重要的对象,是一张虚拟表,通过视图我们可以对结果进行筛选缓存,同时还能实现颗粒化权限控制,如控制指定行,指定列。 + +## 9.13.2 视图的使用 + +在 `Furion` 中实现视图的操作非常简单,只需要创建视图模型,并继承 `EntityNotKey` 基类即可。代码如下: + +### 9.13.2.1 创建视图 `SQL` + +```sql showLineNumbers +CREATE VIEW V_Person AS +SELECT Id,Name,Age,Address FROM person +``` + +### 9.13.2.2 视图模型 + +```cs showLineNumbers {5,10} +using Furion.DatabaseAccessor; + +namespace Furion.Core +{ + public class V_Person : EntityNotKey + { + /// + /// 配置视图名 + /// + public V_Person() : base("V_Person") + { + } + + /// + /// 主键Id + /// + public int Id { get; set; } + + /// + /// 姓名 + /// + public string Name { get; set; } + + /// + /// 年龄 + /// + public int Age { get; set; } + + /// + /// 住址 + /// + public string Address { get; set; } + } +} +``` + +:::important 视图名称 + +视图实体只需要继承 `EntityNotKey` 基类并编写无参构造函数继承 `base("视图名称")` 即可。 + +::: + +## 9.13.3 视图使用 + +视图除了不能操作(写)以外,其他操作和表操作无异。 + +```cs showLineNumbers +var vEntities = v_repository.Where(u => u.Id >10).ToList(); +``` + +## 9.13.4 视图最佳读取方式 ✔ + +由于视图是虚拟表,不应该对其进行写操作,所以应该采用 `只读仓储初始化视图`: + +```cs showLineNumbers {11,13,16} +using Furion.Core; +using Furion.DatabaseAccessor; +using Furion.DynamicApiController; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Furion.Application.Persons +{ + public class FurionService : IDynamicApiController + { + private readonly IReadableRepository _readableRepository; + + public FurionService(IRepository repository) + { + // 初始化只读仓储 + _readableRepository = repository.Constraint>(); + } + + /// + /// 读取视图 + /// + /// + public async Task> GetVPerson() + { + var list = await _readableRepository.AsQueryable().ToListAsync(); + return list; + } + } +} +``` + +:::tip 小知识 + +通过 `.Constraint` 方法可以将仓储约束为特定仓储,如只读仓储,可读可写仓储,只新增仓储等。 + +::: + +## 9.13.5 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/dbcontext.mdx b/handbook/docs/dbcontext.mdx new file mode 100644 index 0000000000000000000000000000000000000000..191feca2ed15eb1762347bacdbd01158c32c24f7 --- /dev/null +++ b/handbook/docs/dbcontext.mdx @@ -0,0 +1,488 @@ +--- +id: dbcontext +title: 9.2 数据库上下文 +sidebar_label: 9.2 数据库上下文 +--- + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **新特性** + + -  新增 `[AppDbContext]` 特性支持 `UseSnakeCaseNaming` 属性配置表名使用蛇形命名 4.9.1.5 ⏱️2023.11.20 [#I8HGR2](https://gitee.com/dotnetchina/Furion/issues/I8HGR2) [!863](https://gitee.com/dotnetchina/Furion/pulls/863) + -  新增 `Db.GetNewDbContext()` 多个重载方法,实现类似 `new DbContext()` 操作 4.8.8.55 ⏱️2023.11.09 [4157629](https://gitee.com/dotnetchina/Furion/commit/41576295473fefc47f6961154909a690d7c5ed58) + +
+
+
+ +:::warning 连接字符串配置注意事项 + +如果连接字符串是配置在自定义的 `.json` 文件中,那么必须在 `Visual Studio` 中配置 `.json` 右键属性,设置 `复制` 输出目录为 `如果较新则复制`,生成操作为:`内容`。 + +否则就会提示找不到配置或连接字符串的错误。 + +::: + +## 9.2.1 数据库上下文 + +简单来说,数据库上下文是负责和数据库交互的对象,提供程序对数据库存取提供了大量的方法。 + +在 `Furion` 框架中,默认集成了微软亲儿子:`EntityFramework Core` ,也就是通常数据库上下文指的是 `DbContext` 类或它的实现类。 + +## 9.2.2 `AppDbContext` + +在我们实际项目开发过程中,使用 `EFCore` 提供的 `DbContext` 操作对象操作数据库有些繁琐和复杂,且默认不具备读写分离、多库等操作功能。 + +所以,`Furion` 框架提供了 `AppDbContext` 数据库上下文,该上下文继承自 `DbContext`。 + +:::note 特别说明 +后续章节,皆采用 `EFCore` 代替 `EntityFramework Core`。 +::: + +## 9.2.3 `AppDbContext` 和 `DbContext` 区别 + +- `AppDbContext` 继承自 `DbContext`,具备 `DbContext` 所有功能。 +- `AppDbContext` 支持多数据库操作泛型版本,如:`AppDbContext` +- `AppDbContext` 自动配置实体信息,无需在 `OnModelCreating` 中配置 +- `AppDbContext` 支持内置多租户支持 +- `AppDbContext` 支持全局模型配置拦截器 +- `AppDbContext` 支持数据提交更改多个事件 +- `AppDbContext` 提供更加强大的模型操作能力,如 `Sql` 操作,读写分离等 +- `AppDbContext` 能够得到 `Furion` 框架更多的功能支持 + +## 9.2.4 如何定义数据库上下文 + +在 `Furion` 框架中了,提供了两种 `AppDbContext` 定义方式: + +- `AppDbContext` 操作默认数据库 +- `AppDbContext` 操作 N 个数据库 + +其中 `AppDbContext` 默认继承自 `AppDbContext`。 + +下面是数据库上下文创建的多个例子: + +### 9.2.4.1 创建默认数据库上下文 + +```cs showLineNumbers {1,6,12} +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore; + +namespace Furion.EntityFramework.Core +{ + [AppDbContext("连接字符串或appsetting.json 键")] + public class FurionDbContext : AppDbContext // 继承 AppDbContext<> 类 + { + /// + /// 继承父类构造函数 + /// + /// + public FurionDbContext(DbContextOptions options) : base(options) + { + } + } +} +``` + +### 9.2.4.2 创建其他数据库上下文 + +```cs showLineNumbers {1,6,12} +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore; + +namespace Furion.EntityFramework.Core +{ + [AppDbContext("连接字符串或appsetting.json 键")] + public class FurOtherDbContext : AppDbContext // 继承 AppDbContext<> 类 + { + /// + /// 继承父类构造函数 + /// + /// + public FurOtherDbContext(DbContextOptions options) : base(options) + { + } + } +} +``` + +:::important 特别注意 +所有数据库上下文都应该在 `Furion.EntityFramework.Core` 项目中创建。另外如果系统用到了多个数据库,那么从第二个开始必须指定数据库上下文定位器。关于 `TDbContextLocator` 将在下一章节 《[9.3 数据库上下文定位器](dbcontext-locator)》阐述。 +::: + +## 9.2.5 配置连接字符串 + +`Furion` 框架提供多种数据库连接字符串配置方式: + +- 在注册数据库服务时配置:`AddDbPool("连接字符串")` 方式 +- 使用 `[AppDbContext("连接字符串/Key")]` 特性方式(只在 `AppDbContext 实现类有效`)**推荐** +- 通过重写 `OnConfiguring(DbContextOptionsBuilder optionsBuilder)` 配置 + +### 9.2.5.1 在注册数据库服务时配置 + +```cs showLineNumbers {12-19} title="Furion.EntityFramework.Core\Startup.cs" +using Furion.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; + +namespace Furion.EntityFramework.Core +{ + [AppStartup(600)] + public sealed class FurEntityFrameworkCoreStartup : AppStartup + { + public void ConfigureServices(IServiceCollection services) + { + // 配置数据库上下文,支持N个数据库 + services.AddDatabaseAccessor(options => + { + // 配置默认数据库 + options.AddDbPool(DbProvider.SqlServer, connectionMetadata:"连接字符串"); + + // 配置多个数据库,多个数据库必须指定数据库上下文定位器 + options.AddDbPool(DbProvider.Sqlite, connectionMetadata:"连接字符串"); + }); + } + } +} +``` + +:::caution 新版 MySQL 注意 + +`MySQL` 在新版本包中注册有所修改,所以注册方式为: + +```cs showLineNumbers +services.AddDatabaseAccessor(options => +{ + options.AddDbPool($"{DbProvider.MySql}@8.0.22"); +}); +``` + +::: + +### 9.2.5.2 `[AppDbContext]` 方式配置 + +```cs showLineNumbers {1,6} +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore; + +namespace Furion.EntityFramework.Core +{ + [AppDbContext("DbConnectionString")] // 支持 `appsetting.json` 名或 连接字符串 + public class FurionDbContext : AppDbContext + { + /// + /// 继承父类构造函数 + /// + /// + public FurionDbContext(DbContextOptions options) : base(options) + { + } + } +} +``` + +:::tip 小提示 + +`Furion` 推荐使用此方式配置数据库连接字符串。 + +::: + +**`[AppDbContext]`** 内置属性: + +- `ConnectionMetadata`:支持数据库连接字符串,配置文件的 `ConnectionStrings` 中的 `Key` 或配置文件的完整的配置路径,如果是内存数据库,则为数据库名称。 +- `TablePrefix`:当前数据库上下文表统一前缀 +- `TableSuffix`:当前数据库上下文表统一后缀 +- `ProviderName`:配置数据库提供器类型,传入 `DbProvider.Xxx` +- `Mode`:配置数据库上下文模式,`DbContextMode` 枚举类型,取值: + - `Cached`:缓存模型数据库上下文,默认值 + - `Dynamic`:动态模型数据库上下文 +- `SlaveDbContextLocators`:主从库配置,设置多个从库定位器,`Type[]` 类型 +- `UseSnakeCaseNaming`:表名使用蛇形命名,`bool` 类型,默认 `false`,`Furion 4.9.1.5+` 版本支持 + +### 9.2.5.3 `OnConfiguring` 方式配置 + +```cs showLineNumbers {16-20} +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore; + +namespace Furion.EntityFramework.Core +{ + public class FurionDbContext : AppDbContext + { + /// + /// 继承父类构造函数 + /// + /// + public FurionDbContext(DbContextOptions options) : base(options) + { + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + + optionsBuilder.UseSqlServer("数据库连接字符串"); + } + } +} +``` + +:::important 特别注意 + +这三种方式可以同时使用,但是有优先级:`[AppDbContext]` -> `在注册数据库服务时配置` -> `OnConfiguring`(低到高) + +也就是 `OnConfiguring` 配置会覆盖 `在注册数据库服务时配置` 配置,`在注册数据库服务时配置` 配置会覆盖 `[AppDbContext]` 配置所配置的连接字符串。 + +::: + +### 9.2.5.4 各类数据库连接字符串配置示例 + +- `Sqlite`:`Data Source=./Furion.db` +- `MySql`:`Data Source=localhost;Database=Furion;User ID=root;Password=000000;pooling=true;port=3306;sslmode=none;CharSet=utf8;` +- `SqlServer`:`Server=localhost;Database=Furion;User=sa;Password=000000;MultipleActiveResultSets=True;` +- `Oracle`:`User Id=orcl;Password=orcl;Data Source=(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=127.0.0.1)(PORT=1521)))(CONNECT_DATA=(SERVICE_NAME=orcl)))` +- `PostgreSQL`:`PORT=5432;DATABASE=postgres;HOST=127.0.0.1;PASSWORD=postgres;USER ID=postgres;` + +## 9.2.6 数据库上下文定义位置 + +:::important 特别注意 + +在 `Furion` 框架中,数据库上下文需定义在 `Furion.EntityFramework.Core` 中,且每一个数据库上下文都必须拥有唯一的 `DbContextLocator` 定位器 + +::: + +## 9.2.7 数据库上下文注册 + +数据库上下文配置好数据库连接字符串后,需要注册该数据库上下文,并指定数据库类型,如: + +```cs showLineNumbers {11-13} title="Furion\framework\Furion.EntityFramework.Core\FurEntityFrameworkCoreStartup.cs" +using Furion.DatabaseAccessor; +using Microsoft.Extensions.DependencyInjection; + +namespace Furion.EntityFramework.Core +{ + [AppStartup(600)] + public sealed class FurEntityFrameworkCoreStartup : AppStartup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddDatabaseAccessor(options => + { + options.AddDbPool(DbProvider.Sqlite); + }); + } + } +} +``` + +如果有多个数据库操作,**那么从第二个起,就需要绑定数据库上下文定位器**,如: + +```cs showLineNumbers +options.AddDbPool(DbProvider.Sqlite); // 第一个数据库 + +options.AddDbPool(DbProvider.SqlServer); // 第二个数据库 + +options.AddDbPool(DbProvider.SqlServer); // 第三个数据库 +``` + +## 9.2.8 自定义高级注册 + +在 `Furion` 框架中,为了能够实现数据库的简单使用进行了注册封装,但有些时候,我们可能需要添加更多配置,这时就需要使用原生自定义配置方式,如: + +```cs showLineNumbers +services.AddDatabaseAccessor(options => +{ + // 自定义原生配置 + options.AddDb((services, builder) => + { + builder.UseSqlite(...); + } +}); +``` + +:::tip 小知识 + +`Furion` 框架提供了快速解析连接字符串的静态方法,自动根据名称读取配置,自动解析 `[AppContext("...")]` 信息,如: + +```cs showLineNumbers +// 获取连接字符串 +var connStr = DbProvider.GetConnectionString(/*这里可写可不写*/); + +options.AddDb((services, builder) => +{ + builder.UseSqlite(connStr, ...); +} +``` + +::: + +## 9.2.9 动态数据库上下文对象 + +在 `Furion` 框架中,数据库上下文是定义在 `Furion.EntityFramework.Core` 项目层,并且该层不被 `Furion.Application` 和 `Furion.Core` 等层引用。 + +所以就不能直接在 `Furion.Application` 项目层直接使用 `Furion.EntityFramework.Core` 定义的数据库上下文。 + +`Furion` 为了解决这个问题,提供了两种方式处理: + +- `repository.Context` :当前数据库上下文对象,返回是 `DbContext` 抽象类型 +- `repository.DynamicContext`:当前数据库上下文对象,返回的是 `dynamic` 类型 + +如果你只是想使用 `DbContext` 的功能,直接使用 `repository.Context` 即可,如: + +```cs showLineNumbers +repository.Context.SaveChanges(); +``` + +如果你想能够获取具体的数据库上下文类型,如 `MyDbContext`,那么使用 `repository.DynamicContext` 就可以获取到具体的 `MyDbContext` 类型。如: + +```cs showLineNumbers +var persons = repository.DynamicContext.Persons.Find(1); +var users = repository.DynamicContext.Users; +``` + +这样就可以直接操作 `MyDbContext` 定义的属性和方法了。 + +## 9.2.10 在后台任务中使用 + +由于 `DbContext` 默认注册为 `Scoped` 生存周期,所以在后台任务中使用 `IServiceScopeFactory` 获取所有服务,如: + +```cs showLineNumbers {8-9,19,21,24} +public class JobService : BackgroundService +{ + // 日志对象 + private readonly ILogger _logger; + + // 服务工厂 + private readonly IServiceScopeFactory _scopeFactory; + public JobService(ILogger logger + , IServiceScopeFactory scopeFactory) + { + _logger = logger; + _scopeFactory = scopeFactory; + } + + protected override Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("写日志~~"); + + using (var scope = _scopeFactory.CreateScope()) + { + var services = scope.ServiceProvider; + + // 获取数据库上下文 + var dbContext = Db.GetDbContext(services); + + // 获取仓储 + var repository = Db.GetRepository(services); + + // 解析其他服务 + var otherService = services.GetService(); + } + + return Task.CompletedTask; + } +} +``` + +:::important 数据库操作注意 + +如果作用域中对**数据库有任何变更操作**,需手动调用 `SaveChanges` 或带 `Now` 结尾的方法。也可以使用 `Scoped.CreateUow(handler)` 代替。 + +::: + +## 9.2.11 `AppDbContext` 全局配置属性 + +- `InsertOrUpdateIgnoreNullValues`:新增或更新忽略空值,默认 `false`,**在构造函数中配置** +- `EnabledEntityStateTracked`:启用实体跟踪,默认 `true`,**在构造函数中配置** +- `EnabledEntityChangedListener`:启用实体数据更改监听,默认 `false`,**在构造函数中配置** +- `Tenant`:默认内置多租户 +- `FailedAutoRollback`:是否启用保存失败后事务自动回滚,默认 `true`,可以在任何地方配置 + +## 9.2.12 配置实体 `懒加载` + +- 第一步:安装 `EFCore` 拓展包 + +在数据库上下文定义所在的层安装 `Microsoft.EntityFrameworkCore.Proxies` 拓展包 + +- 第二步:采用 `AddDb` 方式注册 + +确保数据库上下文采用 `AddDb` 注册而非 `AddDbPool`。 + +- 第三步:重写 `OnConfiguring` 方法 + +```cs showLineNumbers {13,15} +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore; + +namespace Furion.EntityFramework.Core +{ + [AppDbContext("Sqlite3ConnectionString", DbProvider.Sqlite)] + public class DefaultDbContext : AppDbContext + { + public DefaultDbContext(DbContextOptions options) : base(options) + { + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseLazyLoadingProxies() + .UseSqlite(DbProvider.GetConnectionString()); + + base.OnConfiguring(optionsBuilder); + } + } +} +``` + +:::note 小知识 + +更多 `EFCore` 懒加载可查看 **[【EFCore - 延迟加载】](https://docs.microsoft.com/zh-cn/ef/core/querying/related-data/lazy)** 文档。 + +::: + +## 9.2.13 `Db` 静态类获取上下文 + +`Db` 静态类提供了多种获取 `DbContext` 上下文的方法,如: + +```cs showLineNumbers {2,5-6,9,12-13} +// 获取默认数据库上下文 +var dbContext = Db.GetDbContext(); + +// 获取定位器数据库上下文 +var locatorDbContext = Db.GetDbContext(); +var locatorDbContext2 = Db.GetDbContext(typeof(TDbContextLocator)); + +// 创建新的默认数据库上下文(相当于 new YourDbContext) +using var dbContext = Db.GetNewDbContext(); // 别忘记 using,Furion 4.8.8.55+ 版本有效 + +// 创建新的定位器数据库上下文(相当于 new YourDbContext) +using var locatorDbContext = Db.GetNewDbContext(); // 别忘记 using,Furion 4.8.8.55+ 版本有效 +using var locatorDbContext2 = Db.GetNewDbContext(typeof(TDbContextLocator)); // 别忘记 using,Furion 4.8.8.55+ 版本有效 +``` + +:::caution 特别注意 + +默认情况下 `Db.GetDbContext(...)` 创建数据库上下文的作用域和 `Web` 相同,**如果您在后台线程或多线程中使用,那么必须传入第二个 `serviceProvider` 参数。** + +**也可以通过 `Db.GetNewDbContext(...)` 的方式获取一个新的数据库上下文,但是需要手动 `using` 释放。** + +::: + +## 9.2.14 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: + +--- + +:::note 了解更多 + +想了解更多 `数据库上下文` 知识可查阅 [EF Core - 配置 DbContext](https://docs.microsoft.com/zh-cn/ef/core/miscellaneous/configuring-dbcontext) 章节。 + +::: diff --git a/handbook/docs/dependency-injection.mdx b/handbook/docs/dependency-injection.mdx new file mode 100644 index 0000000000000000000000000000000000000000..8dd87be457fc13f137f89a3e803ce44718dcd7d1 --- /dev/null +++ b/handbook/docs/dependency-injection.mdx @@ -0,0 +1,1099 @@ +--- +id: dependency-injection +title: 12. 依赖注入/控制反转 +sidebar_label: 12. 依赖注入/控制反转 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **新特性** + + -  新增 **`WinForm/WPF` 支持依赖注入的 `Native.CreateInstance()` 静态方法** 4.8.7.23 ⏱️2023.03.27 [53d51c3](https://gitee.com/dotnetchina/Furion/commit/53d51c3645f4066f5d68d4726d78e389fd544560) + +- **问题修复** + + -  修复 因 [9d8cb82](https://gitee.com/dotnetchina/Furion/commit/9d8cb82e4ce983839cf13c3c74640b08f258c325) 代码提交导致命名服务解析异常问题 4.8.8.21 ⏱️2023.05.18 [#I76JZR](https://gitee.com/dotnetchina/Furion/issues/I76JZR) + -  修复 因 [9d8cb82](https://gitee.com/dotnetchina/Furion/commit/9d8cb82e4ce983839cf13c3c74640b08f258c325) 代码提交导致服务 `AOP` 异常拦截问题 4.8.8.17 ⏱️2023.05.15 [#I73A8E](https://gitee.com/dotnetchina/Furion/issues/I73A8E) + +
+
+
+ +## 12.1 依赖注入 + +所谓依赖注入,是指程序运行过程中,如果需要调用另一个对象协助时,无须在代码中创建被调用者,而是依赖于外部的注入。 + +通俗来讲,就是把有依赖关系的类放到容器中,然后在我们需要这些类时,容器自动解析出这些类的实例。 + +依赖注入最大的好处是实现类的解耦,利于程序拓展、单元测试、自动化模拟测试等。 + +依赖注入的英文为:`Dependency Injection`,简称 `DI` + +## 12.2 控制反转 + +控制反转只是一个概念,也就是将创建对象实例的控制权(原本是程序员)从代码控制权剥离到 `IOC 容器` 中控制。 + +控制反转的英文为:`Inversion of Control`,简称 `IOC` + +## 12.3 `IOC/DI` 优缺点 + +传统的代码,每个对象负责管理与自己需要依赖的对象,导致如果需要切换依赖对象的实现类时,需要修改多处地方。同时,过度耦合也使得对象难以进行单元测试。 + +- 优点 + + - 依赖注入把对象的创建交给外部去管理,很好的解决了代码紧耦合(tight couple)的问题,是一种让代码实现松耦合(loose couple)的机制 + - 松耦合让代码更具灵活性,能更好地应对需求变动,以及方便单元测试 + +- 缺点 + + - 目前主流的 `IOC/DI` 基本采用反射的方式来实现依赖注入,在一定程度会影响性能 + +:::important 特别说明 + +在本章节不打算细讲 `依赖注入/控制反转` 具体实现和应用场景,想了解更多知识,可查阅 【[ASP.NET Core 依赖注入](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-5.0)】 官方文档。 + +::: + +## 12.4 依赖注入的三种方式 + +### 12.4.1 构造方法注入 + +目前构造方法注入是依赖注入推荐使用方式。 + +- 优点 + + - 在构造方法中体现出对其他类的依赖,一眼就能看出这个类需要依赖哪些类才能工作 + - 脱离了 IOC 框架,这个类仍然可以工作 + - 一旦对象初始化成功了,这个对象的状态肯定是正确的 + +- 缺点 + + - 构造函数会有很多参数 + - 有些类是需要默认构造函数的,比如 MVC 框架的 Controller 类,一旦使用构造函数注入,就无法使用默认构造函数 + - 这个类里面的有些方法并不需要用到这些依赖 + +代码示例: + +```cs showLineNumbers {4} +public class FurionService +{ + private readonly IRepository _repository; + public FurionService(IRepository repository) + { + _repository = repository; + } +} +``` + +### 12.4.2 属性方式注入 + +:::warning 特别声明 + +在 `Furion` 新版本中,已经移除属性注入功能,建议使用构造函数或方法方式注入,也可以通过 `App.GetService` 方式注入。 + +::: + +**通过属性方式注入容易和类的实例属性混淆,不建议使用。** + +- 优点 + + - 在对象的整个生命周期内,可以随时动态的改变依赖 + - 非常灵活 + +- 缺点 + + - 对象在创建后,被设置依赖对象之前这段时间状态是不对的 + - 不直观,无法清晰地表示哪些属性是必须的 + +```cs showLineNumbers {3} +public class FurionService +{ + public IRepository Repository { get; set; } +} +``` + +### 12.4.3 方法参数注入 + +方法参数注入的意思是在创建对象后,通过自动调用某个方法来注入依赖。 + +- 优点: + + - 比较灵活 + +- 缺点: + + - 新加入依赖时会破坏原有的方法签名,如果这个方法已经被其他很多模块用到就很麻烦 + - 与构造方法注入一样,会有很多参数 + +```cs showLineNumbers {3} +public class FurionService +{ + public Person GetById([FromServices]IRepository repository, int id) + { + return repository.Find(id); + } +} +``` + +## 12.5 注册对象生存期 + +### 12.5.1 `暂时/瞬时` 生存期 + +暂时生存期服务是每次从服务容器进行请求时创建的。 这种生存期适合轻量级、 无状态的服务。 + +在处理请求的应用中,在请求结束时会释放暂时服务。 + +通常我们使用 `ITransient` 接口依赖表示该生命周期。 + +### 12.5.2 `作用域` 生存期 + +作用域生存期服务针对每个客户端请求(连接)创建一次。在处理请求的应用中,在请求结束时会释放有作用域的服务。 + +通常我们使用 `IScoped` 接口依赖表示该生命周期。 + +### 12.5.3 `单例` 生存期 + +在首次请求它们时进行创建,之后每个后续请求都使用相同的实例。 + +通常我们使用 `ISingleton` 接口依赖表示该生命周期。 + +:::note 了解更多 + +想了解更多 `服务生存期` 知识可查阅 [ASP.NET Core - 依赖注入 - 服务生存期](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-5.0#service-lifetimes) 章节。 + +::: + +## 12.6 内置依赖接口 + +`Furion` 框架提供三个接口依赖分别对应不同的服务生存期: + +- `ITransient`:对应暂时/瞬时作用域服务生存期 +- `IScoped`:对应请求作用域服务生存期 +- `ISingleton`:对应单例作用域服务生存期 + +:::warning 特别注意 + +以上三个接口只能实例类实现,其他静态类、抽象类、及接口不能实现。 + +::: + +## 12.7 常见使用 + +### 12.7.1 第一个例子 + +创建 `IBusinessService` 接口和 `BusinessService` 实现类,代码如下: + +```cs showLineNumbers {7,12} +using Furion.Core; +using Furion.DatabaseAccessor; +using Furion.DependencyInjection; + +namespace Furion.Application +{ + public interface IBusinessService + { + Person Get(int id); + } + + public class BusinessService : IBusinessService, ITransient + { + private readonly IRepository _personRepository; + + public BusinessService(IRepository personRepository) + { + _personRepository = personRepository; + } + + public Person Get(int id) + { + return _personRepository.Find(id); + } + } +} +``` + +创建 `PersonController` 控制器,代码如下: + +```cs showLineNumbers {11,19} +using Furion.Application; +using Microsoft.AspNetCore.Mvc; + +namespace Furion.Web.Entry.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class PersonController : ControllerBase + { + private readonly IBusinessService _businessService; + public PersonController(IBusinessService businessService) + { + _businessService = businessService; + } + + [HttpGet] + public IActionResult Get(int id) + { + var person = _businessService.Get(id); + return new JsonResult(person); + } + } +} +``` + + + +--- + +**例子解说** + +`Furion` 框架提供了非常灵活且方便的实现依赖注入的方式,只需要实例类继承对应生存期的接口即可,这里继承了 `ITransient`,也就表明了这是一个 `暂时/瞬时` 作用域实例类。该类就可以作为被注入对象,同时也能注入其他接口对象。 + +上面的例子中,`BusinessService` 注入了 `IRepository` 仓储接口,同时 `PersonController` 控制器注入了 `IBusinessService` 接口。 + +这样 `PersonController` 和 `BusinessService` 之间就实现了解耦,不再依赖于具体的 `BusinessService` 实例。 + +这就是依赖注入/控制反转最经典的例子。 + +### 12.7.2 注册泛型实例 + +创建 `IBusinessService` 接口和 `BusinessService` 实现类,代码如下: + +```cs showLineNumbers {7,12} +using Furion.Core; +using Furion.DatabaseAccessor; +using Furion.DependencyInjection; + +namespace Furion.Application +{ + public interface IBusinessService + { + Person Get(int id); + } + + public class BusinessService : IBusinessService, ITransient + { + private readonly IRepository _personRepository; + + public BusinessService(IRepository personRepository) + { + _personRepository = personRepository; + } + + public Person Get(int id) + { + return _personRepository.Find(id); + } + } +} +``` + +创建 `PersonController` 控制器,代码如下: + +```cs showLineNumbers {11,19} +using Furion.Application; +using Microsoft.AspNetCore.Mvc; + +namespace Furion.Web.Entry.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class PersonController : ControllerBase + { + private readonly IBusinessService _businessService; + public PersonController(IBusinessService businessService) + { + _businessService = businessService; + } + + [HttpGet] + public IActionResult Get(int id) + { + var person = _businessService.Get(id); + return new JsonResult(person); + } + } +} +``` + +### 12.7.3 一个接口多个实现 + +默认情况下,一个接口只对应一个实现类,但有些特殊情况,需要多个实现类注册同一个接口,如 `DbContext` 多数据库情况。 + +这个时候我们可以通过依赖注入 `Func` 委托来解析多个实例,其中委托的参数分别为: + +- 参数 1:`string` 类型,不同实现类唯一标识,默认为 `nameof(实现类)` 名称 +- 参数 2:`Type` 类型,`IPrivateDependency` 派生接口,也就是 `ITransient`、`IScoped`、`ISingleton` +- 返回值:`object` 类型,返回具体的实现类实例 + +创建 `IBusinessService` 接口和 `BusinessService`、`OtherBusinessService` 两个实现类,代码如下: + +```cs showLineNumbers {5,10,18} +using Furion.DependencyInjection; + +namespace Furion.Application +{ + public interface IBusinessService + { + string GetName(); + } + + public class BusinessService : IBusinessService, ITransient + { + public string GetName() + { + return "我是:" + nameof(BusinessService); + } + } + + public class OtherBusinessService : IBusinessService, ITransient + { + public string GetName() + { + return "我是:" + nameof(OtherBusinessService); + } + } +} +``` + +:::tip 新版本,推荐使用 + +在 `Furion 3.8.6+` 版本之后新增了 `INamedServiceProvider` 服务接口,可替代 `Func` 方式: + +```cs showLineNumbers {8,15-17,19-23} +using Furion.Application.Services; + +namespace Furion.Application; + +public class TestNamedServices : IDynamicApiController +{ + private readonly INamedServiceProvider _namedServiceProvider; + public TestNamedServices(INamedServiceProvider namedServiceProvider) + { + _namedServiceProvider = namedServiceProvider; + } + + public string GetName() + { + // 第一种用法,通过反射解析服务周期,性能有损耗 + var service1 = _namedServiceProvider.GetService(nameof(BusinessService)); + var service2 = _namedServiceProvider.GetService(nameof(OtherBusinessService)); + + // 第二种用法,无需反射,注意下面的泛型参数传入的是生命周期依赖接口,ITransient, IScoped, ISingleton + var service3 = _namedServiceProvider.GetService(nameof(BusinessService)); + var service4 = _namedServiceProvider.GetService(nameof(OtherBusinessService)); + + return service1.GetName() + "-" + service2.GetName() + "-" + service3.GetName() + "-" + service4.GetName(); + } +} +``` + +::: + +:::important 不再推荐 `Func` 方式 + +```cs showLineNumbers {15,17,18} +using Furion.Application; +using Furion.DependencyInjection; +using Microsoft.AspNetCore.Mvc; +using System; + +namespace Furion.Web.Entry.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class ValueController : ControllerBase + { + private readonly IBusinessService _businessService; + private readonly IBusinessService _otherBusinessService; + + public ValueController(Func resolveNamed) + { + _businessService = resolveNamed("BusinessService", default) as IBusinessService; + _otherBusinessService = resolveNamed("OtherBusinessService", default) as IBusinessService; + } + + [HttpGet] + public string GetName() + { + return _businessService.GetName() + "----------" + _otherBusinessService.GetName(); + } + } +} +``` + +::: + + + +:::tip 小知识 + +如果需要自定义解析名称,只需要贴 `[Injection(Named = "名称")]` 即可,如: + +```cs showLineNumbers {5,11} +using Furion.DependencyInjection; + +namespace Furion.Application +{ + [Injection(Named = "BusName1")] + public class BusinessService : IBusinessService, ITransient + { + // ... + } + + [Injection(Named = "BusName2")] + public class OtherBusinessService : IBusinessService, ITransient + { + // ... + } +} +``` + +解析服务: + +```cs showLineNumbers +_businessService = resolveNamed("BusName1", default) as IBusinessService; +_otherBusinessService = resolveNamed("BusName2", default) as IBusinessService; +``` + +::: + +### 12.7.4 无接口方式 + +有些时候,我们不想定义接口,而是想把实例类作为可依赖注入的对象,如 MVC 中的控制器。 + +创建 `SelfService` 实例类,代码如下: + +```cs showLineNumbers {7,11} +using Furion.Core; +using Furion.DatabaseAccessor; +using Furion.DependencyInjection; + +namespace Furion.Application +{ + public class SelfService : ITransient + { + private readonly IRepository _personRepository; + + public SelfService(IRepository personRepository) + { + _personRepository = personRepository; + } + + public Person Get(int id) + { + return _personRepository.Find(id); + } + } +} +``` + +创建 `ValueController` 控制器,代码如下: + +```cs showLineNumbers {13,21} +using Furion.Application; +using Furion.Core; +using Microsoft.AspNetCore.Mvc; + +namespace Furion.Web.Entry.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class ValueController : ControllerBase + { + private readonly SelfService _selfService; + + public ValueController(SelfService selfService) + { + _selfService = selfService; + } + + [HttpGet] + public Person Get(int id) + { + return _selfService.Get(id); + } + } +} +``` + +## 12.8 `[Injection]` 特性配置 + +`Furion` 框架提供 `[Injection]` 特性可以改变注册方式,同时还能配置 `AOP` 拦截。 + +`[Injection]` 提供以下配置支持: + +- `Action`:配置注册行为,`InjectionActions` 类型,可选值: + - `Add`:**默认值**,表示无限制添加注册服务,该方式支持一个接口多个实现 + - `TryAdd`:表示注册已存在则跳过注册 +- `Pattern`:配置注册选项,`InjectionPatterns` 类型,可选值: + - `Self`:只注册自己 + - `FirstInterface`:只注册第一个接口 + - `SelfWithFirstInterface`:注册自己和第一个接口 + - `ImplementedInterfaces`:注册所有接口 + - `All`:注册自己包括所有接口 ,**默认值** +- `Named`:配置实例别名,通过别名可以解析接口,如同一个接口有多个实现,那么可以通过别名解析不同的实现,默认值为实现类的类名 +- `Order`:注册排序,数字越大,则越在最后注册,默认 `0` +- `Proxy`:配置代理拦截类型,也就是 `AOP`,**代理类型必须继承 `AspectDispatchProxy` 类和 `IDispatchProxy` 接口**,无默认值 +- `ExceptInterfaces`:配置忽略注册的接口,`Type[]` 类型 + +## 12.9 自定义高级注册 + +默认情况下,`Furion` 提供的注册方式可以满足大多数依赖注入的需求,如有特别注册需求,只需要在 `Startup` 中配置即可,如: + +```cs showLineNumbers +services.AddScoped(typeof(ISpecService), provider = > { + // 自定义任何创建实例的方式 + var instance = new SpecService(); // 或者可以通过 AOP插件返回代理实例 + + return instance; +}); +``` + +:::note 补充说明 + +`Furion` 框架中的 `AppDbContext` 数据库上下文还有 `ISqlDispatchProxy` 都是通过这种方式创建的。 + +::: + +:::tip 知识导航 + +想了解更多自定义高级中注册,可查阅 【[ASP.NET Core 依赖注入](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-5.0)】 官方文档。 + +::: + +## 12.10 `appsettings.json` 配置注册 + +除了在代码中实现依赖注入,也可以实现动态依赖注入,无需修改代码或重新编译即可实现热插拔(插件)效果。配置如下: + +```json showLineNumbers {2} +{ + "DependencyInjectionSettings": { + "Definitions": [ + { + "Interface": "Furion.Application;Furion.Application.ITestService", + "Service": "Furion.Application;Furion.Application.TestService", + "RegisterType": "Transient", + "Action": "Add", + "Pattern": "SelfWithFirstInterface", + "Named": "TestService", + "Order": 1, + "Proxy": "Furion.Application;Furion.Application.LogDispathProxy" + } + ] + } +} +``` + +配置说明: + +- `DependencyInjectionSettings`:依赖注入配置根节点 + - `Definitions`:动态依赖注入配置节点,`ExternalService` 数组类型 + - `ExternalService`:配置单个依赖注入信息 + - `Interface`:配置依赖接口信息,格式:`程序集名称;接口完整名称`,如:`Furion.Application;Furion.Application.ITestService` + - `Service`:配置接口实现信息,格式同上 + - `RegisterType`:配置依赖注入的对象生存期,取值:`Transient`,`Scoped`,`Singleton` + - `Action`:注册行为,可选值:`Add`,`TryAdd`,参见 [#128-injection-特性配置](#128-injection-特性配置) + - `Pattern`:注册选项,参见 [#128-injection-特性配置](#128-injection-特性配置) + - `Named`:注册别名,参见 [#128-injection-特性配置](#128-injection-特性配置) + - `Order`:注册排序,参见 [#128-injection-特性配置](#128-injection-特性配置) + - `Proxy`:配置代理拦截,格式:`程序集名称;代理类完整名称`,参见 [#128-injection-特性配置](#128-injection-特性配置) + +:::important 关于外部程序集 + +如果动态注入的对象是外部程序集,那么首先先注册外部程序集: + +```json showLineNumbers +{ + "AppSettings": { + "ExternalAssemblies": ["外部程序集名称", "Taobao.Pay"] // 支持多个 + } +} +``` + +::: + +## 12.11 注册顺序和优先级 + +`Furion` 框架中,默认注册顺序是按照程序集扫描顺序进行注册,如果需要改变注册顺序,可通过 `[Injection(Order)]` 特性指定,`Order` 值越大,则越在最后注册。 + +另外 `appsettings.json` 配置的优先级最大,`appsettings.json` 配置的注册会覆盖之前所有注册。 + +## 12.12 `Aop` 注册拦截 + +:::warning 关于动态 API 和服务的区别 + +如果您的服务是动态 API,那么请使用 [动态 API - AOP 拦截](/docs/dynamic-api-controller#511-关于-aop-拦截),原因是动态 API 本质是控制器,所以采用 `Filter` 方式。 + +::: + +`AOP` 是非常重要的思想和技术,也就是 `面向切面` 编程,可以让我们在不改动原来代码的情况下进行动态篡改业务代码。 + +在 `Furion` 框架中,实现 `Aop` 非常简单,如: + +假设我们有 `ITestService` 和 `TestService` 两个类型: + +```cs showLineNumbers +public interface ITestService +{ + string SayHello(string word); +} +``` + +```cs showLineNumbers +public class TestService: ITestService, ITransient +{ + public string SayHello(string word) + { + return $"Hello {word}"; + } +} +``` + +现在我们有一个需求,我们希望调用 `SayHello` 的时候可以记录日志和权限控制(之前没有考虑到的需求)。 + +这个时候我们只需要创建一个代理类即可,如 `LogDispatchProxy` + +```cs showLineNumbers {1,3,7,25,37,48} +using Furion.DependencyInjection; +using System; +using System.Reflection; + +namespace Furion.Application +{ + public class LogDispatchProxy : AspectDispatchProxy, IDispatchProxy + { + /// + /// 当前服务实例 + /// + public object Target { get; set; } + + /// + /// 服务提供器,可以用来解析服务,如:Services.GetService() + /// + public IServiceProvider Services { get; set; } + + /// + /// 拦截方法 + /// + /// + /// + /// + public override object Invoke(MethodInfo method, object[] args) + { + Console.WriteLine("SayHello 方法被调用了"); + + var result = method.Invoke(Target, args); + + Console.WriteLine("SayHello 方法返回值:" + result); + + return result; + } + + // 异步无返回值 + public async override Task InvokeAsync(MethodInfo method, object[] args) + { + Console.WriteLine("SayHello 方法被调用了"); + + var task = method.Invoke(Target, args) as Task; + await task; + + Console.WriteLine("SayHello 方法调用完成"); + } + + // 异步带返回值 + public async override Task InvokeAsyncT(MethodInfo method, object[] args) + { + Console.WriteLine("SayHello 方法被调用了"); + + var taskT = method.Invoke(Target, args) as Task; + var result = await taskT; + + Console.WriteLine("SayHello 方法返回值:" + result); + + return result; + } + } +} +``` + +:::important 获取特性 + +如果需要获取方法的特性,只需要通过 `method.GetActualCustomAttribute()` 即可。所有获取真实的特性统一采用 `method.GetActual....()` 方法开头。 + +::: + +之后我们只需要为 `TestService` 增加 `[Injection]` 特性即可,如: + +```cs showLineNumbers {1} +[Injection(Proxy = typeof(LogDispatchProxy))] +public class TestService: ITestService, ITransient +{ + public string SayHello(string word) + { + return $"Hello {word}"; + } +} +``` + +之后 `SayHello` 方法被调用的时候就可以实现动态拦截了,比如这里写日志。 + +### 12.12.1 `全局Aop拦截` + +`Furion` 框架也提供了全局拦截的方式,只需要将 `IDispatchProxy` 修改为 `IGlobalDispatchProxy` 即可。 + +```cs showLineNumbers +using Furion; +using System.Reflection; + +namespace Furion.Application +{ + public class LogDispatchProxy : AspectDispatchProxy, IGlobalDispatchProxy + { + // .... + } +} +``` + +这样就会拦截所有的 `Service`,当然也可以通过给特定类贴 `[SuppressProxy]` 跳过全局拦截操作。 + +:::important 拦截优先级 + +`[SuppressProxy]` > `[Injection(Proxy = typeof(LogDispatchProxy))]` > `全局拦截`。 + +::: + +### 12.12.2 `AOP` 注入解析服务 + +`Furion` 框架未提供 `Proxy` 构造函数注入功能,但是提供了 `Services` 属性,如果需要解析服务,则可以通过以下方式: + +```cs showLineNumbers +var someServices = Services.GetService(); // 推荐方式 +// 或 +var someServices = App.GetService(); +``` + +### 12.12.3 `AOP` 的作用 + +这种面向切面的能力(动态拦截/代理)可以实现很多很多功能,如: + +- 动态日志记录 +- 动态修改参数 +- 动态修改返回值 +- 动态方法重定向 +- 动态修改代码逻辑 +- 动态实现异常监听 + +还可以做更多更多的事情。 + +## 12.13 在非 `Web` 或多线程解析服务 + +默认情况下,在 `Web` 请求开始之前会自动创建范围作用域,这个作用域的生存周期是请求之前和响应之后,也就是在这个作用域内的所有服务都实现了自动管理,比如创建服务和释放服务的时机。 + +**但在非 `Web` 或多线程中,框架并不会做这样的事情,也就是框架只负责了服务的创建,但是没有负责销毁,原因是框架无法得知具体的销毁时机,这样就导致了内存溢出。** + +解决方式是:在非 `Web` 或多线程中使用 **非单例** 服务,应该主动创建作用域,类似过去的 `using`,目前框架提供了几种方式。 + +### 12.13.1 `IServiceProvider` 方式 + +```cs showLineNumbers {1-2,5,8,11-12} +using var scope = serviceProvider.CreateScope(); +var services = scope.ServiceProvider; + +// 获取数据库上下文 +var dbContext = Db.GetDbContext(services); + +// 获取仓储 +var respository = Db.GetRepository(services); + +// 解析其他服务 +var otherService = services.GetService(); +var otherService2 = App.GetService(services); +``` + +### 12.13.2 `IServiceScopeFactory` 方式 + +```cs showLineNumbers +using var scope = serviceScopeFactory.CreateScope(); +var services = scope.ServiceProvider; +``` + +### 12.13.3 `Scoped` 静态类 + +为了方法快速创建服务作用域,`Furion` 框架提供了 `Scoped` 静态类,如: + +```cs showLineNumbers {1-2} +Scoped.Create((factory, scope) => { + var services = scope.ServiceProvider; +}); +``` + +## 12.14 在 `WinForm/WPF` 中使用 + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.7.23 +` 版本使用。 + +::: + +默认情况下,`WinForm/WPF` 不支持依赖注入功能,但 `Furion` 框架提供了完整的支持,**只需要将所有 `new Form()` 或 `new Window()` 替换为 `Native.CreateInstance
()` 或 `Native.CreateInstance()` 即可**。 + +### 12.14.1 在 `WinForm` 中使用 + +- 修改 `Progame.cs` 代码 + +```cs showLineNumbers {9,12} title="Progame.cs" +namespace WinFormsApp1; + +internal static class Program +{ + [STAThread] + static void Main() + { + // 初始化 Furion 配置 + Serve.RunNative(); + + ApplicationConfiguration.Initialize(); + Application.Run(Native.CreateInstance()); // 替换 new Form1() 为 Native.CreateInstance() + } +} +``` + +- 使用完整的依赖注入功能 + +```cs showLineNumbers {11-12,16-17,21} +using Furion.DependencyInjection; +using Microsoft.Extensions.Configuration; + +namespace WinFormsApp1; + +public partial class Form1 : Form +{ + private readonly IConfiguration _configuration; + private readonly ITest _test; + + public Form1(IConfiguration configuration + , ITest test) + { + InitializeComponent(); + + _configuration = configuration; + _test = test; + } +} + +public class Test : ITest, ITransient +{ +} + +public interface ITest +{ +} +``` + +- 打开新窗口 `Form2` + +```cs showLineNumbers {10,14-15} +using Furion.DependencyInjection; +using Microsoft.Extensions.Configuration; + +namespace WinFormsApp1; + +public partial class Form2 : Form +{ + private readonly IConfiguration _configuration; + + public Form2(IConfiguration configuration, string parameter) // 支持其他参数 parameter + { + InitializeComponent(); + + _configuration = configuration; + label1.Text = parameter; + } +} +``` + +在 `Form1` 中打开 `From2` + +```cs showLineNumbers {3} +private void button1_Click(object sender, EventArgs e) +{ + Native.CreateInstance("我是其他参数").ShowDialog(); // new Form2() 改为 Native.CreateInstance(); +} +``` + +:::tip 获取打开窗口实例 + +`Native.CreateInstance<>` 每次都会创建新的实例,如果只是想获取打开的窗口的实例可通过以下方式: + +```cs showLineNumbers +var form1 = Application.OpenForms.OfType().FirstOrDefault(f => f is Form1); +``` + +::: + +### 12.14.2 在 `WPF` 中使用 + +- 修改 `App.xaml` 代码并删除 `StartupUri="MainWindow.xaml"` 属性 + +```xml showLineNumbers {5} + + + + + + +``` + +- 修改 `App.xaml.cs` 代码 + +```cs showLineNumbers {7-10,12-16} +using System.Windows; + +namespace WpfApp1; + +public partial class App : Application +{ + public App() + { + Serve.RunNative(); + } + + protected override void OnStartup(StartupEventArgs e) + { + Native.CreateInstance().Show(); + base.OnStartup(e); + } +} +``` + +- 使用完整的依赖注入功能 + +```cs showLineNumbers {12-13,17-18,22} +using Furion.DependencyInjection; +using Microsoft.Extensions.Configuration; +using System.Windows; + +namespace WpfApp1; + +public partial class MainWindow : Window +{ + private readonly IConfiguration _configuration; + private readonly ITest _test; + + public MainWindow(IConfiguration configuration + , ITest test) + { + InitializeComponent(); + + _configuration = configuration; + _test = test; + } +} + +public class Test : ITest, ITransient +{ +} + +public interface ITest +{ +} +``` + +- 打开新窗口 `Window1` + +```cs showLineNumbers {7,11,15-16} +using Furion.DependencyInjection; +using Microsoft.Extensions.Configuration; +using System.Windows; + +namespace WpfApp1; + +public partial class Window1 : Window +{ + private readonly IConfiguration _configuration; + + public Window1(IConfiguration configuration, string parameter) // 支持其他参数 parameter + { + InitializeComponent(); + + _configuration = configuration; + label1.Content = parameter; + } +} + +``` + +在 `MainWindow` 中打开 `Window1` + +```cs showLineNumbers {3} +private void Button_Click(object sender, RoutedEventArgs e) +{ + Native.CreateInstance("我是其他参数").Show(); +} +``` + +:::tip 获取打开窗口实例 + +`Native.CreateInstance<>` 每次都会创建新的实例,如果只是想获取打开的窗口的实例可通过以下方式: + +```cs showLineNumbers +var window1 = Application.Current.Windows.OfType().FirstOrDefault(w => w is MainWindow); +``` + +::: + +### 12.14.3 `Native.CreateInstance()` 静态方法 + +`Furion` 框架提供了 `Native.CreateInstance()` 静态方法可在 `WinForm/WPF` 中创建窗口实例,可替代 `new T()` 操作,通过此方法创建实例支持构造函数注入服务操作。 + +## 12.15 自定义扫描/筛选注册服务 + +`Furion` 框架中并未提供完全自定义依赖注入扫描的机制,但推荐一个非常优秀的 `.NET Core` 依赖注入拓展库:`Scrutor`,使用非常简单,主要通过 `FromAssemblyOf<>` 扫描程序集和 `AddClasses(o)` 进行筛选注册。 + +使用如下: + +```cs showLineNumbers {1,3,6,13,19,22} +services.Scan(scan => scan + // 扫描特定类型所在的程序集,这里是 ITransientService 所在的程序集 + .FromAssemblyOf() + // .AddClasses 在上面获取到的程序集中扫描所有公开、非抽象类型 + // 之后可以通过委托进行类型筛选,例如下面只扫描实现 ITransientService 的类型 + .AddClasses(classes => classes.AssignableTo()) + // 将上面的类型作为它实现的所有接口进行注册 + // 如果类型实现了 N 个接口,那么就会有三个独立的注册 + .AsImplementedInterfaces() + // 最后指定注册的生存期,如瞬时,作用域,还是单例 + .WithTransientLifetime() + // 重复上面操作,比如这里扫描 IScopedService 所在的程序集 + .AddClasses(classes => classes.AssignableTo()) + // 这里和上面不一样的是,这里指定只实现特定的几口,也就是只注册一次 + .As() + // 指定注册的生存期 + .WithScopedLifetime() + // 也支持泛型注册,单个泛型参数 + .AddClasses(classes => classes.AssignableTo(typeof(IOpenGeneric<>))) + .AsImplementedInterfaces() + // 多个泛型参数 + .AddClasses(classes => classes.AssignableTo(typeof(IQueryHandler<,>))) + .AsImplementedInterfaces()); +``` + +详细文档请查阅 [https://github.com/khellang/Scrutor](https://github.com/khellang/Scrutor) + +## 12.16 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/deploy-docker-auto.mdx b/handbook/docs/deploy-docker-auto.mdx new file mode 100644 index 0000000000000000000000000000000000000000..c4fcdab676be27487587958fc1046a8fdcbe4d4a --- /dev/null +++ b/handbook/docs/deploy-docker-auto.mdx @@ -0,0 +1,296 @@ +--- +id: deploy-docker-auto +title: 35.1 Docker 环境持续部署 +sidebar_label: 35.1 Docker 环境持续部署 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +## 35.1.1 关于全 `Docker` 环境部署 + +利用拥有 `.NET` 环境的 `Jenkins`,进行持续化部署 + +## 35.1.2 安装 `Docker` 版 `Jenkins` + +正常在 `Docker` 中拉取的 `Jenkins:lts` 是无法执行 `dotnet` 命令的(就算你宿主机有 `dotnet` 环境、`docker` 中也有 `dotnet` 环境也不可以), +所以我们只能构建一个包含 `dotnet` 的镜像 + +### 35.1.2.1 使用 Dockerfile 制作镜像 + +使用 `Dockerfile` 创建包含 `dotnet` 的 `Jenkins` 镜像 + +- 👉 编写 `Dockerfile` + +```bash showLineNumbers +# 封装Jenkins镜像(带有dotnet环境的) sdk=5.1 +FROM jenkins/jenkins:lts +USER root +WORKDIR /dotnet +RUN apt-get update && apt-get install -y wget && rm -rf /var/lib/apt/lists/* +RUN wget -O dotnet.tar.gz https://download.visualstudio.microsoft.com/download/pr/820db713-c9a5-466e-b72a-16f2f5ed00e2/628aa2a75f6aa270e77f4a83b3742fb8/dotnet-sdk-5.0.100-linux-x64.tar.gz +RUN tar zxf dotnet.tar.gz -C ./ +RUN rm -rf dotnet.tar.gz +ENV PATH="${PATH}:/dotnet:/var/jenkins_home/.dotnet/tools" +ENV DOTNET_ROOT="/dotnet" +RUN apt update -y +RUN apt install icu-devtools vim zip unzip -y +RUN usermod -a -G root jenkins +USER jenkins +``` + +- 👉 命令解释 + +```bash showLineNumbers +- 1. 这个Docker镜像基于jenkins +- 2. 设置当前用户为root,因为后面安装需要使用root +- 3. 设置当前工作目录为dotnet +- 4. 下载dotnet SDK包,保存为dotnet.tar.gz。这里要注意下载正确版本的SDK,可前往微软官方网站获取下载链接:https://dotnet.microsoft.com/download +- 5. 解压dotnet SDK到当前目录,即/dotnet目录 +- 6. 删除dotnet SDK包 +- 7. 把dotnet目录和dotnet tools目录添加到环境变量PATH,这样就可以使用dotnet命令了 +- 8. 设置DOTNET_ROOT变量 +- 9. 更新源 +- 10. 安装一些必需的,常用的工具包,其中icu-devtools是运行dotnet需要的 +- 11. 修改jenkins用户到root附加组 +- 12. 设置当前用户为jenkins +``` + +- 👉 构建 `Docker` 镜像 `name=jenkins:dotnet` + +`cd` 到根目录下(必须含 `Dockerfile`) 只需构建命令: + +```bash showLineNumbers + docker build -t jenkins:dotnet . +``` + +:::important 特别注意 + +结尾 `.` 不能省略 + +::: + +### 35.1.2.2 运行 `Jenkins:dotnet` 镜像 + +```bash showLineNumbers +docker run -d -p 8080:8080 -p 50000:50000 --name mjenkins \ + --privileged=true \ + --restart always \ + -u root \ + -e TZ="Asia/Shanghai" \ + -v /mudata/jenkins:/var/jenkins_home \ + -v /usr/bin/docker:/usr/bin/docker \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v /mudata/webroot/:/mudata/webroot \ + jenkins:dotnet +``` + +接下来就是比较俗套的安装 `Jenkins` 步骤,网上资料很多,不展开了。 + +## 35.1.3 `Jenkins` 的自动化部署 + +### 35.1.3.1 编写 Shell 脚本 + +```bash showLineNumbers +# Jenkins 构建 测试服 + +echo '============查看打包环境================' +pwd +ls +echo $PATH + +image_version=`date +%Y%m%d%H%M`; +echo $image_version; + +dotnet --info +dotnet --version + +# 获取短版本号 +GITHASH=`git rev-parse --short HEAD` + +echo '============================begin restore=======================================' +dotnet restore +echo '============================end restore=======================================' + +#要构建的解决方案名称 +solutionName=MUSaas.SCM.BasicData +#docker run的容器名称 +containerName=jenkinsscmbasic +#指定run的端口 +port=9994 +#.sln文件全路径 +#solutionDir=20-Solution/${solutionName}.sln +#.csproj文件全路径 +csprojDir=${solutionName}/${solutionName}.csproj + +#项目发布的目录 +webDir=/mudata/webroot/jenkins/publish/webapp + +#归档目录 +archivesDir=/mudata/webroot/jenkins/publish/archives + +#清空文件夹 +rm -rf ${webDir}/${JOB_NAME}/* + +#发布网站到webDir +dotnet publish ${JENKINS_HOME}/workspace/${JOB_NAME}/${csprojDir} -c Release -o ${webDir}/${JOB_NAME} /p:Version=1.0.${BUILD_NUMBER} +#复制配置文件 +#cp -rf /vdb1/jenkins/DotNetCoreWebPublishToDockerCommonConfigs/* ${webDir}/${JOB_NAME}/ + +#判斷是否存在 +CID=$(docker ps | grep "${containerName}" | awk '{print $1}') +echo $CID +if [ "$CID" != "" ];then + docker stop ${containerName} + docker rm ${containerName} + docker rmi ${containerName} +#docker stop $CID +#docker rm $CID +fi + + +#通过Dockerfile重新构建镜像 +docker build -t ${containerName} ${webDir}/${JOB_NAME}/. +#docker run容器并绑定到端口 +#docker run -d -p ${port}:80 --name ${containerName} ${containerName} +docker run --name ${containerName} --restart=always -d -p ${port}:${port} -v /etc/localtime:/etc/localtime:ro ${containerName} +echo "success!" + +``` + +> 就这样自动化部署就好了。 测试服的 `Jenkins` 将源码拉下来,`Publish,Docker Build,Docker Run`。 + +> 这里想要发布的时候,每次都需要手动去点击“构建”才会执行。也可以做成当分支合并成功后自动运行。反正 `Jenkins` 装好之后,你想要什么都能玩起来。比如指定分支提交后自动“构建”、比如构建成功后合并到 Master 等等 + +## 35.1.4 `Jenkins` 的自动化远程部署 + +### 35.1.4.1 安装插件 + +> `Publish Over SSH` + +### 35.1.4.2 配置 + +> 系统管理 => `Publish over SSH` + +### 35.1.4.3 写脚本 + +```bash showLineNumbers +# Jenkins 构建 正式服 + +echo '============查看打包环境================' +pwd +ls +echo $PATH + +image_version=`date +%Y%m%d%H%M`; +echo $image_version; + +dotnet --info +dotnet --version + +# 获取短版本号 +GITHASH=`git rev-parse --short HEAD` + +echo '============================begin restore=======================================' +dotnet restore +echo '============================end restore=======================================' + +#要构建的解决方案名称 +solutionName=MUSaas.SCM.BulkOrder +#docker run的容器名称 +containerName=jenkinsscmbulk +#指定run的端口 +port=9986 +#.csproj文件全路径 +csprojDir=/${solutionName}/${solutionName}.csproj + +#项目发布的目录 +webDir=/mudata/webroot/jenkins/publish/webapp + +#归档目录 +archivesDir=/mudata/webroot/jenkins/publish/archives + +#清空文件夹 +rm -rf ${webDir}/${JOB_NAME}/* + +#发布网站到webDir +dotnet publish ${JENKINS_HOME}/workspace/${JOB_NAME}/${csprojDir} -c Release -o ${webDir}/${JOB_NAME} /p:Version=1.0.${BUILD_NUMBER} +#复制配置文件 +#cp -rf /vdb1/jenkins/DotNetCoreWebPublishToDockerCommonConfigs/* ${webDir}/${JOB_NAME}/ + + +#构建远程包 + +rm -rf ${JENKINS_HOME}/workspace/${JOB_NAME}/publish +mkdir ${JENKINS_HOME}/workspace/${JOB_NAME}/publish + +tar -czvf ${JENKINS_HOME}/workspace/${JOB_NAME}/publish/${JOB_NAME}.${BUILD_NUMBER}.tar.gz -C ${webDir}/${JOB_NAME} . + +echo "success!" +``` + +> 大概逻辑就是发布后,打个包。然后丢给远程,远程再执行 `shell` + +> 注意这里一定要发布到自己的 `workspace` 下,防止下一步死活找不到位置。如果找不到位置,只能慢慢用 `ls` 命令,一级一级去测,很麻烦 + +### 35.1.4.4 构建后操作(关键) + +选择 `Send Build artifacts over SSH` + +```bash showLineNumbers +Source files: publish/ +Remove prefix(不填) +Remote directory:/mudata/webroot/publish/ +Exec command:bash /mudata/shell/publish.sh ${JOB_NAME} jenkinsscmbase ${JOB_NAME}.${BUILD_NUMBER} 9994 +``` + +- 选择自己的 SSH 服务器 +- `Source files`:一定是 `workspace` 下的地址 +- `Remote directory`:远程地址,从根目录开始 +- `Exec command`:要执行的 shell。这里所有的 `Jenkins` 环境变量都可以用 + +### 35.1.4.5 远程执行 + +```bash showLineNumbers title="publish.sh" +# Jenkins Prod服 调用脚本 +solutionName=$1 +containerName=$2 +filename=$3 +port=$4 +#.publis +echo ${solutionName} +echo ${containerName} +echo ${filename} +baseDir=/mudata/webroot/publish + +webDir=${baseDir}/publish/${filename} + +rm -rf ${webDir} +mkdir ${webDir} + +tar -zxvf ${baseDir}/publish/${filename}.tar.gz -C ${webDir}/ +rm -f ${webDir}/appsettings.json && mv ${webDir}/appsettings.Prod.json ${webDir}/appsettings.json + +#判斷是否存在 +CID=$(docker ps | grep "${containerName}" | awk '{print $1}') +echo $CID +if [ "$CID" != "" ];then + docker stop ${containerName} + docker rm ${containerName} + docker rmi ${containerName} +#docker stop $CID +#docker rm $CID +fi + +cd ${webDir}/ && docker build -t ${containerName} . +docker run --name ${containerName} --restart=always -d -p ${port}:${port} --link myredis:myredis -v /etc/localtime:/etc/localtime:ro ${containerName} +``` + +> 这里的逻辑就是解压,然后 `Docker` 相关。每次构建都是带着版本号来的。 + +## 35.1.5 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/deploy-docker.mdx b/handbook/docs/deploy-docker.mdx new file mode 100644 index 0000000000000000000000000000000000000000..69af55ea153e5843267ebfe749eccf88c84f9852 --- /dev/null +++ b/handbook/docs/deploy-docker.mdx @@ -0,0 +1,167 @@ +--- +id: deploy-docker +title: 34.2 在 Docker 部署 +sidebar_label: 34.2 在 Docker 部署 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +:::tip 精简发布文件 + +如果需要精简发布后的文件,也就是删除不必要的文件夹,可以编辑 Web 项目的 `.csproj` 并添加 `en-US`,如: + +```cs showLineNumbers {3} + + net6.0 + en-US + +``` + +若无需生成 `.pdb` 文件,可以继续添加: + +```cs showLineNumbers {2-3} + + none + false + +``` + +::: + +## 34.2.1 关于 `Docker` 部署 + +在 `Docker` 中部署网站有两种方式: + +- `发布后构建`:此方式是先发布网站后在再构建镜像,这样可以减少不必要的构建层,而且还能缩减镜像大小。**(推荐)** +- `编译+构建+发布`:也就是说在 `Dockerfile` 中配置网站从构建到发布的完整过程,此方式会速度慢,而且会产生冗余层,增加镜像大小。 + +## 34.2.2 两种方式构建 + +### 34.2.2.1 发布后构建 + +- 👉 发布网站 + +首先在 `Visual Studio` 或 `dotnet cli` 中发布网站,可以参考 [在 IIS 部署-发布网站](deploy-iis#3411-发布网站) + +- 👉 编写 `Dockerfile` + +```bash showLineNumbers +FROM mcr.microsoft.com/dotnet/aspnet:7.0 +WORKDIR /app +EXPOSE 80 +EXPOSE 443 + +COPY . . +ENTRYPOINT ["dotnet", "Furion.Web.Entry.dll"] +``` + +- 👉 将 `Dockerfile` 文件拷贝到发布根目录 + +将编写好的 `Dockerfile` 文件(注意 `D` 大写)拷贝到发布网站的根目录下。 + +- 👉 构建 `Docker` 镜像 + +在网站发布后的路径根目录下(必须含 `Dockerfile`)打开 `CMD/PowerShell` 只需构建命令: + +```bash showLineNumbers +docker build -t 网站名称:网站版本号 . +``` + +:::important 特别注意 + +后端的 `.` 不能省略 + +::: + +- 👉 启动镜像 + +```bash showLineNumbers +docker run --name 容器名称 -p 5000:80 --restart=always -d 网站名称:网站版本号 +``` + +:::caution `.NET8` 中 `80` 端口问题 + +在使用 `.NET8`,默认的端口由原来的 `80` 端口变成了 `8080`。[查看相关说明](https://learn.microsoft.com/zh-cn/dotnet/core/compatibility/containers/8.0/aspnet-port) + +::: + +:::tip 发布到 `hub.docker.com` + +如果需要将该网站的镜像公开出去,那么可以发布到 `hub.docker.com` 中。发布步骤如下: + +- 👉 为镜像打 `tag` 标签 + +```bash showLineNumbers +docker tag 网站名称:网站版本号 docker账号名/网站名称:网站版本号 +``` + +如: + +```bash showLineNumbers +docker tag furion:v2.20 monksoul/furion:v2.20 +``` + +- 👉 登录 `docker` + +```bash showLineNumbers +docker login +``` + +- 👉 推送到 `hub.docker.com` + +```bash showLineNumbers +docker push docker账号名/网站名称:网站版本号 +``` + +如: + +```bash showLineNumbers +docker push monksoul/furion:v2.20 +``` + +::: + +### 34.2.2.2 编译+构建+发布 + +此方式可以偷懒,但是不太推荐,不过在某些场景下非常有用,就是集成 `Devops` 工具链可以做到一步到位。 + +- 👉 编写 `Dockerfile` + +这种方式只需要把 `Dockerfile` 内容替换成以下即可: + +```bash showLineNumbers {6} +FROM mcr.microsoft.com/dotnet/sdk:5.0.9 AS build +WORKDIR /source + +# Download Source +RUN git init +RUN git remote add -t master -m master origin 你的源码Git地址 +RUN git config core.sparseCheckout true +RUN echo samples >> .git/info/sparse-checkout +RUN git pull --depth 1 origin main + +# Restore And Publish +WORKDIR /source/samples +RUN dotnet restore +RUN dotnet publish -c release -o /app --no-restore + +# Run +FROM mcr.microsoft.com/dotnet/aspnet:5.0.9 +WORKDIR /app +COPY --from=build /app ./ +EXPOSE 80 +EXPOSE 443 +ENTRYPOINT ["dotnet", "Furion.Web.Entry.dll"] +``` + +- 👉 在 `Dockerfile` 所在路径构建 + +接下来的步骤和上述步骤一致,不再重复编写。 + +## 34.2.3 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/deploy-iis.mdx b/handbook/docs/deploy-iis.mdx new file mode 100644 index 0000000000000000000000000000000000000000..7d6cb2c0775d1ec69c649c5488e9d7f1d1630c68 --- /dev/null +++ b/handbook/docs/deploy-iis.mdx @@ -0,0 +1,198 @@ +--- +id: deploy-iis +title: 34.1 在 IIS 部署 +sidebar_label: 34.1 在 IIS 部署 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +:::tip 精简发布文件 + +如果需要精简发布后的文件,也就是删除不必要的文件夹,可以编辑 Web 项目的 `.csproj` 并添加 `en-US`,如: + +```cs showLineNumbers {3} + + net6.0 + en-US + +``` + +若无需生成 `.pdb` 文件,可以继续添加: + +```cs showLineNumbers {2-3} + + none + false + +``` + +::: + +## 34.1.1 发布网站 + +### 34.1.1.1 选择启动项发布 + + + +### 34.1.1.2 选择发布到文件夹 + + + +### 34.1.1.3 配置发布后路径 + + + +### 34.1.1.4 点击发布 + + + +## 34.1.2 服务器环境配置 + +### 34.1.2.1 第一步 + +安装.NET Core 运行时捆绑包:[点击下载](https://dotnet.microsoft.com/permalink/dotnetcore-current-windows-runtime-bundle-installer) + +### 34.1.2.2 第二步(命令) + +```bash showLineNumbers +net stop was /y +``` + +### 34.1.2.3 第三步(命令) + +```bash showLineNumbers +net start w3svc +``` + +### 34.1.2.4 第四步(命令) + +```bash showLineNumbers +set ASPNETCORE_ENVIRONMENT=Production +``` + +## 34.1.3 部署到 IIS + +### 34.1.3.1 添加新网站 + + + +### 34.1.3.2 配置网站信息 + + + +### 34.1.3.3 配置应用程序池 + + + +### 34.1.3.4 设置为 `非托管` + + + +### 34.1.3.5 重启网站 + +只需重启网站或应用程序池即可。 + +## 34.1.4 常见问题 + +### 34.1.4.1 405 状态码,不支持 `PUT,DELETE` 请求 + +默认情况下,`IIS`拒绝 `PUT`和 `DELETE` 请求,原因为 `IIS` 默认注册了一个名为 `WebDAVModule` 的自定义 `HttpModule` 导致的。 + +解决该问题,只需要在 `web.config` 移除即可: + +```xml {2-6} + + + + + + + +``` + +微软官方文档:[https://docs.microsoft.com/zh-cn/troubleshoot/developer/webapps/iis/health-diagnostic-performance/http-error-405-website](https://docs.microsoft.com/zh-cn/troubleshoot/developer/webapps/iis/health-diagnostic-performance/http-error-405-website) + + + +### 34.1.4.2 `WebSocket`/ `SignalR` 连接报错 + +如果项目部署在 `IIS` 中出现 `WebSoket`/`SignalR` 不能连接或连接失败等问题,请确保 `IIS` 服务中的 `WebSocket 协议` 是勾选状态 + + + +### 34.1.4.3 部署之后缺失 `api-ms-win.xxxx.dll` 问题 + +有时候将发布文件发布到服务器后,出现丢失 `api.ms-win.xxxx.dll` 文件,这时只需要重新发布并选择服务器特定的架构即可。 + + + + + +## 34.1.5 `IIS` 回收问题和配置 + +通过 `IIS` 部署 `.NET Core` 应用程序,如果启动了系统日志,就会发现经常出现 `Application is shutting down...` 的日志,代表 `IIS` 回收了应用程序池。 + +对于一个长期在线的网站来说,这是非常不合理的,所以我们可以通过以下配置让 `IIS` 进行长时间不访问便回收的机制。 + +--- + +配置步骤如下: + +1. 打开 `IIS` 并点击左侧树根节点(计算机名称)并点击右侧的 `Configuration Editor`(配置编辑器) + + + +2. 在 `Section`(节)选择 `system.applicationHost/applicationPools` 并设置 `startMode` 为 `AlwaysRunning`,之后点击 `Apply` 保存。 + + + +3. 点击左侧树根节点(计算机名称)下的 `Application Pools` 并点击最右侧的 `Set Appliation Pool Defaults...`(设置应用程序池默认配置...) + + + +4. 设置 `Idle Time-out (minutes)`(闲置超时(分钟)为 `0` + + + +这样即可解决 `IIS` 回收问题。 + +## 34.1.6 卷影(无占用)复制发布替换 + +正常情况下如果我们代码重新发布后替换 `IIS` 中的文件,这时候会出现文件占用无法进行替换,过去运维人员都是先停止站点后替换再启动。但我们也可以配置 `web.config` 文件启用卷影复制模式实现类似**热更新**操作,如: + +```xml showLineNumbers {12-18} + + + + + + + + + + + + + + + + + + + + +``` + +关于卷影复制更多知识可查看以下文档: + +- [https://devblogs.microsoft.com/dotnet/asp-net-core-updates-in-net-6-preview-3/#shadow-copying-in-iis](https://devblogs.microsoft.com/dotnet/asp-net-core-updates-in-net-6-preview-3/#shadow-copying-in-iis) +- [https://learn.microsoft.com/zh-cn/dotnet/framework/app-domains/shadow-copy-assemblies](https://learn.microsoft.com/zh-cn/dotnet/framework/app-domains/shadow-copy-assemblies) + +## 34.1.7 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/deploy-nginx.mdx b/handbook/docs/deploy-nginx.mdx new file mode 100644 index 0000000000000000000000000000000000000000..65a23167218cd2e760da2e0e91054130e8f1e82d --- /dev/null +++ b/handbook/docs/deploy-nginx.mdx @@ -0,0 +1,9 @@ +--- +id: deploy-nginx +title: 34.3 在 Nginx 部署 +sidebar_label: 34.3 在 Nginx 部署 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +文档紧急编写中,可以查看官方文档 [https://docs.microsoft.com/zh-cn/aspnet/core/host-and-deploy/linux-nginx?view=aspnetcore-5.0](https://docs.microsoft.com/zh-cn/aspnet/core/host-and-deploy/linux-nginx?view=aspnetcore-5.0) diff --git a/handbook/docs/devops.mdx b/handbook/docs/devops.mdx new file mode 100644 index 0000000000000000000000000000000000000000..98823fb5f1cdbd4692e231df0c80bd10c3e67b3b --- /dev/null +++ b/handbook/docs/devops.mdx @@ -0,0 +1,11 @@ +--- +id: devops +title: 35.2 持续部署集成 +sidebar_label: 35.2 持续部署集成 +--- + +文档紧急编写中,可以先看旧文档 + +- docker:https://monksoul.gitbook.io/hoa/dockerrongqihua + +- Jenkins:https://monksoul.gitbook.io/hoa/devopschixubushujicheng diff --git a/handbook/docs/donate.mdx b/handbook/docs/donate.mdx new file mode 100644 index 0000000000000000000000000000000000000000..61cfb9c2b351face9bcf95027d8d21b4476e39fa --- /dev/null +++ b/handbook/docs/donate.mdx @@ -0,0 +1,299 @@ +--- +id: donate +title: 1.5 支持 Furion +sidebar_label: 1.5 支持 Furion +description: Furion 是采用 MIT 许可的开源项目 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +:::tip 成为赞助商 + +Furion 是采用 MIT 许可的开源项目。 + +维护这样一个庞大的生态系统和为项目开发新功能所需的巨大努力,只有在我们的赞助者慷慨的财务支持下才得以持续。 + +::: + +## 1.5.1 作为企业赞助 + +赞助 Furion 可以让您通过我们的网站和 Gitee 项目[自述文件](https://gitee.com/dotnetchina/Furion)接触全球超过 5 万 Furion 开发人员。此外,支持 OSS 可以提高品牌声誉,这对于任何与开发人员互动的公司来说都是一项重要资产。 + +如果您使用 Furion 来构建一个创收产品,赞助它的开发是有商业意义的:它确保您的产品所依赖的项目保持健康和积极维护。社区中的曝光率和正面的品牌形象也使得吸引和招募 Furion 开发人员变得更加容易。 + +如果您正在构建目标客户是开发人员的产品,您将通过赞助曝光获得高质量的流量,因为我们的所有访问者都是开发人员。赞助还可以建立品牌知名度并提高转化率。 + +联系方式:电子邮箱 \<monksoul@outlook.com> 作者微信 \<`ibaiqian`>。 + +## 1.5.2 作为个人赞助 + +如果您是一个个人用户,并且喜欢使用 Furion 的生产力,请考虑通过**微信手机端扫描微信赞赏码** - 就像偶尔给我买咖啡一样。如果您对微信赞赏码不满意,[Gitee](https://gitee.com/dotnetchina/Furion) 也支持一次性捐赠! + +您也可以尝试说服您的雇主赞助 Furion,这可能并不容易,但商业赞助通常比个人捐赠对 OSS 项目的可持续性产生更大的影响,因此如果您成功了,您将对我们提供更多帮助。 + +**微信手机端扫描以下赞赏码**: + +

+ +

+ +
+ 💖 赞助列表 +
+ +注:排序按赞助顺序书写 + +| 赞助人昵称 | 赞助金额(元) | 附语 | +| ------------------------------ | --------------------- | ------------------------------------------------------------------------ | +| 🤴 爱吃油麦菜 | **100** | 感谢您的开源项目! | +| 👳‍♂️ 麦壳饼 | **200** | 感谢您的开源项目! | +| 👨 Sun | **100** | 感谢您的开源项目! | +| 👶 d617617 | **20** | 感谢您的开源项目! | +| 👦 Diqiguoji008 | **16.66** | 见贤思齐 | +| 👲 nodyang | **100** | 感谢您的开源项目! | +| 👳‍♀️ mictxd | **100** | 吹过的牛都实现。 | +| 🧓 欧流全 | **10** | 希望将来超越 Spring | +| 👨‍⚕️ lionkon | **10** | ...看了框架感觉拿来学习是很不错的... | +| 😤 好人! | **10** | Nice 的小僧,我们的 dotnetchina 马上火起来了 | +| 😮 木木 Woody | **10** | 感谢您的开源项目! | +| 😚 Joker Hou | **QQ 超级会员一个月** | | +| 🤠 ccdfz | **QQ 专属红包 199** | | +| 🌝 天夫李总 | **支付宝 6666** | Furion 非常有意思,持续关注 | +| 😝 六尘子 | **微信红包 199** | 公司已经使用,小小敬意 | +| 🤠 ccdfz | **QQ 专属红包 200** | 赞助一根内存条 | +| 🤑 邓亮灯 | **28.88** | 感谢您的开源项目! | +| 😬 天道酬勤 | **微信二维码 188** | 小小心意 | +| 🥰 却月居士 | **QQ 专属红包 100** | 恭喜发财 | +| 🤩 鲁旭 | **100** | 感谢您的开源项目! | +| 😬 散客行 | **微信二维码 666** | 终于找到一个不错的框架 | +| 😌 本心 | **100** | 项目功能很强大 | +| 🙆‍♂️ 毕业生 | **100** | 请你喝杯咖啡 | +| 🦹‍♀️ 猪鼻子 | **微信二维码 668** | 好 | +| 🧟‍♂️ 明年·今日 | **200** | 使用的框架里最爽的,最理想的!加油 | +| 👨‍🎓 过去的过去 | **50** | 感谢您的开源项目! | +| 👨‍🔧 万里兮 | **100** | 新公司的第一个项目从 Furion 开始 | +| 👴 Muphalem | **20** | 很热心很亲切的开发者,加油! | +| 💂‍♂️ 吃锅巴的码农 | **微信二维码 500** | 就冲你的文档写得好! | +| 🤴 三重罗生门 | **微信红包 200** | 下次烤鱼你请! | +| 🤵 李涛 | **10** | 加油,我们看到了 dotnet 美好的明天 :) | +| 😂 !@#$%^& | **QQ 专属红包 100** | 刚接触 furion,文档真棒 | +| 😵 李斌 | **20** | 看到一个不错的框架,学习学习,感谢作者的无私奉献 | +| 😞 逞强 | **QQ 专属红包 20** | 恭喜发财 | +| 🤒 顾锦松 | **QQ 专属红包 200** | 期待文档完成时候 | +| 🧟‍♀️ 哈哈 | **微信赞赏码 1000** | 点赞作者! | +| 🍍 Z | **微信赞赏码 100** | 恭喜孩子诞生,谢谢大佬 | +| 🍲 海涛 | **50** | 恭喜 | +| 😣 h | **微信赞赏码 100** | qq82683656 | +| 👱‍♀️ 冷大大 | **微信赞赏码 100** | 加快文档和教学补充哦,另外需要钉钉群,平常不用 QQ | +| 👜 腾坤 | **微信赞赏码 10** | 感谢您的开源项目,QQ:565728589 | +| 🚆 文耶耶 | **微信赞赏码 10** | 很棒 | +| 🎟 Cynthiax | **微信转账 5000** | 一路看着过来,小小心意 | +| 🥴 一花一世界 | **微信赞赏码 10** | 感谢开源,感谢持续更新 | +| 🥪 。 | **微信赞赏码 2** | 感谢 | +| 🎄 | **微信赞赏码 20** | 感谢你的开源项目 | +| 🍖 班卓 | **微信赞赏码 9.9** | 新年快乐 | +| 🥖 雨天裸奔的猫 | **微信赞赏码 10** | 加油!好东西,期待更多新特性 | +| 🍖 班卓 | **微信赞赏码 9.9** | 加个鸡腿 | +| 🍘 Egota Tiya | **微信赞赏码 6.66** | 真的不错,准备使用这个框架 | +| 🤩 李氏天下 | **微信赞赏码 200** | 坚持下去 | +| 🎗 Trube | **微信赞赏码 200** | 感恩 | +| 🧦 不长胡子的毛 | **微信赞赏码 100** | Fur | +| 👓 醉酒码农 | **微信赞赏码 1000** | 单次只能赞助 200,捐赠 5 次,支持 Furion | +| 😢 XIUXIN | **微信赞赏码 20** | 谢大佬,春天里的第一杯奶茶 | +| 👩‍🦳 吴鹏 | **微信赞赏码 100** | 感谢这么好的框架和文档 | +| 🧔 Symmmee | **QQ 专属红包 20** | 太牛了 | +| 🤴 隐居~~~ | **QQ 专属红包 100** | 支持一下吧 | +| 👵 饭粥 | **微信赞赏码 66** | 6666666666 | +| 🎭 Free | **微信赞赏码 20** | 感谢开源。比较敬业的架构师,支持一下 | +| 👑 时不待我 | **微信赞赏码 50** | 请你喝咖啡 | +| 😋 Eway5 | **微信赞赏码 99** | 找 abp 搜到 Furion 的,没想到国内有这么优秀的贡献者 | +| 🍛 吃瓜青蛙 | **微信赞赏码 100** | 加油,点赞 | +| 👩‍🎤 陈启表 | **微信赞赏码 100** | 敬佩开源,希望能成为未来.NET 项目框架之星!! | +| 👨‍🔧 sunshuaize | **100** | 感谢您的开源项目! | +| 🚍 淘小涛 | **10** | 略尽绵薄之力,加油~大家 | +| 🍖 常松 | **微信赞赏码 100** | 开源不易,感谢您的开源 | +| 🍙 种一抹馨香 | **微信赞赏码 50** | 每次犯低级错误问你,都很耐心解答 | +| 😐 Ray | **微信赞赏码 8.88** | ray@wwads.cn 求合作 | +| 😁 王歆 | **微信赞赏码 10** | 略表心意,希望你的教程早些出来 | +| 🧶 Mark | **微信赞赏码 166** | | +| 🎡 顾龙飞 | **10** | 感谢你的开源项目 | +| 🎏 石头 | **微信赞赏码 20** | 加油,给力 | +| 😣 慢慢 | **微信赞赏码 100** | 学习学习,挺好的架构 | +| 🎊 Minhoz | **微信赞赏码 8** | Furion 必火,大势所趋 | +| 🏹 瓯印软件 | **88.88** | 感谢你的开源项目 | +| 🤩 独、特 | **10** | 希望越来越好 | +| 🧵 | **微信赞赏码 20** | | +| 👸 李孟良 | **微信赞赏码 100** | 支持开源,感谢付出 | +| 🥰 易旭锋 | **微信赞赏码 50** | | +| 😆 liupan | **微信赞赏码 200** | | +| 😏 君临天下 | **微信赞赏码 20** | | +| 🎉 海绵 | **微信赞赏码 100** | 大佬加油 | +| 🎊 好名字 | **微信赞赏码 10** | 希望尽快使用上 Furion 全家桶 | +| 🤗 A.LO | **微信赞赏码 100** | wash 3rd foot only | +| 🤣 朱鹏程 | **微信赞赏码 50** | 午夜小精灵 | +| 😉 空问 | **微信赞赏码 50** | 加油,感谢! | +| 😃 某人 | **微信赞赏码 50** | 大佬,喝阔落 | +| 😃 june | **微信赞赏码 10** | june-WHQ | +| 😃 A.Mr 厘米 | **微信赞赏码 101** | 加油! | +| 😃 大漠胡杨 | **微信赞赏码 50** | 再接再厉 | +| 😃 Anybody | **微信赞赏码 1000** | 坚持下去 | +| 😃 听风 | **微信赞赏码 200** | | +| 🌝 dZhang Davil | **支付宝 10000** | great project. | +| 🌝 aifie | **50** | YYDS,佩服,感谢您的开源项目! | +| 🌝 🎄++ | **微信赞赏码 10** | 正在学习中,大佬加油! | +| 🌝 无服务 | **微信赞赏码 20** | 感谢你的开源项目 | +| 🌝 铭 | **微信赞赏码 10** | | +| 🌝 生旦净末丑 | **微信赞赏码 200** | 加油,666 | +| 🌝 Mog | **微信赞赏码 50** | 挺好用的 | +| 🌝 其实 ° | **微信赞赏码 20** | 事件总线太好用了 | +| 🌝 SU | **微信赞赏码 10** | 功能强度大,持续关注 | +| 🌝 | **微信赞赏码 100** | 项目挺好! | +| 🌝 M | **微信赞赏码 10** | 希望国货崛起 | +| 🌝 EE | **微信赞赏码 20** | 感谢那么棒的项目 | +| 🌝 陈春胜 | **微信赞赏码 20** | 浪里星辰 | +| 🌝 whd | **微信赞赏码 200** | 支持 | +| 🌝 Dong | **微信赞赏码 20** | 感谢开源,聊表心意! | +| 🌝 琳琅水月 | **微信赞赏码 100** | 非常棒。省时省力。持续关注! | +| 🌝 种花家村长 | **微信赞赏码 20** | 希望事件总线优化一下性能 | +| 🌝 刘强 | **微信赞赏码 200** | 感谢开源!期待更好的未来! | +| 🌝 刘强 | **微信转账 1000** | 请兄弟们吃顿饭 | +| 🌝 芝麻芯 | **微信赞赏码 100** | 初学者,感谢让我们能专注于业务 | +| 🌝 june | **微信赞赏码 10** | june-WHQ | +| 🌝 jamie | **微信赞赏码 10** | 感谢无私奉献,收获满满 | +| 🌝 君子兰 | **微信赞赏码 20** | NET 开发者加油~ | +| 🌝 王雾 | **微信赞赏码 20** | 希望 Furion 越来越好 | +| 🌝 李孟良 | **微信赞赏码 100** | Furion 越来越好 赞 | +| 🌝 黄 | **微信赞赏码 100** | 感谢你的开源项目 | +| 🌝 HOMING_HNLY | **微信赞赏码 10** | 感谢开源,敬佩持之以恒的更新! | +| 🌝 Xukaige | **微信赞赏码 5** | 钱虽少,一片心意,加油 | +| 🌝 快乐的小帅哥 | **微信赞赏码 20** | 支持一下,基础功能不错,节省开发时间 | +| 🌝 TR | **微信赞赏码 10** | 很赞,小小的支持一下开源上项目! | +| 🚍 wangshiqiao125 | **10** | 感谢您的开源项目! | +| 🚍 Xukaige | **微信赞赏码 5** | 钱虽少,一片心意,加油。 | +| 🚍 Coder_Army | **50** | 感谢您的开源项目!和尚辛苦啦 | +| 🚍 fujin | **微信赞赏码 166.66** | 感谢开源。 | +| 🚍 木三科技-软件开发 | **微信赞赏码 66.66** | Furion 很强大,公司已开始使用,感谢开源。 | +| 🚍 dd | **微信赞赏码 50** | 感谢您的开源项目! | +| 🚍 chenYuAn | **微信赞赏码 10** | 感谢您的开源项目! | +| 🚍 keni | **微信赞赏码 20** | 很棒,要是有纯英文版的更容易推广 | +| 🚍 Talk is cheap | **微信赞赏码 30** | 感谢大佬无私奉献,文档细致全面,乃我辈典范 | +| 🚍 李涛 | **微信赞赏码 20** | 感谢您的开源项目,一直在使用 | +| 🚍 七色**^\_^** | **微信赞赏码 100** | 坚持下去,我是你的铁杆粉丝 | +| 🚍 鱼 | **微信赞赏码 50** | 桂电鱼 | +| 🚍 文林 | **50** | 感谢您的开源项目 | +| 🚍 AndyLi | **微信赞赏码 100** | 让.NET 开发更简单,更通用,更流行。 | +| 🚍 未来 | **微信赞赏码 20** | 从 20 年开始用到现在,感谢付出 | +| 🚍 许云 | **微信赞赏码 100** | 我还没有开始用,不管行不行,先赞助一下。 | +| 🚍 王韩广 | **100** | 感谢您的开源项目,若有官方群请私信我,谢谢! | +| 🚍 辉 | **微信赞赏码 50** | 感谢 | +| 🚍 with you | **微信赞赏码 20** | 感谢! | +| 🚍 蒋状先生 | **微信赞赏码 99** | | +| 🚍 江左梅郎-工作在浏览器上的人 | **微信赞赏码 10** | 谢谢,公司做项目里用到了这个框架 | +| 🚍 Bai Jianlong | **微信赞赏码 20** | | +| 🚍 写意 | **微信赞赏码 100** | 很好的框架,文档丰富 | +| 🚍 | **微信赞赏码 10** | 我打不开 app 下载链接 | +| 🚍 木偶 | **微信赞赏码 100** | 偶然看到,个人开源,实属不易 👍!加油! | +| 🚍 九九八十二 | **微信赞赏码 99** | 加油 | +| 🚍 ! | **微信赞赏码 50** | 给大佬上水 | +| 🚍 with you | **微信赞赏码 20** | 谢谢大佬 | +| 🚍 我口说我心 | **微信赞赏码 50** | 希望好的框架能一直持续下去 | +| 🚍 Coder | **微信赞赏码 100** | 感谢大佬记得我!真心祝福你更好 | +| 🚍 小何 | **微信赞赏码 20** | 非常好的框架,帮大忙了 | +| 🚍 哆哆 | **微信赞赏码 100** | 大佬的技术数一数二,大佬的精神万中无一 | +| 🚍 易留洋 | **微信赞赏码 30** | 加油 开源不易 | +| 🚍 talent-tan | **500** | 祝陈总的 Furion 越来越好! | +| 🚍 千金万码 | **2** | 感谢您的开源项目,希望你继续加油,我只是一个学生,贡献出我的一份绵薄之力 | +| 🚍 helf | **微信赞赏码 1** | | +| 🚍 yanqi | **微信赞赏码 20** | 感谢开源 | +| 🚍 蒋状先生 | **微信赞赏码 66** | | +| 🚍 天下无双 | **微信赞赏码 20** | 感谢你的无私奉献,请你喝奶茶 | +| 🚍 脚.icon | **微信赞赏码 50** | 感谢您的开源项目,谢谢! | +| 🚍 | **微信赞赏码 18.88** | 不做白嫖党。不多买杯奶茶解渴 | +| 🚍 兰亭方叙 | **微信赞赏码 20** | 感谢开源,一杯奶茶奉上 | +| 🚍 Mog | **微信赞赏码 50** | good | +| 🚍 發 | **微信赞赏码 1** | furion! | +| 🚍 海纳百川 | **微信赞赏码 20** | 加油(●'◡'●) | +| 🚍 鹿 | **微信赞赏码 50** | 继续加油,请作者喝杯咖啡 | +| 🚍 海鸥 | **微信赞赏码 100** | 我是海鸥,也在中山工作 | +| 🚍 asplx | **10** | 感谢您的开源项目 | +| 🚍 | **微信赞赏码 20** | 大佬喝杯咖啡 | +| 🚍 null | **微信赞赏码 20** | | +| 🚍 天空 | **微信赞赏码 200** | 感谢开源! | +| 🚍 panda | **微信赞赏码 66.66** | 喝咖啡 | +| 🚍 谭 qijiang scada | **微信赞赏码 10** | | +| 🚍 国东 | **微信赞赏码 50** | furion yyds | +| 🚍 MonstorUncle | **微信赞赏码 50** | 感谢大佬,支持一下 | +| 🚍 Xpovoc | **微信赞赏码 1** | angelshaka | +| 🚍 晓宇 | **微信赞赏码 66.66** | 老大辛苦。请喝茶。 | +| 🚍 刘金岩 | **微信赞赏码 36.00** | 加油,要不改成会员形式也行,开个知识星球 | +| 🚍 tiny | **微信赞赏码 16.66** | 感谢您的开源项目,学到了很多! | +| 🚍 一路向前 | **微信赞赏码 50** | 非常好用,希望您长期维护 | +| 🚍 老隋 | **微信赞赏码 50** | 喜欢快速开发,方便加个微信 | +| 🚍 iknow | **微信赞赏码 20** | 感谢,共勉 | +| 🚍 蒋状先生 | **微信赞赏码 66** | | +| 🚍 ToT | **微信赞赏码 10** | 千言万语,感谢有你 | +| 🚍 iknow | **微信赞赏码 50** | 加油,期待 v5 | +| 🚍 Pray(22 点睡觉) | **微信赞赏码 10** | 小僧牛逼! | +| 🚍 松鼠 | **微信赞赏码 20** | 恭喜 Furion v5 发版成功 | +| 🚍 Vampire | **微信赞赏码 18** | 大佬我爱你 | +| 🚍 erwa.cn | **10** | 感谢您的开源项目。 | +| 🚍 沐雨乘风 | **微信赞赏码 66** | 感谢有你,越来越好用了。 | +| 🚍 hg | **微信赞赏码 50** | 文档详细,十分难得,大佬加油! | +| 🚍 王华伟 | **微信赞赏码 10** | 加油,刚学习中 | +| 🚍 roleya | **50** | 略尽微博之力,希望这么优秀的项目能长期做下去 | +| 🚍 随运而安 | **微信赞赏码 50** | 期待更美好的前行,加油!向你学习 | +| 🚍 五块四 | **微信赞赏码 20** | 一路向北笑嘻嘻 | +| 🚍 🐮 | **微信赞赏码 6** | 感谢开源 | +| 🚍 清子 | **1** | 感谢您的开源项目! | +| 🚍 MCYQ | **微信赞赏码 100** | 一点心意,感谢和尚 | +| 🚍 Hans | **微信赞赏码 10** | 辛苦 | +| 🚍 。 | **微信赞赏码 10** | | +| 🚍 Goon | **微信赞赏码 20** | 感谢你们的付出 | +| 🚍 蒋状先生 | **微信公众号 6** | | +| 🚍 ThinkMore | **微信公众号 5** | | +| 🚍 Kevin | **微信公众号 5** | | +| 🚍 毅心 | **微信赞赏码 50** | 为这样的初心点赞,希望越来越好 | +| 🚍 杨 | **微信赞赏码 100** | 感谢您的开源项目 | +| 🚍 孔乙己 YOLO | **微信赞赏码 6.66** | 学习中,感谢付出 | +| 🚍 CC | **微信公众号 20** | | +| 🚍 春辉 | **微信公众号 5** | | +| 🚍 Uncle_Cheng | **支付宝 99** | | +| 🚍 搞围脖呢 | **支付宝 10** | | +| 🚍 KillSabbath | **支付宝 666** | | +| 🚍 二根苦瓜 | **支付宝 12.88** | | +| 🚍 Jeremy、 | **支付宝 500** | | +| 🚍 emo 了 | **支付宝 100** | | +| 🚍 暴风雨大虾 | **微信赞赏码 100** | 希望坚持下去 | +| 🚍 技术-李工 ه٥ | **微信赞赏码 22** | 支持 | +| 🚍 贰仟 | **微信赞赏码 22** | 小小心意 | +| 🚍 彼岸花落、 | **微信赞赏码 199** | 坚持三年不易,支持一下 | +| 🚍 猫侍 c | **微信赞赏码 166** | 加油 | +| 🚍 地主家也没有余粮 | **微信赞赏码 22** | 用的是什么洗发水 | +| 🚍 Hans | **微信赞赏码 22** | 辛苦 | +| 🚍 花叶 | **微信赞赏码 50** | 感谢付出期待 v8 发动机,努力长发 | +| 🚍 春辉 | **微信公众号 5** | | +| 🚍 宏伟 | **微信公众号 100** | | +| 🚍 柏拉图永恒 | **微信公众号 20** | | +| 🚍 Hans | **微信公众号 20** | | +| 🚍 rolly | **微信公众号 5** | | +| 🚍 Q1.. | **微信公众号 20** | | +| 🚍 登友 | **微信公众号 5** | | +| 🚍 | **微信赞赏码 5** | 支持 | +| 🚍 coolcalf | **微信赞赏码 100** | | +| 🚍 Amazingbow | **微信赞赏码 22** | 咨询问题 | +| 🚍 王 coco | **微信公众号 5** | | +| 🚍 Andy | **微信公众号 100** | | +| 🚍 姚亮 | **微信赞赏码 10** | | +| 🚍 开心爱 176\*\*\*\*4001 | **微信公众号 50** | | +| 🚍 成强 | **微信赞赏码 22** | | + +
+
+ +## 1.5.3 帮助推广支持 + +此外,您也可以将 Furion 的宣传海报转发到你的社交朋友圈、个人博客或者其他平台,让更多的人知道 Furion,这对我们有不小的帮助。 + +

+ +

diff --git a/handbook/docs/dotnet-tools.mdx b/handbook/docs/dotnet-tools.mdx new file mode 100644 index 0000000000000000000000000000000000000000..8994393cf3dc97b67a933c3698159325889279d2 --- /dev/null +++ b/handbook/docs/dotnet-tools.mdx @@ -0,0 +1,708 @@ +--- +id: dotnet-tools +title: 37. 编写包管理工具 +sidebar_label: 37. 编写包管理工具 (Tools) +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +## 37.1 关于包管理工具 + +使用过 `NodeJs` 的朋友一定对 `npm` 命令不会陌生,可以通过 `npm` 安装项目需要的包或环境需要的工具,在 `.NET Core 2.1+` 之后,微软也推出了新的特性,`Global/Local Tools`,该特性功能也正是受到 `npm` 启发下诞生的。 + +不同的是,`npm` 中的包采用的是 `JavaScript` 编写并发布到 [https://www.npmjs.com/](https://www.npmjs.com/) 平台,而 `dotnet tools` 采用 `C#` 编写并发布到 `https://www.nuget.org/` 平台供安装使用。 + +### 37.1.1 `dotnet tools` 包管理好处 + +- 跨平台,支持 `Linux/Mac/Windows` 平台供安装使用 +- 完整的 `C#` 生态支持 +- 为所欲为~~~(拥有操作系统的权限) + +## 37.2 了解包命令语法 + +通常包命令语法都遵循以下规则: + +```bash showLineNumbers +<-|--|/>argument-name<=|:| >["|']value['|"] [--] [operand] ... [operand] +``` + +在这里,`Furion` 将简单介绍命令常用的知识: + +- `工具符`:通常指的是你工具的唯一名称,也就是关键字,而且总是在最开头编写,如:`dotnet`,`npm`,`node` +- `短参数`:短参数指的是 `单个字符` 的字符串,我们通常使用 `-` 一个横杆指定参数及值,如:`-v` 或 `-v 0.0.1` +- `长参数`:长参数指的是一个或多个单词连接的字符串,该参数通常和 `短参数` 同时存在,通常使用 `--` 指定参数及值,如:`--version` 或 `--version 0.0.1` +- `操作符`:字符串中与参数值格式不匹配的任何文本都被视为操作数,任何出现在双连字符 `--` 之后且未包含在单引号或双引号中且两侧有空格的文本都被视为操作数,无论它是否与参数值格式匹配,通常用于归类/分类作用。 + +### 37.2.1 短参数例子 + +- `-a foo` + +| 短参数 | 参数值 | +| ------ | ------ | +| a | foo | + +- `-ab` + +| 短参数 | 参数值 | +| ------ | ------ | +| a | | +| b | | + +- `-abc bar` + +| 短参数 | 参数值 | +| ------ | ------ | +| a | | +| b | | +| c | bar | + +### 37.2.2 长参数例子 + +- `--foo bar` + +| 长参数 | 参数值 | +| ------ | ------ | +| foo | bar | + +- `--foo --bar` + +| 长参数 | 参数值 | +| ------ | ------ | +| foo | | +| bar | | + +- `--foo bar --hello world` + +| 长参数 | 参数值 | +| ------ | ------ | +| foo | bar | +| hello | world | + +### 37.2.3 混合参数例子 + +- `-abc foo --hello world /new="slashes are ok too"` + +| 短/长参数 | 参数值 | +| --------- | ------------------ | +| a | +| b | +| c | foo | +| hello | world | +| new | slashes are ok too | + +### 37.2.4 多个值参数 + +- `--list 1 --list 2 --list 3` + +| 长参数 | 参数值 | +| ------ | ------ | +| list | 1,2,3 | + +### 37.2.5 操作符 + +- `-a foo bar "hello world" -b -- -explicit operand` + +| 短参数 | 参数值 | +| ------ | ------ | +| a | foo | +| b | + +| 操作符 | +| ------------- | +| bar | +| "hello world" | +| -explicit | +| operand | + +了解更多关于包命令语法的官方知识:[https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html) + +## 37.3 编写第一个包 + +`dotnet tools` 工具实际上是一个 `控制台` 应用程序,不同的是 `.csproj` 项目文件需要添加特定配置。下面将给大家编写一个 `HelloTools` 包管理工具。 + +### 37.3.1 创建 `HelloTools` 控制台应用 + + + +### 37.3.2 编辑 `HelloTools.csproj` + +将控制台项目标记成 `dotnet tools` 需要配置以下节点,如下图所示: + +```xml showLineNumbers {6-11} + + + + Exe + net5.0 + 0.0.1 + 第一个 dotnet tools 工具 + hello-tools + true + true + ./nupkg + + + +``` + +#### 配置关键节点说明 + +- `Version`:包工具版本号 +- `Description`:包工具介绍 +- `ToolCommandName`:包工具关键字,如 `dotnet`、`npm`,后续使用都是通过该关键字使用 +- `PackAsTool`:是否声明为包管理工具,设置 `true` +- `GeneratePackageOnBuild`:是否编译时自动生成 `.nupkg` 包,方便后续上传到 `NuGet` 平台 +- `PackageOutputPath`:配置 `.nupkg` 包存储目录,推荐使用 `./nupkg` + +### 37.3.3 安装 `Furion.Tools.CommandLine` 包 + +为了方便管理工具包开发,`Furion` 官方特意开发了 `Furion.Tools.CommandLine` 包,帮助大家快速开发管理工具包。 + + + +### 37.3.4 编写逻辑代码 + +我们先定义几个需求,然后编写逻辑代码: + +> 需求一:输入 `hello-tools` 打印介绍信息 +> +> 需求二:输入 `-n` 或 `--name` 输出 `Hello 名字` +> +> 需求三:输入 `-v` 或 `--version` 输出当前版本 +> +> 需求四:输入 `-h` 或 `--help` 输出帮助文档 + +:::tip 生成控制台 `LOGO` + +- 不支持中文:[http://patorjk.com/software/taag/#p=display&f=Big&t=Furion%20Tools](http://patorjk.com/software/taag/#p=display&f=Big&t=Furion%20Tools) +- 支持所有字符:[https://www.qqxiuzi.cn/zh/dianzhenzi-zifu/](https://www.qqxiuzi.cn/zh/dianzhenzi-zifu/) + +::: + +```cs showLineNumbers {1,10,15-18,26-29,37-40,45} +using Furion.Tools.CommandLine; +using System; +using System.Collections.Generic; + +namespace HelloTools +{ + class Program + { + // 通过 Cli.Inject() 完成准备工作 + static void Main(string[] args) => Cli.Inject(); + + /// + /// 输出 Hello 名字 + /// + [Argument('n', "name", "您的名字")] + static string Name { get; set; } + // 定义参数处理程序,必须 [属性名]+Handler + static void NameHandler(ArgumentMetadata argument) + { + Console.WriteLine($"Hello {Name}"); + } + + /// + /// 查看版本 + /// + [Argument('v', "version", "工具版本号")] + static bool Version { get; set; } + // 定义参数处理程序,必须 [属性名]+Handler + static void VersionHandler(ArgumentMetadata argument) + { + Console.WriteLine(Cli.GetVersion()); + } + + /// + /// 查看帮助文档 + /// + [Argument('h', "help", "查看帮助文档")] + static bool Help { get; set; } + // 定义参数处理程序,必须 [属性名]+Handler + static void HelpHandler(ArgumentMetadata argument) + { + Cli.GetHelpText("hello-tools"); + } + + // 所有未匹配的参数/操作符处理程序,固定 NoMatchesHandler 方法名 + static void NoMatchesHandler(bool isEmpty, string[] operands, Dictionary noMatches) + { + if (isEmpty) + { + Console.WriteLine(@" + _ _ _ _ _______ _ + | | | | | | | |__ __| | | + | |__| | ___| | | ___ | | ___ ___ | |___ + | __ |/ _ \ | |/ _ \ | |/ _ \ / _ \| / __| + | | | | __/ | | (_) | | | (_) | (_) | \__ \ + |_| |_|\___|_|_|\___/ |_|\___/ \___/|_|___/ + + +"); + Console.WriteLine($"欢迎使用{Cli.GetDescription()}"); + } + } + } +} +``` + +:::tip 代码说明 + +- `Furion` 工具包提供了非常方便的 `Cli.Inject()` 方法,可以实现一次性完成所有初始化工作,只需要在 `Main` 方法调用即可 +- 通过 `[Argument(短参数,长参数,提示文档)]` 定义每一个参数属性,参数必须是 `static` 静态 +- 通过 `[属性名]Handler` 定义每个参数匹配后的处理程序,如:`VersionHandler`,格式为:`static void 属性名Handler(ArgumentMetadata argument)` +- 通过固定方法名 `NoMatchesHandler` 定义未匹配的参数及操作符,该方法有三个参数: + - `isEmpty`:判断是否没有传递任何参数,通常用于输出介绍 + - `operands`:获取所有操作符列表 + - `noMatches`:获取所有未匹配的参数字典 + +::: + +### 37.3.5 如何调试包工具 👏 + +包管理工具调试有别于普通的控制台,主要区别是测试各个参数的使用,也就是如何传递 `Main` 方法的 `args` 参数。只需要以下两个步骤即可: + +- 在项目根目录添加 `Properties` 目录 +- 在 `Properties` 目录中添加 `launchSettings.json` 文件,并遵循以下规则: + +```json showLineNumbers {3,5} +{ + "profiles": { + "项目名称": { + "commandName": "Project", + "commandLineArgs": "你的命令" + } + } +} +``` + +- `项目名称`:写你的项目实际名称,如:`HelloTools` +- `commandName`:固定为 `Project` +- `commandLineArgs`:编写测试命令,只需要写参数/操作符部分即可,如:`-v`,`-v -h --Name Furion` + +如,我们需要测试 `HelloTools` 的 `-n` 参数 + +```json showLineNumbers {3,5} +{ + "profiles": { + "HelloTools": { + "commandName": "Project", + "commandLineArgs": "-n Furion" + } + } +} +``` + + + +点击 `运行/调试/F5` 启动调试 + + + +### 37.3.6 测试各个参数情况 + +> 需求一:输入 `hello-tools` 打印介绍信息 + +```json showLineNumbers {5} +{ + "profiles": { + "HelloTools": { + "commandName": "Project", + "commandLineArgs": "" + } + } +} +``` + + + +> 需求二:输入 `-n` 或 `--name` 输出 `Hello 名字` + +```json showLineNumbers {5} +{ + "profiles": { + "HelloTools": { + "commandName": "Project", + "commandLineArgs": "-n Furion" + } + } +} +``` + + + +> 需求三:输入 `-v` 或 `--version` 输出当前版本 + +```json showLineNumbers {5} +{ + "profiles": { + "HelloTools": { + "commandName": "Project", + "commandLineArgs": "--version" + } + } +} +``` + + + +> 需求四:输入 `-h` 或 `--help` 输出帮助文档 + +```json showLineNumbers {5} +{ + "profiles": { + "HelloTools": { + "commandName": "Project", + "commandLineArgs": "-h" + } + } +} +``` + + + +## 37.4 打包(本机)测试 + +刚刚我们已经学会调试包工具了,但是还未做到类似 `npm` 包一样,在 `cmd/powershell` 中安装之后可在命令行全局测试,下面将教大家如何实现 `全局安装` 和 `本地安装`。 + +### 37.4.1 全局打包安装 + +**全局打包安装就是配置在系统环境变量中,在任何地方都可以使用。** + +在 `HelloTools` 项目根目录下打开 `cmd/powershell`(**尽量使用管理员工具**)执行以下命令: + +#### ✔ 安装全局包 + +```bash showLineNumbers +dotnet tool install --global --add-source ./nupkg HelloTools +``` + +其中 `HelloTools` 就是 `项目名称`。 + + + +之后我们就可以通过之前 `HelloTools.csproj` 中配置的 `hello-tools` 使用了。 + +#### ✔ 测试全局包 + + + +#### ✔ 更新全局包 + +如果源码发生改变,只需要编译项目后重新更新包工具即可: + +```bash showLineNumbers +dotnet tool update --global --add-source ./nupkg HelloTools +``` + +#### ✔ 卸载全局包 + +```bash showLineNumbers +dotnet tool uninstall --global HelloTools +``` + +想了解更多全局打包安装知识查阅官方文档即可:[https://docs.microsoft.com/zh-cn/dotnet/core/tools/global-tools-how-to-use](https://docs.microsoft.com/zh-cn/dotnet/core/tools/global-tools-how-to-use) + +### 37.4.2 本地打包安装 + +**本地打包安装就是只有在项目所在目录及子孙目录方可使用。** + +在 `HelloTools` 项目根目录下打开 `cmd/powershell` 执行以下命令: + +#### ✔ 创建本地清单文件 + +```bash showLineNumbers +dotnet new tool-manifest +``` + +执行该命令后会自动创建 `.config` 文件夹并添加 `dotnet-tools.json` 文件: + +```json showLineNumbers +{ + "version": 1, + "isRoot": true, + "tools": {} +} +``` + +:::warning 注意事项 + +通常该文件内容不需要手动更改。 + +::: + +#### ✔ 安装本地包 + +```bash showLineNumbers +dotnet tool install --add-source ./nupkg HelloTools +``` + + + +#### ✔ 测试本地包 + +本地包测试和全局包不一样的是本地包是通过 `dotnet 关键字 参数` 测试: + +```bash showLineNumbers +dotnet hello-tools -n Furion +``` + + + +#### ✔ 更新本地包 + +如果源码发生改变,只需要编译项目后重新更新包工具即可: + +```bash showLineNumbers +dotnet tool update --add-source ./nupkg HelloTools +``` + +#### ✔ 卸载本地包 + +```bash showLineNumbers +dotnet tool uninstall HelloTools +``` + +想了解更多本地打包安装知识查阅官方文档即可:[https://docs.microsoft.com/zh-cn/dotnet/core/tools/local-tools-how-to-use](https://docs.microsoft.com/zh-cn/dotnet/core/tools/local-tools-how-to-use) + +## 37.5 发布到 `NuGet` 平台 👏 + +发布到 `NuGet` 平台非常简单,只需要两个步骤即可: + +- 切换项目 `Debug` 模式到 `Release` 并重新编译项目 +- 在 `NuGet` 平台上传 `nupkg` 文件夹对应 `项目名称.版本号.nupkg` 文件即可:[https://www.nuget.org/packages/manage/upload](https://www.nuget.org/packages/manage/upload) + +:::tip 上传 NuGet 平台补齐信息 + +建议上传到 `NuGet` 平台编辑 `.csproj` 文件补齐以下信息: + +```cs showLineNumbers {13-21} + + + + Exe + net5.0 + 0.0.1 + 第一个 dotnet tools 工具 + hello-tools + true + true + ./nupkg + + 百小僧 + 百签科技(广东)有限公司 + Furion + © 2020-present 百小僧, 百签科技(广东)有限公司 + https://gitee.com/dotnetchina/Furion + Gitee + true + MIT + http://furion.baiqian.ltd + + + + + + + +``` + +::: + + + + + +发布到 `NuGet` 平台后,别人就可以通过: + +#### ✔ 安装 NuGet 包到本地 + +```bash showLineNumbers +dotnet tool install --global 项目名 --version 版本号 +``` + +## 37.6 `Cli` 静态类说明 + +为了简化包工具的开发,`Furion.Tools.CommandLine` 的 `Cli` 静态类提供了很多方便的静态方法: + +### 37.6.1 消息类 + +```cs showLineNumbers +// 输出空行 +Cli.EmptyLine(); + +// 输出一行 +Cli.WriteLine("消息"); +Cli.WriteLine("消息", ConsoleColor.Blue); // 字体颜色 +Cli.WriteLine("消息", ConsoleColor.Blue, ConsoleColor.White); // 背景颜色 +Cli.WriteLine("消息", ConsoleColor.Blue, ConsoleColor.White, fillLine: true); // 填充整行 + +// 输出(不换行) +Cli.Write("消息"); +Cli.Write("消息", ConsoleColor.Blue); // 字体颜色 +Cli.Write("消息", ConsoleColor.Blue, ConsoleColor.White); // 背景颜色 +Cli.Write("消息", ConsoleColor.Blue, ConsoleColor.White, fillLine: true); // 填充整行 + +// 输出提示消息 +Cli.Success("成功"); +Cli.Warn("警告"); +Cli.Error("错误"); +Cli.Tip("提示"); + +// 收集用户输入(支持多行) +var inputs = Cli.ReadInput(); // 输入 exit 退出输入 + +// 选择消息 +var selectId = Cli.ReadOptions("请选择喜欢的水果:", new []{ "西瓜", "苹果", "凤梨"}); // selectId 从 1 开始 +``` + +### 37.6.2 工具类 + +```cs showLineNumbers +// 完成参数填充属性初始化操作 +Cli.Inject(); + +// 获取参数所有信息 +var arguments = Cli.ArgumentMetadatas; + +// 手动检查参数是否匹配 +Cli.Check(nameof(属性名), argument => { + // 如果用户输入该参数 + if(argument?.IsTransmission == true){ + Cli.WriteLine(argument.Value); + } + else { + Cli.Error("用户没有输入"); + } +}); + +// 只有参数匹配才进入 +Cli.CheckMatch(nameof(属性名), argument => { + Cli.WriteLine(argument.Value); +}) + +// 无属性检查 +Cli.Check(new[] {"v", "version"}, (isMatch, value) => { + // 如果用户输入该参数 + if(isMatch){ + Cli.WriteLine(value); + } + else { + Cli.Error("用户没有输入"); + } +}); + +// 无属性匹配 +Cli.CheckMatch(new[] {"v", "version"}, value => { + Cli.WriteLine(value); +}); + +// 所有未匹配的参数、操作符 +Cli.CheckNoMatches((isEmpty, operands, noMatches) => { + if (isEmpty) Cli.WriteLine($"欢迎使用 {Cli.GetDescription()}"); + if (operands.Length > 0) Cli.Error($"未找到该操作符:{string.Join(",", operands)}"); + if (noMatches.Count > 0) Cli.Error($"未找到该参数:{string.Join(",", noMatches.Keys)}"); +}); + +// 解析 Main 方法参数信息 +var argumentModel = Cli.Parse(); + +// 手动解析命令字符串 +var argumentModel = Cli.Parse("-abc foo --hello world"); + +// 终止输出/结束输出 +Cli.Exit(); +``` + +### 37.6.2 信息类 + +```cs showLineNumbers +// 获取当前工具包版本号 +var version = Cli.GetVersion(); + +// 获取当前工具包描述 +var description = Cli.GetDescription(); +``` + +### 37.6.3 其他类 + +我们可以通过 `Environment` 获取当前环境更多信息,如下图所示: + +```cs showLineNumbers +// 当前执行命令目录 +var currentDirectory = Environment.CurrentDirectory; + +// 获取机器名称 +var machineName = Environment.MachineName; + +// 等等。。。。。 +``` + +## 37.7 集成 `Spectre.Console` + +`Spectre.Console` 是 `.NET/C#` 平台非常优秀的控制台应用程序 `UI` 框架库,提供非常多开箱可用且非常好看的 `UI` 组件。官网地址:[https://spectreconsole.net/](https://spectreconsole.net/) + +使用非常简单,只需要通过 `NuGet` 安装 `Spectre.Console.Cli` 拓展包即可。 + +```bash showLineNumbers +dotnet add package Spectre.Console.Cli +``` + +之后在控制台输出带下划线红色的 `Hello` 和 `world!`: + +```cs showLineNumbers {1,7} +using Spectre.Console; + +public static class Program +{ + public static void Main(string[] args) + { + AnsiConsole.Markup("[underline red]Hello[/] World!"); + } +} +``` + +:::tip `Spectre.Console` 和 `Spectre.Console.Cli` 区别 + +`Spectre.Console` 是不包含 `args` 命令行参数解析的,但作为一个 `tools` 工具对于解析 `args` 参数显得非常重要,所以推荐安装 `Spectre.Console.Cli`。 + +如果安装了 `Spectre.Console.Cli` 之后则无需安装 `Furion.Tools.CommandLine` 拓展包了。 + +::: + +下面是 `Spectre.Console` 的 `UI` 组件 预览图: + + + +## 37.8 集成 `CliWrap` + +`CliWrap` 同样也是 `.NET/C#` 平台非常优秀的执行本地命令的库,可通过该库实现 `CMD/Powershell` 命令执行,非常强大。仓库地址:[https://github.com/Tyrrrz/CliWrap](https://github.com/Tyrrrz/CliWrap) + +使用非常简单,只需要通过 `NuGet` 安装 `Spectre.Console.Cli` 拓展包即可。 + +```bash showLineNumbers +dotnet add package CliWrap +``` + +比如执行 `git commit -m "my commit"` 操作: + +```cs showLineNumbers +// 字符串参数方式 +var cmd = Cli.Wrap("git") + .WithArguments("commit -m \"my commit\""); + +// 数组参数方式 +var cmd = Cli.Wrap("git") + .WithArguments(new[] {"commit", "-m", "my commit"}); + +// 执行命令 +var result = cmd.ExecuteAsync(); +``` + +## 37.9 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/dynamic-api-controller.mdx b/handbook/docs/dynamic-api-controller.mdx new file mode 100644 index 0000000000000000000000000000000000000000..4bf60da5ccffd0e049aec80b9eb09aedf51a4458 --- /dev/null +++ b/handbook/docs/dynamic-api-controller.mdx @@ -0,0 +1,2275 @@ +--- +id: dynamic-api-controller +title: 5.1 动态 WebAPI +sidebar_label: 5.1 动态 WebAPI +description: 不想手动配置路由了,试试它 +--- + +import Tag from "@site/src/components/Tag.js"; +import useBaseUrl from "@docusaurus/useBaseUrl"; + +
+ 📝 模块更新日志 +
+
+ +- **新特性** + + -  新增 动态 `WebAPI` 支持 `text/plain` 格式的 `Body` 参数 4.8.8.9 ⏱️2023.05.04 [b49fe50](https://gitee.com/dotnetchina/Furion/commit/b49fe5087cdf97b04b7c2c9d90231f1b9d5fc6ee) + -  新增 **插件化 `IDynamicApiRuntimeChangeProvider` 接口,可在运行时动态添加 `WebAPI/Controller`** 4.8.8.8 ⏱️2023.05.04 [322ea59](https://gitee.com/dotnetchina/Furion/commit/322ea599ed58b1804e9f8ab85d7ed44882b3e5a8) + +
+ 查看变化 +
+ +在一些特定的需求中,我们需要在运行时**动态编译代码,如动态编写 `WebAPI`**,之后能够在不重启主机服务的情况下即可有效。比如这里动态添加 `SomeClass` 动态 `WebAPI`,然后在 `Swagger/路由系统` 中立即有效: + +```cs showLineNumbers {10,21-30,36-39} +using Furion; +using Furion.DynamicApiController; +using Microsoft.AspNetCore.Mvc; + +namespace YourProject.Application; + +public class PluginApiServices : IDynamicApiController +{ + private readonly IDynamicApiRuntimeChangeProvider _provider; + public PluginApiServices(IDynamicApiRuntimeChangeProvider provider) + { + _provider = provider; + } + + /// + /// 动态添加 WebAPI/Controller + /// + /// + /// 可自行指定程序集名称 + /// + public string Compile([FromBody] string csharpCode, [FromQuery] string assemblyName = default) + { + // 编译 C# 代码并返回动态程序集 + var dynamicAssembly = App.CompileCSharpClassCode(csharpCode, assemblyName); + + // 将程序集添加进动态 WebAPI 应用部件 + _provider.AddAssembliesWithNotifyChanges(dynamicAssembly); + + // 返回动态程序集名称 + return dynamicAssembly.GetName().Name; + } + + /// + /// 移除动态程序集 WebAPI/Controller + /// + public void Remove(string assemblyName) + { + _provider.RemoveAssembliesWithNotifyChanges(assemblyName); + } +} +``` + +这时只需要请求 `api/plugin-api/compile` 接口同时设置请求 `Content-Type` 为 `text/plain`,接下来传入 `C# 代码字符串` 即可,如: + +```cs showLineNumbers title="动态C#代码字符串" +using Furion.DynamicApiController; + +namespace YourProject.Application; + +public class SomeClass : IDynamicApiController +{ + public string GetName() + { + return nameof(Furion); + } +} +``` + + + +之后刷新浏览器即可看到最新的 `API`: + + + +还可以在运行时动态卸载,使用 `DELETE` 请求 `api/plugin-api` 即可。 + +
+
+ +- -  新增 **动态 `WebAPI` 自动检查路由是否包含重复参数,如果有自动修正而不是抛异常** 4.8.6.5 ⏱️2023.02.17 [5f15ea1](https://gitee.com/dotnetchina/Furion/commit/5f15ea1edd2e7793d86dd074ffbbecbebf7f683f) + +
+ 查看变化 +
+ +在 `Furion 4.8.6.5` 之前,下列代码**会抛出异常**:`The route parameter name 'roleid' appears more than one time in the route template.` + +**原因是生成的路由包含了多个 `{roleId}`**:`/api/with-class/system/role/deptTree/{roleId}/{roleId}`。 + +```cs showLineNumbers {3} +public class WithClass : IDynamicApiController +{ + [HttpGet("system/role/deptTree/{roleId}")] // 过去版本抛异常,Furion 4.8.6.5+ 正常~ + public string GetResult2(string roleId) + { + return nameof(Furion); + } +} +``` + +新版本 `Furion 4.8.6.5+` 修正了该错误,**自动移除后面重复的路由参数且不再抛异常**,也就是最终生成路由为:`/api/with-class/system/role/deptTree/{roleId}` + +
+
+ +- -  新增 动态 `WebAPI` 支持 `[RouteConstraint(":*")]` 路由约束 4.8.6.2 ⏱️2023.02.10 [#I6E6JA](https://gitee.com/dotnetchina/Furion/issues/I6E6JA) +- -  新增 **动态 `WebAPI` 支持更加强大的路由组合功能** 4.8.5.7 ⏱️2023.02.03 [#I6CLPT](https://gitee.com/dotnetchina/Furion/issues/I6CLPT) + +
+ 查看变化 +
+ +```cs showLineNumbers {8,19,36} +using Furion.DynamicApiController; +using Microsoft.AspNetCore.Mvc; + +namespace WebApplication38; + +[Route("api/[controller]")] +[Route("api2/[controller]")] +public class Test1Service : IDynamicApiController +{ + [HttpGet("test")] + [HttpPost] + [AcceptVerbs("PUT", "PATCH")] + public async Task GetTestName() + { + await Task.CompletedTask; + } +} + +public class Test2Service : IDynamicApiController +{ + [HttpGet("/root/test")] + [HttpGet("test")] + [HttpGet(Name = "other-test")] + [HttpGet("template-test", Name = "other-test")] + [HttpPost] + [AcceptVerbs("PUT", "PATCH")] + public async Task GetTestName() + { + await Task.CompletedTask; + } +} + +[Route("api/[controller]")] +[Route("api2/[controller]/second")] +[Route("api3/[controller]/three")] +public class Test3Service : IDynamicApiController +{ + [HttpGet] + [HttpGet("get/[action]")] + [HttpPost] + [HttpPost("post/cus-version")] + public string GetVersion() + { + return "1.0.0"; + } +} +``` + + + +
+
+ +- -  新增 动态 `WebAPI` 方法支持通过 `[ActionName(名称)]` 和 `[HttpMethod(Name=名称)]` 指定路由名称 4.8.4.12 ⏱️2023.01.10 [#I69AOJ](https://gitee.com/dotnetchina/Furion/issues/I69AOJ) [f699540](https://gitee.com/dotnetchina/Furion/commit/f699540989e688f25597c509249e024ec014dc4e) + +- **突破性变化** + + -  调整 **动态 `WebAPI` 生成路由 `[HttpMethod(template)]` 规则** 4.8.5.7 ⏱️2023.02.03 [#I6CLPT](https://gitee.com/dotnetchina/Furion/issues/I6CLPT) + +
+ 查看变化 +
+ +在过去,`TestMethod` 生成路由为:`/mytest` + +```cs showLineNumbers {1,4} +// 注意这里没有 [Route] 特性 +public class ClassService: IDynamicApiController +{ + [HttpPost("mytest")] + public void TestMethod() + { + } +} +``` + +新版本:`TestMethod` 生成路由为:`/api/class/mytest`,`TestMethod2` 生成路由为:`/mytest`。 + +```cs showLineNumbers {1,4,9} +// 注意这里没有 [Route] 特性 +public class ClassService: IDynamicApiController +{ + [HttpPost("mytest")] + public void TestMethod() + { + } + + [HttpPost("/mytest")] + public void TestMethod2() + { + } +} +``` + +也就是新版本如果不需要自动添加前缀,需在前面添加 `/`,旧版本不需要。 + +
+
+ +- **问题修复** + + -  修复 动态 `WebAPI` 不能正确移除 `AppService` 命名的 `Service` 问题 4.8.8.47 ⏱️2023.10.10 [#I86NL](https://gitee.com/dotnetchina/Furion/issues/I86NLO) + -  修复 动态 `WebAPI` 自定义路由模板参数和自动拼接参数冲突问题 4.8.8.15 ⏱️2023.05.15 [#I72ZZ2](https://gitee.com/dotnetchina/Furion/issues/I72ZZ2) + -  修复 动态 `WebAPI` 去除叠词类型命名如 `ServiceService` 前后缀异常问题 4.8.7.32 ⏱️2023.04.02 [#I6SB3Z](https://gitee.com/dotnetchina/Furion/issues/I6SB3Z) + -  修复 动态 `WebAPI` 不支持嵌套继承 `[Route]` 特性问题 4.8.6.8 ⏱️2023.02.18 [#I6CLPT](https://gitee.com/dotnetchina/Furion/issues/I6CLPT) + +
+ 查看变化 +
+ +过去版本生成错误重复路由,如:`api/system/SystemDictionary/api/system/SystemDictionary/Add`,现已修正。 + +```cs showLineNumbers {1,3,16,17} +public class WithClass : IDynamicApiController +{ + [Route("Add")] + public void Add() + { + + } + + [Route("Edit")] + public void Edit() + { + + } +} + +[Route("api/system/SystemDictionary")] +public class SystemService : WithClass +{ + public void Some() + { + } +} +``` + +
+
+ +- -  修复 动态 `WebAPI` 自定义 `[HttpMethod(template)]` 之后生成错误路由 4.8.6.1 ⏱️2023.02.08 [59fe53b](https://gitee.com/dotnetchina/Furion/commit/59fe53b5a9715ed139aff9075cb7fcaad01565b7) +- -  修复 动态 `WebAPI` 配置 `[Consumes]` 特性后 `Swagger` 不显示问题 4.8.4.12 ⏱️2023.01.10 [daf25f8](https://gitee.com/dotnetchina/Furion/commit/daf25f8c91a24904381d3536c0c8825ef833e9c9) + +
+
+
+ +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +:::tip 小知识 + +`动态WebAPI` 实际上就是将普通的类变为 `Controller`,也就是 `动态WebAPI` 就是控制器,支持控制器一切功能。 + +::: + +## 5.1.1 什么是控制器 + +简单来说,控制器是一个承上启下的作用,根据用户输入,执行响应行为(动作方法),同时在行为中调用模型的业务逻辑,返回给用户结果(视图)。 + + + +

+ +在 `ASP.NET Core` 中,控制器有两种表现形式: + +- `Mvc`(带视图) +- `WebAPI`(RESTful API) + + + + +```cs showLineNumbers {1,5,7} +using Microsoft.AspNetCore.Mvc; + +namespace Furion.Web.Entry.Controllers +{ + public class MvcController : Controller + { + public IActionResult Index() + { + return View(); + } + } +} +``` + + + + +```cs showLineNumbers {1,5,6,8,9} +using Microsoft.AspNetCore.Mvc; + +namespace Furion.Web.Entry.Controllers +{ + [Route("api/[controller]")] + public class WebApiController : ControllerBase + { + [HttpGet] + public IActionResult Get() + { + return Content(nameof(Furion)); + } + } +} +``` + + + + +`Mvc` 控制器和 `WebAPI` 控制器最大的区别是 `WebAPI` 控制器不带 **视图** 和通过 **请求谓词和路由地址响应行为**。 + +## 5.1.2 `Mvc 控制器` 约定和缺点 + +在学习动态 `WebAPI` 控制器之前,首先了解 `ASP.NET Core` 中 `WebAPI` 的一些约定和注意事项。 + +### 5.1.2.1 `WebAPI` 约定 + +在 `ASP.NET Core` 应用中,一个 `WebAPI` 控制器需遵循以下约定: + +- 控制器类**必须继承 `ControllerBase` 或间接继承** +- 动作方法**必须贴有 `[HttpMethod]` 特性,如:`[HttpGet]`** +- 控制器或动作方法**至少有一个配置 `[Route]` 特性** +- 生成 `WebAPI` 路由地址时会自动去掉控制器名称 `Controller` 后缀,同时也会去掉动作方法匹配的 `HttpVerb` 谓词,如 `GET,POST,DELETE,PUT` 等 +- **不支持返回非 `IEnumerable` 泛型对象** +- **不支持类类型参数在 `GET,HEAD` 请求下生成 `Query` 参数** + +除了上述约定外,`WebAPI` 路由地址**基本靠手工完成**,不利于书写,不利于维护,再者,在移动应用对接中难以进行多版本控制。 + +### 5.1.2.2 `.NET Core WebAPI` 缺点 + +通过上一章节可以看出,`ASP.NET Core` 应用实现 `WebAPI` 需要遵循种种约定,而且容易出错。 + +除了这些约定,`.NET Core WebAPI` 有以下缺点: + +- 路由地址基本靠手工完成 +- 在现在移动为王的时代,不利于进行多版本控制 +- 对接 `Swagger` 文档分组比较复杂 +- 实现 `Policy` 策略授权也比较复杂 +- 不支持控制器热插拔插件化 +- 难以实现复杂自定义的 `RESTful API` 风格 + +## 5.1.3 动态 `WebAPI` 控制器 + +针对以上 `ASP.NET Core` 提供的 `WebAPI` 必须遵循的约定和不可避免的缺点,`Furion` 框架推出一种更加灵活创建 `WebAPI` 控制器的方式。 + +这个方式在继承了 `ASP.NET Core WebAPI` 所有优点,同时进行了大量拓展和优化。优化后的 `WebAPI` 具有以下优点: + +- 具备原有的 `ControllerBase` 所有功能 +- 支持**任意公开 非静态 非抽象 非泛型类**转控制器 +- 提供更加灵活方便的 `IDynamicApiController` 空接口或 `[DynamicApiController]` 特性替代 `ControllerBase` 抽象类 +- 可直接在**任意公开 非静态 非抽象 非泛型类**贴 `[Route]` 特性自动转控制器 +- 无需手动配置 `[HttpMethod]` 特性,同时支持一个动作方法多个 `HttpVerb` +- 无需手动配置 `[Route]` 特性,支持更加灵活的配置及自动路由生成 +- 支持返回泛型接口,泛型类 +- 和 `Swagger` 深度结合,提供极其方便的创建 `Swagger` 分组配置 +- 支持 `Basic Auth,Jwt,ApiKey` 等多种权限灵活配置 +- 支持控制器、动作方法**版本控制**功能 +- 支持 `GET、HEAD` 请求自动转换 `类类型参数` +- 支持生成 `OAS3` 接口规范 + +## 5.1.4 注册动态 `WebAPI` 服务 + +:::tip 小提示 + +`.AddDynamicApiControllers()` 默认已经集成在 `AddInject()` 中了,**无需再次注册**。也就是下列代码可不配置。 + +::: + +```cs showLineNumbers {11} title="Furion.Web.Core\FurWebCoreStartup.cs" +using Microsoft.Extensions.DependencyInjection; + +namespace Furion.Web.Core +{ + [AppStartup(800)] + public sealed class FurWebCoreStartup : AppStartup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers() + .AddDynamicApiControllers(); + } + } +} +``` + +:::caution 特别注意 + +`.AddDynamicApiControllers()` 必须在 `services.AddControllers()` 之后注册。 + +::: + +## 5.1.5 第一个例子 + +创建一个 `FurionAppService` 类继承 `IDynamicApiController` 接口 或 贴 `[DynamicApiController]` 特性,并在这个类中编写一个 `Get` 方法。 + +- **`IDynamicApiController` 方式** + +```cs showLineNumbers {1,5,7} +using Furion.DynamicApiController; + +namespace Furion.Application +{ + public class FurionAppService : IDynamicApiController + { + public string Get() + { + return $"Hello {nameof(Furion)}"; + } + } +} +``` + +- **`[DynamicApiController]` 方式** + +```cs showLineNumbers {1,5,8} +using Furion.DynamicApiController; + +namespace Furion.Application +{ + [DynamicApiController] + public class FurionAppService + { + public string Get() + { + return $"Hello {nameof(Furion)}"; + } + } +} +``` + +如下图所示,一个 `WebAPI` 接口就这么生成了。 + + + +## 5.1.6 动态 `WebAPI` 原理解析 + +### 5.1.6.1 控制器特性提供器 + +`Furion` 框架会在应用启动时注册 `DynamicApiControllerFeatureProvider` 控制器特性提供器,该提供器继承自 `ControllerFeatureProvider` 类。 + +接着重写 `bool IsController(TypeInfo typeInfo)` 方法,用来标识控制器类型。在 `Furion` 框架中,**继承自 `ControllerBase` 类或 `IDynamicApiController` 接口或 `[DynamicApiController]` 特性都会被标记为控制器类型。** + +### 5.1.6.2 应用模型转换器 + +`Furion` 框架同时在应用启动时注册 `DynamicApiControllerApplicationModelConvention` 应用模型转换器,该转换器继承自 `IApplicationModelConvention` 接口。 + +接着实现 `void Apply(ApplicationModel application)` 接口方法。在该方法中配置控制器名称、路由、导出可见性及动作方法名称、路由、导出可见性等。 + +实际上该方法做的就是按照 **[WebAPI 约定](#521-webapi-约定)** 提前帮我们配置好路由、请求谓词等信息。避免了手动配置的同时还增加了许多新特性,如**版本控制。** + +## 5.1.7 动态 `WebAPI` 配置约定 + +### 5.1.7.1 控制器默认约定 + +- 生成控制器名称默认去除以 `AppServices,AppService,ApiController,Controller,Services,Service` 作为前后缀的字符串。见第一个例子中的 `FurionAppService -> Furion` **支持自定义配置** +- 控制器名称带 `V[0-9_]` 结尾的,会自动生成控制器版本号,如 `FurionAppServiceV2 -> Furion@2`,`FurionAppServiceV1_1_0 -> Furion@1.1.0`。**支持版本分隔符配置** +- 控制名称以 `骆驼命名(CamelCase)` 会自动切割成多个单词 `-` 连接。**支持自定义配置** + +### 5.1.7.2 动作方法默认约定 + +- 生成的动作方法名称默认去除以 `Post/Add/Create/Insert/Submit,GetAll/GetList/Get/Find/Fetch/Query/Search,Put/Update,Delete/Remove/Clear,Patch` 开头的字符串。**支持自定义配置** +- 生成的动作方法名称默认去除以 `Async` 作为前后缀的字符串。**支持自定义配置** +- 动作方法名称带 `V[0-9_]` 结尾的,会自动生成动作方法版本号,如 `ChangePasswordV2 -> ChangePassword@2`,`ChangePasswordV1_1_0 -> ChangePassword@1.1.0`。**支持版本分隔符配置** +- 动作方法名称以 `骆驼(驼峰)/帕斯卡命名(CamelCase/Pascal)` 会自动切割成多个单词 `-` 连接。**支持自定义配置** +- 动作方法参数将自动转为小写。**支持自定义配置** + +### 5.1.7.3 请求谓词默认约定 + +- 动作方法名 + - 以 `Post/Add/Create/Insert/Submit/Change` 开头,则添加 `[HttpPost]` 特性。 + - 以 `GetAll/GetList/Get/Find/Fetch/Query` 开头,则添加 `[HttpGet]` 特性。 + - 以 `Put/Update` 开头,则添加 `[HttpPut]` 特性。 + - 以 `Delete/Remove/Clear` 开头,则添加 `[HttpDelete]` 特性。 + - 以 `Patch` 开头,则添加 `[HttpPatch]` 特性 + - **支持自定义配置** +- 如果不在上面约定中,则默认添加 `[HttpPost]` 特性。**支持自定义配置** + +### 5.1.7.4 路由地址默认约定 + +- 默认以 `api` 开头。**支持自定义配置** +- 默认转换为小写路由地址。**支持自定义配置** +- 生成控制器路由模板格式为:`api/前置参数列表/模块名或默认区域名/[controller@版本号]/后置参数列表` +- 生成动作方法路由模板格式为:`前置参数列表/模块名/[action@版本号]/后置参数列表` + +### 5.1.7.5 其他约定 + +- 默认不处理 `ControllerBase` 控制器类型。**支持自定义配置** +- 默认不处理 `GET,HEAD` 请求的引用类型参数。**支持自定义配置** + +## 5.1.8 更多例子 + +### 5.1.8.1 多种请求谓词方法 + +```cs showLineNumbers {7,12,17,22,27} +using Furion.DynamicApiController; + +namespace Furion.Application +{ + public class FurionAppService : IDynamicApiController + { + public string Get() + { + return $"GET 请求"; + } + + public string Post() + { + return $"POST 请求"; + } + + public string Delete() + { + return $"DELETE 请求"; + } + + public string Put() + { + return $"PUT 请求"; + } + + public string Patch() + { + return $"PATCH 请求"; + } + } +} +``` + +如下图所示: + + + +### 5.1.8.2 多个自定义动作方法 + +```cs showLineNumbers {7,12,17} +using Furion.DynamicApiController; + +namespace Furion.Application +{ + public class FurionAppService : IDynamicApiController + { + public string GetVersion() + { + return $"v1.0.0"; + } + + public string ChangeProfile() + { + return "修改成功"; + } + + public string DeleteUser() + { + return "删除成功"; + } + } +} +``` + +如下图所示: + + + +### 5.1.8.3 带参数动作方法 + +```cs showLineNumbers {7,12,17} +using Furion.DynamicApiController; + +namespace Furion.Application +{ + public class FurionAppService : IDynamicApiController + { + public string GetUser(int id) + { + return $"{id}"; + } + + public string GetUser(int id, string name) + { + return $"{id} {name}"; + } + + public TestDto Add(TestDto testDto) + { + return testDto; + } + } +} +``` + +如下图所示: + + + +### 5.1.8.4 `GET/HEAD` 类类型参数 + +默认情况下,`ASP.NET Core` 会将 `GET/HEAD` 请求中的 `类类型参数` 设置为 `[FromBody]` 绑定,如: + +```cs showLineNumbers {7} +using Furion.DynamicApiController; + +namespace Furion.Application +{ + public class FurionAppService : IDynamicApiController + { + public TestDto GetTest(TestDto testDto) + { + return testDto; + } + } +} +``` + +如下图所示: + + + +但是,`GET、HEAD` 请求不支持 `From Body` 绑定。所以我们需要转换为 `Query` 查询参数。 + +`Furion` 框架支持以下两种方式配置: + + + + +```cs showLineNumbers {2,8} +using Furion.DynamicApiController; +using Microsoft.AspNetCore.Mvc; + +namespace Furion.Application +{ + public class FurionAppService : IDynamicApiController + { + public TestDto GetTest([FromQuery] TestDto testDto) + { + return testDto; + } + } +} +``` + + + + +```json showLineNumbers {2-4} title="Furion.Web.Entry/appsettings.json" +{ + "DynamicApiControllerSettings": { + "ModelToQuery": true + } +} +``` + + + + +如下图所示: + + + +### 5.1.8.5 自定义参数位置 + +`Furion` 框架提供了非常方便的自定义参数位置的特性 `[ApiSeat]`,通过 `[ApiSeat]` 可配置参数位置,支持以下四种位置: + +- `ApiSeats.ControllerStart`:控制器之前 +- `ApiSeats.ControllerEnd`:控制器之后 +- `ApiSeats.ActionStart`:动作方法之前 +- `ApiSeats.ActionEnd`:动作方法之后。**默认值** + +```cs showLineNumbers {8-9,15-20} +using Furion.DynamicApiController; +using System; + +namespace Furion.Application +{ + public class FurionAppService : IDynamicApiController + { + // 参数默认为 ApiSeats.ActionEnd + public string RouteSeat(int id, string name) + { + return "配置路由参数位置"; + } + + public string RouteSeat( + [ApiSeat(ApiSeats.ControllerStart)] int id, // 控制器名称之前 + [ApiSeat(ApiSeats.ControllerEnd)] string name, // 控制器名称之后 + [ApiSeat(ApiSeats.ControllerEnd)] int age, // 控制器名称之后 + [ApiSeat(ApiSeats.ActionStart)] decimal weight, // 动作方法名称之前 + [ApiSeat(ApiSeats.ActionStart)] float height, // 动作方法名称之前 + [ApiSeat(ApiSeats.ActionEnd)] DateTime birthday) // 动作方法名称之后(默认值) + { + return "配置路由参数位置"; + } + } +} +``` + +如下图所示: + + + +:::note 温馨提示 + +多个 **`同位置`** 配置的参数将按照 **`定义参数顺序`** 进行排序。 + +::: + +:::caution 特别注意 + +`[ApiSeat]` 只能应用于贴了 `[FromRoute]` 特性的参数或 `基元类型、值类型、可空基元类型和可空值类型`。 + +::: + +### 5.1.8.6 自定义请求谓词 + +```cs showLineNumbers {2,8} +using Furion.DynamicApiController; +using Microsoft.AspNetCore.Mvc; + +namespace Furion.Application +{ + public class FurionAppService : IDynamicApiController + { + [HttpPost] + public string GetVersion() + { + return "1.0.0"; + } + } +} +``` + +如下图所示: + + + +### 5.1.8.7 支持多个谓词 + +```cs showLineNumbers {2,8} +using Furion.DynamicApiController; +using Microsoft.AspNetCore.Mvc; + +namespace Furion.Application +{ + public class FurionAppService : IDynamicApiController + { + [HttpPost, HttpGet, AcceptVerbs("PUT", "DELETE")] + public string GetVersion() + { + return "1.0.0"; + } + } +} +``` + +如下图所示: + + + +:::caution 特别注意 + +如果动作方法中含有 `类类型参数`,且含有 `POST/PUT/DELETE` 任意请求谓词,那么该参数会自动添加 `[FromBody]` 参数,即使在 `GET/HEAD` 请求中不支持。 + +::: + +### 5.1.8.8 支持自定义路由 + +支持控制器和动作方法自定义路由: + + + + +```cs showLineNumbers {2,6} +using Furion.DynamicApiController; +using Microsoft.AspNetCore.Mvc; + +namespace Furion.Application +{ + [Route("customapi/mobile/[controller]")] + public class FurionAppService : IDynamicApiController + { + public string GetVersion() + { + return "1.0.0"; + } + } +} +``` + +如下图所示: + + + + + + +```cs showLineNumbers {2,8} +using Furion.DynamicApiController; +using Microsoft.AspNetCore.Mvc; + +namespace Furion.Application +{ + public class FurionAppService : IDynamicApiController + { + [Route("customapi/[action]")] + public string GetVersion() + { + return "1.0.0"; + } + } +} +``` + +如下图所示: + + + + + + +```cs showLineNumbers {2,6,9} +using Furion.DynamicApiController; +using Microsoft.AspNetCore.Mvc; + +namespace Furion.Application +{ + [Route("customapi/mobile/[controller]")] + public class FurionAppService : IDynamicApiController + { + [Route("get/[action]")] + public string GetVersion() + { + return "1.0.0"; + } + } +} +``` + +如下图所示: + + + + + + +```cs showLineNumbers {9} +using Furion.DynamicApiController; +using Microsoft.AspNetCore.Mvc; + +namespace Furion.Application +{ + [Route("api/[controller]")] + public class FurionAppService : IDynamicApiController + { + [HttpGet("get/[action]")] + public string GetVersion() + { + return "1.0.0"; + } + } +} +``` + +如下图所示: + + + + + + +:::important 小提示 + +动作方法自定义路由如果以 **`/`** 开头,则不会合并控制器路由。 + +::: + +:::tip 推荐配置 + +自定义路由如果需要用到 **控制器/动作方法名称**,推荐使用 `[controller]` 或 `[action]` 占位符,因为该占位符已经自动处理了 **前后缀、版本号、模块名称**等。 + +::: + +### 5.1.8.9 多路由随意组合 + +`Furion` 框架提供了非常灵活的各种路由组合方式,支持一对多,多对多路由组合: + +```cs showLineNumbers {6-8,11-14} +using Furion.DynamicApiController; +using Microsoft.AspNetCore.Mvc; + +namespace Furion.Application +{ + [Route("api/[controller]")] + [Route("api/[controller]/second")] + [Route("api/[controller]/three")] + public class FurionAppService : IDynamicApiController + { + [HttpGet] + [HttpGet("get/[action]")] + [HttpPost] + [HttpPost("post/cus-version")] + public string GetVersion() + { + return "1.0.0"; + } + } +} +``` + +如下图所示: + + + +:::caution 特别注意 + +动作方法不能同时贴 `[Route]` 和 `[HttpMethod]` 特性,只能二取一。 + +::: + +--- + +在 `Furion 4.8.5.7+` 版本提供更加强大的路由组合方式: + +```cs showLineNumbers {8,19,36} +using Furion.DynamicApiController; +using Microsoft.AspNetCore.Mvc; + +namespace WebApplication38; + +[Route("api/[controller]")] +[Route("api2/[controller]")] +public class Test1Service : IDynamicApiController +{ + [HttpGet("test")] + [HttpPost] + [AcceptVerbs("PUT", "PATCH")] + public async Task GetTestName() + { + await Task.CompletedTask; + } +} + +public class Test2Service : IDynamicApiController +{ + [HttpGet("/root/test")] + [HttpGet("test")] + [HttpGet(Name = "other-test")] + [HttpGet("template-test", Name = "other-test")] + [HttpPost] + [AcceptVerbs("PUT", "PATCH")] + public async Task GetTestName() + { + await Task.CompletedTask; + } +} + +[Route("api/[controller]")] +[Route("api2/[controller]/second")] +[Route("api3/[controller]/three")] +public class Test3Service : IDynamicApiController +{ + [HttpGet] + [HttpGet("get/[action]")] + [HttpPost] + [HttpPost("post/cus-version")] + public string GetVersion() + { + return "1.0.0"; + } +} +``` + + + +### 5.1.8.10 支持版本控制 + + + + +```cs showLineNumbers {5,13,21} +using Furion.DynamicApiController; + +namespace Furion.Application +{ + public class FurionAppServiceV1 : IDynamicApiController + { + public string Get() + { + return nameof(Furion); + } + } + + public class FurionAppServiceV1_2 : IDynamicApiController + { + public string Get() + { + return nameof(Furion); + } + } + + public class FurionAppServiceV1_2_1 : IDynamicApiController + { + public string Get() + { + return nameof(Furion); + } + } +} +``` + +如下图所示: + + + + + + +```cs showLineNumbers {7,12,16} +using Furion.DynamicApiController; + +namespace Furion.Application +{ + public class FurionAppService : IDynamicApiController + { + public string Get() + { + return nameof(Furion); + } + + public string GetV1() + { + return nameof(Furion); + } + public string GetV2_1() + { + return nameof(Furion); + } + } +} +``` + +如下图所示: + + + + + + +:::note 版本生成原理 + +**`V[0-9_]`** 结尾的命名自动解析成版本号,如 **`FurionAppServiceV2 -> Furion@2`**。 + +::: + +:::tip 版本复写 + +除了通过特定后缀方式以外,版本还直接通过 `[ApiDescriptionSettings]` 进行复写。如: + +```cs showLineNumbers {1,2} +[ApiDescriptionSettings(Version = "4.0")] +public string GetV1() +{ + return nameof(Furion); +} +``` + +这时,生成版本将采用 `4.0` 替代 `1` + +::: + +### 5.1.8.11 不公开控制器或动作方法 + +有些时候,我们无需导出某个动作方法或控制器(**不显示到 Swagger**),只需要添加 `[ApiDescriptionSettings(false)]` 或 `[ApiDescriptionSettings(IgnoreApi = true)]`即可。 + +另外动作方法还支持 `[NonAction]` 标记不是一个有效的控制器或 Action。 + +```cs showLineNumbers {8,14,20,27} +public class FurionAppService: IDynamicApiController +{ + public string Export() + { + // .... + } + + [ApiDescriptionSettings(false)] // 不在 Swagger 上显示 + public string NoExport() + { + // ... + } + + [ApiDescriptionSettings(IgnoreApi = true)] // 不在 Swagger 上显示 + public string NoExport2() + { + // ... + } + + [NonAction] // 不是一个 API + public string IsNotAPI() + { + // ... + } +} + +[ApiDescriptionSettings(false)] // 不导出 +public class NoExportServices: IDynamicApiController +{ + // .... +} +``` + +### 5.1.8.12 保持控制器和方法命名 + +默认情况下,动态 API 会将控制器和方法名输出为 `RESTFul` 风格的路由,如需保留原有设计,只需配置: + +```json showLineNumbers {2-5} +{ + "DynamicApiControllerSettings": { + "KeepName": true, + "KeepVerb": true, + "LowercaseRoute": false + } +} +``` + +### 5.1.8.13 方法参数 `[FromQuery]` 化/参数非必填/参数可选 + +默认情况下,所有的基元类型参数都会贴上 `[FromRoute]` 特性,如果需要将参数调整为 `[FromQuery]` 修饰,只需要在方法上面贴 `[QueryParameters]` 特性即可,如: + +```cs showLineNumbers {1} +[QueryParameters] +public string Get(int id, string name) +{ + return nameof($"{id} {name}"); +} +``` + +生成的路由为:`https://xxx.com?id=1&name=Furion` + +如果不喜欢每个都配置,也可以全局配置(**只会影响基元类型的参数**): + +```json showLineNumbers {2-3} +{ + "DynamicApiControllerSettings": { + "UrlParameterization": true + } +} +``` + +:::important 特别注意 + +贴了 `[QueryParameters]` 之后,会对所有参数影响,包括类类型参数,如果不需要处理某个参数,只需要贴 `[FromXXX]` 特性即可。 + +::: + +### 5.1.8.14 参数绑定配置 + +`Furion` 框架提供了多种参数特性配置参数绑定规则: + +- `[FromRoute]`:通过路由参数绑定值 +- `[FromQuery]`:通过 `Url` 地址参数绑定值 +- `[FromBody]`:通过 `Request Body` 参数绑定值 +- `[FromForm]`:通过表单提交绑定值 +- `[FromHeader]`:通过 `Request Header` 参数绑定值 + +### 5.1.8.15 自定义根据方法名生成 `[HttpMethod]` 规则 + +在 `Furion` 框架中,在没有配置 `[HttpMethod]` 特性的情况下,会自动根据方法名第一个参数进行分析,并生成对应的 `[HttpMethod]` 特性,规则如下: + +- 动作方法名 + - 以 `Post/Add/Create/Insert/Submit` 开头,则添加 `[HttpPost]` 特性。 + - 以 `GetAll/GetList/Get/Find/Fetch/Query` 开头,则添加 `[HttpGet]` 特性。 + - 以 `Put/Update` 开头,则添加 `[HttpPut]` 特性。 + - 以 `Delete/Remove/Clear` 开头,则添加 `[HttpDelete]` 特性。 + - 以 `Patch` 开头,则添加 `[HttpPatch]` 特性 + - 以 `Head` 开头,则添加 `[HttpHead]` 特性 + - **支持自定义配置** +- 如果不在上面约定中,则默认添加 `[HttpPost]` 特性。**支持自定义配置** + +**但是,有些时候这不是我们想要的规则**,这时我们只需要在 `appsettings.json` 中配置即可: + +```json showLineNumbers {2,3} +{ + "DynamicApiControllerSettings": { + "VerbToHttpMethods": [ + ["getall", "HEAD"], // => getall 会被复写为 `[HttpHead]` + ["other", "PUT"] // => 新增一条新规则,比如,一 `[other]` 开头会转换为 `[HttpPut]` 请求 + ] + } +} +``` + +:::important 特别注意 + +二维数组中的每一个元素的第一个元素**必须是全小写**,第二个元素**必须是全大写大写**,第二个元素取值有:`HEAD, GET, PUT, POST, PATCH, DELETE` + +::: + +### 5.1.8.16 路由参数非必填/选填 + +在 `Furion v2.8.6` 版本中实现了 `[FromRoute]` 参数非必填功能,支持以下几种方式: + +```cs showLineNumbers {2,6,11,16} +// 方式一,通过可空 ? +public object Method1(int id, Datetime? dateTime) +{ +} + +// 方式二,通过默认值 +public object Method1(int id, int age = 10) +{ +} + +// 方式三,默认值 + 可空 ? +public object Method1(int id, int? age = 10) +{ +} + +// 方式四,[FromQuery] 修饰 +public object Method1(int id, [FromQuery]string keyword) +{ +} +``` + +### 5.1.8.17 `[FormRoute]` 路由约束 + +在 `Furion v2.8.6` 版本中,添加了 `[RouteConstraint]` 特性,可配置路由约束,如:`[RouteConstraint(":min(10)")]` + +```cs showLineNumbers {2} +// 最小值 10 +public object Method1([RouteConstraint(":min(10)")] int id) +{ +} +``` + +`[RouteConstraint]` 支持路由约束符号如下: + +| 符号 | 描述 | 例子 | +| ----------- | --------------------------------------- | ------------------------------- | +| `*` | 匹配路由 0-n 长度,Furion 4.8.6.2+ 支持 | `:*` | +| `alpha` | 匹配大写或小写拉丁字母字符(a-z、A-Z) | `:alpha` | +| `bool` | bool 类型 | `:bool` | +| `datetime` | DateTime 类型 | `:datetime` | +| `decimal` | decimal 类型 | `:decimal` | +| `double` | double 类型 | `:double` | +| `float` | float 类型 | `:float` | +| `guid` | guid 类型 | `:guid` | +| `int` | int 类型 | `:int` | +| `long` | long 类型 | `:long` | +| `length` | 匹配长度(字符串) | `:length(6)` 或 `:length(1,20)` | +| `max` | 最大值 | `:max(10)` | +| `maxlength` | 最大长度(字符串) | `:maxlength(10)` | +| `min` | 最小值 | `:min(10)` | +| `minlength` | 最小长度(字符串) | `:minlength(10)` | +| `range` | 取值范围 | `:range(10,50)` | +| `regex` | 正则表达式 | `:regex(^\d{3}-\d{3}-\d{4}$)` | + +### 5.1.8.18 `小驼峰` 路由路径 + +```json showLineNumbers {2-5} +{ + "DynamicApiControllerSettings": { + "LowercaseRoute": false, + "KeepName": true, + "AsLowerCamelCase": true + } +} +``` + +### 5.1.8.19 `application/xml` 报文参数支持 + +1. 在 `Startup.cs` 中启用 `XML` 请求报文格式支持 + +```cs showLineNumbers {2-3} +services.AddControllers() // .AddControllersWithViews() + .AddXmlSerializerFormatters() + .AddXmlDataContractSerializerFormatters() +``` + +:::warning 异常处理 + +如果出现 `XmlSerializer` 异常,那么只需要移除 `.AddXmlSerializerFormatters()` 即可,如: + +```cs showLineNumbers {2} +services.AddControllers() // .AddControllersWithViews() + .AddXmlDataContractSerializerFormatters() +``` + +::: + +2. 定义实体类型 + +```cs showLineNumbers +// 实体类型 +public class People +{ + // 基础类型 + public int Age { get; set; } + public string Name { get; set; } + public bool IsDeleted { get; set; } + + // 数组类型 + public string[] Address { get; set; } + + // 集合类型 + public List Emails { get; set; } + + // 类类型 + public Child Child { get; set; } +} + +public class Child +{ + public string Name { get; set; } +} +``` + +3. 在动态 `WebAPI` 中使用 + +```cs showLineNumbers {3-4} +public class XmlDemo : IDynamicApiController +{ + //[Consumes("application/xml")] // 如果设置了 [Consumes] 那么就表示只能传递 `accept= application/xml` 格式,不设置则支持多种(XML/JSON) + public People Test(People people) + { + return people; + } +} +``` + +:::important `[Consumes]` 和 `[Produces]` 说明 + +`[Consumes]` 特性是用来定义输入参数格式,对应请求报文的 `accept`,`[Produces]` 特性是用来定义返回值格式,对应响应报文的 `content-type`。 + +::: + +4. `XML` 格式说明及注意事项 + +支持两种 `XML` 格式报文,如: + +:::caution 注意事项 + +`XML` 标签区分大小写,必须严格对照 `C#` 类型定义声明。对于集合/数组类型,必须遵循下列格式: + +```xml showLineNumbers +<属性名> + + +``` + +如: + +```xml showLineNumbers {1,5,9,13,17,21} + + monksoul@outlook.com + rustln@outlook.com + + + 1 + 2 + + + true + false + + + 1.0 + 2.0 + + + 1.0 + 2.0 + + + + Furion + + +``` + +::: + +- 第一种(常用格式) + +```xml showLineNumbers {1-2} + + + 30 + 百小僧 + true +
+ 广东省中山市 + 广东省珠海市 +
+ + monksoul@outlook.com + rustln@outlook.com + + + Furion + +
+``` + +- 第二种(标准格式) + +```xml showLineNumbers {1} + + 30 + 百小僧 + true +
+ 广东省中山市 + 广东省珠海市 +
+ + monksoul@outlook.com + rustln@outlook.com + + + Furion + +
+``` + +## 5.1.9 `[ApiDescriptionSettings]` + +除了上述 `ASP.NET Core` 提供的配置外,`Furion` 框架还提供了非常强大且灵活的 `[ApiDescriptionSettings]` 特性。 + +### 5.1.9.1 内置配置 + +- `Name`:自定义控制器/动作方法名称,`string`,默认 `null` +- `KeepName`:是否保持原有名称不处理,`bool`,默认 `false` +- `SplitCamelCase`:切割骆驼(驼峰)/帕斯卡命名,`bool`,默认 `true` +- `KeepVerb`:是否保留动作方法请求谓词,`bool`,默认 `false` +- `Enabled`:是否导出接口,`bool`,默认 `true` +- `Module`:模块名,`string`,默认 `null` +- `Version`:版本号,`string`,默认 `null` +- `Groups`:接口分组,可结合 `Swagger` 一起使用,`string[]`,默认 `null` +- `Tags`:接口标签,可结合 `Swagger` 一起使用,`string[]`,默认 `null` +- `Order`:配置控制器/动作方法排序,数值越大越靠前 +- `LowercaseRoute`:是否采用小写路由,`bool` 类型,默认 `true` +- `AsLowerCamelCase`:启用小驼峰命名(首字母小写),默认 `false` +- `Area`:配置区域名称,默认空,**只作用于类中贴** +- `Description`:配置单一接口更多描述功能,只在 `方法` 中有效,**仅限 v3.3.5+版本有效** +- `ForceWithRoutePrefix`:配置是否强制添加 `DefaultRoutePrefix`,当控制器自定义了 `[Route]` 有效,默认 `false`,**仅限 v3.4.1+版本有效** + +### 5.1.9.2 `Name` 配置 + +`Name` 参数可以覆盖动态 `WebAPI` 自动生成的控制器或动作方法名称。如: + +```cs showLineNumbers {5,8,14,20} +using Furion.DynamicApiController; + +namespace Furion.Application +{ + [ApiDescriptionSettings(Name = "MyFur")] + public class FurionAppService : IDynamicApiController + { + [ApiDescriptionSettings(Name = "MyGet")] + public string Get() + { + return nameof(Furion); + } + + [ActionName("MyTest")] // Furion 4.8.4.12+ 支持 + public string Test() + { + return nameof(Furion); + } + + [HttpGet(Name = "MyTest")] // Furion 4.8.4.12+ 支持,此配置有效的前提是控制器贴有 [Route] 特性 + public string Test2() + { + return nameof(Furion); + } + } +} +``` + +如下图所示: + + + +### 5.1.9.3 `KeepName` 配置 + +`KeepName` 参数可以保留原有的控制器或动作方法名称。如: + +```cs showLineNumbers {5,8} +using Furion.DynamicApiController; + +namespace Furion.Application +{ + [ApiDescriptionSettings(KeepName = true)] + public class FurionAppService : IDynamicApiController + { + [ApiDescriptionSettings(KeepName = true)] + public string Get() + { + return nameof(Furion); + } + } +} +``` + +如下图所示: + + + +### 5.1.9.4 `SplitCamelCase` 配置 + +`SplitCamelCase` 参数默认将骆驼(驼峰)命名切割成多个单词并通过指定 `占位符` 连接起来。默认 `占位符` 为 `-`。默认为 `true`。如: + +```cs showLineNumbers {5,8} +using Furion.DynamicApiController; + +namespace Furion.Application +{ + [ApiDescriptionSettings(SplitCamelCase = false)] + public class MyFurionAppService : IDynamicApiController + { + [ApiDescriptionSettings(SplitCamelCase = true)] + public string ChangeUserName() + { + return nameof(Furion); + } + } +} +``` + +如下图所示: + + + +:::important 特别注意 + +`KeepName` 优先级高于 `SplitCamelCase`,也就是 `KeepName` 设置为 `true`,则不会处理 `SplitCamelCase` 参数。 + +::: + +### 5.1.9.5 `KeepVerb` 配置 + +`KeepVerb` 参数作用于动作方法,标识是否保留动作谓词。如: + +```cs showLineNumbers {7} +using Furion.DynamicApiController; + +namespace Furion.Application +{ + public class FurionAppService : IDynamicApiController + { + [ApiDescriptionSettings(KeepVerb = true)] + public string GetVersion() + { + return nameof(Furion); + } + } +} +``` + +如下图所示: + + + +### 5.1.9.6 `Enabled` 配置 + +`Enabled` 参数配置接口是否导出。通常用于动作方法,如果用于控制器实际作用不大。 + +```cs showLineNumbers {12} +using Furion.DynamicApiController; + +namespace Furion.Application +{ + public class FurionAppService : IDynamicApiController + { + public string GetVersion() + { + return nameof(Furion); + } + + [ApiDescriptionSettings(false)] + public string NoExport() + { + return nameof(Furion); + } + } +} +``` + +如下图所示: + + + +### 5.1.9.7 `Module` 配置 + +`Module` 参数可以配置路由分离,类似于 `Mvc 区域` 的作用。 + +```cs showLineNumbers {5,8} +using Furion.DynamicApiController; + +namespace Furion.Application +{ + [ApiDescriptionSettings(Module = "mobile")] + public class FurionAppService : IDynamicApiController + { + [ApiDescriptionSettings(Module = "user")] + public string GetVersion() + { + return nameof(Furion); + } + } +} +``` + +如下图所示: + + + +### 5.1.9.8 `Version` 配置 + +`Version` 参数可以配置接口版本,同时又可以复写特殊版本命名配置。默认版本分隔符为 `@`。如: + +```cs showLineNumbers {5,9-10} +using Furion.DynamicApiController; + +namespace Furion.Application +{ + [ApiDescriptionSettings(Version = "1.0")] + public class FurionAppService : IDynamicApiController + { + // V2.0.0 被复写成 V2.1.1 + [ApiDescriptionSettings(Version = "2.1.1")] + public string GetVersionV2_0_0() + { + return nameof(Furion); + } + } +} +``` + +如下图所示: + + + +### 5.1.9.9 `Groups` 配置 + +`Groups` 配置主要用于配置 `Swagger` 分组信息。 + +通过配置 `Groups` 参数可以将`控制器和动作方法` 进行归类和多个分组直接共享。可通过 `[ApiDescriptionSettings(params Groups)]` 构造函数传入或指定 `Groups` 参数配置接口是否导出。通常用于动作方法,如果用于控制器实际作用不大。 + +```cs showLineNumbers {5,13} +using Furion.DynamicApiController; + +namespace Furion.Application +{ + [ApiDescriptionSettings("Default", "Common")] + public class FurionAppService : IDynamicApiController + { + public string Get() + { + return nameof(Furion); + } + + [ApiDescriptionSettings("Custom")] + public int Get(int id) + { + return id; + } + } +} +``` + +如下图所示: + + + +### 5.1.9.10 `Tag` 配置 + +`Tag` 配置主要用于配置 `Swagger` 标签分组信息及合并标签。也就是 `组中组`: + + + + +#### 未贴标签之前 + +```cs showLineNumbers +using Furion.DynamicApiController; + +namespace Furion.Application +{ + public class FurionAppService : IDynamicApiController + { + public string Get() + { + return nameof(Furion); + } + + public int Get(int id) + { + return id; + } + } + + public class TestAppService : IDynamicApiController + { + public string Get() + { + return nameof(Furion); + } + + public int Get(int id) + { + return id; + } + } +} +``` + +#### 贴标签之后 + +```cs showLineNumbers {5,19} +using Furion.DynamicApiController; + +namespace Furion.Application +{ + [ApiDescriptionSettings(Tag = "分组一")] + public class FurionAppService : IDynamicApiController + { + public string Get() + { + return nameof(Furion); + } + + public int Get(int id) + { + return id; + } + } + + [ApiDescriptionSettings(Tag = "分组二")] + public class TestAppService : IDynamicApiController + { + public string Get() + { + return nameof(Furion); + } + + public int Get(int id) + { + return id; + } + } +} +``` + +如下图所示: + + + + + + +```cs showLineNumbers {5,19} +using Furion.DynamicApiController; + +namespace Furion.Application +{ + [ApiDescriptionSettings(Tag = "合并所有标签")] + public class FurionAppService : IDynamicApiController + { + public string Get() + { + return nameof(Furion); + } + + public int Get(int id) + { + return id; + } + } + + [ApiDescriptionSettings(Tag = "合并所有标签")] + public class TestAppService : IDynamicApiController + { + public string Get() + { + return nameof(Furion); + } + + public int Get(int id) + { + return id; + } + } +} +``` + +如下图所示: + + + + + + +:::tip 小知识 + +如果 `Tag` 名字一样,则会自动合并,否则只是命名。 + +::: + +## 5.1.10 `DynamicApiControllerSettings` 配置 + +`Furion` 还提供动态 `WebAPI` 接口一些全局配置选项,如: + +- `DefaultRoutePrefix`:默认路由前缀,`string`,默认 `api` +- `DefaultHttpMethod`:默认请求谓词,`string`,默认:`POST` +- `DefaultModule`:默认模块名称(区域),可用作接口版本,`string`,默认:`v1` +- `LowercaseRoute`:小写路由格式,`bool`,默认:`true` +- `AsLowerCamelCase`:启用小驼峰命名(首字母小写),默认 `false` +- `KeepVerb`:是否保留动作谓词,`bool`,默认:`false` +- `KeepName`:是否保留默认名称,`bool`,默认:`fasle` +- `CamelCaseSeparator`:骆驼(驼峰)/帕斯卡命名分隔符,`string`,默认:`-` +- `VersionSeparator`:版本分隔符,`string`,默认:`@` +- `ModelToQuery`:`GET/HEAD` 请求将 `类类型参数转查询参数`,`bool`,默认 `false` +- `SupportedMvcController`:是否支持 `Mvc Controller` 动态配置,`bool`,默认 `false` +- `UrlParameterization`:路由参数采用 `[FromQuery]` 化,默认 `false`(`[FromRoute]` 方式) +- `DefaultArea`:配置默认区域,默认 `null` +- `ForceWithRoutePrefix`:配置是否强制添加 `DefaultRoutePrefix`,当控制器自定义了 `[Route]` 有效,**仅限 v3.4.1+版本有效** +- `AbandonControllerAffixes`:默认去除控制器名称前后缀列表名,`string[]`,默认: + - `AppServices` + - `AppService` + - `ApiController` + - `Controller` + - `Services` + - `Service` +- `AbandonActionAffixes`:默认去除动作方法名称前后缀列表名,`string[]`,默认: + - `Async` +- `VerbToHttpMethods`:复写默认方法名转 `[HttpMethod]` 规则,`string[][]` 二维数组类型,内置匹配规则为: + ```cs showLineNumbers + ["post"] = "POST", + ["add"] = "POST", + ["create"] = "POST", + ["insert"] = "POST", + ["submit"] = "POST", + ["get"] = "GET", + ["find"] = "GET", + ["fetch"] = "GET", + ["query"] = "GET", + ["put"] = "PUT", + ["update"] = "PUT", + ["delete"] = "DELETE", + ["remove"] = "DELETE", + ["clear"] = "DELETE", + ["patch"] = "PATCH" + ``` + - 复写示例 + ```json showLineNumbers + "DynamicApiControllerSettings": { + "VerbToHttpMethods": [ + [ "getall", "HEAD" ], // => getall 会被复写为 `[HttpHead]` + [ "other", "PUT" ] // => 新增一条新规则,比如,一 `[other]` 开头会转换为 `[HttpPut]` 请求 + ] + } + ``` + +### 5.1.10.1 支持 `Mvc 控制器` 动态配置 + +默认情况下,`Furion` 动态 `WebAPI` 接口不对 `ControllerBase` 类型进行任何处理。当然,我们也可以手动启用 `ControllerBase` 支持。 + +```json showLineNumbers {2-4} title="Furion.Web.Entry/appsettings.json" +{ + "DynamicApiControllerSettings": { + "SupportedMvcController": true + } +} +``` + +设置 `SupportedMvcController: true` 后,`Mvc ControllerBase` 类型也能和动态 `WebAPI` 一样的灵活了。代码如下: + +```cs showLineNumbers {5} +using Microsoft.AspNetCore.Mvc; + +namespace Furion.Web.Entry.Controllers +{ + public class MvcController : ControllerBase + { + public string Get() + { + return nameof(Furion); + } + } +} + +``` + +:::warning 注意事项 + +启用该配置后,如果 `Mvc 控制器` 没有任何 `[Route]` 特性,但是贴了 `[ApiController]` 特性将会报错。原因是 `[ApiController]` 特性内部做了路由特性检测。所以建议使用 `[ApiDataValidation]` 代替。 + +查看 [ASP.NET Core - ApiBehaviorApplicationModelProvider 源码](https://github.com/dotnet/aspnetcore/blob/c565386a3ed135560bc2e9017aa54a950b4e35dd/src/Mvc/Mvc.Core/src/ApplicationModels/ApiBehaviorApplicationModelProvider.cs#L90) + +::: + +## 5.1.11 关于 AOP 拦截 + +`动态WebAPI` 支持 `Controller` 的所有过滤器/筛选器拦截,也就是可以通过 `ActionFilter`,`ResultFilter` 进行拦截操作。如: + +```cs showLineNumbers {1,3} +public class SampleAsyncActionFilter : IAsyncActionFilter +{ + public async Task OnActionExecutionAsync(ActionExecutingContext context,ActionExecutionDelegate next) + { + // 拦截之前 + + var resultContext = await next(); + + // 拦截之后 + + // 异常拦截 + if(resultContext.Exception != null) + { + + } + } +} +``` + +详细用法可参见 [ASP.NET Core 5.0 - 筛选器](https://docs.microsoft.com/zh-cn/aspnet/core/mvc/controllers/filters?view=aspnetcore-5.0) + +## 5.1.12 设置 `api` 超时请求时间 + +在 `Program.cs` 中添加 `.UseKestrel` 配置即可,如: + +- `.NET5 版本` + +```cs showLineNumbers {8-12} +public static IHostBuilder CreateHostBuilder(string[] args) +{ + return Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.Inject() + .UseStartup() + .UseKestrel(option => + { + option.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(20); + option.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(20); + }); + }); +} +``` + +- `.NET6 版本` + +```cs showLineNumbers {3-7} +var app = builder.Build(); + +app.Configuration.Get().ConfigureKestrel(x => +{ + x.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(20); + x.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(20); +}); +``` + +## 5.1.13 获取路由/控制器/`Action` 列表 + +有时候我们需要获取当前路由信息,或所有控制器、`Action` 列表,而已通过以下代码获取: + +- **获取当前路由表信息(简易)** + +```cs showLineNumbers {4,11,14} +public class MetadataService : IDynamicApiController +{ + private readonly IHttpContextAccessor _httpContextAccessor; + public MetadataService(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public RouteValueDictionary Print() + { + var routeValuesFeature = _httpContextAccessor.HttpContext.Features.Get(); + + // 获取路由信息 + var routeValues = routeValuesFeature.RouteValues; + + return routeValues; + } +} +``` + +输出: + +```json showLineNumbers +{ + "action": "print", + "controller": "metadata" +} +``` + +- **获取当前终点路由信息(简易)** + +```cs showLineNumbers {4,9,12,14-17,19-21} +public class MetadataService : IDynamicApiController +{ + private readonly IHttpContextAccessor _httpContextAccessor; + public MetadataService(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + [Description("这是一段描述")] + public void Print() + { + var endpointFeature = _httpContextAccessor.HttpContext.Features.Get(); + + // 获取路由终点信息 + var routeEndpoint = endpointFeature.Endpoint as RouteEndpoint; + var displayName = routeEndpoint.DisplayName; // 路由映射方法 FullName + var routePattern = routeEndpoint.RoutePattern; // 路由表达式(路径) + + // 获取路由元数据(特性) + var metadata = routeEndpoint.Metadata; + var attribute = metadata.GetMetadata(); // 获取 [Description] 特性 + } +} +``` + +- **获取所有控制器列表** + +```cs showLineNumbers {4,11-12,15} +public class MetadataService : IDynamicApiController +{ + private readonly ApplicationPartManager _applicationPartManager; + public MetadataService(ApplicationPartManager applicationPartManager) + { + _applicationPartManager = applicationPartManager; + } + + public void Print() + { + var controllerFeature = new ControllerFeature(); + _applicationPartManager.PopulateFeature(controllerFeature); + + // 获取所有控制器列表 + IList controllers = controllerFeature.Controllers; + } +} +``` + +- **获取所有 `Action` 列表(强大)** + +```cs showLineNumbers {4,12,16-25} +public class MetadataService : IDynamicApiController +{ + private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider; + public MetadataService(IActionDescriptorCollectionProvider actionDescriptorCollectionProvider) + { + _actionDescriptorCollectionProvider = actionDescriptorCollectionProvider; + } + + public void Print() + { + // 获取所有 Action 列表 + var actionDescriptors = _actionDescriptorCollectionProvider.ActionDescriptors.Items; + + foreach (ActionDescriptor actionDescriptor in actionDescriptors) + { + // 获取请求的方法 + var method = (actionDescriptor as ControllerActionDescriptor).MethodInfo; + + // 获取路由地址 + var route = actionDescriptor.AttributeRouteInfo.Template; + + // 获取 HttpMethod + var httpMethod = actionDescriptor.ActionConstraints?.OfType().FirstOrDefault()?.HttpMethods.First(); + + // 任何关于这个路由/方法/控制器/特性的信息都有 + } + } +} +``` + +- **获取所有 `Action` 列表带分组信息(强大)** + +```cs showLineNumbers {4,12,17,21-36} +public class MetadataService : IDynamicApiController +{ + private readonly IApiDescriptionGroupCollectionProvider _apiDescriptionGroupCollectionProvider; + public MetadataService(IApiDescriptionGroupCollectionProvider apiDescriptionGroupCollectionProvider) + { + _apiDescriptionGroupCollectionProvider = apiDescriptionGroupCollectionProvider; + } + + public void Print() + { + // 获取所有控制器列表 + var apiDescriptionGroups = _apiDescriptionGroupCollectionProvider.ApiDescriptionGroups.Items; + + foreach (ApiDescriptionGroup group in apiDescriptionGroups) + { + // 获取当前分组的所有 Actions + var actions = group.Items; + + foreach (ApiDescription action in actions) + { + // 路由地址 + var route = action.RelativePath; + + // HttpMethod + var httpMethod = action.HttpMethod; + + // 分组名 + var groupName = action.GroupName; + + // Action 描述器 + var actionDescriptor = action.ActionDescriptor; + + // 获取请求的方法 + var method = (actionDescriptor as ControllerActionDescriptor).MethodInfo; + + // 任何关于这个路由/方法/控制器/特性的信息都有 + } + } + } +} +``` + +## 5.1.14 插件化 `IDynamicApiRuntimeChangeProvider` + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.8.8 +` 版本使用。 + +::: + +在一些特定的需求中,我们需要在运行时**动态编译代码,如动态编写 `WebAPI`**,之后能够在不重启主机服务的情况下即可有效。比如这里动态添加 `SomeClass` 动态 `WebAPI`,然后在 `Swagger/路由系统` 中立即有效: + +```cs showLineNumbers {10,21-30,36-39} +using Furion; +using Furion.DynamicApiController; +using Microsoft.AspNetCore.Mvc; + +namespace YourProject.Application; + +public class PluginApiServices : IDynamicApiController +{ + private readonly IDynamicApiRuntimeChangeProvider _provider; + public PluginApiServices(IDynamicApiRuntimeChangeProvider provider) + { + _provider = provider; + } + + /// + /// 动态添加 WebAPI/Controller + /// + /// + /// 可自行指定程序集名称 + /// + public string Compile([FromBody] string csharpCode, [FromQuery] string assemblyName = default) + { + // 编译 C# 代码并返回动态程序集 + var dynamicAssembly = App.CompileCSharpClassCode(csharpCode, assemblyName); + + // 将程序集添加进动态 WebAPI 应用部件 + _provider.AddAssembliesWithNotifyChanges(dynamicAssembly); + + // 返回动态程序集名称 + return dynamicAssembly.GetName().Name; + } + + /// + /// 移除动态程序集 WebAPI/Controller + /// + public void Remove(string assemblyName) + { + _provider.RemoveAssembliesWithNotifyChanges(assemblyName); + } +} +``` + +这时只需要请求 `api/plugin-api/compile` 接口同时设置请求 `Content-Type` 为 `text/plain`,接下来传入 `C# 代码字符串` 即可,如: + +```cs showLineNumbers title="动态C#代码字符串" +using Furion.DynamicApiController; + +namespace YourProject.Application; + +public class SomeClass : IDynamicApiController +{ + public string GetName() + { + return nameof(Furion); + } +} +``` + + + +之后刷新浏览器即可看到最新的 `API`: + + + +还可以在运行时动态卸载,使用 `DELETE` 请求 `api/plugin-api` 即可。 + +## 5.1.15 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/efcore-recommend.mdx b/handbook/docs/efcore-recommend.mdx new file mode 100644 index 0000000000000000000000000000000000000000..bed2c5d59c94556502fa81053b50caf16f850984 --- /dev/null +++ b/handbook/docs/efcore-recommend.mdx @@ -0,0 +1,44 @@ +--- +id: efcore-recommend +title: 9.30 EFCore 最佳实践 +sidebar_label: 9.30 EFCore 最佳实践 +--- + +## 9.30.1 EFCore 高性能 + +在 `Furion` 框架,默认推荐使用 `EFCore` 操作数据库,但很多朋友对 `EFCore` 使用不当,特意编写此文档说明。 + +- 尽可能的采用 `IRepository/IRepository` 仓储方式在构造函数中初始化,**避免使用 `Db.GetRepository` 方式**。 +- 请以**异步方式**调用所有数据访问 api。 +- 检索的数据不是必需的。 编写查询以**仅返回当前 HTTP 请求所必需的数据**。 +- 如果数据可以接受,请考虑**缓存经常访问的从数据库或远程服务检索的数据**。 使用 MemoryCache 或 microsoft.web.distributedcache ,具体取决于方案。 +- **尽量减少网络往返次数**。 目标是使用单个调用而不是多个调用来检索所需数据。 +- **如果当前请求只有数据查询,请使用无跟踪查询方式**。 +- **如果请求中含有操作数据时,请不要在 Entity Framework Core 中使用无跟踪查询**。 EF Core 可以更有效地返回无跟踪查询的结果。 筛选和聚合 LINQ 查询(例如, .Where 使用.Select、或.Sum 语句),以便数据库执行筛选。 +- 对于需要进行复杂逻辑计算查询数据情况,请尽可能在返回查询后再在客户端计算。 +- **不要对集合使用投影查询**,这可能会导致执行 "N + 1" 个 SQL 查询。 +- 使用 ·DbContextPool· 池来管理 DbContext,类似 ADO.NET 的连接池。 +- 手动或显式编译的查询 API,允许应用程序缓存查询转换,使其可仅被计算一次并执行多次。 + +```cs showLineNumbers +// Create an explicitly compiled query +private static Func _customerById = + EF.CompileQuery((CustomerContext db, int id) => + db.Customers + .Include(c => c.Address) + .Single(c => c.Id == id)); + +// Use the compiled query by invoking it +using (var db = new CustomerContext()) +{ + var customer = _customerById(db, 147); +} +``` + +## 9.30.2 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/encryption.mdx b/handbook/docs/encryption.mdx new file mode 100644 index 0000000000000000000000000000000000000000..fc923ae99be0ba9299071d99a0250dabd05af4de --- /dev/null +++ b/handbook/docs/encryption.mdx @@ -0,0 +1,235 @@ +--- +id: encryption +title: 20. 数据加解密 +sidebar_label: 20. 数据加解密 +--- + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **新特性** + + -  新增 `AES` 支持对文件(含超大文件)进行加解密 4.8.8.11 ⏱️2023.05.05 [1d2265b](https://gitee.com/dotnetchina/Furion/commit/1d2265be04cfd7c6c2b9db932a77ebd620ef6054) + -  新增 `RSA` 支持对超长字符(超 `245` 位)进行分段加解密 4.8.8.2 ⏱️2023.04.19 [!788](https://gitee.com/dotnetchina/Furion/pulls/788) 感谢 [@YaChengMu](https://gitee.com/YaChengMu) + -  新增 `byte[]` 类型 `MD5` 加密/比较重载方法 4.8.6.3 ⏱️2023.02.15 [#I6F1NT](https://gitee.com/dotnetchina/Furion/issues/I6F1NT) + +
+
+
+ +## 20.1 数据加解密 + +由于现在的互联网越具发达,数据成为了我们生活的一部分,当然也带来了很多数据安全性的问题,比如用户密码明文存储,用户信息明文存在在浏览器 `cookies` 中等等不安全操作。 + +所以,对数据的加解密是系统开发必要的环节。 + +## 20.2 内置加密算法 + +- `MD5` 加密 +- `DESC` 加解密 +- `AES` 加解密 +- `JWT` 加解密 +- `PBKDF2` 加密(`Furion v2.12 +` 版本已移除) +- `RSA` 加解密 + +## 20.3 加解密使用 + +### 20.3.1 `MD5` 加密 + +```cs showLineNumbers +// 测试 MD5 加密,比较 +var md5Hash = MD5Encryption.Encrypt("百小僧"); // 加密 +var isEqual = MD5Encryption.Compare("百小僧", md5Hash); // 比较 +return (md5Hash, isEqual); + +// 输出大写 MD5 加密 +var md5Hash = MD5Encryption.Encrypt("百小僧", true); + +// 输出 16位 MD5 加密,Furion 4.2.6+ 版本 +var md5Hash16 = MD5Encryption.Encrypt("百小僧", is16: true); + +// Furion 4.8.6.3+ 版本支持 byte[] 类型,如获取文件 MD5 Hash +var bytes = File.ReadAllBytes("image.png"); +var md5Hash = MD5Encryption.Encrypt(bytes); // 加密 +var isEqual = MD5Encryption.Compare(bytes, md5Hash); // 比较 +``` + +### 20.3.2 `DESC` 加解密 + +```cs showLineNumbers +// 测试 DESC 加解密 +var descHash = DESCEncryption.Encrypt("百小僧", "Furion"); // 加密 +var str = DESCEncryption.Decrypt(descHash, "Furion"); // 解密 +return (descHash, str); +``` + +### 20.3.3 `AES` 加解密 + +```cs showLineNumbers +// 测试 AES 加解密 +var key = Guid.NewGuid().ToString("N"); // 密钥,长度必须为24位或32位 + +var aesHash = AESEncryption.Encrypt("百小僧", key); // 加密 +var str2 = AESEncryption.Decrypt(aesHash, key); // 解密 +return (aesHash, str2); +``` + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.8.11 +` 版本使用。 + +::: + +- **对文件(含超大文件)进行加解密** + +```cs showLineNumbers {3,8} +// 加密 +var originBytes = File.ReadAllBytes("xxx.rar"); // 读取源文件内容 +var encryptBytes = AESEncryption.Encrypt(originBytes, "123456"); +// 可以通过 encryptBytes.CopyToSave("xxx.加密.rar"); 保存到磁盘 + +// 解密 +var encryptBytes = File.ReadAllBytes("xxx.加密.rar"); // 读取加密文件内容 +var originBytes = AESEncryption.Decrypt(encryptBytes, "123456"); +// 可以通过 originBytes.CopyToSave("xxx.真实.rar"); 保存到磁盘 +``` + +### 20.3.4 `JWT` 加解密 + +```cs showLineNumbers +var token = JWTEncryption.Encrypt(new Dictionary() // 加密 + { + { "UserId", user.Id }, + { "Account",user.Account } + }); + +var tokenData = JWTEncryption.ReadJwtToken("你的token"); // 解密 + +var (isValid, tokenData, validationResult) = JWTEncryption.Validate("你的token"); // 验证token有效期 +``` + +:::important 特别注意 + +`JWTEncryption` 加解密并未包含在 `Furion` 框架中,需要安装 `Furion` 框架提供的 `Furion.Extras.Authentication.JwtBearer` 拓展包。 + +::: + +### 20.3.5 `PBKDF2` 加密 + +**`Furion v2.12 +` 版本已移除。** + +```cs showLineNumbers +// 测试 PBKDF2 加密,比较 +var basestring = PBKDF2Encryption.Encrypt("百小僧"); // 加密 +var isEqual = PBKDF2Encryption.Compare("百小僧", basestring); // 比较 +``` + +:::important 支持选择更多参数 + +`PBKDF2` 还可以配置更多参数: + +- `Startup.cs` 中注册服务 + +```cs showLineNumbers +services.AddPBKDF2EncryptionOptions(); +``` + +- `appsettings.json` 配置: + +```json showLineNumbers +{ + "PBKDF2EncryptionSettings": { + "InitialIterationCount": 110, // 初始迭代次数累加值 + "KeyDerivation": "HMACSHA256", // 加密算法规则 KeyDerivationPrf.HMACSHA256 + "NumBytesRequested": 64 // 派生密钥的长度 (以字节为单位) 512 / 8 + } +} +``` + +- `KeyDerivation` 可选值有:`HMACSHA1`,`HMACSHA256`,`HMACSHA512` + +::: + +### 20.3.6 `RSA` 加密 + +```cs showLineNumbers +// 测试 RSA 加密 +var (publicKey, privateKey) = RSAEncryption.GenerateSecretKey(2048); //生成 RSA 秘钥 秘钥大小必须为 2048 到 16384,并且是 8 的倍数 +var basestring = RSAEncryption.Encrypt("百小僧", publicKey); // 加密 +var str2 = RSAEncryption.Decrypt(basestring, privateKey); // 解密 +return (basestring, str2); +``` + +:::important 关于 `RSA` 签名和校验 + +`Furion` 框架底层不内置 `RSA` 签名和校验功能,如需添加该功能可查阅开发者提交的代码:[查看 RSA 签名和校验](https://gitee.com/dotnetchina/Furion/pulls/349) + +::: + +## 20.4 字符串拓展方式 + +`Furion` 框架也提供了字符串拓展方式进行 `MD5加密、AES/DESC加解密、RSA加解密`。 + +```cs showLineNumbers +using Furion.DataEncryption.Extensions; + +// MD5 加密 +var s = "Furion".ToMD5Encrypt(); +var b = "Furion".ToMD5Compare(s); // 比较 +// Furion 4.8.6.3+ 支持 bytes +var b = bytes.ToMD5Encrypt(); +var z = bytes2.ToMD5Compare(bytes); + +// AES加解密 +var s = "Furion".ToAESEncrypt("sfdsfdsfdsfdsfdsfdsfdsfdsfdfdsfdsfdfdfdfd"); +var str = s.ToAESDecrypt("sfdsfdsfdsfdsfdsfdsfdsfdsfdfdsfdsfdfdfdfd"); + +// DESC 加解密 +var s = "Furion".ToDESCEncrypt("sfdsfdsfdsfdsfdsfdsfdsfdsfdfdsfdsfdfdfdfd"); +var str = s.ToDESCDecrypt("sfdsfdsfdsfdsfdsfdsfdsfdsfdfdsfdsfdfdfdfd"); + +// PBKDF2 加密(`Furion v2.12 +` 版本已移除!!!!!!!!) +var s = "Furion".ToPBKDF2Encrypt(); +var b = "Furion".ToPBKDF2Compare(s); // 比较 + +// RSA 加解密 +var (publicKey, privateKey) = RSAEncryption.GenerateSecretKey(2048); //生成 RSA 秘钥 秘钥大小必须为 2048 到 16384,并且是 8 的倍数 +var s= "Furion".ToRSAEncrpyt(publicKey); // 加密 +var str=s.ToRSADecrypt(privateKey); // 解密 +``` + +## 20.5 `SM2`、`SM3`,`SM4` 国密 + +`Furion` 框架未内置国密算法 `SM2-4`,但是已有开发者贡献实现并开源,可查阅 [Gitee 仓库](https://gitee.com/xxcxy/SM/tree/master/GfsDemo/Smcrypto),感谢 `QQ(373696184)形影相印²º²²` 贡献 + +```cs showLineNumbers +var data = "{\"lx\":\"1\",\"wxid\":\"\",\"ehealth_code_id\":\"68A018036186B717CC1B051C10996F4EEE805F5F81EB1594C9EB43592545F7F6\",\"ehealth_code\":\"68A018036186B717CC1B051C10996F4EEE805F5F81EB1594C9EB43592545F7F6\",\"xm\":\"测试\"}"; + +// SM2 +var b = SM2Utils.加密("123"); +var b1 = SM2Utils.解密(b); + +// SM3 一般用于数字签名 +var sM3Utils = new SM3Utils(); +sM3Utils.secretKey = "ASAFSDFDSGSDFSDFSDFSFSF"; +var token= sM3Utils.加密("123"); + +// SM4 +var sM4Utils = new SM4Utils(); +sM4Utils.secretKey = "BDBDBDBDBDBDBDBDBDBDBDBDBDBDBD"; + +var a = sM4Utils.加密(data); +var a1 = sM4Utils.解密(a); +``` + +## 20.6 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/entity.mdx b/handbook/docs/entity.mdx new file mode 100644 index 0000000000000000000000000000000000000000..2f92c389730076b61b4de16f79d3616f11165329 --- /dev/null +++ b/handbook/docs/entity.mdx @@ -0,0 +1,356 @@ +--- +id: entity +title: 9.4 数据库实体 +sidebar_label: 9.4 数据库实体 +--- + +:::important 特别提醒 + +一旦定义了实体或改变了实体结构或实体配置,需要重新执行 `Add-Migration` 和 `Update-Database` 命令。 + +::: + +## 9.4.1 数据库实体 + +在面向对象开发思想中,最重要尤为**对象**二字,在 .NET 开发过去,操作数据库往往采用 `DataTable` 和 `DataSet` 来接收数据库返回结果集,而操作数据库也离不开手写 `sql` 语句。 + +在过去面向过程和应用不发达的时代,这些操作确实好使。然后随着中国互联网网民的激增,电子化时代的到来,各行各业对应用需求也达到了前所未有的量级。 + +所以,在过去手写 `sql` 的时代各种问题显露无疑: + +- 程序员能力参差不齐,写出的 `sql` 性能自然也天差地别 +- `sql` 属于字符串硬编程,后期维护难上加难 +- 许多单表甚至多表结构一致,出现大量重复 `sql` 代码 +- `sql` 本身在不同的数据库提供器中写法有差,后续迁移头痛不已 + +当然,`sql` 是时代的产物,我们也离不开 `sql`,但对于大多数程序员和项目来说,`sql` 未必能够带给他们多大的效益。 + +所以,`ORM` 就诞生了,所谓的 `ORM` 就是对象关系映射,英文:`Object Relational Mapping`,简单点说,`ORM` 根据特有的 `POCO 贫血模型` 规则生成 `sql` 语句。大大避免了重复 `sql` 和 `sql` 能力参差不齐等问题。(当然 `ORM` 作者 `sql` 能力也会影响最终性能) + +上面所说的 `POCO` 贫血模型正是我们本章节的 **数据库实体**。 + +简单来说,数据库实体就是数据库表的类表现,通过一定的规则使这个类能够一一对应表结构。通常这样的类也称为:`POCO` 贫血模型,也就是只有定义,没有行为。 + +## 9.4.2 如何定义实体 + +`Furion` 框架提供多种定义实体的接口依赖: + +- `IEntity`:实体基接口,是所有实体的基接口 +- `IEntityNotKey`:无键实体接口,也就是视图、存储过程、函数依赖接口 +- `EntityBase`:实体基抽象类,内置了 `Id`,`TenantId` 字段 +- `Entity`:实体通用抽象类,继承自 `EntityBase`,同时内置 `CreatedTime`,`UpdatedTime` 字段 +- `EntityNotKey`:无键实体抽象类,视图、存储过程、函数依赖抽象类 + +:::important 实体定义位置 + +`Furion` 框架中有约定,实体统一定义在 `Furion.Core` 层。 + +::: + +### 9.4.2.1 实体继承选用原则 + +- 如果你不需要 `Furion` 为实体添加任何内置特性,选用 `IEntity`,无键实体选用 `IEntityNotKey` +- 如果你只需要 `Id` 属性,选用 `EntityBase` +- 如果你需要 `Furion` 为你自动添加常用字段,则选用 `Entity` +- 如果你需要视图、存储过程、函数可以通过 `DbSet` 操作,则继承 `EntityNotKey` + +### 9.4.2.2 `IEntity` 示范: + +```cs showLineNumbers {1,5} +using Furion.DatabaseAccessor; + +namespace Furion.Core +{ + public class User : IEntity + { + /// + /// 手工定义 Id + /// + public int Id { get; set; } + + /// + /// 名称 + /// + public string Name { get; set; } + } +} +``` + +### 9.4.2.3 `EntityBase` 示范: + +```cs showLineNumbers {1,5} +using Furion.DatabaseAccessor; + +namespace Furion.Core +{ + public class User : EntityBase + { + // 无需定义 Id 属性 + + /// + /// 名称 + /// + public string Name { get; set; } + } +} +``` + +### 9.4.2.4 `Entity` 示范: + +```cs showLineNumbers {1,5} +using Furion.DatabaseAccessor; + +namespace Furion.Core +{ + public class User : Entity + { + // 无需定义 Id 属性 + // 并自动添加 CreatedTime,UpdatedTime 属性 + + /// + /// 名称 + /// + public string Name { get; set; } + } +} +``` + +### 9.4.2.5 `EntityNotKey` 示范: + +```cs showLineNumbers {1,5,7-9} +using Furion.DatabaseAccessor; + +namespace Furion.Core +{ + public class UserView : EntityNotKey + { + public UserView() : base("视图名称") + { + } + + /// + /// Id + /// + public int Id { get; set; } + + /// + /// 名称 + /// + public string Name { get; set; } + } +} +``` + +:::note 特别注意 + +在 `Furion` 框架中,数据库实体必须直接或间接继承 `IEntity` 才能进行仓储等操作。 + +::: + +## 9.4.3 自定义公共实体 + +在实际项目开发中,我们通常每个应用的数据库表都有一些公共的类,比如创建人,创建时间等,这个时候我们就需要自定义公共实体类了。 + +在 `Furion` 框架中,创建公共实体类需要满足以下条件: + +- 公共实体类**必须是公开且是抽象类** +- 公共实体类必须含有无参构造函数 +- 公共实体类必须提供数据库定位器的支持 + +如: + +```cs showLineNumbers {83-91} +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Your.Namespace +{ + public abstract class CommonEntity : CommonEntity + { + } + + public abstract class CommonEntity : CommonEntity + { + } + + public abstract class CommonEntity : PrivateCommonEntity + where TDbContextLocator1 : class, IDbContextLocator + { + } + + public abstract class CommonEntity : PrivateCommonEntity + where TDbContextLocator1 : class, IDbContextLocator + where TDbContextLocator2 : class, IDbContextLocator + { + } + + public abstract class CommonEntity : PrivateCommonEntity + where TDbContextLocator1 : class, IDbContextLocator + where TDbContextLocator2 : class, IDbContextLocator + where TDbContextLocator3 : class, IDbContextLocator + { + } + + public abstract class CommonEntity : PrivateCommonEntity + where TDbContextLocator1 : class, IDbContextLocator + where TDbContextLocator2 : class, IDbContextLocator + where TDbContextLocator3 : class, IDbContextLocator + where TDbContextLocator4 : class, IDbContextLocator + { + } + + public abstract class CommonEntity : PrivateCommonEntity + where TDbContextLocator1 : class, IDbContextLocator + where TDbContextLocator2 : class, IDbContextLocator + where TDbContextLocator3 : class, IDbContextLocator + where TDbContextLocator4 : class, IDbContextLocator + where TDbContextLocator5 : class, IDbContextLocator + { + } + + public abstract class CommonEntity : PrivateCommonEntity + where TDbContextLocator1 : class, IDbContextLocator + where TDbContextLocator2 : class, IDbContextLocator + where TDbContextLocator3 : class, IDbContextLocator + where TDbContextLocator4 : class, IDbContextLocator + where TDbContextLocator5 : class, IDbContextLocator + where TDbContextLocator6 : class, IDbContextLocator + { + } + + public abstract class CommonEntity : PrivateCommonEntity + where TDbContextLocator1 : class, IDbContextLocator + where TDbContextLocator2 : class, IDbContextLocator + where TDbContextLocator3 : class, IDbContextLocator + where TDbContextLocator4 : class, IDbContextLocator + where TDbContextLocator5 : class, IDbContextLocator + where TDbContextLocator6 : class, IDbContextLocator + where TDbContextLocator7 : class, IDbContextLocator + { + } + + public abstract class CommonEntity : PrivateCommonEntity + where TDbContextLocator1 : class, IDbContextLocator + where TDbContextLocator2 : class, IDbContextLocator + where TDbContextLocator3 : class, IDbContextLocator + where TDbContextLocator4 : class, IDbContextLocator + where TDbContextLocator5 : class, IDbContextLocator + where TDbContextLocator6 : class, IDbContextLocator + where TDbContextLocator7 : class, IDbContextLocator + where TDbContextLocator8 : class, IDbContextLocator + { + } + + public abstract class PrivateCommonEntity : IPrivateEntity + { + // 注意是在这里定义你的公共实体 + public virtual TKey Id { get; set; } + + public virtual DateTime CreatedTime { get; set; } + + // 更多属性定义 + } +} +``` + +:::important 特别说明 + +通过上面的格式定义可以完美的支持多数据库操作,建议采用这种格式,而且所有的公共属性都应该定义在 `PrivateXXXX` `私`有类中。 + +::: + +## 9.4.4 数据库实体配置 + +在过去的 `EF Core` 项目开发中,数据库实体配置需要在 `DbContext` 的 `OnModelCreating` 中配置。`Furion` 为了简化配置和提高开发效率,抽象出了 `IEntityTypeBuilder` 接口。 + +通过 `IEntityTypeBuilder` 接口,我们无需在 `DbContext` 的 `OnModelCreating` 中配置,可在任意地方配置。 + +### 9.4.4.1 在数据库实体中配置 + +```cs showLineNumbers {1,5,20-25} +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using System; + +namespace Furion.Core +{ + public class User : Entity, IEntityTypeBuilder + { + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 年龄 + /// + public int Age { get; set; } + + // 配置数据库实体 + public void Configure(EntityTypeBuilder entityBuilder, DbContext dbContext, Type dbContextLocator) + { + entityBuilder.HasKey(u => u.Id); + entityBuilder.HasIndex(u => u.Name); + } + } +} +``` + +### 9.4.4.2 在任何实例类中配置 + +```cs showLineNumbers {1,8,10-14} +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using System; + +namespace Furion.Core +{ + public class SomeClass : IEntityTypeBuilder + { + public void Configure(EntityTypeBuilder entityBuilder, DbContext dbContext, Type dbContextLocator) + { + entityBuilder.HasKey(u => u.Id); + entityBuilder.HasIndex(u => u.Name); + } + } +} +``` + +如上面例子,通过 `SomeClass` 配置 `User` 数据库实体。 + +:::note 特别注意 + + `SomeClass`必须声明为`public`,否则无法自动注册。 + +::: + +:::important 更多知识 + +如需了解实体配置支持哪些配置可查阅 [【EFCore - 创建模型】](https://docs.microsoft.com/zh-cn/ef/core/modeling/) 章节。 + +::: + +## 9.4.5 数据库实体配置说明 + +`Furion` 框架会自动扫描所有继承 `IEntity` 接口的类进行 `DbSet` 注册,也就是实现自动配置 `DbContext` 的 `OnModelCreating`。 + +如果需要跳过自动注册,只需要贴 `[Manual]` 或 `[SuppressSniffer]` 特性即可。一旦贴了此特性,那么就需要手动配置 `DbContext` 的 `OnModelCreating` + +## 9.4.6 配置列名及列类型 + +有时候我们需要手动设置列名或列类型,比如 `decimal(18,2)`,这时候只需要在属性上面贴 `[Column("列名", TypeName="decimal(18,2)")]` 即可。 + +## 9.4.7 配置数据库表名和 `Schema` + +可以通过在实体类型贴 `[Table("表名", Schema = "dbo")]` 配置。 + +## 9.4.8 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/event-bus-old.mdx b/handbook/docs/event-bus-old.mdx new file mode 100644 index 0000000000000000000000000000000000000000..850b664cbae7f15e8483dad3d9731756a30cb6f9 --- /dev/null +++ b/handbook/docs/event-bus-old.mdx @@ -0,0 +1,157 @@ +--- +id: event-bus-old +title: 22. 事件总线 +sidebar_label: 22. 事件总线 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +:::warning v2.20+ 版本说明 + +**在 `Furion v2.20+` 版本采用 [Jaina](https://gitee.com/dotnetchina/Jaina) 事件总线替换原有的 `EventBus`**,**😶[查看新文档](/docs/event-bus)** + +::: + +## 22.1 什么是事件总线 + +事件总线是对发布-订阅模式的一种实现。它是一种集中式事件处理机制,允许不同的组件之间进行彼此通信而又不需要相互依赖,达到一种解耦的目的。 + +我们来看看事件总线的处理流程: + + + +## 22.2 `MessageCenter` 消息中心 + +在 `Furion` 框架中,实现了一种轻量级的事件总线实现机制:`MessageCenter`(消息中心),`MessageCenter` 采用字符串消息机制进行广播, 可以在绝大多数中小型项目中发挥作用,缺点是消息处理是在主线程中完成并且消息不支持分布式存储。 + +另外,`MessageCenter` 支持单播、多播发布及多订阅。如图所示: + + + +### 22.2.1 注册 `轻量级事件总线服务` + +如果想使用 `MessageCenter` 轻量级事件总线,只需要在 `Startup.cs` 中注册服务即可,如: + +```cs showLineNumbers {3} +public void ConfigureServices(IServiceCollection services) +{ + services.AddSimpleEventBus(); +} +``` + +### 22.2.2 定义订阅处理程序 + +`MessageCenter` 是根据 `MesseageId` 消息 Id 来触发对应的处理程序的,所以我们需要定义 `订阅处理程序类`,该类需实现 `ISubscribeHandler` 接口,如: + +```cs showLineNumbers {1,4-5,11-13} +public class UserChangeSubscribeHandler : ISubscribeHandler +{ + // 定义一条消息 + [SubscribeMessage("create:user")] + public void CreateUser(string eventId, object payload) + { + Console.WriteLine("我是"+eventId); + } + + // 多条消息共用同一个处理程序 + [SubscribeMessage("delete:user")] + [SubscribeMessage("remove:user")] + public void RemoveUser(string eventId, object payload) + { + Console.WriteLine("我是"+eventId); + } + + // 支持异步 + [SubscribeMessage("delete:user")] + public async Task SupportAsync(string eventId, object payload) + { + await MethodAsync(); + } +} +``` + +### 22.2.3 发布消息 + +定义好订阅处理程序后,我们就可以在程序任何地方进行广播消息,事件总线会自动根据 `消息 Id` 触发对应的处理程序方法: + +```cs showLineNumbers +MessageCenter.Send("create:user", new User {}); // => 打印:我是create:user + +MessageCenter.Send("delete:user", new User {}); // => 打印:我是delete:user +MessageCenter.Send("remove:user", new User {}); // => 打印:我是remove:user +``` + +### 22.2.4 直接订阅消息 + +在上面的例子中,我们需要创建 `ISubscribeHandler` 的派生类进行相关配置才能实现订阅处理。 + +在某些特殊情况下,可能只需要订阅一次即可。所以,在 `Furion` 框架中,为了更简便的订阅消息,也提供了 `MessageCenter.Subscribe` 静态方法,如: + +```cs showLineNumbers +MessageCenter.Subscribe("create:user", (i,p) => { + // do something。。。 +}); +``` + +## 22.3 同步方式执行 + +默认情况下,事件总线总是采用新线程方式执行,但是我们可以配置为同步方式,如: + +```cs showLineNumbers +MessageCenter.Send("create:user", isSync: true); +``` + +## 22.4 关于依赖注入 + +在 `Furion` 框架中,事件总线是不支持构造函数注入的,而且构造函数也只会执行一次。所以需要用到服务,应该通过静态类解析,`App.GetService()` 或 `Db.GetRepository()`。 + +```cs showLineNumbers {5,12-20} +public class UserChangeSubscribeHandler : ISubscribeHandler +{ + public UserChangeSubscribeHandler() + { + // 不支持这里解析服务!!!!!!!!!!! + } + + // 定义一条消息 + [SubscribeMessage("create:user")] + public void CreateUser(string eventId, object payload) + { + // 创建一个作用域,对象使用完成自动释放 + Scoped.Create((_, scope) => + { + var services = scope.ServiceProvider; + + var repository = Db.GetRepository(services); // services 传递进去 + var someService = App.GetService(services); // services 传递进去 + var otherService = services.GetService(); // 直接用 services 解析 + }); + } +} +``` + +:::note 关于 `App.GetService()` 解析服务 + +在高频定时任务中调用`App.GetService(TService)`,可能会出现内存无法回收的情况,建议始终使用`scope.ServiceProvider.GetService(TService)`来获取`TService` + +::: + +:::important 数据库操作注意 + +如果作用域中对**数据库有任何变更操作**,需手动调用 `SaveChanges` 或带 `Now` 结尾的方法。也可以使用 `Scoped.CreateUow(handler)` 代替。 + +::: + +:::warning 关于依赖注入 + +`ISubscribeHandler` 接口主要是用来查找定义事件对象的,也就是它的实现类并未提供依赖注入功能,所以在实现类并不支持构造函数注入依赖项。 + +::: + +## 22.5 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/event-bus.mdx b/handbook/docs/event-bus.mdx new file mode 100644 index 0000000000000000000000000000000000000000..90a8d653702392dbad5298d26935ef1f76d3562d --- /dev/null +++ b/handbook/docs/event-bus.mdx @@ -0,0 +1,1346 @@ +--- +id: event-bus +title: 22. 事件总线 +sidebar_label: 22. 事件总线 (EventBus) +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **新特性** + + -  新增 事件总线支持简单的 `Order` 编排规则 4.8.0 [833c0d4](https://gitee.com/dotnetchina/Furion/commit/833c0d4d069bca5f5304aa21cb32c7f902c20c69) + -  新增 事件总线 `.ReplaceStorerOrFallback` 自定义事件源存储器方法,可在自定义初始失败时回退到默认值 4.7.6 [#I602NU](https://gitee.com/dotnetchina/Furion/issues/I602NU) + -  新增 事件总线模块重试失败后支持回调 4.6.1 [#I5UVMV](https://gitee.com/dotnetchina/Furion/issues/I5UVMV) + -  新增 事件总线 `LogEnabled` 配置,可控制是否输出服务日志 [#I5QLY5](https://gitee.com/dotnetchina/Furion/issues/I5QLY5) + -  新增 **事件总线 `MessageCenter` 静态类,解决从 `Fur v1.x` 版本升级问题 [a29fc7c](https://gitee.com/dotnetchina/Furion/commit/a29fc7cf63a3ea41b1617a6ad98a701a243e24f8)** + -  新增 **事件总线工厂,支持运行时动态添加订阅程序和移除订阅程序** [#I5NNQX](https://gitee.com/dotnetchina/Furion/issues/I5NNQX) + -  新增 **事件总线 `[EventSubscribe]` 事件 `Id` 支持正则表达式匹配** [#I5NNQX](https://gitee.com/dotnetchina/Furion/issues/I5NNQX) + -  新增 **事件总线 `[EventSubscribe]` 支持局部失败重试配置** [#I5NNQX](https://gitee.com/dotnetchina/Furion/issues/I5NNQX) + -  新增 事件总线 `options.AddSubscriber(Type)` 重载 [42446078](https://gitee.com/dotnetchina/Furion/blob/424460780b630e1c71de4db84ad8fd14e33a09f5/framework/Furion.Pure/EventBus/Builders/EventBusOptionsBuilder.cs) + -  新增 事件总线 `UseUtcTimestamp` 选项配置,可选择使用 `DateTime.UtcNow` 还是 `DateTime.Now`,默认是 `DateTime.Now` [#I5JSEU](https://gitee.com/dotnetchina/Furion/issues/I5JSEU) + -  新增 事件总线模块事件 `Id` 支持枚举类型 [2f328aa](https://gitee.com/dotnetchina/Furion/commit/2f328aa8213c8efe7a8480116985573cc6b7fce6) + -  新增 事件总线模块发布者 `PublishAsync` 和 `PublishDelayAsync` 重载 [2f328aa](https://gitee.com/dotnetchina/Furion/commit/2f328aa8213c8efe7a8480116985573cc6b7fce6) + -  新增 事件总线模块拓展方法:`Enum.ParseToString()` 和 `String.ParseToEnum()` [2f328aa](https://gitee.com/dotnetchina/Furion/commit/2f328aa8213c8efe7a8480116985573cc6b7fce6) + +- **突破性变化** + + -  调整 **事件总线触发处理程序的逻辑,由过去的 `foreach` 改为 `Parallel.ForEach`,吞吐量提升近 4 倍** 4.6.4 [7384c9c](https://gitee.com/dotnetchina/Furion/commit/7384c9c3efb94883421379828565e61870f1640c) + -  调整 **事件总线 `IEventBusFactory` 事件工厂方法 `AddSubscriber -> Subscribe`,`RemoveSubscriber -> Unsubscribe` [a29fc7c](https://gitee.com/dotnetchina/Furion/commit/a29fc7cf63a3ea41b1617a6ad98a701a243e24f8)** + +- **问题修复** + + -  修复 超高频率下发送事件总线消息,但是 `GC` 来不及回收导致内存和 `CPU` 爆掉问题 4.6.8 [dbc7935](https://gitee.com/dotnetchina/Furion/commit/dbc7935618ff60d7f41d82c0d49042cd189e58b9) + -  修复 基于 `Redis` 重写事件存储器序列化 `IEventSource` 实例异常问题 4.4.7 [3e45020](https://gitee.com/dotnetchina/Furion/commit/3e45020eca5948d18d5cb405a665aae44088fd20) + -  修复 事件总线默认 `Channel` 管道初始化时机过晚问题,解决部分第三方依赖使用问题 [#I5MM3O](https://gitee.com/dotnetchina/Furion/issues/I5MM3O) + -  修复 事件总线默认开启模糊匹配(正则表达式)导致不必要的订阅 [#I5NVOP](https://gitee.com/dotnetchina/Furion/issues/I5NVOP) + +- **其他调整** + + -  调整 事件总线默认日志类名为 `System.Logging.EventBusService` [#I5QLY5](https://gitee.com/dotnetchina/Furion/issues/I5QLY5) + -  调整 事件总线默认 `Channel` 管道初始化时机,解决部分第三方依赖使用问题 [#I5MM3O](https://gitee.com/dotnetchina/Furion/issues/I5MM3O) + +
+
+
+ +:::warning v2.20 以下版本说明 + +**在 `Furion v2.20+` 版本采用 [Jaina](https://gitee.com/dotnetchina/Jaina) 事件总线替换原有的 `EventBus`**,[查看旧文档](/docs/event-bus-old) + +::: + +:::important 版本说明 + +以下内容仅限 `Furion 2.20.0 +` 版本使用。 + +::: + +## 22.1 关于事件总线 + +事件总线是对发布-订阅模式的一种实现。它是一种集中式事件处理机制,允许不同的组件之间进行彼此通信而又不需要相互依赖,达到一种解耦的目的。 + + + +## 22.2 快速入门 + +### 22.2.1 定义事件处理程序 + +定义事件订阅者 `ToDoEventSubscriber`: + +```cs showLineNumbers {2,10-11,19-21,28-30,32,36-38,44-48,54-56} +// 实现 IEventSubscriber 接口 +public class ToDoEventSubscriber : IEventSubscriber +{ + private readonly ILogger _logger; + public ToDoEventSubscriber(ILogger logger) + { + _logger = logger; + } + + [EventSubscribe("ToDo:Create")] + public async Task CreateToDo(EventHandlerExecutingContext context) + { + var todo = context.Source; + _logger.LogInformation("创建一个 ToDo:{Name}", todo.Payload); + await Task.CompletedTask; + } + + // 支持多个 + [EventSubscribe("ToDo:Create")] + [EventSubscribe("ToDo:Update")] + public async Task CreateOrUpdateToDo(EventHandlerExecutingContext context) + { + var todo = context.Source; + _logger.LogInformation("创建或更新一个 ToDo:{Name}", todo.Payload); + await Task.CompletedTask; + } + + // 支持枚举类型,v3.4.3+ 版本支持 + [EventSubscribe(YourEnum.Some)] + public async Task EnumHandler(EventHandlerExecutingContext context) + { + var eventEnum = context.Source.EventId.ParseToEnum(); // 将事件 Id 转换成枚举对象 + await Task.CompletedTask; + } + + // 支持正则表达式匹配,4.2.10+ 版本支持 + [EventSubscribe("(^1[3456789][0-9]{9}$)|((^[0-9]{3,4}\\-[0-9]{3,8}$)|(^[0-9]{3,8}$)|(^\\([0-9]{3,4}\\)[0-9]{3,8}$)|(^0{0,1}13[0-9]{9}$))", FuzzyMatch = true)] + public async Task RegexHandler(EventHandlerExecutingContext context) + { + var eventId = context.Source.EventId; + await Task.CompletedTask; + } + + // 支持多种异常重试配置,Furion 4.2.10+ 版本支持 + [EventSubscribe("test:error", NumRetries = 3)] + [EventSubscribe("test:error", NumRetries = 3, RetryTimeout = 1000)] // 重试间隔时间 + [EventSubscribe("test:error", NumRetries = 3, ExceptionTypes = new[] { typeof(ArgumentException) })] // 特定类型异常才重试 + public async Task ExceptionHandler(EventHandlerExecutingContext context) + { + var eventId = context.Source.EventId; + await Task.CompletedTask; + } + + // 支持简单 Order 编排,Furion 4.8.0+ 版本支持 + [EventSubscribe("test:order", Order = 1)] + public async Task ExceptionHandler(EventHandlerExecutingContext context) + { + var eventId = context.Source.EventId; + await Task.CompletedTask; + } +} +``` + +### 22.2.2 发布事件消息 + +创建控制器 `ToDoController`,依赖注入 `IEventPublisher` 服务: + +```cs showLineNumbers {5,13-15,21-25} +public class ToDoController : ControllerBase +{ + // 依赖注入事件发布者 IEventPublisher + private readonly IEventPublisher _eventPublisher; + public ToDoController(IEventPublisher eventPublisher) + { + _eventPublisher = eventPublisher; + } + + // 发布 ToDo:Create 消息 + public async Task CreateDoTo(string name) + { + await _eventPublisher.PublishAsync(new ChannelEventSource("ToDo:Create", name)); + // 也可以延迟发布,比如延迟 3s + await _eventPublisher.PublishDelayAsync(new ChannelEventSource("ToDo:Create", name), 3000); + } + + // v3.4.3+ 版本支持发送消息简化 + public async Task CreateDoTo(string name) + { + await _eventPublisher.PublishAsync("ToDo:Create", name); + // 也可以延迟发布,比如延迟 3s + await _eventPublisher.PublishDelayAsync("ToDo:Create", 3000, name); + // 也支持枚举 + await _eventPublisher.PublishAsync(YourEnum.Some); + } +} +``` + +### 22.2.3 注册事件服务 + +在 `Startup.cs` 注册 `EventBus` 服务: + +```cs showLineNumbers {2,5} +// 注册 EventBus 服务 +services.AddEventBus(builder => +{ + // 注册 ToDo 事件订阅者 + builder.AddSubscriber(); + + // 通过类型注册,Furion 4.2.1+ 版本 + builder.AddSubscriber(typeof(ToDoEventSubscriber)); + + // 批量注册事件订阅者 + builder.AddSubscribers(ass1, ass2, ....); +}); +``` + +:::tip 懒人提醒 + +在 `Furion` 中可以不用通过 `builder.AddSubscriber()` 方式一一注册,只需要实现 `ISingleton` 接口即可,如: + +```cs showLineNumbers {1} +public class ToDoEventSubscriber : IEventSubscriber, ISingleton +{ +} +``` + +这样就无需写 ~~`builder.AddSubscriber();`~~ 代码了,只需保留 `services.AddEventBus()` 服务即可。 + +::: + +### 22.2.4 运行项目 + +运行项目并触发事件发布接口或方法。 + +```bash showLineNumbers +info: Jaina.Samples.ToDoEventSubscriber[0] + 创建一个 ToDo:Jaina +``` + +## 22.3 自定义事件源 + +`Furion` 使用 `IEventSource` 作为消息载体,任何实现该接口的类都可以充当消息载体。 + +如需自定义,只需实现 `IEventSource` 接口即可: + +```cs showLineNumbers {1,3,14,19,24,29,35-37} +public class ToDoEventSource : IEventSource +{ + public ToDoEventSource() + { + } + + public ToDoEventSource(string eventId, string todoName) + { + EventId = eventId; + ToDoName = todoName; + } + + // 自定义属性 + public string ToDoName { get; set; } + + /// + /// 事件 Id + /// + public string EventId { get; set; } + + /// + /// 事件承载(携带)数据 + /// + public object Payload { get; set; } + + /// + /// 事件创建时间 + /// + public DateTime CreatedTime { get; set; } = DateTime.UtcNow; + + /// + /// 取消任务 Token + /// + /// 用于取消本次消息处理 + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public CancellationToken CancellationToken { get; set; } +} +``` + +使用: + +```cs showLineNumbers +await _eventPublisher.PublishAsync(new ToDoEventSource ("ToDo:Create", "我的 ToDo Name")); +``` + +## 22.4 自定义事件源存储器 + +`Furion` 默认采用 `Channel` 作为事件源 `IEventSource` 存储器,开发者可以使用任何消息队列组件进行替换,如 `Kafka、RabbitMQ、ActiveMQ` 等,也可以使用部分数据库 `Redis、SQL Server、MySql` 实现。 + +如需自定义,只需实现 `IEventSourceStorer` 接口即可。 + +### 22.4.1 `Redis` 自定义指南 + +:::tip `Windows` 版本 `Redis` 安装包下载 + +[https://github.com/tporadowski/redis](https://github.com/tporadowski/redis) + +::: + +**1. 安装 `Redis` 拓展包** + +```bash showLineNumbers +Install-Package Microsoft.Extensions.Caching.StackExchangeRedis +``` + +也可以直接安装 `StackExchange.Redis` 包。 + +**2. 创建 `RedisEventSourceStorer` 自定义存储器** + +```cs showLineNumbers {11,34,49-59,77-90,101-102,110} +using Furion.EventBus; +using StackExchange.Redis; +using System; +using System.Text.Json; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; + +namespace Furion.Core; + +public sealed class RedisEventSourceStorer : IEventSourceStorer, IDisposable +{ + /// + /// 内存通道事件源存储器 + /// + private readonly Channel _channel; + + /// + /// Redis 连接对象 + /// + private readonly ConnectionMultiplexer _connectionMultiplexer; + + /// + /// 路由键 + /// + private readonly string _routeKey; + + /// + /// 构造函数 + /// + /// Redis 连接对象 + /// 路由键 + /// 存储器最多能够处理多少消息,超过该容量进入等待写入 + public RedisEventSourceStorer(ConnectionMultiplexer connectionMultiplexer, string routeKey, int capacity) + { + // 配置通道,设置超出默认容量后进入等待 + var boundedChannelOptions = new BoundedChannelOptions(capacity) + { + FullMode = BoundedChannelFullMode.Wait + }; + + // 创建有限容量通道 + _channel = Channel.CreateBounded(boundedChannelOptions); + + _connectionMultiplexer = connectionMultiplexer; + _routeKey = routeKey; + + // 获取一个订阅对象 + var subscriber = connectionMultiplexer.GetSubscriber(); + + // 订阅消息 + subscriber.Subscribe(routeKey, (channel, data) => + { + // 转换为 IEventSource,这里可以选择自己喜欢的序列化工具,如果自定义了 EventSource,注意属性是可读可写 + var eventSource = JsonSerializer.Deserialize(data.ToString()); + + // 写入内存管道存储器 + _channel.Writer.WriteAsync(eventSource); + }); + } + + /// + /// 将事件源写入存储器 + /// + /// 事件源对象 + /// 取消任务 Token + /// + public async ValueTask WriteAsync(IEventSource eventSource, CancellationToken cancellationToken) + { + // 空检查 + if (eventSource == default) + { + throw new ArgumentNullException(nameof(eventSource)); + } + + // 这里判断是否是 ChannelEventSource 或者 自定义的 EventSource + if (eventSource is ChannelEventSource source) + { + // 序列化,这里可以选择自己喜欢的序列化工具 + var data = JsonSerializer.Serialize(source); + + // 获取一个订阅对象 + var subscriber = _connectionMultiplexer.GetSubscriber(); + await subscriber.PublishAsync(_routeKey, data); + } + else + { + // 这里处理动态订阅问题 + await _channel.Writer.WriteAsync(eventSource, cancellationToken); + } + } + + /// + /// 从存储器中读取一条事件源 + /// + /// 取消任务 Token + /// 事件源对象 + public async ValueTask ReadAsync(CancellationToken cancellationToken) + { + // 读取一条事件源 + var eventSource = await _channel.Reader.ReadAsync(cancellationToken); + return eventSource; + } + + /// + /// 释放非托管资源 + /// + public void Dispose() + { + _connectionMultiplexer.Dispose(); + } +} +``` + +**3. 替换默认事件存储器** + +```cs showLineNumbers {1,4,7,10-13} +services.AddEventBus(options => +{ + // 创建 Redis 连接对象 + var connectionMultiplexer = ConnectionMultiplexer.Connect("localhost"); + + // 创建默认内存通道事件源对象,可自定义队列路由key,比如这里是 eventbus + var redisEventSourceStorer = new RedisEventSourceStorer(connectionMultiplexer, "eventbus", 3000); + + // 替换默认事件总线存储器 + options.ReplaceStorer(serviceProvider => + { + return redisEventSourceStorer; + }); +}); +``` + +:::tip 关于 `StackExchange.Redis` 超时问题 + +如遇到 `StackExchange.Redis` 超时(Timeout)问题,可编辑启动层的 `.csproj` 添加以下配置: + +```xml showLineNumbers {4} + + + + 50 + + + +``` + +如果以上配置依然存在超时问题,那么可以在 `Program.cs/Startup.cs` 中添加 `ThreadPool.SetMinThreads(200, 200);`。 + +::: + +### 22.4.2 `RabbitMQ` 自定义指南 + +:::important 版本说明 + +以下内容仅限 `Furion 4.3.4 +` 版本使用。 + +::: + +由于使用 `RabbitMQ` 作为事件总线存储器的比较多,所以这里提供了完整的使用例子。 + +**1. 安装 `RabbitMQ.Client` 拓展包** + +```bash showLineNumbers +Install-Package RabbitMQ.Client +``` + +**2. 创建 `RabbitMQEventSourceStorer` 自定义存储器** + +```cs showLineNumbers {13,41,60,66-78,99-112,122-124,130-134} +using Furion.EventBus; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using System; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; + +namespace Furion.Core; + +public sealed class RabbitMQEventSourceStorer : IEventSourceStorer, IDisposable +{ + /// + /// 内存通道事件源存储器 + /// + private readonly Channel _channel; + + /// + /// 通道对象 + /// + private readonly IModel _model; + + /// + /// 连接对象 + /// + private readonly IConnection _connection; + + /// + /// 路由键 + /// + private readonly string _routeKey; + + /// + /// 构造函数 + /// + /// 连接工厂 + /// 路由键 + /// 存储器最多能够处理多少消息,超过该容量进入等待写入 + public RabbitMQEventSourceStorer(ConnectionFactory factory, string routeKey, int capacity) + { + // 配置通道,设置超出默认容量后进入等待 + var boundedChannelOptions = new BoundedChannelOptions(capacity) + { + FullMode = BoundedChannelFullMode.Wait + }; + + // 创建有限容量通道 + _channel = Channel.CreateBounded(boundedChannelOptions); + + // 创建连接 + _connection = factory.CreateConnection(); + _routeKey = routeKey; + + // 创建通道 + _model = _connection.CreateModel(); + + // 声明路由队列 + _model.QueueDeclare(routeKey, false, false, false, null); + + // 创建消息订阅者 + var consumer = new EventingBasicConsumer(_model); + + // 订阅消息并写入内存 Channel + consumer.Received += (ch, ea) => + { + // 读取原始消息 + var stringEventSource = Encoding.UTF8.GetString(ea.Body.ToArray()); + + // 转换为 IEventSource,这里可以选择自己喜欢的序列化工具,如果自定义了 EventSource,注意属性是可读可写 + var eventSource = JsonSerializer.Deserialize(stringEventSource); + + // 写入内存管道存储器 + _channel.Writer.WriteAsync(eventSource); + + // 确认该消息已被消费 + _model.BasicAck(ea.DeliveryTag, false); + }; + + // 启动消费者 设置为手动应答消息 + _model.BasicConsume(routeKey, false, consumer); + } + + /// + /// 将事件源写入存储器 + /// + /// 事件源对象 + /// 取消任务 Token + /// + public async ValueTask WriteAsync(IEventSource eventSource, CancellationToken cancellationToken) + { + // 空检查 + if (eventSource == default) + { + throw new ArgumentNullException(nameof(eventSource)); + } + + // 这里判断是否是 ChannelEventSource 或者 自定义的 EventSource + if (eventSource is ChannelEventSource source) + { + // 序列化,这里可以选择自己喜欢的序列化工具 + var data = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(source)); + + // 发布 + _model.BasicPublish("", _routeKey, null, data); + } + else + { + // 这里处理动态订阅问题 + await _channel.Writer.WriteAsync(eventSource, cancellationToken); + } + } + + /// + /// 从存储器中读取一条事件源 + /// + /// 取消任务 Token + /// 事件源对象 + public async ValueTask ReadAsync(CancellationToken cancellationToken) + { + // 读取一条事件源 + var eventSource = await _channel.Reader.ReadAsync(cancellationToken); + return eventSource; + } + + /// + /// 释放非托管资源 + /// + public void Dispose() + { + _model.Dispose(); + _connection.Dispose(); + } +} +``` + +**3. 替换默认事件存储器** + +```cs showLineNumbers {1,4-8,11,14-16} +services.AddEventBus(options => +{ + // 创建连接工厂 + var factory = new ConnectionFactory + { + UserName = "admin", + Password = "q1w2e3", + }; + + // 创建默认内存通道事件源对象,可自定义队列路由key,比如这里是 eventbus + var rbmqEventSourceStorer = new RabbitMQEventSourceStorer(factory, "eventbus", 3000); + + // 替换默认事件总线存储器 + options.ReplaceStorer(serviceProvider => + { + return rbmqEventSourceStorer; + }); +}); +``` + + + +### 22.4.3 `Kafka` 自定义指南 + +**1. 安装 `Confluent.Kafka` 拓展包** + +```bash showLineNumbers +Install-Package Confluent.Kafka +``` + +**2. 创建 `EventConsumer` 订阅类** + +```cs showLineNumbers {10} +using Confluent.Kafka; + +namespace Furion.Core; + +/// +/// Kafka 消息扩展 +/// +/// +/// +public class EventConsumer : IDisposable +{ + private Task _consumerTask; + private CancellationTokenSource _consumerCts; + + /// + /// 消费者 + /// + public IConsumer Consumer { get; } + + /// + /// ConsumerBuilder + /// + public ConsumerBuilder Builder { get; set; } + + /// + /// 消息回调 + /// + public event EventHandler> Received; + + /// + /// 异常回调 + /// + public event EventHandler OnConsumeException; + + /// + /// 构造函数 + /// + /// + public EventConsumer(IEnumerable> config) + { + Builder = new ConsumerBuilder(config); + Consumer = Builder.Build(); + } + + /// + /// 启动 + /// + /// + public void Start() + { + if (Consumer.Subscription?.Any() != true) + { + throw new InvalidOperationException("Subscribe first using the Consumer.Subscribe() function"); + } + if (_consumerTask != null) + { + return; + } + _consumerCts = new CancellationTokenSource(); + var ct = _consumerCts.Token; + _consumerTask = Task.Factory.StartNew(() => + { + while (!ct.IsCancellationRequested) + { + try + { + var cr = Consumer.Consume(TimeSpan.FromSeconds(1)); + if (cr == null) continue; + Received?.Invoke(this, cr); + } + catch (ConsumeException e) + { + OnConsumeException?.Invoke(this, e); + } + } + }, ct, TaskCreationOptions.LongRunning, System.Threading.Tasks.TaskScheduler.Default); + } + + /// + /// 停止 + /// + /// + public async Task Stop() + { + if (_consumerCts == null || _consumerTask == null) return; + _consumerCts.Cancel(); + try + { + await _consumerTask; + } + finally + { + _consumerTask = null; + _consumerCts = null; + } + } + + /// + /// 释放 + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// 释放 + /// + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (_consumerTask != null) + { + Stop().Wait(); + } + Consumer?.Dispose(); + } + } +} +``` + +**3. 创建 `KafkaEventSourceStore` 自定义存储器** + +```cs showLineNumbers {11,55-63,84-99,109-110,119-120} +using Confluent.Kafka; +using Furion.EventBus; +using Newtonsoft.Json; +using System.Threading.Channels; + +namespace Furion.Core; + +/// +/// Kafka 存储源 +/// +public class KafkaEventSourceStore : IEventSourceStorer, IDisposable +{ + /// + /// 内存通道事件源存储器 + /// + private readonly Channel _channel; + /// + /// 主题 + /// + private readonly string _topic; + /// + /// 消费者 + /// + private readonly EventConsumer _eventConsumer; + /// + /// 生产者 + /// + private readonly IProducer _producer; + + /// + /// 构造函数 + /// + /// 消费者配置 + /// 生产者配置 + /// 主题 + /// 存储器最多能够处理多少消息,超过该容量进入等待写入 + public KafkaEventSourceStore(ConsumerConfig consumerConf, ProducerConfig producerConf, string topic, int capacity) + { + // 配置通道,设置超出默认容量后进入等待 + var boundedChannelOptions = new BoundedChannelOptions(capacity) + { + FullMode = BoundedChannelFullMode.Wait + }; + + // 创建有限容量通道 + _channel = Channel.CreateBounded(boundedChannelOptions); + + // 主题 + _topic = topic; + + // 创建消息订阅者 + _eventConsumer = new EventConsumer(consumerConf); + _eventConsumer.Consumer.Subscribe(new[] { topic }); + + // 订阅消息写入 Channel + _eventConsumer.Received += (send, cr) => + { + // 反序列化消息 + var eventSource = JsonConvert.DeserializeObject(cr.Message.Value); + + // 写入内存管道存储器 + _channel.Writer.WriteAsync(eventSource); + }; + + // 启动消费者 + _eventConsumer.Start(); + + // 创建生产者 + _producer = new ProducerBuilder(producerConf).Build(); + } + + /// + /// 将事件源写入存储器 + /// + /// 事件源对象 + /// 取消任务 Token + /// + public async ValueTask WriteAsync(IEventSource eventSource, CancellationToken cancellationToken) + { + if (eventSource == default) + { + throw new ArgumentNullException(nameof(eventSource)); + } + // 这里判断是否是 ChannelEventSource 或者 自定义的 EventSource + if (eventSource is ChannelEventSource source) + { + // 序列化 + var data = JsonConvert.SerializeObject(source); + // 异步发布 + await _producer.ProduceAsync(_topic, new Message + { + Value = data + }, cancellationToken); + } + else + { + // 这里处理动态订阅问题 + await _channel.Writer.WriteAsync(eventSource, cancellationToken); + } + } + + /// + /// 从存储器中读取一条事件源 + /// + /// 取消任务 Token + /// 事件源对象 + public async ValueTask ReadAsync(CancellationToken cancellationToken) + { + // 读取一条事件源 + var eventSource = await _channel.Reader.ReadAsync(cancellationToken); + return eventSource; + } + + /// + /// 释放非托管资源 + /// + public async void Dispose() + { + await _eventConsumer.Stop(); + GC.SuppressFinalize(this); + } +} +``` + +**4. 替换默认事件存储器** + +```cs showLineNumbers {1,3-8,10-15,18,21-24} +services.AddEventBus(options => +{ + var consumerConf = new ConsumerConfig + { + BootstrapServers = "xxx.xxx.xxx.xxx:9092", + GroupId = "Consumer", + AutoOffsetReset = AutoOffsetReset.Earliest // 从最早的开始消费起 + }; + + var producerConf = new ProducerConfig + { + BootstrapServers = "xxx.xxx.xxx.xxx:9092", + BatchSize = 16384, // 修改批次大小为16K + LingerMs = 20 // 修改等待时间为20ms + }; + + // 创建默认内存通道事件源对象,可自定义队列路由key,比如这里是 eventbus + var kafkaEventSourceStorer = new KafkaEventSourceStore(consumerConf, producerConf, "testTopic", 30000); + + // 替换默认事件总线存储器 + options.ReplaceStorer(serviceProvider => + { + return kafkaEventSourceStorer; + }); +}); +``` + +## 22.5 自定义事件发布者 + +`Furion` 默认内置基于 `Channel` 的事件发布者 `ChannelEventPublisher`。 + +如需自定义,只需实现 `IEventPublisher` 接口即可: + +```cs showLineNumbers {1,10} +public class ToDoEventPublisher : IEventPublisher +{ + private readonly IEventSourceStorer _eventSourceStorer; + + public ToDoEventPublisher(IEventSourceStorer eventSourceStorer) + { + _eventSourceStorer = eventSourceStorer; + } + + public async Task PublishAsync(IEventSource eventSource) + { + await _eventSourceStorer.WriteAsync(eventSource, eventSource.CancellationToken); + } + + // .... 其他接口实现 +} +``` + +最后,在注册 `EventBus` 服务中替换默认 `IEventPublisher`: + +```cs showLineNumbers {4} +services.AddEventBus(builder => +{ + // 替换事件源存储器 + builder.ReplacePublisher(); +}); +``` + +## 22.6 添加事件执行监视器 + +`Furion` 提供了 `IEventHandlerMonitor` 监视器接口,实现该接口可以监视所有订阅事件,包括 `执行之前、执行之后,执行异常,共享上下文数据`。 + +如添加 `ToDoEventHandlerMonitor`: + +```cs showLineNumbers {1,9,15} +public class ToDoEventHandlerMonitor : IEventHandlerMonitor +{ + private readonly ILogger _logger; + public ToDoEventHandlerMonitor(ILogger logger) + { + _logger = logger; + } + + public Task OnExecutingAsync(EventHandlerExecutingContext context) + { + _logger.LogInformation("执行之前:{EventId}", context.Source.EventId); + return Task.CompletedTask; + } + + public Task OnExecutedAsync(EventHandlerExecutedContext context) + { + _logger.LogInformation("执行之后:{EventId}", context.Source.EventId); + + if (context.Exception != null) + { + _logger.LogError(context.Exception, "执行出错啦:{EventId}", context.Source.EventId); + } + + return Task.CompletedTask; + } +} +``` + +最后,在注册 `EventBus` 服务中注册 `ToDoEventHandlerMonitor`: + +```cs showLineNumbers {4} +services.AddEventBus(builder => +{ + // 添加事件执行监视器 + builder.AddMonitor(); +}); +``` + +## 22.7 添加事件执行器 + +`Furion` 提供了 `IEventHandlerExecutor` 执行器接口,可以让开发者自定义事件处理函数执行策略,如 `超时控制,失败重试、熔断等等`。 + +如添加 `RetryEventHandlerExecutor`: + +```cs showLineNumbers {1,3} +public class RetryEventHandlerExecutor : IEventHandlerExecutor +{ + public async Task ExecuteAsync(EventHandlerExecutingContext context, Func handler) + { + // 如果执行失败,每隔 1s 重试,最多三次 + await Retry.InvokeAsync(async () => { + await handler(context); + }, 3, 1000); + } +} +``` + +最后,在注册 `EventBus` 服务中注册 `RetryEventHandlerExecutor`: + +```cs showLineNumbers {4} +services.AddEventBus(builder => +{ + // 添加事件执行器 + builder.AddExecutor(); +}); +``` + +## 22.8 使用有作用域的服务 + +在 `Furion` 中, `Event Bus` 所有服务均注册为单例,如需使用作用域服务(单例服务可直接注入),可通过依赖注入 `IServiceScopeFactory` 实例并通过 `CreateScope()` 创建一个作用域,如: + +```cs showLineNumbers {5,8,10,13,18-22} +public class ToDoEventSubscriber : IEventSubscriber +{ + private readonly ILogger _logger; + + public ToDoEventSubscriber(IServiceScopeFactory scopeFactory // 单例服务可以直接注入 + , ILogger logger) // 单例服务可以直接注入 + { + ScopeFactory = scopeFactory; + _logger = logger; + // 避免在这里解析非单例服务 + } + + public IServiceScopeFactory ScopeFactory { get; } + + [EventSubscribe("ToDo:Create")] + public async Task CreateToDo(EventHandlerExecutingContext context) + { + // 创建新的作用域 + using var scope = ScopeFactory.CreateScope(); + + // 解析服务 + var scopedProcessingService = scope.ServiceProvider.GetRequiredService(); + // .... + } +} +``` + +:::important 特别注意 + +如果是非单例对象,应避免在构造函数中解析服务,如把 `scope.ServiceProvider.GetRequiredService()` 放在构造函数中初始化会导致一些服务无法使用。 + +::: + +## 22.9 订阅执行任务意外异常 + +事件处理程序使用的是 `Task` 对象进行创建并执行,但可能存在一些意外且难以捕获的异常,这时候可以通过以下方式订阅: + +```cs showLineNumbers {4} +services.AddEventBus(builder => +{ + // 订阅 EventBus 意外未捕获异常 + builder.UnobservedTaskExceptionHandler = (obj, args) => + { + // .... + }; +}); +``` + +## 22.10 事件总线工厂 + +:::important 版本说明 + +以下内容仅限 `Furion 4.2.10 +` 版本使用。 + +::: + +在该版本中,`Furion` 提供了 `IEventBusFactory` 工厂服务,可在运行时动态新增或删除订阅,如: + +```cs showLineNumbers {4,14-17,24} +public class TestEventBus : IDynamicApiController +{ + private readonly IEventPublisher _eventPublisher; + private readonly IEventBusFactory _eventBusFactory; + public TestEventBus(IEventPublisher eventPublisher, IEventBusFactory eventBusFactory) + { + _eventPublisher = eventPublisher; + _eventBusFactory = eventBusFactory; + } + + // 运行时动态添加一个订阅器 + public async Task AddSubscriber() + { + await _eventBusFactory.Subscribe("xxx", async (ctx) => + { + Console.WriteLine("我是动态的"); + await Task.CompletedTask; + }); + } + + // 运行时动态删除一个订阅器 + public async Task RemoveDynamic(string eventId) + { + await _eventBusFactory.Unsubscribe(eventId); + } +} +``` + +## 22.11 `MessageCenter` 静态类 + +:::important 版本说明 + +以下内容仅限 `Furion 4.3.3 +` 版本使用。 + +::: + +在 `Furion 4.3.3` 版本新增了 `MessageCenter` 静态类,可在任何地方发送事件消息或订阅消息。 + +```cs showLineNumbers {2,5,11} +// 发送消息(含诸多重载) +await MessageCenter.PublishAsync("messageId", new {}); + +// 动态订阅消息 +MessageCenter.Subscribe("messageId", async (ctx) => { + Console.WriteLine("我是动态的"); + await Task.CompletedTask; +}); + +// 取消订阅 +MessageCenter.Unsubscribe("messageId"); +``` + +## 22.12 配置重试失败回调 + +:::important 版本说明 + +以下内容仅限 `Furion 4.6.1 +` 版本使用。 + +::: + +1. 创建 `IEventFallbackPolicy` 实现类并实现 `Callback` 方法,如 `EventFallbackPolicy`: + +```cs showLineNumbers {1,9} +public class EventFallbackPolicy : IEventFallbackPolicy +{ + private readonly ILogger _logger; + public EventFallbackPolicy(ILogger logger) + { + _logger = logger; + } + + public async Task CallbackAsync(EventHandlerExecutingContext context, Exception ex) + { + _logger.LogError(ex, "重试了多次最终还是失败了"); + await Task.CompletedTask; + } +} +``` + +2. 注册 `EventFallbackPolicy` 类型服务 + +```cs showLineNumbers {1,3} +services.AddEventBus(options => +{ + options.AddFallbackPolicy(); +}); +``` + +3. 通过 `[EventSubscribe]` 特性配置 `FallbackPolicy` 属性使用 + +```cs showLineNumbers {1} +[EventSubscribe("test:error", NumRetries = 3, FallbackPolicy = typeof(EventFallbackPolicy))] // 重试三次 +public async Task TestError(EventHandlerExecutingContext context) +{ + Console.WriteLine("我执行啦~~"); + throw new NotImplementedException(); +} +``` + + + +:::note 小知识 + +可以定义多个 `IEventFallbackPolicy` 实现类,然后通过 `options.AddFallbackPolicy()` 注册多个,这样实现不同的事件订阅程序执行不同的策略。如: + +```cs showLineNumbers {1,3} +[EventSubscribe("test:error", NumRetries = 3, FallbackPolicy = typeof(EventFallbackPolicy))] + +[EventSubscribe("test:error", NumRetries = 1010, FallbackPolicy = typeof(OtherEventFallbackPolicy))] +``` + +::: + +## 22.13 自定义事件源存储器不可用时回退 + +有时候我们自定义了事件源存储器如使用 `Redis`、`RabbitMQ` 等第三方存储介质,但由于一些原因导致启动时不可用如 `Redis` 服务未启动,这时候我们可以配置选择回退到默认事件源存储器(内存)中,如: + +```cs showLineNumbers {3,9-12} +services.AddEventBus(options => +{ + options.ReplaceStorerOrFallback(() => new RedisEventSourceStorer(......)); +}); + +// 还可以解析服务传入 +services.AddEventBus(options => +{ + options.ReplaceStorerOrFallback(serviceProvider => // 提供 IServiceProvider 可解析服务 + { + // var someService = serviceProvider.GetRequiredService(); + return new RedisEventSourceStorer(......); + }); +}); +``` + +:::tip 特别说明 + +目前只支持在应用启动时检查,不支持在运行时实现故障转移切换。 + +::: + +## 22.14 `EventBusOptionsBuilder` 配置 + +`EventBusOptionsBuilder` 是 `AddEventBus` 构建服务选项,该选项包含以下属性和方法: + +- 属性 + - `ChannelCapacity`:默认内存通道容量 + - `UnobservedTaskExceptionHandler`:订阅执行任务未察觉异常 + - `UseUtcTimestamp`:是否使用 `UTC` 时间,`bool` 类型,默认 `false` + - `FuzzyMatch`:是否开启全局模糊匹配(正则表达式)事件 `Id`,`bool` 类型,默认 `false` + - `LogEnabled`:是否启用日志输出,`bool` 类型,默认 `true` +- 方法 + - `AddSubscriber`:添加订阅者 + - `ReplacePublisher`:替换发布者 + - `ReplaceStorer(Func)`:替换存储器 + - `ReplaceStorerOrFallback(Func)`:替换存储器如果失败则回滚默认 + - `AddMonitor`:添加监视器 + - `AddExecutor`:添加执行器 + +## 22.15 关于高频消息处理方式 + +在一些高频发送消息的场景如 `IoT`、日志记录、数据采集,为避免频繁解析服务和创建作用域,可使用 **类全局作用域** 和所有服务都采取单例的方式: + +```cs showLineNumbers {1,4,6,9,17,24-26} +public class ToDoEventSubscriber : IEventSubscriber, IDisposable +{ + private readonly ILogger _logger; + private readonly IServiceScope _serviceScope; + + public ToDoEventSubscriber(IServiceScopeFactory scopeFactory + , ILogger logger) + { + _serviceScope = scopeFactory.CreateScope(); + _logger = logger; + } + + [EventSubscribe("iot:log")] + public async Task LogFromIoT(EventHandlerExecutingContext context) + { + // 解析服务 + var scopedProcessingService = _serviceScope.ServiceProvider.GetRequiredService(); + // .... + } + + /// + /// 释放服务作用域 + /// + public void Dispose() + { + _serviceScope.Dispose(); + } +} +``` + +## 22.16 IIS 部署回收设置 + +如果在项目中使用了事件总线且部署到 `IIS` 中,那么需要设置 `IIS` 禁止回收,避免事件总线服务进入休眠,[点击查看 `IIS` 回收问题解决方案](./deploy-iis#3415-iis-%E5%9B%9E%E6%94%B6%E9%97%AE%E9%A2%98%E5%92%8C%E9%85%8D%E7%BD%AE) + +## 22.17 使用第三方事件总线 `CAP` 示例 + +```cs showLineNumbers {1,5} +builder.Services.AddCap(options => +{ + options.UseInMemoryStorage(); + options.UseInMemoryMessageQueue(); +}).AddSubscriberAssembly(App.Assemblies.ToArray()); +``` + +## 22.18 在 `WinForm/WPF` 中使用 + +事件总线支持在 `WinForm/WPF` 中完整的使用。 + +- 发送事件 + +```cs showLineNumbers {3} +private async void button1_Click(object sender, EventArgs e) +{ + await _eventPublisher.PublishAsync("update.textbox", textBox1); // 可将 TextBox 控件作为 Payload 传递 +} +``` + +- 订阅事件并更新 `UI` + +```cs showLineNumbers {7,10,16} +public class UIEventSubscriber : IEventSubscriber +{ + [EventSubscribe("update.textbox")] + public async Task UpdateTextBox(EventHandlerExecutingContext context) + { + // 将 Palload 转 TextBox + var textbox = context.Source.Payload as TextBox; + + // 多线程更新 UI 组件(WinForm) + textbox?.BeginInvoke(() => + { + text.Text = "我是事件总线更新的"; + }); + + // 多线程更新 UI 组件(WPF) + textbox?.Dispatcher?.BeginInvoke(() => + { + text.Content = " 我是事件总线更新的"; + }); + + await Task.CompletedTask; + } +} +``` + +## 22.19 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/file-provider.mdx b/handbook/docs/file-provider.mdx new file mode 100644 index 0000000000000000000000000000000000000000..fe9d22516efb6a735cfef102f379581edfc85899 --- /dev/null +++ b/handbook/docs/file-provider.mdx @@ -0,0 +1,574 @@ +--- +id: file-provider +title: 31. 虚拟文件系统 +sidebar_label: 31. 虚拟文件系统(上传下载) +--- + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **新特性** + + -  新增 `.m3u8` 和 `.ts` 文件类型 `MIME` 支持 4.8.7.5 ⏱️2023.03.07 [#I6KKEM](https://gitee.com/dotnetchina/Furion/issues/I6KKEM) + -  新增 `*.bcmap` 和 `.properties` 文件类型 `MIME` 支持 4.8.4.9 ⏱️2023.01.06 [!694](https://gitee.com/dotnetchina/Furion/pulls/694) + +
+
+
+ +:::important 版本说明 + +以下内容仅限 `Furion 2.5.0 +` 版本使用。 + +::: + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +## 31.1 关于文件系统 + +本章所谓的 `文件系统` 有点名不副实,其实根本算不上一个系统,它仅仅是利用一个抽象化的 `IFileProvider` 以统一的方式提供所需的文件而已。通过该 `文件系统` 可以读取物理文件和嵌入资源文件,包括目录结果读取,文件内容读取,文件内容监听等等。 + +### 31.1.1 文件系统类型 + +`Furion` 提供了两种文件系统类型: + +- `Physical`:物理文件系统类型,也就是物理机中实际存在的文件 +- `Embedded`:嵌入资源文件系统类型,也就是资源文件嵌入到了程序集中,常用于模块化开发 + +## 31.2 注册虚拟文件系统服务 + +```cs showLineNumbers +services.AddVirtualFileServer(); +``` + +## 31.3 获取文件系统 `IFileProvider` 实例 + +### 31.3.1 `Func` 方式 + +`Furion` 框架提供了 `Func` 委托供构造函数注入或解析服务,如: + +```cs showLineNumbers {6,8-9,11-12} +public class PersonServices +{ + private readonly IFileProvider _physicalFileProvider; + private readonly IFileProvider _embeddedFileProvider; + + public PersonServices(Func fileProviderResolve) + { + // 解析物理文件系统 + _physicalFileProvider = fileProviderResolve(FileProviderTypes.Physical, @"c:/test"); + + // 解析嵌入资源文件系统 + _embeddedFileProvider = fileProviderResolve(FileProviderTypes.Embedded, Assembly.GetEntryAssembly()); + } +} +``` + +### 31.3.2 `FS` 静态类方式 + +`Furion` 框架也提供了 `FS` 静态类方式创建,如: + +```cs showLineNumbers +// 解析物理文件系统 +var physicalFileProvider = FS.GetPhysicalFileProvider(@"c:/test"); + +// 解析嵌入资源文件系统 +var embeddedFileProvider = FS.GetEmbeddedFileProvider(Assembly.GetEntryAssembly()); +``` + +## 31.4 `IFileProvider` 常见操作 + +### 31.4.1 读取文件内容 + +```cs showLineNumbers +byte[] buffer; +using (Stream readStream = _fileProvider.GetFileInfo("你的文件路径").CreateReadStream()) +{ + buffer = new byte[readStream.Length]; + await readStream.ReadAsync(buffer.AsMemory(0, buffer.Length)); +} + +// 读取文件内容 +var content = Encoding.UTF8.GetString(buffer); +``` + +### 31.4.2 获取文件目录内容(需递归查找) + +```cs showLineNumbers +var rootPath = "当前目录路径"; +var fileinfos = _fileProvider.GetDirectoryContents(rootPath); +foreach (var fileinfo in fileinfos) +{ + if(fileinfo.IsDirectory) + { + // 这里递归。。。 + } +} +``` + +### 31.4.3 监听文件变化 + +```cs showLineNumbers +ChangeToken.OnChange(() => _fileProvider.Watch("监听的文件"), () => +{ + // 这里写你的逻辑 +}); +``` + +## 31.5 模块化静态资源配置 + +通常我们采用模块化开发,静态资源都是嵌入进程序集中,这时候我们需要通过配置 `UseFileServer` 指定模块静态资源路径,如: + +```cs showLineNumbers +// 默认静态资源调用,wwwroot +app.UseStaticFiles(); + +// 配置模块化静态资源 +app.UseFileServer(new FileServerOptions +{ + FileProvider = new EmbeddedFileProvider(模块程序集), + RequestPath = "/模块名称", // 后续所有资源都是通过 /模块名称/xxx.css 调用 + EnableDirectoryBrowsing = true +}); +``` + +## 31.6 文件上传下载 + +在应用开发中,文件上传下载属于非常常用的功能,这里贴出常见的文件上传下载示例。 + +### 31.6.1 文件下载 + +- 文件路径的方式 + +```cs showLineNumbers {1,5} +[HttpGet, NonUnify] +public IActionResult FileDownload() +{ + string filePath = "这里获取完整的文件下载路径"; + return new FileStreamResult(new FileStream(filePath, FileMode.Open), "application/octet-stream") + { + FileDownloadName = fileName // 配置文件下载显示名 + }; +} +``` + +- `byte[]` 方式 + +```cs showLineNumbers {1,4} +[HttpGet, NonUnify] +public IActionResult FileDownload() +{ + return new FileContentResult(byte数组, "application/octet-stream") + { + FileDownloadName = fileName // 配置文件下载显示名 + }; +} +``` + +- `stream` 方式 + +```cs showLineNumbers {1,4,7-9,11} +[HttpGet, NonUnify] +public async Task FileDownload() +{ + var (stream, _) = await "http://furion.baiqian.ltd/img/rm1.png".GetAsStreamAsync(); + + // 将 stream 转 byte[] + byte[] bytes = new byte[stream.Length]; + await stream.ReadAsync(bytes); + stream.Seek(0, SeekOrigin.Begin); + + return new FileContentResult(bytes, "application/octet-stream") + { + FileDownloadName = fileName // 配置文件下载显示名 + }; +} +``` + +:::note 关于前端获取文件名 + +如果前端获取不到文件,可添加以下配置: + +```cs showLineNumbers +_httpContextAccessor.HttpContext.Response.Headers.Add("Content-Disposition", $"attachment; filename={文件名}"); +_httpContextAccessor.HttpContext.Response.Headers.Add("Access-Control-Expose-Headers", "Content-Disposition"); +``` + +如果依然不能解决问题可尝试添加以下配置: + +```json showLineNumbers {2,4-5} +{ + "CorsAccessorSettings": { + "WithExposedHeaders": [ + "Content-Disposition", + "Access-Control-Expose-Headersx-access-token" + ] + } +} +``` + +::: + +### 31.6.2 文件上传 + +:::tip 小提醒 + +`IFormFile` 类型对应前端的 `Content-Type` 为: `multipart/form-data` + +::: + +- **单文件 `IFormFile` 类型参数(存储到硬盘)** + +```cs showLineNumbers {1,2,18} +[HttpPost] +public async Task UploadFileAsync(IFormFile file) +{ + // 如:保存到网站根目录下的 uploads 目录 + var savePath = Path.Combine(App.HostEnvironment.ContentRootPath, "uploads"); + if (!Directory.Exists(savePath)) Directory.CreateDirectory(savePath); + + //// 这里还可以获取文件的信息 + // var size = file.Length / 1024.0; // 文件大小 KB + // var clientFileName = file.FileName; // 客户端上传的文件名 + // var contentType = file.ContentType; // 获取文件 ContentType 或解析 MIME 类型 + + // 避免文件名重复,采用 GUID 生成 + var fileName = Guid.NewGuid().ToString("N") + Path.GetExtension(file.FileName); + var filePath = Path.Combine(savePath, fileName); + + // 保存到指定路径 + using (var stream = System.IO.File.Create(filePath)) + { + await file.CopyToAsync(stream); + } + + // 返回文件名(这里可以自由返回更多信息) + return fileName; +} +``` + +- **单文件 `Base64` 类型参数(存储到硬盘)** + +```cs showLineNumbers {1,2,9,19} +[HttpPost] +public async Task UploadFileAsync([FromBody] string fileBase64, string clientFileName) +{ + // 如:保存到网站根目录下的 uploads 目录 + var savePath = Path.Combine(App.HostEnvironment.ContentRootPath, "uploads"); + if (!Directory.Exists(savePath)) Directory.CreateDirectory(savePath); + + // 将 base64 字符串转 byte[] + var bytes = Convert.FromBase64String(fileBase64); + + // 这里还可以获取文件的信息 + // var size = bytes.Length / 1024.0; // 文件大小 KB + + // 避免文件名重复,采用 GUID 生成 + var fileName = Guid.NewGuid().ToString("N") + Path.GetExtension(clientFileName); + var filePath = Path.Combine(savePath, fileName); + + // 保存到指定路径 + using (var fs = new FileStream(filePath, FileMode.Create)) + { + await fs.WriteAsync(bytes); + } + + // 返回文件名(这里可以自由返回更多信息) + return filename; +} +``` + +:::warning 特别注意 + +文件 `Base64` 字符串如果带 `data:text/plain;base64,` 开头则,需要手动去掉 `,` 之前(含逗号)的字符串。 + +::: + +- **多文件 `List` 类型参数(存储到硬盘)** + +:::tip 参数提示 + +通常多文件上传用的最多的是 `List files` 参数,但 `.NET5+` 更推荐使用 `IFormFileCollection files`。 + +::: + +代码和 `单文件处理一致`,只需 `foreach` 即可。 + +```cs showLineNumbers {1-2,12} +[HttpPost] +public async Task UploadFileAsync(List files) // 可改为 IFormFileCollection files +{ + // 保存到网站根目录下的 uploads 目录 + var savePath = Path.Combine(App.HostEnvironment.ContentRootPath, "uploads"); + if(!Directory.Exists(savePath)) Directory.CreateDirectory(savePath); + + // 总上传大小 + long size = files.Sum(f => f.Length); + + // 遍历所有文件逐一上传 + foreach (var formFile in files) + { + if (formFile.Length > 0) + { + // 避免文件名重复,采用 GUID 生成 + var fileName = Guid.NewGuid().ToString("N") + Path.GetExtension(formFile.FileName); + var filePath = Path.Combine(savePath, fileName); + + // 保存到指定路径 + using (var stream = System.IO.File.Create(filePath)) + { + await formFile.CopyToAsync(stream); + } + } + } + + // 这里可自行返回更多信息 + return new { count = files.Count, size }; +} +``` + +- **多文件 `List` `Base64` 类型参数(存储到硬盘)** + +代码和 `单文件处理一致`,只需 `foreach` 即可(参上)。 + +### 31.6.3 将 `IFormFile` 转 `byte[]` + +有时候我们需要将文件转换成 `byte[]` 存储到数据库,而不是存储到硬盘中。 + +```cs showLineNumbers {4-8} +[HttpPost] +public async Task UploadFileAsync(IFormFile file) +{ + var fileLength = file.Length; + using var stream = file.OpenReadStream(); + var bytes = new byte[fileLength]; + + stream.Read(bytes, 0, (int)fileLength); + + // 这里将 bytes 存储到你想要的介质中即可 +} +``` + +:::tip 便捷拓展方法 + +在 Furion `v3.2.0` 新增了 `IFormFile` 的 `ToByteArray` 拓展,如: + +```cs showLineNumbers {4} +[HttpPost] +public async Task UploadFileAsync(IFormFile file) +{ + var bytes = file.ToByteArray(); + + // 这里将 bytes 存储到你想要的介质中即可 +} +``` + +:::tip + +### 31.6.4 将 `byte[]` 输出为 `Url` 地址 + +由于一些项目直接将文件二进制存储在数据库中,读取到内存的时候都是 `byte[]` 数组,比如我们将图片文件存储在数据库中,然后前端通过 `Url` 链接进行访问,这个时候就需要将 `byte[]` 转换为有效的资源路径格式,如: + +```cs showLineNumbers {1,7} +[NonUnify, HttpGet, AllowAnonymous] +public async Task attachment(string resourceId) +{ + // 根据 resourceId 查询 byte[] 字节数组和 content-type + + // 返回 FileContentResult 类型 + return new FileContentResult(字节数组,content-type); +} +``` + +之后我们就可以通过 `https://localhost/attachment/资源id` 访问文件或图片了。 + +### 31.6.5 配置上传文件目录 + +很多时候我们会将文件存储在特定的目录中,如 `uploads` 中,这时候只需要添加以下配置即可: + +- 编辑启动层 `YourPoject.Web.Entry.csproj` 项目,添加以下代码: + +```xml showLineNumbers {2-3} + + + PreserveNewest + + +``` + +- 配置文件服务中间件 + +```cs showLineNumbers {3-5,7-11} +app.UseStaticFiles(); + +// 如果目录不存在则创建 +var staticRoot = Path.Combine(AppContext.BaseDirectory, "uploads"); // 注意这里不要添加 / +if (!Directory.Exists(staticRoot)) Directory.CreateDirectory(staticRoot); + +app.UseFileServer(new FileServerOptions +{ + RequestPath = "/uploads", // 配置访问地址,需以 / 开头,通常和目录名称一致,也可以不同 + FileProvider = new PhysicalFileProvider(staticRoot) +}); +``` + +- 通过浏览器访问文件 + +之后就可以通过浏览器访问 `http://地址:端口/uploads/xxx.png` 了。 + +## 31.7 请求大小控制(上传文件大小控制) + +在 `Web` 项目中,`Kestrel` 和 `HttpSys` 都强制实施 `30M (~28.6MiB)` 的最大请求正文大小限制,如果请求正文大小超过配置的最大请求正文大小限制,则引发 `Request body too large. The max request body size is xxxxx` 异常,状态码为 `413` 或 `500`。 + +### 31.7.1 对特定的接口进行控制 + +可通过 `[RequestSizeLimit]` 特性进行特定限制 + +```cs showLineNumbers {2,3} +[HttpPost] +[RequestSizeLimit(100_000_000)] // 通常设置为 [RequestSizeLimit(long.MaxValue)] +[RequestFormLimits(MultipartBodyLengthLimit = long.MaxValue)] +public IActionResult MyAction([FromBody] MyViewModel data) +{ +} +``` + +相关 `Issue`:[https://gitee.com/dotnetchina/Furion/issues/I8669C](https://gitee.com/dotnetchina/Furion/issues/I8669C) + +### 31.7.2 对特定接口取消限制 + +如果不需要对请求大小进行限制,也就是支持提交无限大小,则贴 `[DisableRequestSizeLimit]` 特性即可。 + +### 31.7.3 通用中间件进行控制 + +我们也可以通过中间件的方式在 `Startup.cs` 中进行配置: + +```cs showLineNumbers {1,3} +app.Run(async context => +{ + context.Features.Get().MaxRequestBodySize = 100_000_000; // 设置 null 就是不限制,具体值就是限制最大多少 M +} +``` + +如果设置 `MaxRequestBodySize` 为 `null` ,则等同于取消限制,也就是 `[DisableRequestSizeLimit]` 的效果。 + +:::tip 小注意 + +有时候配置了中间件效果发现没起作用,很有可能和中间件顺序有关,可以通过 `.IsReadOnly` 属性判断,如果为 `true` ,说明你的配置无效,只有 `false` 才有效。 + +::: + +### 31.7.4 全局配置 + +- `IIS` 方式: + +1. **开发环境(IISExpress)** + +在 `Web` 启动层(通常是 `XXX.Web.Entry`)根目录下创建 `web.config` 文件,内容如下: + +```xml showLineNumbers {3-9} + + + + + + + + + + +``` + +2. **生产环境** + +通常生产环境 `IIS` 自动项目添加了 `web.config` 文件,这时候只需要在 `` 节点下添加下面内容即可: + +```xml showLineNumbers {3-5} + + + + + + + +``` + +- `Kestrel` 方式: + +:::tip 小知识 + +未使用 `IIS` 托管时,`ASP.NET Core` 默认使用 `Kestrel` 方式。 + +::: + +```cs showLineNumbers {1,8} +// .NET5 方式,在 .ConfigureWebHostDefaults 里面配置 +.UseStartup() +.UseKestrel(options => +{ + options.Limits.MaxRequestBodySize = null; // 设置 null 就是不限制,具体值就是限制最大多少 M +} + +// .NET6 方式,在 progame.cs 文件 var app = builder.Build(); 之后配置 +app.Configuration.Get().ConfigureKestrel(options => +{ + options.Limits.MaxRequestBodySize = null; // 设置 null 就是不限制,具体值就是限制最大多少 M +}); +``` + +- `HttpSys` 方式: + +:::tip 小知识 + +`HTTP.sys` 是仅在 `Windows` 上运行的适用于 `ASP.NET Core` 的 `Web` 服务器。 `HTTP.sys` 是 `Kestrel` 服务器的替代选择,提供了一些 `Kestrel` 不提供的功能。 + +::: + +```cs showLineNumbers +// .NET5 方式同上 +.UseHttpSys(options => +{ + options.MaxRequestBodySize = 100_000_000; // 设置 null 就是不限制,具体值就是限制最大多少 M +} + +// .NET6 方式同上 +``` + +## 31.8 特定文件类型(文件后缀)处理 + +有时我们在服务器静态资源目录下存放如 `.apk`,`.m3u8` 等文件时,通过 `URL` 请求返回 `404 Not Found`,那是因为默认情况下框架未配置此类文件类型 `MIME` 类型。 + +`Furion` 框架提供了 `400` 多个常见的文件类型 `MIME`,只需要在 `Startup.cs` 或 `Progame.cs` 中注册即可,如: + +```cs showLineNumbers {2,3} +// 为 UseStaticFiles 添加 StaticFileOptions 参数并指定 ContentTypeProvider 属性 +app.UseStaticFiles(new StaticFileOptions { + ContentTypeProvider = FS.GetFileExtensionContentTypeProvider() +}); +``` + +如果配置了还是出现 `404`,那么可能访问的文件类型的 `MIME` 在 `Furion` 框架中未被包含,这时可自行添加,如: + +```cs showLineNumbers {1-2,6} +var contentTypeProvider = FS.GetFileExtensionContentTypeProvider(); +contentTypeProvider.Mappings[".文件后缀"] = "MIME 类型"; // 注意文件后缀以 . 开头 +// 可多个.... + +app.UseStaticFiles(new StaticFileOptions { + ContentTypeProvider = contentTypeProvider; +}); +``` + +## 31.9 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: + +--- diff --git a/handbook/docs/filter.mdx b/handbook/docs/filter.mdx new file mode 100644 index 0000000000000000000000000000000000000000..8f4f1bbdb4af0595d4514facd465acd26a8f7178 --- /dev/null +++ b/handbook/docs/filter.mdx @@ -0,0 +1,1117 @@ +--- +id: filter +title: 5.3 筛选器/拦截器/过滤器/AOP +sidebar_label: 5.3 筛选器/拦截器/过滤器/AOP +description: 面向切面编程可以最低成本解决全局问题 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +## 5.3.1 关于筛选器 + +筛选器又名过滤器,拦截器,在 `ASP.NET Core` 中,可在请求处理管道中特定阶段之前或之后运行代码。**筛选器是非常经典的面向切面编程方式**,也就是所谓的 `AOP` 操作。 + +通俗点说就是可以在控制器 `Action` 执行前后进行切面操作或返回 `Result` 结果前后操作。 + +## 5.3.2 应用场景 + +通过自定义筛选器可以实现**错误处理,缓存处理,授权处理,日志记录,实现工作单元事务(`Uow`)等等切面操作**,从而使业务逻辑和系统行为逻辑进行分离。 + +### 5.3.2.1 筛选器优点 + +- 易拓展,易集成 +- 业务和系统逻辑分离,不干扰业务代码 +- 可实现接口多维度控制,如请求参数篡改,返回值篡改,限流,分布式事务支持 +- ... + +## 5.3.3 支持拦截应用 + +- `Mvc/WebAPI` 控制器/`Action` +- `Razor Pages` 页面 +- 框架提供的 `动态 WebAPI` +- 所有请求资源 +- 全局异常 + +## 5.3.4 筛选器类型 + + + +### 5.3.4.1 接口类型 + +- **授权筛选器**:该筛选器是最先运行,用于确定是否已针对请求为用户授权。 如果请求未获授权,授权筛选器可以让管道短路。 + - `IAuthorizationFilter` + - `IAsyncAuthorizationFilter` + - `AuthorizeFilter` +- **资源筛选器**:授权后运行,如果需要是大部分请求管道短路,它将会很有用 + - `IResourceFilter` + - `IAsyncResourceFilter` +- **操作筛选器**:在调用操作方法之前和之后运行代码,可以更改传递的参数,返回结果等,**不可在 `Razor Pages` 中使用**。 + - `IActionFilter` + - `IAsyncActionFilter` +- **异常筛选器**:在向响应正文写入任何内容之前,对未经处理的异常应用全局策略。 + - `IExceptionFilter` + - `IAsyncExceptionFilter` +- **结果筛选器**:在执行操作结果之前和之后立即运行代码,**仅当操作方法成功执行时,它们才会运行**。 对于必须围绕视图或格式化程序的执行的逻辑,它们很有用。 + - `IResultFilter` + - `IAsyncResultFilter` + - `IAlwaysRunResultFilter`:该接口主要针对所有操作结果运行拦截,也就是即使 `IResourceFilter` 设置了 `Result` 仍会执行并获取最终的 `Result` + - `IAsyncAlwaysRunResultFilter` +- **Razor Pages 筛选器**:允许 `Razor Pages` 在运行 `Razor` 页面处理程序前后运行代码,和**操作筛选器**类似,但它们不能应用单个页面处理程序方法。 + - `IPageFilter` + - `IAsyncPageFilter` + +### 5.3.4.2 特性 `Attribute` 类型 + +- **授权特性筛选器** (`Attribute` + `IAsyncAuthorizationFilter`):同上接口类型 +- **操作特性筛选器** (`ActionFilterAttribute`):同上接口类型 +- **异常特性筛选器** (`ExceptionFilterAttribute`):同上接口类型 +- **结果特性筛选器** (`ResultFilterAttribute`):同上接口类型 +- **服务特性筛选器** (`ServiceFilterAttribute`):**支持依赖注入**的服务筛选器特性 +- **类型特性筛选器** (`TypeFilterAttribute`):**不支持依赖注入**但可以传入自定义构造函数参数 +- **组合特性筛选器** (`Attribute` + 接口类型方式):可以通过派生自 `Attribute` 和 特定接口实现,如 `Attribute, IActionFilter` + +:::tip 筛选器选用技巧 + +关于选择哪种类型的筛选器有一个小技巧,**当你不需要全局筛选器的时候使用特性筛选器,否则使用接口类型筛选器**。 + +**另外尽可能的使用带 `IAsync` 开头的异步筛选器,这样无需分开多个方法,可在一个方法中操作,还能提高吞吐量。** + +::: + +:::important 同步异步筛选器 + +**筛选器接口的同步和异步版本任意实现一个,而不是同时实现。** + +**运行时会先查看筛选器是否实现了异步接口**,如果是,则调用该接口。 如果不是,则调用同步接口的方法。 + +如果在一个类中同时实现异步和同步接口,则仅调用异步方法。 使用抽象类(如 `ActionFilterAttribute`)时,将为每种筛选器类型仅重写同步方法或仅重写异步方法。 + +::: + +## 5.3.5 筛选器注册 + +`ASP.NET Core` 提供了多种筛选器注册方式,通常情况下不同的注册方式执行顺序不同,服务类型注册最先执行,其次是 `Mvc Filter` 方式,最后是特性方式。相同的方式中又按照注册前后来决定执行顺序,先注册先执行。 + +同时也提供了 `IOrderedFilter` 接口重写执行顺序,其 `Order` 属性值越高的先执行。 + +### 5.3.5.1 在 `Startup.cs` 中注册 + +最常见的注册筛选器的方式就是在 `Startup.cs` 中注册,**这种方式表示全局注册,应用所有控制器/`Action`** + +```cs showLineNumbers {1,3-9,11-15,17-18} title="Startup.cs" +public void ConfigureServices(IServiceCollection services) +{ + // Mvc 方式注册一,全局执行 + services.AddControllersWithViews(options => + { + options.Filters.Add(new AddHeaderAttribute("GlobalAddHeader", "Result filter added to MvcOptions.Filters")); // 手动创建实例,带构造参数 + options.Filters.Add(typeof(MySampleActionFilter)); // 类型 Type 方式 + options.Filters.Add(new SampleGlobalActionFilter()); // 手动创建实例,无构造参数 + }); + + // Mvc 方式注册二,全局执行 + services.Configure(options => + { + options.Filters.Add(); + }); + + // Mvc 注册方式三,全局执行,Furion 框架提供方式 + services.AddMvcFilter(); +} +``` + +### 5.3.5.2 特性方式注册 + +如果筛选器派生自 `特性`,则可通过特性方式注册,**这种方式表示局部注册,只作用于特定的控制器/`Action`** + +- 直接贴方式 + +```cs showLineNumbers {1-2,7-9} +// 定义结果特性筛选器 +public class AddHeaderAttribute : ResultFilterAttribute +{ + // ... +} + +// 直接贴方式,对于动态 WebAPI 也是一样的 +[AddHeader] +public class SampleController : Controller +{ +} +``` + +- 通过 `[ServiceFilter]` 方式 + +这种方式适用于自定义的特性筛选器包含构造函数注入服务应用场景,这种方式必须在 `ConfigureService` 中通过 `services.AddScoped` 注册。 + +```cs showLineNumbers {1,3-5} +public class MyActionFilterAttribute : ActionFilterAttribute +{ + // 注入服务 + private readonly PositionOptions _settings; + public MyActionFilterAttribute(IOptions options) + { + } +} +``` + +**需先在 `Startup.cs` 中注册筛选器** + +```cs showLineNumbers title="Startup.cs" +services.AddScoped(); +``` + +使用: + +```cs showLineNumbers {3-5} +public class SampleController : Controller +{ + // 通过 [ServiceFilter] 方式 + [ServiceFilter(typeof(MyActionFilterAttribute))] + public IActionResult Index2() + { + // ... + } +} +``` + +- 通过 `[TypeFilter]` 方式 + +`[TypeFilter]` 和 `[ServiceFilter]` 类似,唯一的区别就是 `[TypeFilter]` **不支持构造函数注入服务,但可以传递基元类型构造函数参数**。 + +```cs showLineNumbers {1,3-4,11-13} +public class MyLogFilterAttribute : ActionFilterAttribute +{ + // 构造函数包含基元类型参数 + public MyLogFilterAttribute(string message, int level) + { + } +} + +public class SampleController : Controller +{ + // 通过 [TypeFilter] 方式 + [TypeFilter(typeof(MyLogFilterAttribute), Arguments = new object[] { "Message", 10 })] + public IActionResult Index2() + { + // ... + } +} +``` + +## 5.3.6 `授权筛选器` + +通过授权筛选器可以实现在所有请求到达控制器/`Action` 之前进行验证,如果授权失败,直接跳转到登录或者返回 `401`。 + +### 5.3.6.1 `接口定义方式` + +```cs showLineNumbers {9-12,32-50} +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Authorization; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace WebApplication4.Filters; + +/// +/// 自定义授权筛选器 +/// +public class MyAuthorizationFilter : IAsyncAuthorizationFilter +{ + public async Task OnAuthorizationAsync(AuthorizationFilterContext context) + { + Console.WriteLine("授权检查......"); + + // 获取控制器信息 + var actionDescriptor = context.ActionDescriptor as ControllerActionDescriptor; + + // 获取控制器类型 + var controllerType = actionDescriptor!.ControllerTypeInfo; + + // 获取 Action 类型 + var methodType = actionDescriptor.MethodInfo; + + // 是否匿名访问 + var allowAnonymouse = context.Filters.Any(u => u is IAllowAnonymousFilter) + || controllerType.IsDefined(typeof(AllowAnonymousAttribute), true) + || methodType.IsDefined(typeof(AllowAnonymousAttribute), true); + + // 不是匿名才处理权限检查 + if (!allowAnonymouse) + { + Console.Write("逻辑检查~~~~"); + + // 在 MVC 项目中,如果检查失败,则跳转到登录页 + if (typeof(Controller).IsAssignableFrom(controllerType.AsType())) + { + context.Result = new RedirectResult("~/Login"); + } + // WebAPI 或者其他类型 + else + { + // 返回未授权 + context.Result = new UnauthorizedResult(); + } + } + // 否则直接跳过处理 + else await Task.CompletedTask; + } +} +``` + +- `全局注册` + +在 `ConfigureService` 中注册,该方式会作用所有的控制器/ `Action`: + +```cs showLineNumbers {1,3} +services.Configure(options => +{ + options.Filters.Add(); +}); +``` + +:::tip 更简易方式 + +`Furion` 框架中提供了 `services.AddMvcFilter()`,可使用它代替上面多行注册。 + +::: + +- `局部特性方式` + +`[TypeFilter]`: + +```cs showLineNumbers {1} +[TypeFilter(typeof(MyAuthorizationFilter))] +public IActionResult Get() +{ + // ... +} +``` + +`[ServiceFilter]` + +```cs showLineNumbers title="Starup.cs" +services.AddScoped(); +``` + +```cs showLineNumbers {1} +[ServiceFilter(typeof(MyAuthorizationFilter))] +public IActionResult Get() +{ + // ... +} +``` + +### 5.3.6.2 `特性定义方式(组合)` + +只需要上述接口方式中添加派生 `Attribute` 并设置 `[AttributeUsage]` 接口,如: + +```cs showLineNumbers {1-2,4} +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited=true, AllowMultiple=false)] +public class MyAuthorizationFilterAttribute : Attribute, IAsyncAuthorizationFilter +{ + public async Task OnAuthorizationAsync(AuthorizationFilterContext context) + { + // 代码同上 + } +} +``` + +使用: + +```cs showLineNumbers {1} +[MyAuthorizationFilter] +public IActionResult Get() +{ + // ... +} +``` + +## 5.3.7 `资源筛选器` + +资源筛选器使用频率较少,通常用来处理资源缓存或者阻止模型(值)绑定操作。 + +### 5.3.7.1 `接口定义方式` + +```cs showLineNumbers {6,8,11-26} +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace WebApplication4.Filters; + +public class MyResourceFilter : IAsyncResourceFilter +{ + public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next) + { + // 获取所有资源提供器工厂 + var valueProviderFactories = context.ValueProviderFactories; + + // 比如这里判断如果是 Form 表单方式提交就就不给参数复制 + var formValueProviderFactory = valueProviderFactories + .OfType() + .FirstOrDefault(); + if (formValueProviderFactory != null) + { + // 移除 Form 表单绑定值提供器器 + context.ValueProviderFactories.Remove(formValueProviderFactory); + } + + // .... 更多操作 + + // 资源请求成功后 + var resourceContext = await next(); + } +} +``` + +- `全局注册` + +在 `ConfigureService` 中注册,该方式会作用所有的控制器/ `Action`: + +```cs showLineNumbers {1,3} +services.Configure(options => +{ + options.Filters.Add(); +}); +``` + +:::tip 更简易方式 + +`Furion` 框架中提供了 `services.AddMvcFilter()`,可使用它代替上面多行注册。 + +::: + +- `局部特性方式` + +`[TypeFilter]`: + +```cs showLineNumbers {1} +[TypeFilter(typeof(MyResourceFilter))] +public IActionResult Get() +{ + // ... +} +``` + +`[ServiceFilter]` + +```cs showLineNumbers title="Starup.cs" +services.AddScoped(); +``` + +```cs showLineNumbers {1} +[ServiceFilter(typeof(MyResourceFilter))] +public IActionResult Get() +{ + // ... +} +``` + +### 5.3.7.2 `特性定义方式(组合)` + +只需要上述接口方式中添加派生 `Attribute` 并设置 `[AttributeUsage]` 接口,如: + +```cs showLineNumbers {1-2,4} +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited=true, AllowMultiple=false)] +public class MyResourceFilterAttribute : Attribute, IAsyncResourceFilter +{ + public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next) + { + // 代码同上 + } +} +``` + +使用: + +```cs showLineNumbers {1} +[MyResourceFilter] +public IActionResult Get() +{ + // ... +} +``` + +## 5.3.8 `操作筛选器` + +操作筛选器是使用频率最高的筛选器,通常用来控制进入 `Action` 之前(此时模型绑定已经完成)和 `Action` 执行之后(此时 `Result` 还未返回)。 + +可以使用操作筛选器实现各种骚操作,如**篡改参数,篡改返回值,统一参数验证,审计日志,实现数据库事务自动开启关闭**等等。 + +### 5.3.8.1 `接口定义方式` + +```cs showLineNumbers {6,8,10,12-53} +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Filters; +using System.Diagnostics; +using System.Security.Claims; + +namespace WebApplication4.Filters; + +public class MyActionFilter : IAsyncActionFilter +{ + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + //============== 这里是执行方法之前获取数据 ==================== + // 获取控制器、路由信息 + var actionDescriptor = context.ActionDescriptor as ControllerActionDescriptor; + + // 获取请求的方法 + var method = actionDescriptor!.MethodInfo; + + // 获取 HttpContext 和 HttpRequest 对象 + var httpContext = context.HttpContext; + var httpRequest = httpContext.Request; + + // 获取客户端 Ipv4 地址 + var remoteIPv4 = httpContext.GetRemoteIpAddressToIPv4(); + + // 获取请求的 Url 地址 + var requestUrl = httpRequest.GetRequestUrlAddress(); + + // 获取来源 Url 地址 + var refererUrl = httpRequest.GetRefererUrlAddress(); + + // 获取请求参数(写入日志,需序列化成字符串后存储),可以自由篡改!!!!!! + var parameters = context.ActionArguments; + + // 获取操作人(必须授权访问才有值)"userId" 为你存储的 claims type,jwt 授权对应的是 payload 中存储的键名 + var userId = httpContext.User?.FindFirstValue("userId"); + + // 请求时间 + var requestedTime = DateTimeOffset.Now; + + //============== 这里是执行方法之后获取数据 ==================== + var actionContext = await next(); + + // 获取返回的结果 + var returnResult = actionContext.Result; + + // 判断是否请求成功,没有异常就是请求成功 + var isRequestSucceed = actionContext.Exception == null; + + // 获取调用堆栈信息,提供更加简单明了的调用和异常堆栈 + var stackTrace = EnhancedStackTrace.Current(); + + // 其他操作,如写入日志 + } +} +``` + +- `全局注册` + +在 `ConfigureService` 中注册,该方式会作用所有的控制器/ `Action`: + +```cs showLineNumbers {1,3} +services.Configure(options => +{ + options.Filters.Add(); +}); +``` + +:::tip 更简易方式 + +`Furion` 框架中提供了 `services.AddMvcFilter()`,可使用它代替上面多行注册。 + +::: + +- `局部特性方式` + +`[TypeFilter]`: + +```cs showLineNumbers {1} +[TypeFilter(typeof(MyActionFilter))] +public IActionResult Get() +{ + // ... +} +``` + +`[ServiceFilter]` + +```cs showLineNumbers title="Starup.cs" +services.AddScoped(); +``` + +```cs showLineNumbers {1} +[ServiceFilter(typeof(MyActionFilter))] +public IActionResult Get() +{ + // ... +} +``` + +### 5.3.8.2 `ActionFilterAttribute` 方式 + +```cs showLineNumbers {5,13,26} +using Microsoft.AspNetCore.Mvc.Filters; + +namespace WebApplication4.Filters; + +public class MyActionAttribute : ActionFilterAttribute +{ + /// + /// 执行操作前后 + /// + /// + /// + /// + public override Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + // 代码参考接口方式 + + return base.OnActionExecutionAsync(context, next); + } + + /// + /// 返回结果前后 + /// + /// + /// + /// + public override Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) + { + // 代码参考接口方式 + + return base.OnResultExecutionAsync(context, next); + } +} +``` + +- `全局注册` + +在 `ConfigureService` 中注册,该方式会作用所有的控制器/ `Action`: + +```cs showLineNumbers {1,3} +services.Configure(options => +{ + options.Filters.Add(); +}); +``` + +:::tip 更简易方式 + +`Furion` 框架中提供了 `services.AddMvcFilter()`,可使用它代替上面多行注册。 + +::: + +- `局部特性方式` + +```cs showLineNumbers {1} +[MyAction] +public IActionResult Get() +{ + // ... +} +``` + +### 5.3.8.3 `特性定义方式(组合)` + +只需要上述接口方式中添加派生 `Attribute` 并设置 `[AttributeUsage]` 接口,如: + +```cs showLineNumbers {1-2,4} +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited=true, AllowMultiple=false)] +public class MyActionFilterAttribute : Attribute, IAsyncActionFilter +{ + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + // 代码同上 + } +} +``` + +使用: + +```cs showLineNumbers {1} +[MyActionFilter] +public IActionResult Get() +{ + // ... +} +``` + +## 5.3.9 `异常筛选器` + +异常筛选器使用频率仅次于操作筛选器,更多用于程序出现异常时记录日志或者返回统一的页面操作。 + +### 5.3.9.1 `接口定义方式` + +```cs showLineNumbers {7,9,11-33} +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Filters; +using System.Diagnostics; + +namespace WebApplication4.Filters; + +public class MyExceptionFilter : IAsyncExceptionFilter +{ + public async Task OnExceptionAsync(ExceptionContext context) + { + // 如果异常在其他地方被标记了处理,那么这里不再处理 + if (context.ExceptionHandled) return; + + // 获取控制器信息 + ControllerActionDescriptor? actionDescriptor = context.ActionDescriptor as ControllerActionDescriptor; + + // 获取请求的方法 + var method = actionDescriptor!.MethodInfo; + + // 获取异常对象 + var exception = context.Exception; + + // 获取调用堆栈信息,提供更加简单明了的调用和异常堆栈 + var stackTrace = EnhancedStackTrace.Current(); + + // 其他处理~~~ + // 1. MVC 直接返回自定义的错误页面,或者 BadPageResult 类型,如:context.Result = new BadPageResult(StatusCodes.Status500InternalServerError) { } + + // 2. WebAPI 可以直接返回 context.Result = new JsonResult(.....); + + // 3. 记录日志。。。。 + + await Task.CompletedTask; + } +} +``` + +- `全局注册` + +在 `ConfigureService` 中注册,该方式会作用所有的控制器/ `Action`: + +```cs showLineNumbers {1,3} +services.Configure(options => +{ + options.Filters.Add(); +}); +``` + +:::tip 更简易方式 + +`Furion` 框架中提供了 `services.AddMvcFilter()`,可使用它代替上面多行注册。 + +::: + +- `局部特性方式` + +`[TypeFilter]`: + +```cs showLineNumbers {1} +[TypeFilter(typeof(MyExceptionFilter))] +public IActionResult Get() +{ + // ... +} +``` + +`[ServiceFilter]` + +```cs showLineNumbers title="Starup.cs" +services.AddScoped(); +``` + +```cs showLineNumbers {1} +[ServiceFilter(typeof(MyExceptionFilter))] +public IActionResult Get() +{ + // ... +} +``` + +### 5.3.9.2 `ExceptionFilterAttribute` 方式 + +```cs showLineNumbers {5,12} +using Microsoft.AspNetCore.Mvc.Filters; + +namespace WebApplication4.Filters; + +public class MyExceptionAttribute : ExceptionFilterAttribute +{ + /// + /// 异常拦截器 + /// + /// + /// + public override Task OnExceptionAsync(ExceptionContext context) + { + // 代码参考接口方式 + return base.OnExceptionAsync(context); + } +} +``` + +- `全局注册` + +在 `ConfigureService` 中注册,该方式会作用所有的控制器/ `Action`: + +```cs showLineNumbers {1,3} +services.Configure(options => +{ + options.Filters.Add(); +}); +``` + +:::tip 更简易方式 + +`Furion` 框架中提供了 `services.AddMvcFilter()`,可使用它代替上面多行注册。 + +::: + +- `局部特性方式` + +```cs showLineNumbers {1} +[MyException] +public IActionResult Get() +{ + // ... +} +``` + +### 5.3.9.3 `特性定义方式(组合)` + +只需要上述接口方式中添加派生 `Attribute` 并设置 `[AttributeUsage]` 接口,如: + +```cs showLineNumbers {1-2,4} +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited=true, AllowMultiple=false)] +public class MyExceptionFilterAttribute : Attribute, IAsyncExceptionFilter +{ + public async Task OnExceptionAsync(ExceptionContext context) + { + // 代码同上 + } +} +``` + +使用: + +```cs showLineNumbers {1} +[MyExceptionFilter] +public IActionResult Get() +{ + // ... +} +``` + +## 5.3.10 `结果筛选器` + +结果控制器常用于对返回的结果附加更多数据,比如 `Mvc` 中的 `ViewBag`,`ViewData`,换句话说主要用来控制输出到浏览器的界面视图对象。 + +### 5.3.10.1 `接口定义方式` + +```cs showLineNumbers {7,9,11-37} +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace WebApplication4.Filters; + +public class MyResultFilter : IAsyncResultFilter +{ + public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) + { + // 获取控制器信息 + ControllerActionDescriptor? actionDescriptor = context.ActionDescriptor as ControllerActionDescriptor; + + // 获取路由表信息 + var routeData = context.RouteData; + var controllerName = routeData.Values["controller"]; + var actionName = routeData.Values["action"]; + var areaName = routeData.DataTokens["area"]; + + // 判断如果是 MVC 视图,可以动态添加数据到页面中 + if (context.Result is ViewResult viewResult) + { + // 动态添加数据,在 Razor 中就可以直接使用 @TempData["Name"] 获取数据了 + viewResult.TempData["Name"] = "Furion"; + + // 动态添加数据,在 Razor 中就可以直接使用 @ViewBag.Version 或 @ViewData["Name"] 获取数据了 + viewResult.ViewData["Version"] = 1; + } + + // 这里还可以强制性换掉 Result + // context.Result = new ContentResult("...."); + + // 执行下一个结果过滤器,如果直接短路返回,可设置 context.Cancel = true; 这样就不会执行下一个过滤器,这个和下列代码是互斥的 + var resultContext = await next(); + + // 获取返回的结果 + var result = resultContext.Result; + } +} +``` + +- `全局注册` + +在 `ConfigureService` 中注册,该方式会作用所有的控制器/ `Action`: + +```cs showLineNumbers {1,3} +services.Configure(options => +{ + options.Filters.Add(); +}); +``` + +:::tip 更简易方式 + +`Furion` 框架中提供了 `services.AddMvcFilter()`,可使用它代替上面多行注册。 + +::: + +- `局部特性方式` + +`[TypeFilter]`: + +```cs showLineNumbers {1} +[TypeFilter(typeof(MyResultFilter))] +public IActionResult Get() +{ + // ... +} +``` + +`[ServiceFilter]` + +```cs showLineNumbers title="Starup.cs" +services.AddScoped(); +``` + +```cs showLineNumbers {1} +[ServiceFilter(typeof(MyResultFilter))] +public IActionResult Get() +{ + // ... +} +``` + +### 5.3.10.2 `ResultFilterAttribute` 方式 + +```cs showLineNumbers {5,7} +using Microsoft.AspNetCore.Mvc.Filters; + +namespace WebApplication4.Filters; + +public class MyResultAttribute : ResultFilterAttribute +{ + public override Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) + { + // 代码参考接口方式 + + return base.OnResultExecutionAsync(context, next); + } +} +``` + +- `全局注册` + +在 `ConfigureService` 中注册,该方式会作用所有的控制器/ `Action`: + +```cs showLineNumbers {1,3} +services.Configure(options => +{ + options.Filters.Add(); +}); +``` + +:::tip 更简易方式 + +`Furion` 框架中提供了 `services.AddMvcFilter()`,可使用它代替上面多行注册。 + +::: + +- `局部特性方式` + +```cs showLineNumbers {1} +[MyResult] +public IActionResult Get() +{ + // ... +} +``` + +### 5.3.10.3 `特性定义方式(组合)` + +只需要上述接口方式中添加派生 `Attribute` 并设置 `[AttributeUsage]` 接口,如: + +```cs showLineNumbers {1-2,4} +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited=true, AllowMultiple=false)] +public class MyResultFilterAttribute : Attribute, IAsyncResultFilter +{ + public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) + { + // 代码同上 + } +} +``` + +使用: + +```cs showLineNumbers {1} +[MyResultFilter] +public IActionResult Get() +{ + // ... +} +``` + +### 5.3.10.4 `IAlwaysRunResultFilter` + +`IAlwaysRunResultFilter` 和 `IAsyncAlwaysRunResultFilter` 接口声明了一个针对所有操作结果运行的 `IResultFilter` 实现。 这包括由以下对象生成的操作结果: + +- 设置短路的授权筛选器和资源筛选器。 +- 异常筛选器。 + +详细使用可查看微软官方文档 [https://docs.microsoft.com/zh-cn/aspnet/core/mvc/controllers/filters?view=aspnetcore-6.0#ialwaysrunresultfilter-and-iasyncalwaysrunresultfilter](https://docs.microsoft.com/zh-cn/aspnet/core/mvc/controllers/filters?view=aspnetcore-6.0#ialwaysrunresultfilter-and-iasyncalwaysrunresultfilter) + +## 5.3.11 `RazorPages 筛选器` + +`Razor Pages` 筛选器仅支持 `Razor Pages` 项目类型。 + +### 5.3.11.1 `接口定义方式` + +```cs showLineNumbers {5,13,14-24,32} +using Microsoft.AspNetCore.Mvc.Filters; + +namespace WebApplication1; + +public class MyPageFilter : IAsyncPageFilter +{ + /// + /// 调用方法之前 + /// + /// + /// + /// + public async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next) + { + // 路由信息 + var routeData = context.RouteData; + + // 请求方法信息 + var actionDescriptor = context.ActionDescriptor; + + // 处理方法信息 + var methodDescriptor = context.HandlerMethod; + + await next.Invoke(); + } + + /// + /// 模型绑定之前 + /// + /// + /// + public Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context) + { + return Task.CompletedTask; + } +} +``` + +- `全局注册` + +在 `ConfigureService` 中注册,该方式会作用所有 `Page Handler` + +```cs showLineNumbers {1,3} +services.Configure(options => +{ + options.Filters.Add(new MyPageFilter()); +}); +``` + +### 5.3.11.2 `ResultFilterAttribute` 方式 + +```cs showLineNumbers {5,7} +using Microsoft.AspNetCore.Mvc.Filters; + +namespace WebApplication1.Filters; + +public class MyResultAttribute : ResultFilterAttribute +{ + public override Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) + { + // 代码参考接口方式 + + return base.OnResultExecutionAsync(context, next); + } +} +``` + +- `局部特性方式` + +```cs showLineNumbers {1} +[MyResult] +public void OnGet() +{ + // ... +} +``` + +## 5.3.12 筛选器取消和短路 + +通常筛选器支持多个,正常情况下,只要调用 `await next()` 方法都会进去下一个筛选器,但如果通过 `context.Result = new XXXResult()` 之后,就可以使其短路,也就是不会再执行下一个筛选器。 + +**但也有例外**: + +- 在 `IResultFilter`/`IAsyncResultFilter` 结果筛选器中,则使用标记 `context.Cancel = true;` 设置短路。 +- 在 `IExceptionFilter`/`IAsyncExceptionFilter` 异常筛选器中,则使用标记 `context.ExceptionHandled = true;` 设置短路。 + +## 5.3.13 筛选器执行顺序控制 + +默认情况下,筛选器是按照以下执行顺序执行: + +### 5.3.13.1 不同类型筛选器执行顺序 + +`IAuthorizationFilter` -> `IResourceFilter` -> `IActionFilter` -> `IExceptionFilter` -> `IResultFilter` -> `IAlwaysRunResultFilter` + +异步也是如此。 + +### 5.3.13.2 相同类型筛选器执行顺序 + +默认情况下,通过 `services.Configure(...)` 方式先注册先执行,之后到特性方式,也是采用先注册先执行。 + +如果使用同一种方式,如 `services.Configuration(...)` 或同一种特性方式,也可以控制其执行顺序,如: + +- `IOrderedFilter` 方式: + +```cs showLineNumbers {1,3-4} +public class MyActionFilter : IAsyncActionFilter, IOrderedFilter +{ + // 值越大,越优先执行 + public int Order => 1000; + + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + // .... + } +} +``` + +- `特性` 方式: + +```cs showLineNumbers {1} +[MyActionFilter(Order = 1000)] +public class ControllerFiltersController : Controller +{ + // ... +} +``` + +## 5.3.14 筛选器依赖注入 + +筛选器是支持构造函数依赖注入服务的,使用它们的前提是在 `Startup.cs` 中注册,如: + +```cs showLineNumbers title="Startup.cs" +services.AddScoped(); +``` + +## 5.3.15 了解更多 + +想了解更多筛选器知识可查阅官方文档 [https://docs.microsoft.com/zh-cn/aspnet/core/mvc/controllers/filters?view=aspnetcore-6.0](https://docs.microsoft.com/zh-cn/aspnet/core/mvc/controllers/filters?view=aspnetcore-6.0) + +## 5.3.16 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/friendly-exception.mdx b/handbook/docs/friendly-exception.mdx new file mode 100644 index 0000000000000000000000000000000000000000..0ba7ace945c34528d5eeea86ccd532f03f6e45ce --- /dev/null +++ b/handbook/docs/friendly-exception.mdx @@ -0,0 +1,654 @@ +--- +id: friendly-exception +title: 7. 友好异常处理 +sidebar_label: 7. 友好异常处理 +--- + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **新特性** + + -  新增 `BadPageResult.Status401Unauthorized` 等常见状态码 `401,403,404,500` 静态属性 4.8.4.11 ⏱️2023.01.09 [#I69KQF](https://gitee.com/dotnetchina/Furion/issues/I69KQF) + +- **问题修复** + + -  修复 配置友好异常 `FriendlyExceptionSettings:DefaultErrorMessage` 无效问题 4.8.8.23 ⏱️2023.05.31 [#I79LIG](https://gitee.com/dotnetchina/Furion/issues/I79LIG) + +
+
+
+ +import useBaseUrl from "@docusaurus/useBaseUrl"; + +## 7.1 什么是异常 + +异常一般是指运行期(此处特指 `Exception` 类)会发生的导致程序意外中止的问题,是一种对问题的描述后的封装对象。 + +在过去开发中,通常异常由系统运行时出错抛出,但现在的开发过程中,我们应在程序开发中合理的抛出异常,比如更新一条不存在的实体,或查询一个不存在的数据等等。 + +## 7.2 处理异常方式 + +- 不处理,直接中断程序执行(不推荐) +- 通过 `try catch finally` 处理(不推荐) +- 全局统一处理,并记录异常信息 **(推荐)** +- 异常注解方式处理,支持**本地化** **(推荐)** + +## 7.3 什么是友好异常处理 + +### 7.3.1 非友好异常处理 + +在了解友好异常处理之前可以看看非友好异常处理: + +- 对终端用户抛出 `500状态码` 堆栈信息 +- 大量的 `try catch` 代码,污染正常业务逻辑 +- 没有规范化的异常状态码和异常消息管理 +- 没有异常日志收集记录 +- 不支持异常消息本地化处理 +- 不支持异常策略,失败后程序立即终止 +- 不支持分布式事务 CAP +- 不支持异常传播 +- 返回的异常格式杂乱 + +### 7.3.2 友好异常处理 + +- 对终端用户提示友好 +- 对后端开发人员提供详细的异常堆栈 +- 不干扰正常业务逻辑代码,如 没有 `try catch` 代码 +- 支持异常状态码多方设置 +- 支持异常消息本地化 +- 异常信息统一配置管理 +- 支持异常策略,如重试 +- 支持异常日志收集记录 +- 支持 CAP 分布式事务关联 +- 支持内部异常外部传播 +- 支持返回统一的异常格式数据 + +## 7.4 友好异常处理使用示例 + +`Furion` 框架提供了非常灵活的友好异常处理方式。 + +:::tip 小提示 + +`.AddFriendlyException()` 默认已经集成在 `AddInject()` 中了,**无需再次注册**。也就是 `7.4.1` 章节可不配置。 + +::: + +### 7.4.1 注册友好异常服务 + +```cs showLineNumbers {11} title="Furion.Web.Core\FurWebCoreStartup.cs" +using Microsoft.Extensions.DependencyInjection; + +namespace Furion.Web.Core +{ + [AppStartup(800)] + public sealed class FurWebCoreStartup : AppStartup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers() + .AddFriendlyException(); + } + } +} +``` + +:::important 特别注意 + +`.AddFriendlyException()` 需在 `services.AddControllers()` 之后注册。 + +::: + +### 7.4.2 两个例子 + +#### 简单抛个异常 + +```cs showLineNumbers {2,12} +using Furion.DynamicApiController; +using Furion.FriendlyException; + +namespace Furion.Application +{ + public class FurionAppService : IDynamicApiController + { + public int Get(int id) + { + if (id < 3) + { + throw Oops.Oh($"{id} 不能小于3"); + } + + return id; + } + } +} +``` + +如下图所示: + + + +#### 抛出特定类型异常 + +```cs showLineNumbers {2,13} +using Furion.DynamicApiController; +using Furion.FriendlyException; +using System; + +namespace Furion.Application +{ + public class FurionAppService : IDynamicApiController + { + public int Get(int id) + { + if (id < 3) + { + throw Oops.Oh($"{id} 不能小于3。", typeof(InvalidOperationException)); + } + + return id; + } + } +} +``` + +如下图所示: + + + +## 7.5 关于 Oops.Oh + +通过上面的例子可以看出,`Oops.Oh(errorMessage)` 可以结合 `throw` 抛出异常。对于熟悉`C#`的人员来说,`throw` 后面只能 `Exception` 实例。`Oops.Oh(...)` 方法返回正是 `Exception` 实例。 + +### 7.5.1 为什么起这个名字? + +这个名字来源于一个英语句子:`Oh, Oops!`,意思是 **噢(哎),出错了!**,所以就有了 `Oops.Oh`。 + +### 7.5.2 `Oops.Oh` 重载方法 + +```cs showLineNumbers {13,22,30,39} +using System; + +namespace Furion.FriendlyException +{ + public static class Oops + { + /// + /// 抛出字符串异常 + /// + /// 异常消息 + /// String.Format 参数 + /// 异常实例 + public static Exception Oh(string errorMessage, params object[] args); + + /// + /// 抛出字符串异常 + /// + /// 异常消息 + /// 具体异常类型 + /// String.Format 参数 + /// 异常实例 + public static Exception Oh(string errorMessage, Type exceptionType, params object[] args); + + /// + /// 抛出错误码异常 + /// + /// 错误码 + /// String.Format 参数 + /// 异常实例 + public static Exception Oh(object errorCode, params object[] args); + + /// + /// 抛出错误码异常 + /// + /// 错误码 + /// 具体异常类型 + /// String.Format 参数 + /// 异常实例 + public static Exception Oh(object errorCode, Type exceptionType, params object[] args); + } +} +``` + +## 7.6 最佳实践 🤗 + +在 `Furion` 框架中,提供了非常灵活且规范化的友好异常处理方式,通过这个方式可以方便管理异常状态码、异常信息及异常本地化。 + +### 7.6.1 创建异常信息类型 + +实现自定义异常信息类型必须遵循以下配置: + +- 类型必须是公开且是 `Enum` 枚举类型 +- 枚举类型必须贴有 `[ErrorCodeType]` 特性 +- 枚举中每一项必须贴有 `[ErrorCodeItemMetadata]` 特性 + +```cs showLineNumbers {1,5,8,11,14,17} +using Furion.FriendlyException; + +namespace Furion.Application +{ + [ErrorCodeType] + public enum ErrorCodes + { + [ErrorCodeItemMetadata("{0} 不能小于 {1}")] + z1000, + + [ErrorCodeItemMetadata("数据不存在")] + x1000, + + [ErrorCodeItemMetadata("{0} 发现 {1} 个异常", "百小僧", 2)] + x1001, + + [ErrorCodeItemMetadata("服务器运行异常", ErrorCode = "Error")] + SERVER_ERROR + } +} +``` + +:::important + +`Furion` 框架提供了 `[ErrorCodeType]` 特性和 `IErrorCodeTypeProvider` 提供器接口来提供异常信息扫描,这里用的是 `[ErrorCodeType]` 特性类。 + +::: + +### 7.6.2 关于 `[ErrorCodeItemMetadata]` + +`Furion` 框架提供了`[ErrorCodeItemMetadata]` 特性用来标识**枚举字段**异常元数据,该特性支持传入 `消息内容` 和 `格式化参数`。最终会使用 `String.Format(消息内容,格式化参数)` 进行格式化。 + +如果消息内容中包含`格式化占位符`但未指定`格式化参数`,那么会查找异常所在方法是否贴有 `[IfException]` 特性且含有格式化参数,接着就会查找 `Oops.Oh` 中指定的 `格式化参数`。 + +### 7.6.3 静态异常类使用 + +```cs showLineNumbers {2,12} +using Furion.DynamicApiController; +using Furion.FriendlyException; + +namespace Furion.Application +{ + public class FurionAppService : IDynamicApiController + { + public int Get(int id) + { + if (id < 3) + { + throw Oops.Oh(ErrorCodes.z1000, id, 3); + } + + return id; + } + } +} +``` + +如下图所示: + + + +### 7.6.4 异常方法重试 + +:::important 调整说明 + +`v2.17.0+` 版本下面方法请使用 `Retry.Invoke()` 替代。 + +::: + +```cs showLineNumbers +Oops.Retry(() => { + // Do..... +}, 3, 1000); + +// 带返回值 +var value = Oops.Retry(() => { + // Do..... +}, 3, 1000); + +// 只有特定异常才监听 +Oops.Retry(() => { + +}, 3, 1000, typeof(ArgumentNullException)); +``` + +### 7.6.5 更多例子 + +```cs showLineNumbers {6} +throw Oops.Oh(1000); +throw Oops.Oh(ErrorCodes.x1000); +throw Oops.Oh("哈哈哈哈"); +throw Oops.Oh(errorCode: "x1001"); +throw Oops.Oh(1000, typeof(Exception)); +throw Oops.Oh(1000).StatusCode(400); // 设置错误码 +throw Oops.Oh(1000).WithData(new Model {}); // 设置额外数据 +throw Oops.Bah("用户名或密码错误"); // 抛出业务异常,状态码为 400 +throw Oops.Bah(1000); +``` + +## 7.7 多个异常信息类型 + +```cs showLineNumbers {5-6,21-22} +using Furion.FriendlyException; + +namespace Furion.Application +{ + [ErrorCodeType] + public enum ErrorCodes + { + [ErrorCodeItemMetadata("{0} 不能小于 {1}")] + z1000, + + [ErrorCodeItemMetadata("数据不存在")] + x1000, + + [ErrorCodeItemMetadata("{0} 发现 {1} 个异常", "百小僧", 2)] + x1001, + + [ErrorCodeItemMetadata("服务器运行异常", ErrorCode = "Error")] + SERVER_ERROR + } + + [ErrorCodeType] + public enum UserErrorCodes + { + [ErrorCodeItemMetadata("用户数据不存在")] + u1000, + + [ErrorCodeItemMetadata("其他异常")] + u1001 + } +} +``` + +:::important 特别注意 + +多个异常静态类中也必须保证常量值唯一性,不可重复。 + +::: + +## 7.8 `IErrorCodeTypeProvider` 提供器 + +在 `Furion` 框架中,还提供了 `IErrorCodeTypeProvider` 异常消息提供器接口,方便在不能贴 `[ErrorCodeType]` 特性情况下使用: + +```cs showLineNumbers {2,6,8-11} +using Furion.FriendlyException; +using System; + +namespace Furion.Application +{ + public class CustomErrorCodeTypeProvider : IErrorCodeTypeProvider + { + public Type[] Definitions => new[] { + typeof(ErrorCodes), + typeof(ErrorCodes2) + }; + } +} +``` + +启用 `IErrorCodeTypeProvider` 提供器: + +```cs showLineNumbers {11} title="Furion.Web.Core\FurWebCoreStartup.cs" +using Microsoft.Extensions.DependencyInjection; + +namespace Furion.Web.Core +{ + [AppStartup(800)] + public sealed class FurWebCoreStartup : AppStartup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers() + .AddFriendlyException(); + } + } +} +``` + +:::tip 小知识 + +只有使用 `IErrorCodeTypeProvider` 方式才需使用泛型方式注册。通过上面的方式注册可以同时支持 `IErrorCodeTypeProvider` 和 `[ErrorCodeType]` 方式。 + +::: + +## 7.9 `appsetting.json` 中配置 + +`Furion` 框架还提供了非常灵活的配置文件配置异常,通过这种方式可以实现异常信息后期配置,也就是无需在开发阶段预先定义。 + +```json showLineNumbers {2-8} title="Furion.Web.Entry/appsettings.json" +{ + "ErrorCodeMessageSettings": { + "Definitions": [ + ["5000", "{0} 不能小于 {1}"], + ["5001", "我叫 {0} 名字", "百小僧"], + ["5002", "Oops! 出错了"] + ] + } +} +``` + +`Definitions` 类型为二维数组,二维数组中的每一个数组第一个参数为 `ErrorCode` 也就是错误码,第二个参数为 `ErrorMessage` 消息内容,剩余参数作为 `ErrorMessage` 的格式化参数。 + +#### 使用示例 + +```cs showLineNumbers {2,12} +using Furion.DynamicApiController; +using Furion.FriendlyException; + +namespace Furion.Application +{ + public class FurionAppService : IDynamicApiController + { + public int Get(int id) + { + if (id < 3) + { + throw Oops.Oh(5000, id, 3); // 可以将 5000作为常量配置起来 + } + + return id; + } + } +} +``` + +:::tip 小知识 + +`[ErrorCodeType]` 和 `IErrorCodeTypeProvider` 和 `appsettings.json` 可以同时使用。 + +::: + +## 7.10 `[IfException]` 使用 + +`Furion` 框架提供了 `[IfException]` 特性可以**覆盖默认消息配置**。也就是覆盖 `异常消息类型` 和 `appsettings.json` 中的配置。 + +:::caution 特别注意 + +`[IfException]` 只能贴在方法上,支持多个。 + +::: + +### 7.10.1 使用示例 + +- 异常消息类定义 + +```cs showLineNumbers {1,4} +[ErrorCodeType] +public enum ErrorCodes +{ + [ErrorCodeItemMetadata("{0} 不能小于 {1}")] + z1000 +} +``` + +- 覆盖默认配置 + +```cs showLineNumbers {8} +using Furion.DynamicApiController; +using Furion.FriendlyException; + +namespace Furion.Application +{ + public class FurionAppService : IDynamicApiController + { + [IfException(ErrorCodes.z1000, ErrorMessage = "我覆盖了默认的:{0} 不能小于 {1}")] + public int Get(int id) + { + if (id < 3) + { + throw Oops.Oh(ErrorCodes.z1000, id, 3); + } + + return id; + } + } +} +``` + +如下图所示: + + + +### 7.10.2 更多例子 + +```cs showLineNumbers {2,8-11} +using Furion.DynamicApiController; +using Furion.FriendlyException; + +namespace Furion.Application +{ + public class FurionAppService : IDynamicApiController + { + [IfException(typeof(ExceptionType), ErrorMessage = "特定异常类型全局拦截")] + [IfException(ErrorMessage = "全局异常拦截")] + [IfException(ErrorCodes.z1000, ErrorMessage = "我覆盖了默认的:{0} 不能小于 {1}")] + [IfException(ErrorCodes.x1001, "格式化参数1", "格式化参数2", ErrorMessage = "我覆盖了默认的:{0} 不能小于 {1}")] + [IfException(ErrorCodes.x1000, "格式化参数1", "格式化参数2")] + [IfException(ErrorCodes.SERVER_ERROR, "格式化参数1", "格式化参数2")] + public int Get(int id) + { + if (id < 3) + { + throw Oops.Oh(ErrorCodes.z1000, id, 3); + } + + return id; + } + } +} +``` + +:::important 格式化流程 + +如果消息内容中包含`格式化占位符`但未指定`格式化参数`,那么会查找异常所在方法是否贴有 `[IfException]` 特性且含有格式化参数,接着就会查找 `Oops.Oh` 中指定的 `格式化参数`。 + +::: + +## 7.11 异常消息优先级 + +`[ErrorCodeItemMetadata]` -> `appsettings.json` -> `[IfException]`。**(低 -> 高)** + +- `[IfException]` 会覆盖 `appsettings.json` 定义的状态码消息。 +- `appsettings.json` 会覆盖 `[ErrorCodeItemMetadata]` 定义的消息。 + +## 7.12 多语言支持 + +参见 [【全球化和本地化(多语言)】](./local-language) 章节 + +## 7.13 规范化结果异常处理 + +:::tip 查看规范化结果文档 + +如需自定义规范化结果可查阅 【[6.7 统一返回值模型](./specification-document.mdx#67-统一返回值模型规范化结果api-返回值)】 + +::: + +## 7.14 全局异常处理提供器 + +通常我们需要在异常捕获的时候写日志,这时候就需要使用到 `IGlobalExceptionHandler` 异常定义处理程序,如: + +```cs showLineNumbers {8} +using Furion.DependencyInjection; +using Furion.FriendlyException; +using Microsoft.AspNetCore.Mvc.Filters; +using System.Threading.Tasks; + +namespace Furion.Application +{ + public class LogExceptionHandler : IGlobalExceptionHandler, ISingleton + { + public Task OnExceptionAsync(ExceptionContext context) + { + // 写日志 + + return Task.CompletedTask; + } + } +} +``` + +## 7.15 `FriendlyExceptionSettings` 配置 + +- `HideErrorCode`:隐藏错误码,`bool` 类型,默认 `false` +- `DefaultErrorCode`:默认错误码,`string` 类型 +- `DefaultErrorMessage`:默认错误消息,`string` 类型 +- `ThrowBah`:是否将 `Oops.Oh` 默认抛出为业务异常,`bool` 类型,默认 `false`,设置 `true` 之后 `Oops.Oh` 默认进入 `OnValidateFailed` 处理,而不是 `OnException` +- `LogError`:是否输出异常日志,`bool` 类型,默认 `true` + +配置示例 + +```json showLineNumbers +{ + "FriendlyExceptionSettings": { + "DefaultErrorMessage": "系统异常,请联系管理员" + } +} +``` + +## 7.16 `BadPageResult` 错误页 + +:::important 版本说明 + +以下内容仅限 `Furion 3.6.1 +` 版本使用。 + +::: + +`Furion` 在该版本之后内置了 `BadPageResult` 错误结果类型,该类型派生自 `IActionResult`,如需返回只需要在 `Action` 中返回即可。 + +```cs showLineNumbers{1,7} +using Furion.FriendlyException; + +public IActionResult Add(Person person) +{ + if(!ModelState.IsValid) + { + return new BadPageResult(); + } +} +``` + + + +- `BadPageResult` 实例属性和方法 + - 构造函数 `statusCode`:状态码,`int` 类,默认 `400` + - `Title`:页面标题,`string` 类型,默认 `ModelState Invalid` + - `Description`:页面描述,`string` 类型,默认 `User data verification failed. Please input it correctly.` + - `Code`:详细错误代码,`string` 类型,支持 `代码`,默认空字符串 + - `CodeLang`:详细错误代码语言,`string` 类型,默认 `json` + - `Base64Icon`:页面图标,`string` 类型,带默认值,自定义必须是 `base64` 格式图标 + - `ToString()`:将对象转换成 `HTML` 字符串 + - `ToByteArray()` 方法,可将对象转换成 `byte[]` 数组,通常使用 `Respose.Body.WriteAsync` 写入。 +- `BadPageResult` 静态属性 + - `Status401Unauthorized`:输出常规 `401` 状态码视图结果 + - `Status403Forbidden`:输出常规 `403` 状态码视图结果 + - `Status404NotFound`:输出常规 `404` 状态码视图结果 + - `Status500InternalServerError`:输出常规 `500` 状态码视图结果 + +## 7.17 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/get-start-net5.mdx b/handbook/docs/get-start-net5.mdx new file mode 100644 index 0000000000000000000000000000000000000000..3b911858587c63b3424fd253501b5df8de14e8a2 --- /dev/null +++ b/handbook/docs/get-start-net5.mdx @@ -0,0 +1,159 @@ +--- +id: get-start-net5 +title: 2.2 ASP.NET 5 集成 +sidebar_label: 2.2 ASP.NET 5 集成 +description: 学习如何在 ASP.NET 5 中集成 Furion +--- + +:::caution 尽快升级 + +截至 **2022 年 05 月 31 日**,微软已经停止了 `.NET5` 的技术支持,所以请尽快升级到 `.NET6` 版本,详细升级可查阅 【[.NET5 升级 .NET6](./net5-to-net6.mdx)】 + +::: + +:::tip 推荐使用脚手架 + +`Furion` 官方提供了非常灵活方便的脚手架,可以快速的创建多层架构项目。 + +推荐使用 《[2.6 官方脚手架](template.mdx)》代替本章节功能。 + +::: + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +:::note 入门条件 + +对 `.NET Core/ASP.NET Core` 有一定基础了解,还未接触的可先看 [【ASP.NET Core 基础】](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/?view=aspnetcore-5.0&tabs=windows) + +::: + +## 2.2.1 创建 `Web` 项目 + +:::note 环境要求 + +使用 `Furion` 之前先确保安装了最新的 `.NET 5 SDK` 并升级 `Visual Studio 2019` 至最新版。 + +::: + +### 2.2.1.1 创建 `ASP.NET Core Web 应用程序` + +- 打开 `Visual Studio 2019` 并创建 `Web` 项目 + + + +- 配置项目名称 + + + +- 选择 `WebAPI` 项目 + + + +:::warning 特别注意 + +`Furion` 已经内置了 `Swagger` 规范化库,所以创建时**无需勾选** `Enable OpenAPI support` 选项。否则提示版本不一致产生冲突。 + +::: + +## 2.2.2 添加 `Furion` 依赖包 + + + +## 2.2.3 `Furion` 基本配置 + +### 2.2.3.1 `Program.cs` 添加 `Inject()` + +```cs showLineNumbers {18} +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace FurionStart +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder + .Inject() // 添加这一行 + .UseStartup(); + }); + } +} +``` + +### 2.2.3.2 在 `Startup.cs` 中添加两个 `Inject()` + +```cs showLineNumbers {20,37} +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace FurionStart +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers().AddInject(); // 添加 AddInject(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthorization(); + + // 添加这一行,如果是 MVC和API共存项目,无需添加 string.Empty + app.UseInject(string.Empty); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } +} +``` + +:::important 小提醒 + +如果 `app.UseInject()` 不输入参数,则默认地址为 `/api`,如果输入 `string.Empty` 则为 `/` 目录。如果输入任意字符串,则为 `/任意字符串` 目录。 + +::: + +## 2.2.4 启动浏览器 + +启动浏览器查看效果。 + + + +:::note 小知识 + +默认情况下,通过 `Visual Studio 2019` 创建的项目会自动配置了启动页,如果使用 `F5` 运行,可能不会自动打开首页,这时候我们只需要配置 `launchSettings.json` 的 `launchUrl` 即可: + + + +::: diff --git a/handbook/docs/get-start-net6.mdx b/handbook/docs/get-start-net6.mdx new file mode 100644 index 0000000000000000000000000000000000000000..e6db3c013a02d3a603ed2ba7e8a15660fe49298e --- /dev/null +++ b/handbook/docs/get-start-net6.mdx @@ -0,0 +1,108 @@ +--- +id: get-start-net6 +title: 2.3 ASP.NET 6 集成 +sidebar_label: 2.3 ASP.NET 6 集成 +description: 学习如何在 ASP.NET 6 中集成 Furion +--- + +:::tip 推荐使用脚手架 + +`Furion` 官方提供了非常灵活方便的脚手架,可以快速的创建多层架构项目。 + +推荐使用 《[2.6 官方脚手架](template.mdx)》代替本章节功能。 + +::: + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +:::note 入门条件 + +对 `.NET Core/ASP.NET Core` 有一定基础了解,还未接触的可先看 [【ASP.NET Core 基础】](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/?view=aspnetcore-6.0&tabs=windows) + +::: + +## 2.3.1 创建 `Web` 项目 + +:::note 环境要求 + +使用 `Furion` 之前先确保安装了最新的 `.NET 6 SDK` 并安装 `Visual Studio 2022` 最新版。 + +::: + +### 2.3.1.1 创建 `ASP.NET Core Web 应用程序` + +- 打开 `Visual Studio 2022` 并创建 `Web` 项目 + + + +- 配置项目名称 + + + +- 选择 `WebAPI` 项目 + + + +:::warning 特别注意 + +`Furion` 已经内置了 `Swagger` 规范化库,所以创建时**无需勾选** `Enable OpenAPI support` 选项。否则提示版本不一致产生冲突。 + +::: + +## 2.3.2 添加 `Furion` 依赖包 + + + +## 2.3.3 `Furion` 基本配置 + +### 2.3.3.1 `Program.cs` 添加 `Inject()` + +```cs showLineNumbers {1,3,11} +var builder = WebApplication.CreateBuilder(args).Inject(); + +builder.Services.AddControllers().AddInject(); + +var app = builder.Build(); + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.UseInject(); + +app.MapControllers(); + +app.Run(); +``` + +:::important 小提醒 + +如果 `app.UseInject()` 不输入参数,则默认地址为 `/api`,如果输入 `string.Empty` 则为 `/` 目录。如果输入任意字符串,则为 `/任意字符串` 目录。 + +::: + +## 2.3.4 启动浏览器 + +启动浏览器查看效果。 + + + +:::note 小知识 + +默认情况下,通过 `Visual Studio 2022` 创建的项目会自动配置了启动页,如果使用 `F5` 运行,可能不会自动打开首页,这时候我们只需要配置 `launchSettings.json` 的 `launchUrl` 即可: + + + +::: + + +## 2.3.5 `WebApplication` 说明 🎃 + +`.NET6` 版本新增了 `WebApplication` 对象,如果我们需要注册服务,只需要通过 `builder.Services.AddXXX()` 即可。如: + +```cs showLineNumbers {4} +var builder = WebApplication.CreateBuilder(args).Inject(); + +builder.Services.AddControllers().AddInject(); +builder.Services.AddRemoteRequest(); +``` \ No newline at end of file diff --git a/handbook/docs/get-start-net7.mdx b/handbook/docs/get-start-net7.mdx new file mode 100644 index 0000000000000000000000000000000000000000..a5b637484776d4d465217a3f95d4d174107abd41 --- /dev/null +++ b/handbook/docs/get-start-net7.mdx @@ -0,0 +1,108 @@ +--- +id: get-start-net7 +title: 2.4 ASP.NET 7 集成 +sidebar_label: 2.4 ASP.NET 7 集成 +description: 学习如何在 ASP.NET 7 中集成 Furion +--- + +:::tip 推荐使用脚手架 + +`Furion` 官方提供了非常灵活方便的脚手架,可以快速的创建多层架构项目。 + +推荐使用 《[2.6 官方脚手架](template.mdx)》代替本章节功能。 + +::: + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +:::note 入门条件 + +对 `.NET Core/ASP.NET Core` 有一定基础了解,还未接触的可先看 [【ASP.NET Core 基础】](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/?view=aspnetcore-7.0&tabs=windows) + +::: + +## 2.4.1 创建 `Web` 项目 + +:::note 环境要求 + +使用 `Furion` 之前先确保安装了最新的 `.NET 7 SDK` 并安装 `Visual Studio 2022 Preview` 最新版。 + +::: + +### 2.4.1.1 创建 `ASP.NET Core Web 应用程序` + +- 打开 `Visual Studio 2022 Preview` 并创建 `Web` 项目 + + + +- 配置项目名称 + + + +- 选择 `WebAPI` 项目 + + + +:::warning 特别注意 + +`Furion` 已经内置了 `Swagger` 规范化库,所以创建时**无需勾选** `Enable OpenAPI support` 选项。否则提示版本不一致产生冲突。 + +::: + +## 2.4.2 添加 `Furion` 依赖包 + + + +## 2.4.3 `Furion` 基本配置 + +### 2.4.3.1 `Program.cs` 添加 `Inject()` + +```cs showLineNumbers {1,3,11} +var builder = WebApplication.CreateBuilder(args).Inject(); + +builder.Services.AddControllers().AddInject(); + +var app = builder.Build(); + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.UseInject(); + +app.MapControllers(); + +app.Run(); +``` + +:::important 小提醒 + +如果 `app.UseInject()` 不输入参数,则默认地址为 `/api`,如果输入 `string.Empty` 则为 `/` 目录。如果输入任意字符串,则为 `/任意字符串` 目录。 + +::: + +## 2.4.4 启动浏览器 + +启动浏览器查看效果。 + + + +:::note 小知识 + +默认情况下,通过 `Visual Studio 2022 Preview` 创建的项目会自动配置了启动页,如果使用 `F5` 运行,可能不会自动打开首页,这时候我们只需要配置 `launchSettings.json` 的 `launchUrl` 即可: + + + +::: + + +## 2.4.5 `WebApplication` 说明 🎃 + +`.NET6` 版本新增了 `WebApplication` 对象,如果我们需要注册服务,只需要通过 `builder.Services.AddXXX()` 即可。如: + +```cs showLineNumbers {4} +var builder = WebApplication.CreateBuilder(args).Inject(); + +builder.Services.AddControllers().AddInject(); +builder.Services.AddRemoteRequest(); +``` \ No newline at end of file diff --git a/handbook/docs/get-start-net8.mdx b/handbook/docs/get-start-net8.mdx new file mode 100644 index 0000000000000000000000000000000000000000..c8e8732781788cb56c93da8ba6b0d20eaed34c0d --- /dev/null +++ b/handbook/docs/get-start-net8.mdx @@ -0,0 +1,107 @@ +--- +id: get-start-net8 +title: 2.5 ASP.NET 8 集成 +sidebar_label: 2.5 ASP.NET 8 集成 ✨ +description: 学习如何在 ASP.NET 8 中集成 Furion +--- + +:::tip 推荐使用脚手架 + +`Furion` 官方提供了非常灵活方便的脚手架,可以快速的创建多层架构项目。 + +推荐使用 《[2.6 官方脚手架](template.mdx)》代替本章节功能。 + +::: + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +:::note 入门条件 + +对 `.NET Core/ASP.NET Core` 有一定基础了解,还未接触的可先看 [【ASP.NET Core 基础】](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/?view=aspnetcore-8.0&tabs=windows) + +::: + +## 2.5.1 创建 `Web` 项目 + +:::note 环境要求 + +使用 `Furion` 之前先确保安装了最新的 `.NET 8 SDK` 并安装 `Visual Studio 2022 Preview` 最新版。 + +::: + +### 2.5.1.1 创建 `ASP.NET Core Web 应用程序` + +- 打开 `Visual Studio 2022 Preview` 并创建 `Web` 项目 + + + +- 配置项目名称 + + + +- 选择 `WebAPI` 项目 + + + +:::warning 特别注意 + +`Furion` 已经内置了 `Swagger` 规范化库,所以创建时**无需勾选** `Enable OpenAPI support` 选项。否则提示版本不一致产生冲突。 + +::: + +## 2.5.2 添加 `Furion` 依赖包 + + + +## 2.5.3 `Furion` 基本配置 + +### 2.5.3.1 `Program.cs` 添加 `Inject()` + +```cs showLineNumbers {1,3,11} +var builder = WebApplication.CreateBuilder(args).Inject(); + +builder.Services.AddControllers().AddInject(); + +var app = builder.Build(); + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.UseInject(); + +app.MapControllers(); + +app.Run(); +``` + +:::important 小提醒 + +如果 `app.UseInject()` 不输入参数,则默认地址为 `/api`,如果输入 `string.Empty` 则为 `/` 目录。如果输入任意字符串,则为 `/任意字符串` 目录。 + +::: + +## 2.5.4 启动浏览器 + +启动浏览器查看效果。 + + + +:::note 小知识 + +默认情况下,通过 `Visual Studio 2022 Preview` 创建的项目会自动配置了启动页,如果使用 `F5` 运行,可能不会自动打开首页,这时候我们只需要配置 `launchSettings.json` 的 `launchUrl` 即可: + + + +::: + +## 2.5.5 `WebApplication` 说明 🎃 + +`.NET6` 版本新增了 `WebApplication` 对象,如果我们需要注册服务,只需要通过 `builder.Services.AddXXX()` 即可。如: + +```cs showLineNumbers {4} +var builder = WebApplication.CreateBuilder(args).Inject(); + +builder.Services.AddControllers().AddInject(); +builder.Services.AddRemoteRequest(); +``` diff --git a/handbook/docs/global/app.mdx b/handbook/docs/global/app.mdx new file mode 100644 index 0000000000000000000000000000000000000000..1819c273e6f5dbf48357c1861c531e09bcbea75c --- /dev/null +++ b/handbook/docs/global/app.mdx @@ -0,0 +1,337 @@ +--- +id: app +title: 1. App 静态类 +sidebar_label: 1. App 静态类 +--- + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **新特性** + + -  新增 `App.CompileCSharpClassCode(code)` 动态编译类定义代码 4.8.8.7 ⏱️2023.04.30 [fe1e8a1](https://gitee.com/dotnetchina/Furion/commit/fe1e8a1768c7020477684689b35a2a1349ec2b01) + -  新增 `App.GetServices(type)` 和 `App.GetServices()` 获取服务实例集合 4.8.7.33 ⏱️2023.04.03 [c3e9957](https://gitee.com/dotnetchina/Furion/commit/c3e9957fd276920b3a8366eda3e347500334458e) + -  新增 `App.GetServiceLifetime(type)` 获取服务注册生命周期类型 4.8.5.3 ⏱️2023.01.31 [4a573a8](https://gitee.com/dotnetchina/Furion/commit/4a573a8934784b1d7e11f7d1fa3cfa65b5ec2b3a) + -  新增 **`App.GetThreadId()` 和 `App.GetTraceId()` 获取线程 `Id` 和请求 `TraceId`** 4.8.2.4 ⏱️2022.11.29 [910fc1f](https://gitee.com/dotnetchina/Furion/commit/910fc1fbad9e40245c5694f30457cd4d2ca0d630) + -  新增 **`App.GetExecutionTime(() => { /*Your Code*/ })` 获取代码执行耗时** 4.8.2.4 ⏱️2022.11.29 [5ab4b19](https://gitee.com/dotnetchina/Furion/commit/5ab4b19786e53d8934de08fd2f5430fc66c3b9a1) + +- **问题修复** + + -  修复 `App.CompileCSharpClassCode(code)` 运行时添加匿名程序集编译异常问题 4.8.8.8 ⏱️2023.05.04 [322ea59](https://gitee.com/dotnetchina/Furion/commit/322ea599ed58b1804e9f8ab85d7ed44882b3e5a8) + +
+
+
+ +## 1.1 获取全局配置 + +```cs showLineNumbers +var settings = App.Settings; +``` + +## 1.2 获取配置对象 + +```cs showLineNumbers {1,6,9} +// 获取 IConfiguration 对象 +var configuration = App.Configuration; +var value = configuration["xxx:xxx"]; + +// 获取指定节点值并转成 T 类型 +var data = App.GetConfig("key:key2"); + +// 重载/刷新配置 +App.Configuration.Reload(); +``` + +## 1.3 获取环境对象 + +```cs showLineNumbers +var webHostEnvironment = App.HostEnvironment; +``` + +## 1.4 获取项目所有程序集 + +```cs showLineNumbers +var assemblies = App.Assemblies; +``` + +## 1.5 获取项目所有有效类型 + +```cs showLineNumbers +var types = App.EffectiveTypes; +``` + +## 1.6 获取 `HttpContext` + +```cs showLineNumbers +var httpContext = App.HttpContext; +``` + +## 1.7 获取登录的 `User` 对象 + +```cs showLineNumbers +var contextUser = App.User; + +// 获取 `Jwt` 存储的信息 +var userId = App.User?.FindFirstValue("键"); +``` + +**注意引入 `System.Security.Claims` 命名空间** + +## 1.8 获取服务提供器 + +```cs showLineNumbers +var serviceProvider = App.ServiceProvider; + +// 获取根服务,通常用来解析单例,可优化性能 +var rootService = App.RootServices; +``` + +## 1.9 解析服务 + +```cs showLineNumbers +var service = App.GetService([IServiceProvider]); +var service2 = App.GetService(typeof(TService), [IServiceProvider]); + +var service3 = App.GetRequiredService([IServiceProvider]); +var service4 = App.GetRequiredService(typeof(TService), [IServiceProvider]); + +// Furion 4.8.7.33+ 支持 +var services = App.GetServices([IServiceProvider]); +var services = App.GetServices(typeof(TService), [IServiceProvider]); +``` + +## 1.10 获取选项配置 + +```cs showLineNumbers +var options = App.GetOptions([IServiceProvider]); +var options2 = App.GetOptionsMonitor([IServiceProvider]); +var options3 = App.GetOptionsSnapshot([IServiceProvider]); +``` + +## 1.11 打印数据到 `MiniProfiler` + +```cs showLineNumbers +App.PrintToMiniProfiler("分类", "状态", "要打印的消息"); +``` + +## 1.12 获取应用名称 + +```cs showLineNumbers +var applicationName = App.HostEnvironment.ApplicationName; +``` + +## 1.13 获取启动项目根目录 + +```cs showLineNumbers +var webRootPath = App.HostEnvironment.ContentRootPath; +``` + +## 1.14 获取网站根目录 `wwwroot` 目录 + +```cs showLineNumbers +var wwwroot = App.WebHostEnvironment.WebRootPath; +``` + +注意:可能个别操作系统获取值为 `null`。 + +## 1.15 获取启动项目所在程序集 + +```cs showLineNumbers +var webAssembly = Assembly.GetEntryAssembly(); +``` + +## 1.16 获取启动项目 `bin` 目录 + +```cs showLineNumbers +var binPath = AppContext.BaseDirectory; +``` + +## 1.17 获取环境变量名 + +```cs showLineNumbers +var environmentName = App.HostEnvironment.EnvironmentName; +``` + +## 1.18 判断系统环境 + +```cs showLineNumbers {2,5,8,11} +// 判断是否开发环境 +var isDevelopment = App.HostEnvironment.IsDevelopment(); + +// 判断是否生产环境 +var isProduction = App.HostEnvironment.IsProduction(); + +// 判断是否 Stage 环境 +var isStaging = App.HostEnvironment.IsStaging(); + +// 判断是否是特定环境,比如自定义测试环境 +var isTest = App.HostEnvironment.IsEnvironment("TestEnvironment"); +``` + +**注意,需引用 `Microsoft.Extensions.Hosting` 命名空间** + +## 1.19 获取服务器信息 + +```cs showLineNumbers {2,5,8,11} +// 获取系统架构 +var osArchitecture = RuntimeInformation.OSArchitecture; // => X64 + +// 获取系统名称 +var osDescription = RuntimeInformation.OSDescription; // => Windows 10 企业版 + +// 获取进程架构 +var processArchitecture = RuntimeInformation.ProcessArchitecture; // => X64 + +// 是否是64位操作系统 +var is64BitOperatingSystem = Environment.Is64BitOperatingSystem; // => True +``` + +## 1.20 获取框架底层所有未托管对象 + +```cs showLineNumbers +var objs = App.UnmanagedObjects; +``` + +## 1.21 手动释放非托管对象 + +```cs showLineNumbers +App.DisposeUnmanagedObjects(); // 通常在非 `Web` 环境中手动处理释放时机 +``` + +## 1.22 判断是否是单文件环境 + +:::important 版本说明 + +以下内容仅限 `Furion 3.6.8 +` 版本使用。 + +::: + +```cs showLineNumbers +bool isSingleFileEnviroment = App.SingleFileEnvironment; +``` + +## 1.23 解析命令行参数 + +:::important 版本说明 + +以下内容仅限 `Furion 4.4.5 +` 版本使用。 + +::: + +```cs showLineNumbers +var cmdConfig = App.GetCommandLineConfiguration(args); +cmdConfig.TryGet("参数", out var value); +``` + +## 1.24 获取当前线程 `Id` + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.2.4 +` 版本使用。 + +::: + +```cs showLineNumbers +var threadId = App.GetThreadId(); +``` + +## 1.25 获取当前请求 `TraceId` + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.2.4 +` 版本使用。 + +::: + +```cs showLineNumbers +var traceId = App.GetTraceId(); +``` + +## 1.26 获取代码执行耗时 + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.2.4 +` 版本使用。 + +::: + +```cs showLineNumbers +var elapsedMilliseconds = App.GetExecutionTime(() => +{ + Console.WriteLine("Hello, Furion"); +}); +``` + +## 1.27 获取服务注册生命周期类型 + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.5.3 +` 版本使用。 + +::: + +```cs showLineNumbers +var lifetime = App.GetServiceLifetime(typeof(IConfiguration)); // => ServiceLifetime.Singleton +var license = App.GetServiceLifetime(typeof(IRepository)); // => ServiceLifetime.Scoped +``` + +## 1.28 动态编译类定义代码 + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.8.7 +` 版本使用。 + +::: + +```cs showLineNumbers {1,28} +var jobAssembly = App.CompileCSharpClassCode(@" +using Furion.Schedule; +using Microsoft.Extensions.Logging; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace YourProject; + +public class MyJob : IJob +{ + private readonly ILogger _logger; + + public MyJob(ILogger logger) + { + _logger = logger; + } + + public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + _logger.LogInformation($""我是 Roslyn 方式创建的:{context}""); + await Task.CompletedTask; + } +} +"); + +// 生成运行时 MyJob 类型 +var jobType = jobAssembly.GetType("YourProject.MyJob"); +``` + +- 支持返回 `MemoryStream` 对象 + +```cs showLineNumbers {2} +// 返回内存 +var memoryStream = App.CompileCSharpClassCodeToStream("C# 类定义代码"); + +// 转换成程序集 +var assembly = Assembly.Load(memoryStream.ToArray()); +``` + +- 支持保存为 `dll` 文件 + +```cs showLineNumbers {2} +// 保存为 .dll 文件并返回程序集 +var assembly = App.CompileCSharpClassCodeToDllFile("C# 类定义代码"); +``` diff --git a/handbook/docs/global/datavalidator.mdx b/handbook/docs/global/datavalidator.mdx new file mode 100644 index 0000000000000000000000000000000000000000..d698bd31c9b4e449ffd1842f6a17f9c3cf696b19 --- /dev/null +++ b/handbook/docs/global/datavalidator.mdx @@ -0,0 +1,31 @@ +--- +id: datavalidator +title: 3. DataValidator 静态类 +sidebar_label: 3. DataValidator 静态类 +--- + +## 3.1 验证对象数据 + +```cs showLineNumbers +var result = DataValidator.TryValidateObject(obj); +``` + +## 3.2 验证单个值 + +```cs showLineNumbers +var result = DataValidator.TryValidateValue(value, typeof(RequiredAttribure), typeof(RangeAttribute)); +``` + +## 3.3 正则表达式验证单个值 + +```cs showLineNumbers +var result = DataValidator.TryValidateValue(value,"正则表达式"); +``` + +## 3.4 验证类型验证单个值 + +```cs showLineNumbers +var result = DataValidator.TryValidateValue(value, ValidationTypes.Number); + +var result2 = DataValidator.TryValidateValue(value, ValidationPattern.AllOfThem, ValidationTypes.Number, ValidationTypes.Required); +``` diff --git a/handbook/docs/global/db.mdx b/handbook/docs/global/db.mdx new file mode 100644 index 0000000000000000000000000000000000000000..60e8def76046332714112eb6414b22852fe20979 --- /dev/null +++ b/handbook/docs/global/db.mdx @@ -0,0 +1,106 @@ +--- +id: db +title: 2. Db 静态类 +sidebar_label: 2. Db 静态类 +--- + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **新特性** + + -  新增 `Db.GetNewDbContext()` 多个重载方法,实现类似 `new DbContext()` 操作 4.8.8.55 ⏱️2023.11.09 [4157629](https://gitee.com/dotnetchina/Furion/commit/41576295473fefc47f6961154909a690d7c5ed58) + +
+
+
+ +## 2.1 获取非泛型仓储 + +```cs showLineNumbers +var repository = Db.GetRepository(); +``` + +## 2.2 获取泛型仓储 + +```cs showLineNumbers +var entityRepository = Db.GetRepository(); +``` + +## 2.3 获取带定位器泛型仓储 + +```cs showLineNumbers +var locatorRepository = Db.GetRepository(); +``` + +## 2.4 获取 `Sql` 仓储 + +```cs showLineNumbers +var sqlRepository = Db.GetSqlRepository(); +``` + +## 2.5 获取 `Sql` 定位器仓储 + +```cs showLineNumbers +var sqlLocatorRepository = Db.GetSqlRepository(); +``` + +## 2.6 获取 `Sql` 代理对象 + +```cs showLineNumbers +var sqlProxy= Db.GetSqlProxy(); +``` + +## 2.7 获取默认数据库上下文 + +```cs showLineNumbers +var dbContext = Db.GetDbContext(); +``` + +## 2.8 获取定位器数据库上下文 + +```cs showLineNumbers +var locatorDbContext = Db.GetDbContext(); +var locatorDbContext2 = Db.GetDbContext(typeof(TDbContextLocator)); +``` + +## 2.9 创建新的默认数据库上下文 + +```cs showLineNumbers +using var dbContext = Db.GetNewDbContext(); // 别忘记 using,Furion 4.8.8.55+ 版本有效 +``` + +## 2.10 创建新的定位器数据库上下文 + +```cs showLineNumbers +using var locatorDbContext = Db.GetNewDbContext(); // 别忘记 using,Furion 4.8.8.55+ 版本有效 +using var locatorDbContext2 = Db.GetNewDbContext(typeof(TDbContextLocator)); // 别忘记 using,Furion 4.8.8.55+ 版本有效 +``` + +## 2.11 根据定位器类型获取仓储 + +```cs showLineNumbers +var repository = Db.GetRepository(dbContextLocatorType); +``` + +## 2.12 获取 `主从库` 仓储 + +```cs showLineNumbers +var msRepository = Db.GetMSRepository(); +``` + +## 2.13 获取 `Sql` 主库定位器仓储 + +```cs showLineNumbers +var msLocatorRepository = Db.GetMSRepository(); +``` + +## 2.14 获取特定定位器仓储 + +```cs showLineNumbers +var msLocatorRepository = Db.GetDbRepository(); +``` \ No newline at end of file diff --git a/handbook/docs/global/fs.mdx b/handbook/docs/global/fs.mdx new file mode 100644 index 0000000000000000000000000000000000000000..c8bf0eeb0399b2b2145ec0fcd1749d1a26579533 --- /dev/null +++ b/handbook/docs/global/fs.mdx @@ -0,0 +1,53 @@ +--- +id: fs +title: 13. FS 静态类 +sidebar_label: 13. FS 静态类 +--- + +## 13.1 获取物理文件提供器 + +```cs showLineNumbers +var fileProvider = FS.GetPhysicalFileProvider(@"c:/test"); +``` + +## 13.2 获取嵌入资源文件提供器 + +```cs showLineNumbers +var fileProvider = FS.GetEmbeddedFileProvider(Assembly.GetEntryAssembly()); +``` + +## 13.3 获取文件提供器 + +```cs showLineNumbers +var fileProvider = FS.GetFileProvider(FileProviderTypes.Physical, @"c:/test"); +``` + +## 13.4 特别注意 + +使用该功能需确保 `services.AddVirtualFileServer()` 已注册。 + +```cs showLineNumbers +services.AddVirtualFileServer(); +``` + +## 13.5 获取文件 `ContentType` 或 `Mime` + +:::warning 版本说明 + +以下内容仅限 `Furion 3.3.1 +` 版本使用。 + +::: + +```cs showLineNumbers +var success = FS.TryGetContentType("image.png", out var contentType); // image/png +``` + +## 13.6 解决不受支持的文件 `MIME` 类型 + +解决不受支持的文件 `MIME` 出现 `404` 问题。 + +```cs showLineNumbers +app.UseStaticFiles(new StaticFileOptions { + ContentTypeProvider = FS.GetFileExtensionContentTypeProvider() +}) +``` diff --git a/handbook/docs/global/json.mdx b/handbook/docs/global/json.mdx new file mode 100644 index 0000000000000000000000000000000000000000..e3c22910264e3135011648dbde1dee1d7766b975 --- /dev/null +++ b/handbook/docs/global/json.mdx @@ -0,0 +1,35 @@ +--- +id: json +title: 10. JSON 静态类 +sidebar_label: 10. JSON 静态类 +--- + +:::important 版本说明 + +以下内容仅限 `Furion 1.16.0 +` 版本使用。 + +::: + +## 10.1 获取序列化提供器 + +```cs showLineNumbers +var serializer = JSON.GetJsonSerializer(); +``` + +## 10.2 序列化 + +```cs showLineNumbers +var str = JSON.Serialize(obj, [options]); +``` + +## 10.3 反序列化 + +```cs showLineNumbers +var obj = JSON.Deserialize(str, [options]); +``` + +## 10.4 获取全局配置 + +```cs showLineNumbers +var options = JSON.GetSerializerOptions(); +``` diff --git a/handbook/docs/global/jsonserializer.mdx b/handbook/docs/global/jsonserializer.mdx new file mode 100644 index 0000000000000000000000000000000000000000..2f845a400dd9214ac09ad1dc1646194eb3eab085 --- /dev/null +++ b/handbook/docs/global/jsonserializer.mdx @@ -0,0 +1,43 @@ +--- +id: jsonserializer +title: 7. JsonSerializerUtility 静态类 +sidebar_label: 7. JsonSerializerUtility 静态类 +--- + +:::warning 重要声明 + +以下内容在 `Furion 1.16.0 +` 版本中已移除。请使用 [【23. JSON 序列化章节】](/docs/json-serialization) + +::: + +## 7.1 序列化 + +```cs showLineNumbers +var str = JsonSerializerUtility.Serialize(obj, [options]); +``` + +## 7.2 反序列化 + +```cs showLineNumbers +var obj = JsonSerializerUtility.Deserialize(str, [options]); +``` + +## 7.3 获取默认序列化配置 + +```cs showLineNumbers +var options = JsonSerializerUtility.GetDefaultJsonSerializerOptions(); +``` + +## 7.4 设置序列化属性首字母大写 + +```cs showLineNumbers {2} +services.AddControllersWithViews() + .AddJsonSerializerPascalPropertyNaming(); +``` + +## 7.5 设置时间输出统一格式化 + +```cs showLineNumbers {2} +services.AddControllersWithViews() + .AddDateTimeJsonConverter("yyyy-MM-dd HH:mm:ss"); +``` diff --git a/handbook/docs/global/jwt.mdx b/handbook/docs/global/jwt.mdx new file mode 100644 index 0000000000000000000000000000000000000000..979b627c2e2f0a1a9e2f043d71c68f1ac9948ca2 --- /dev/null +++ b/handbook/docs/global/jwt.mdx @@ -0,0 +1,75 @@ +--- +id: jwt +title: 14. JWTEncryption 静态类 +sidebar_label: 14. JWTEncryption 静态类 +--- + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **问题修复** + + -  修复 `JWTEncryption.GetJWTSettings()` 独立使用时无法获取自定义配置 4.9.1.4 ⏱️2023.11.18 [c045e08](https://gitee.com/dotnetchina/Furion/commit/c045e084670a98f71d5ea5ed55ca5cbbfc981e0b) + +
+
+
+ +## 14.1 生成 `Token` + +```cs showLineNumbers +// 读取配置信息生成 Token +var token = JWTEncryption.Encrypt(new Dictionary { { "UserId", 1 } }); + +// 配置 Token 过期时间 +var token = JWTEncryption.Encrypt(new Dictionary { { "UserId", 1 } }, 20); + +// 配置 Token 密钥 +var token = JWTEncryption.Encrypt("monksoul@outlook.com", new Dictionary { { "UserId", 1 } }); + +// 配置算法 +var token = JWTEncryption.Encrypt("monksoul@outlook.com", new Dictionary { { "UserId", 1 } }, SecurityAlgorithms.HmacSha256); +``` + +## 14.2 生成刷新 `Token` + +```cs showLineNumbers +var refreshToken = JWTEncryption.GenerateRefreshToken("token", 43200); +``` + +## 14.3 根据过期 `Token` 交换新 `Token` + +```cs showLineNumbers +var newToken = JWTEncryption.Exchange("过期 token", "与之匹配的刷新 token", [新的token过期时间], [容错值]); +``` + +## 14.4 授权处理程序自动刷新 `Token` + +```cs showLineNumbers +JWTEncryption.AutoRefreshToken(context, httpContext); +``` + +## 14.5 验证 `Token` 有效性 + +```cs showLineNumbers +var (isValid, tokenData, validationResult) = JWTEncryption.Validate("token"); +``` + +## 14.6 验证请求中 `Token` 有效性 + +```cs showLineNumbers +var isValid = JWTEncryption.ValidateJwtBearerToken(httpContext, out tokenInfo); +``` + +## 14.7 读取 `Token` 信息(不含验证) + +```cs showLineNumbers +var tokenInfo = JWTEncryption.ReadJwtToken("token"); + +// 3.8.2+ 之后支持更强大的读取 +var securityToken = JWTEncryption.SecurityReadJwtToken("token"); +``` diff --git a/handbook/docs/global/l.mdx b/handbook/docs/global/l.mdx new file mode 100644 index 0000000000000000000000000000000000000000..bc550f0f31dbe2ebde460b7c98925df84fba0df5 --- /dev/null +++ b/handbook/docs/global/l.mdx @@ -0,0 +1,49 @@ +--- +id: l +title: 8. L 静态类 +sidebar_label: 8. L 静态类 +--- + +## 8.1 转换文本多语言 + +```cs showLineNumbers +var apiInterface = L.Text["API 接口"]; +``` + +## 8.2 转换 Html 多语言 + +```cs showLineNumbers +var name = L.Html["Hello {0}", name]; +``` + +## 8.3 设置当前语言 + +```cs showLineNumbers +L.SetCulture("en-US"); // 默认只对下一次请求有效 + +L.SetCulture("en-US"); // 立即有效 +``` + +## 8.4 获取系统语言列表 + +```cs showLineNumbers +var list = L.GetCultures(); +``` + +## 8.5 获取当前选中语言 + +```cs showLineNumbers +var list = L.GetSelectCulture(); +``` + +## 8.6 设置当前线程 UI 区域性 + +```cs showLineNumbers +L.SetCurrentUICulture("en-US"); +``` + +## 8.7 获取当前线程 UI 区域性 + +```cs showLineNumbers +var culture = L.GetCurrentUICulture(); +``` \ No newline at end of file diff --git a/handbook/docs/global/linqexpression.mdx b/handbook/docs/global/linqexpression.mdx new file mode 100644 index 0000000000000000000000000000000000000000..a89da401862bb96f43a400c3c2f98e492abfa0ee --- /dev/null +++ b/handbook/docs/global/linqexpression.mdx @@ -0,0 +1,31 @@ +--- +id: linqexpression +title: 5. LinqExpression 静态类 +sidebar_label: 5. LinqExpression 静态类 +--- + +## 5.1 创建一个表达式 + +```cs showLineNumbers +var expression = LinqExpression.Create(u => u.Id == 1); +var expression2 = LinqExpression.Create((u,i) => u.Id == 1 && i > 0); +``` + +## 5.2 拼接两个表达式 + +```cs showLineNumbers +// 创建一个初始化的表达式 +var expression = LinqExpression.And(); +var expression2 = LinqExpression.IndexAnd(); +var expression3 = LinqExpression.Or(); +var expression4 = LinqExpression.IndexOr(); + +// 拼接表达式 +var expression5 = expression.And(expression2); +var expression6 = expression.AndIf(age > 18, expression2); +var expression7 = expression.Or(expression2); +var expression8 = expression.OrIf(age > 18, expression2); + +// 获取表达式属性名 +var properyName = expression.GetExpressionPropertyName(u => u.Name); // Name +``` diff --git a/handbook/docs/global/log.mdx b/handbook/docs/global/log.mdx new file mode 100644 index 0000000000000000000000000000000000000000..427a8303bee78f501b23fe51778c45ea92ea0f65 --- /dev/null +++ b/handbook/docs/global/log.mdx @@ -0,0 +1,33 @@ +--- +id: log +title: 16. Log 静态类 +sidebar_label: 16. Log 静态类 +--- + +:::important 版本说明 + +以下内容仅限 `Furion 4.2.1 +` 版本使用。 + +::: + +## 16.1 常见操作 + +```cs showLineNumbers {2,5,10-15} +// 创建日志对象 +var logger = Log.CreateLogger("日志名称"); + +// 创建日志工厂 +using var loggerFactory = Log.CreateLoggerFactory(builder => { + // .... +}); + +// 日志记录 +Log.Information("Information"); +Log.Warning("Warning"); +Log.Error("Error"); +Log.Debug("Debug"); +Log.Trace("Trace"); +Log.Critical("Critical"); +``` + + diff --git a/handbook/docs/global/messagecenter.mdx b/handbook/docs/global/messagecenter.mdx new file mode 100644 index 0000000000000000000000000000000000000000..d1bb4a2ed8362aec43eeb0aa0d0eb259bd9406ed --- /dev/null +++ b/handbook/docs/global/messagecenter.mdx @@ -0,0 +1,28 @@ +--- +id: messagecenter +title: 9. MessageCenter 静态类 +sidebar_label: 9. MessageCenter 静态类 +--- + +## 9.1 动态订阅消息 + +```cs showLineNumbers +MessageCenter.Subscribe("messageId", async (ctx) => { + Console.WriteLine("我是动态的"); + await Task.CompletedTask; +}); +``` + +## 9.2 发送消息 + +```cs showLineNumbers +await MessageCenter.PublishAsync("messageId", new {}); + +// 诸多重载 +``` + +## 9.3 取消订阅 + +```cs showLineNumbers +MessageCenter.Unsubscribe("messageId"); +``` diff --git a/handbook/docs/global/native.mdx b/handbook/docs/global/native.mdx new file mode 100644 index 0000000000000000000000000000000000000000..d7eef5706cbae8f03f0be7df029526d036ca24bd --- /dev/null +++ b/handbook/docs/global/native.mdx @@ -0,0 +1,25 @@ +--- +id: native +title: 19. Native 静态类 +sidebar_label: 19. Native 静态类 +--- + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.7.23 +` 版本使用。 + +::: + +## 19.1 在 `WinForm/WPF` 中创建窗口实例 + +```cs showLineNumbers +var form1 = Native.CreateInstance(); + +var form1 = Native.CreateInstance(typeof(Form1)) as Form; +``` + +## 19.2 获取随机空闲端口 + +```cs showLineNumbers +var port = Native.GetIdlePort(); +``` \ No newline at end of file diff --git a/handbook/docs/global/oops.mdx b/handbook/docs/global/oops.mdx new file mode 100644 index 0000000000000000000000000000000000000000..59c62044ae14ecc174445b93fa292902cdb27738 --- /dev/null +++ b/handbook/docs/global/oops.mdx @@ -0,0 +1,76 @@ +--- +id: oops +title: 4. Oops 静态类 +sidebar_label: 4. Oops 静态类 +--- + +## 4.1 抛出字符串异常 + +```cs showLineNumbers +throw Oops.Oh("异常消息"); +throw Oops.Oh("异常消息:{0}", "出错了"); +``` + +## 4.2 指定类型的异常 + +```cs showLineNumbers +throw Oops.Oh("异常消息", typeof(ArgumentNullException)); +throw Oops.Oh("异常消息:{0}", typeof(ArgumentNullException), "出错了"); +``` + +## 4.3 状态码异常 + +```cs showLineNumbers +throw Oops.Oh(1000); +throw Oops.Oh(1000, "出错了"); +``` + +## 4.4 状态码异常 + +```cs showLineNumbers +throw Oops.Oh(1000, typeof(ArgumentNullException)); +throw Oops.Oh(1000, typeof(ArgumentNullException), "出错了"); +``` + +## 4.5 异常方法重试 + +:::important 调整说明 + +`v2.17.0+` 版本下面方法请使用 `Retry.Invoke()/Retry.InvokeAsync()` 替代。 + +::: + +```cs showLineNumbers +Oops.Retry(() => { + // Do..... +}, 3, 1000); + +// 带返回值 +var value = Oops.Retry(() => { + // Do..... +}, 3, 1000); + +// 只有特定异常才监听 +Oops.Retry(() => { + +}, 3, 1000, typeof(ArgumentNullException)); +``` + +## 4.6 抛出业务异常 + +```cs showLineNumbers +throw Oops.Bah("用户名或密码错误"); +throw Oops.Bah(1000); +``` + +## 4.7 设置响应状态码 + +```cs showLineNumbers +throw Oops.Oh("错误了").StatusCode(502); +``` + +## 4.8 携带额外数据 + +```cs showLineNumbers +throw Oops.Oh("错误了").WithData(new Model {}); +``` \ No newline at end of file diff --git a/handbook/docs/global/schedular.mdx b/handbook/docs/global/schedular.mdx new file mode 100644 index 0000000000000000000000000000000000000000..67170c88e9fe38d1ce822f46ac79b2d2cfcd403f --- /dev/null +++ b/handbook/docs/global/schedular.mdx @@ -0,0 +1,78 @@ +--- +id: schedular +title: 17. Schedular 静态类 +sidebar_label: 17. Schedular 静态类 +--- + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.0 +` 版本使用。 + +::: + +## 17.1 启动调度作业服务 + +该功能 **建议** 仅限不能通过 `services.AddXXX` 方式使用,比如控制台,`Winfrom/WPF` 等。 + +```cs showLineNumbers {1,3} +IDisposable dispose = ScheduleServe.Run(options => +{ + options.AddJob(Triggers.Secondly()); +}); +``` + +这种方式有一个隐藏的巨大隐藏 “骚操作”:**可以在任何地方创建作业调度服务,多次调用可以创建多个作业调度器。** + +:::tip 推荐使用 `Serve.Run()` 或 `Serve.RunGeneric()` 方式替代 + +`Furion` 框架提供了 `Serve.Run()` 方式支持跨平台使用,还能支持注册更多服务,如: + +```cs showLineNumbers {1,3,5} +Serve.Run(services => +{ + services.AddSchedule(options => + { + options.Add(Triggers.Secondly()); + }); +}) +``` + +如无需 `Web` 功能,可通过 `Serve.RunGeneric` 替代 `Serve.Run`。 + +::: + +## 17.2 获取作业调度计划工厂 + +```cs showLineNumbers +var schedulerFactory = Schedular.GetFactory(); +``` + +## 17.3 获取作业 + +```cs showLineNumbers +var scheduler = Schedular.GetJob("作业 Id"); +``` + +## 17.4 序列化作业触发器参数或作业信息额外数据 + +```cs showLineNumbers {1,5} +// 作业触发器参数 +var args = new object[] { "* * * * * *", CronStringFormat.WithSeconds }; +var stringArgs = Schedular.Serialize(args); + +// 作业额外数据 +var jobData = new Dictionary { { "name", "Furion" } }; +var stringJobData = Schedular.Serialize(jobData); +``` + +## 17.5 反序列化作业触发器参数或作业信息额外数据 + +```cs showLineNumbers {1,5} +// 作业触发器参数 +var stringArgs = "[\"* * * * *\",0]"; +var args = Schedular.Deserialize(stringArgs); + +// 作业额外数据 +var stringJobData = "{\"name\":\"Furion\"}"; +var args = Schedular.Deserialize>(stringJobData); +``` \ No newline at end of file diff --git a/handbook/docs/global/scoped.mdx b/handbook/docs/global/scoped.mdx new file mode 100644 index 0000000000000000000000000000000000000000..1981323c1018031b178bfcf0c59ac773623f246e --- /dev/null +++ b/handbook/docs/global/scoped.mdx @@ -0,0 +1,90 @@ +--- +id: scoped +title: 11. Scoped 静态类 +sidebar_label: 11. Scoped 静态类 +--- + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **问题修复** + + -  修复 `Scoped.CreateUowAsync` 作用域工作单元异常无法回滚问题 4.8.8.44 ⏱️2023.09.23 [#I833I9](https://gitee.com/dotnetchina/Furion/issues/I833I9) + +
+
+
+ +## 11.1 创建一个依赖注入作用域范围 + +```cs showLineNumbers +// 同步 +Scoped.Create((factory, scope) => { + var services = scope.ServiceProvider; +}); + +// 异步 +await Scoped.CreateAsync(async (factory, scope) => { + var services = scope.ServiceProvider; + await _calcService.GetAsync(); + // ... +}) +``` + +:::important 数据库操作注意 + +如果作用域中对**数据库有任何变更操作**,需手动调用 `SaveChanges` 或带 `Now` 结尾的方法。也可以使用 `Scoped.CreateUow(handler)` 代替。 + +::: + +## 11.2 创建一个带工作单元的作用域 + +```cs showLineNumbers +// 同步 +Scoped.CreateUow((factory, scope) => { + var services = scope.ServiceProvider; +}); + +// 异步 +await Scope.CreateUowAsync(async (factory, scope) => { + var services = scope.ServiceProvider; + await _calcService.GetAsync(); + // ... +}); +``` + +此方法将在执行完毕之后自动调用 `SaveChanges()` + +## 11.3 创建一个依赖注入作用域范围(带返回值) + +:::warning 移除声明 + +在 `Furion v2.18+` 版本移除带返回值的作用域(该方法属于多余操作) + +::: + +```cs showLineNumbers {1} +var obj = Scoped.CreateRef((factory, scope) => { + var services = scope.ServiceProvider; + return "返回值"; +}); +``` + +## 11.4 创建一个带工作单元的作用域(带返回值) + +:::warning 移除声明 + +在 `Furion v2.18+` 版本移除带返回值的作用域(该方法属于多余操作) + +::: + +```cs showLineNumbers +var obj = Scoped.CreateUowRef((factory, scope) => { + var services = scope.ServiceProvider; + return "返回值"; +}); +``` diff --git a/handbook/docs/global/shttp.mdx b/handbook/docs/global/shttp.mdx new file mode 100644 index 0000000000000000000000000000000000000000..bd05bf272c8e5a2b0732fda86398911d4daa93a6 --- /dev/null +++ b/handbook/docs/global/shttp.mdx @@ -0,0 +1,11 @@ +--- +id: shttp +title: 6. Http 静态类 +sidebar_label: 6. Http 静态类 +--- + +## 6.1 获取远程代理服务 + +```cs showLineNumbers +var http = Http.GetHttpProxy(); +``` diff --git a/handbook/docs/global/sparetime.mdx b/handbook/docs/global/sparetime.mdx new file mode 100644 index 0000000000000000000000000000000000000000..cdae0a6348093f31b69eb64cab42f34000494997 --- /dev/null +++ b/handbook/docs/global/sparetime.mdx @@ -0,0 +1,102 @@ +--- +id: sparetime +title: 12. SpareTime 静态类 +sidebar_label: 12. SpareTime 静态类 +--- + +:::warning 重要声明 + +以下内容在 `Furion 4.8.4 +` 版本中已移除。 + +::: + +`SpareTime` 静态类提供了一些方法方便初始化和管理任务 + +## 12.1 初始化任务 + +```cs showLineNumbers +// 开启间隔任务 +SpareTime.Do(interval, [options]); + +// 开启 Cron 表达式任务 +SpareTime.Do(expression, [options]); + +// 只执行一次任务 +SpareTime.DoOnce(interval, [options]); + +// 实现自定义任务 +SpareTime.Do(()=>{ + return DateTime.Now.AddMinutes(10); +},[options]); +``` + +## 12.2 实现后台执行 + +```cs showLineNumbers +// 实现后台执行 +SpareTime.DoIt(()=>{}); +``` + +## 12.3 开始一个任务 + +```cs showLineNumbers +SpareTime.Start("任务标识"); +``` + +## 12.4 暂停一个任务 + +```cs showLineNumbers +SpareTime.Stop("任务标识"); +// 还可以标记一个任务执行失败 +SpareTime.Stop("任务标识", true); +``` + +## 12.5 取消一个任务 + +```cs showLineNumbers +SpareTime.Cancel("任务名称"); +``` + +## 12.6 销毁所有任务 + +```cs showLineNumbers +SpareTime.Dispose(); +``` + +## 12.7 获取所有任务 + +```cs showLineNumbers +var workers = SpareTime.GetWorkers(); +``` + +## 12.8 获取单个任务 + +```cs showLineNumbers +var worker = SpareTime.GetWorker("workerName"); +``` + +## 12.9 解析 `Cron` 表达式 + +```cs showLineNumbers +var nextTime = SpareTime.GetCronNextOccurrence("* * * * *"); +``` + +## 12.10 `BackgroundService` 间隔定时任务 + +```cs showLineNumbers +// 间隔执行任务 +await SpareTime.DoAsync(1000, () => +{ + _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); +}, stoppingToken); +``` + +## 12.11 `BackgroundService` `Cron` 定时任务 + +```cs showLineNumbers + // 执行 Cron 表达式任务 +await SpareTime.DoAsync("*/5 * * * * *", () => +{ + _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); +}, stoppingToken, CronFormat.IncludeSeconds); +``` diff --git a/handbook/docs/global/taskqueued.mdx b/handbook/docs/global/taskqueued.mdx new file mode 100644 index 0000000000000000000000000000000000000000..69a7b6ae17be0922159224cba56e0196459c95d1 --- /dev/null +++ b/handbook/docs/global/taskqueued.mdx @@ -0,0 +1,25 @@ +--- +id: taskqueued +title: 18. TaskQueued 静态类 +sidebar_label: 18. TaskQueued 静态类 +--- + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.3 +` 版本使用。 + +::: + +## 18.1 同步入队 + +```cs showLineNumbers +TaskQueued.Enqueue((provider) => {}, [delay]); +TaskQueued.Enqueue((provider) => {}, cronExpression, [format]); +``` + +## 18.2 异步入队 + +```cs showLineNumbers +await TaskQueued.EnqueueAsync(async (provider, token) => {}, [delay]); +await TaskQueued.EnqueueAsync(async (provider, token) => {}, cronExpression, [format]); +``` \ No newline at end of file diff --git a/handbook/docs/global/tp.mdx b/handbook/docs/global/tp.mdx new file mode 100644 index 0000000000000000000000000000000000000000..dd3fc76902f9e03dbd29bffd093a898e233d3525 --- /dev/null +++ b/handbook/docs/global/tp.mdx @@ -0,0 +1,151 @@ +--- +id: tp +title: 15. TP 静态类 +sidebar_label: 15. TP 静态类 +--- + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **新特性** + + -  新增 `TP.WrapperRectangle` 绘制矩形日志模板 4.8.8.25 ⏱️2023.06.14 [60ffd76](https://gitee.com/dotnetchina/Furion/commit/60ffd76783633ac4fc7baaf845e14cb59518b795) + +- **问题修复** + + -  修复 `TP.Wrapper` 静态类不能准确识别多行内容问题 4.8.7.40 ⏱️2023.04.10 [#I6UAC8](https://gitee.com/dotnetchina/Furion/issues/I6UAC8) + +
+
+
+ +## 15.1 生成规范的日志模板 + +:::important 版本说明 + +以下内容仅限 `Furion 3.5.3 +` 版本使用。 + +::: + +```cs showLineNumbers {2} +// 生成模板字符串 +var template = TP.Wrapper("Furion 框架", "让 .NET 开发更简单,更通用,更流行。", + "##作者## 百小僧", + "##当前版本## v3.5.3", + "##文档地址## http://furion.baiqian.ltd", + "##Copyright## 百小僧, 百签科技(广东)有限公司"); + +Console.WriteLine(template); +``` + +日志打印模板如下: + +```bash showLineNumbers +┏━━━━━━━━━━━ Furion 框架 ━━━━━━━━━━━ +┣ 让 .NET 开发更简单,更通用,更流行。 +┣ +┣ 作者: 百小僧 +┣ 当前版本: v3.5.3 +┣ 文档地址: http://furion.baiqian.ltd +┣ Copyright: 百小僧, 百签科技(广东)有限公司 +┗━━━━━━━━━━━ Furion 框架 ━━━━━━━━━━━ + +``` + +:::tip 关于属性生成 + +如果列表项以 `##属性名##` 开头,自动生成 `属性名:` 作为行首且自动等宽对齐。 + +`Furion 3.9.1` 之前版本使用 `[属性名]` 开头。 + +::: + +## 15.2 生成矩形日志模板 + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.8.25 +` 版本使用。 + +::: + +```cs showLineNumbers {1} +var template = TP.WrapperRectangle(new[] { + "百小僧", + "让 .NET 开发更简单,更通用,更流行。", + "一个应用程序框架,您可以将它集成到任何 .NET/C# 应用程序中。" +}); + +Console.WriteLine(template); +``` + +日志打印模板如下: + +```bash showLineNumbers ++-----------------------------------------------------------------------------+ +| 百小僧 | +| 让 .NET 开发更简单,更通用,更流行。 | +| 一个应用程序框架,您可以将它集成到任何 .NET/C# 应用程序中。 | ++-----------------------------------------------------------------------------+ +``` + +还可以配置左对齐,居中对齐,右对齐: + +```cs showLineNumbers {6,13,20} +// 左对齐 +var template = TP.WrapperRectangle(new[] { + "百小僧", + "让 .NET 开发更简单,更通用,更流行。", + "一个应用程序框架,您可以将它集成到任何 .NET/C# 应用程序中。" +}, -1); // -1 表示左对齐 + +// 居中对齐 +var template = TP.WrapperRectangle(new[] { + "百小僧", + "让 .NET 开发更简单,更通用,更流行。", + "一个应用程序框架,您可以将它集成到任何 .NET/C# 应用程序中。" +}, 0); // 0 表示居中对齐 + +// 右对齐 +var template = TP.WrapperRectangle(new[] { + "百小僧", + "让 .NET 开发更简单,更通用,更流行。", + "一个应用程序框架,您可以将它集成到任何 .NET/C# 应用程序中。" +}, 1); // 1 表示右对齐 +``` + +输出如下: + +```bash showLineNumbers ++-----------------------------------------------------------------------------+ +| 百小僧 | +| 让 .NET 开发更简单,更通用,更流行。 | +| 一个应用程序框架,您可以将它集成到任何 .NET/C# 应用程序中。 | ++-----------------------------------------------------------------------------+ + ++-----------------------------------------------------------------------------+ +| 百小僧 | +| 让 .NET 开发更简单,更通用,更流行。 | +| 一个应用程序框架,您可以将它集成到任何 .NET/C# 应用程序中。 | ++-----------------------------------------------------------------------------+ + ++-----------------------------------------------------------------------------+ +| 百小僧 | +| 让 .NET 开发更简单,更通用,更流行。 | +| 一个应用程序框架,您可以将它集成到任何 .NET/C# 应用程序中。 | ++-----------------------------------------------------------------------------+ +``` + +另外还可以配置矩形最长字符串的追加长度。 + +```cs +// 右对齐 +var template = TP.WrapperRectangle(new[] { + "百小僧", + "让 .NET 开发更简单,更通用,更流行。", + "一个应用程序框架,您可以将它集成到任何 .NET/C# 应用程序中。" +}, 1, 20); // 20 表示最长字符串长度 + 20 +``` diff --git a/handbook/docs/globalusing.mdx b/handbook/docs/globalusing.mdx new file mode 100644 index 0000000000000000000000000000000000000000..06b74e9e047bae37254fc36b3d1ec2112165959b --- /dev/null +++ b/handbook/docs/globalusing.mdx @@ -0,0 +1,123 @@ +--- +id: globalusing +title: 2.12 GlobalUsing 使用 +sidebar_label: 2.12 GlobalUsing 使用 +description: 学习如何避免代码头部大量 Using +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +## 2.12.1 关于 `GlobalUsing` + +在 `.NET6/C#10` 之后,微软新增了 `GlobalUsings` 机制,可以在项目的根目录下创建一个 `GlobalUsings.cs` 文件,把常用的 `using` 放置其中。 + +这样 `GlobalUsings.cs` 所在的项目 `.cs` 文件就无需重复 `using` 了,大大的提高开发效率,也让代码变的更加简洁。 + +## 2.12.2 必要配置 + +**启用 `GlobalUsings` 机制需要以下两个步骤:** + +1. 在你需要全局 `using` 的项目层根目录创建 `GlobalUsings.cs` 文件,如果多个项目层需要,则每个层都应该有一个 `GlobalUsings.cs` +2. 编辑项目的 `.csproj` 文件,添加 `enable`,注意是在 `` 中添加,通常和 `` 同父同级 + +## 2.12.3 基本使用 + +配置之后,现在就可以把常用的 `using` 放到 `GlobalUsings.cs` 中了,写法如下: + +```cs showLineNumbers title="Furion 推荐的全局命名空间" +global using Furion; +global using Furion.DatabaseAccessor; +global using Furion.DataEncryption; +global using Furion.DataValidation; +global using Furion.DependencyInjection; +global using Furion.DynamicApiController; +global using Furion.Extensions; +global using Furion.FriendlyException; +global using Furion.Logging; +global using Mapster; +global using Microsoft.AspNetCore.Authorization; +global using Microsoft.AspNetCore.Http; +global using Microsoft.AspNetCore.Mvc; +global using Microsoft.CodeAnalysis; +global using Microsoft.EntityFrameworkCore; +global using System.ComponentModel.DataAnnotations; +``` + +**注意必须以 `global` 开头!** + +:::tip 小知识 + +一般推荐把实体类的命名空间也放进去,因为仓储 `IRepository` 使用的频率非常高。 + +另外推荐大家在 `Visual Studio` 中安装 `CodeMaid` 插件(注意 `2019` 和 `2022` 版本),自动清理解决方案所有无用的 `using`,结合 `GlobalUsings.cs` 非常棒! + +::: + +:::important 个别情况 + +可能由于 `Visual Studio` 版本的问题,导致 `GlobalUsings.cs` 定义出错,这时候需要在 `using` 后面加 `global::`,如: + +```cs showLineNumbers +global using global::Furion; +``` + +::: + +接下来在代码中使用: + +```cs showLineNumbers {1} +// 无需 using Furion 的命名空间了哦,清爽了不少 + +namespace Your.Application; + +public class DefaultAppService : IDynamicApiController +{ + private readonly IRepository _boardCardRepository; + private readonly IRepository _boardGroupRepository; + private readonly IRepository _boardCardAttachmentRepository; + private readonly IRepository _boardCardUserRepository; +} + +// .... +``` + + + +### 2.12.3.1 默认全局 `using` + +**实际上微软已经自动把一些常用的 `using` 在编译后的代码中自动补上了**,路径在 `项目/obj/Debug/net6.0/项目.GlobalUsings.cs` 文件中,文件内容如下: + +```cs showLineNumbers +// +global using global::System; +global using global::System.Collections.Generic; +global using global::System.IO; +global using global::System.Linq; +global using global::System.Net.Http; +global using global::System.Threading; +global using global::System.Threading.Tasks; +``` + +**也就是以上的 `using` 无需写在你创建的 `GlobalUsings.cs` 中了,微软会在编译时自动合并。** + +## 2.12.4 `.NET5` 项目开启支持 + +默认情况下,`.NET5` 采用 `C# 9.0` 编译,而 `GlobalUsing` 是从 `C# 10.0` 开始,这时候只需要编辑项目的 `.csproj` 并添加 `10.0` 即可: + +```xml showLineNumbers {3} + + net5.0 + 10.0 + + +``` + +如需使用最新版可配置为 `latest`。 + +## 2.12.5 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/http.mdx b/handbook/docs/http.mdx new file mode 100644 index 0000000000000000000000000000000000000000..2bfb219cedcc8a2c05ba4b0d08384a1ffce79ca2 --- /dev/null +++ b/handbook/docs/http.mdx @@ -0,0 +1,1714 @@ +--- +id: http +title: 19. 远程请求 +sidebar_label: 19. 远程请求 (HttpClient) +--- + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **新特性** + + -  新增 远程请求 `[HttpMethod]ToSaveAsync` 下载远程文件并保存到磁盘方法 4.8.7.32 ⏱️2023.04.02 [bfd02c1](https://gitee.com/dotnetchina/Furion/commit/bfd02c1a2ce4229e90fc825fe5657ada59e1892f) + -  新增 远程请求支持 `Content-Type` 为 `text/html` 和 `text/plain` 处理 4.8.7.22 ⏱️2023.03.27 [#I6QMLR](https://gitee.com/dotnetchina/Furion/issues/I6QMLR) + -  新增 远程请求 `HttpRequestMessage` 拓展方法 `AppendHeaders` 4.8.7.10 ⏱️2023.03.14 [#I6MVHT](https://gitee.com/dotnetchina/Furion/issues/I6MVHT) + -  新增 远程请求配置 `SetHttpVersion(version)` 配置,可配置 `HTTP` 请求版本,默认为 `1.1` 4.8.5.8 ⏱️2023.02.06 [#I6D64H](https://gitee.com/dotnetchina/Furion/issues/I6D64H) + -  新增 远程请求 `[QueryString]` 特性添加时间格式化 `Format` 属性 4.8.1.2 ⏱️2022.11.24 [!670](https://gitee.com/dotnetchina/Furion/pulls/670) + +- **问题修复** + + -  修复 远程请求获取响应 `Cookies` 被截断问题 4.8.8.54 ⏱️2023.11.08 [#I8EV1Z](https://gitee.com/dotnetchina/Furion/issues/I8EV1Z) + -  修复 远程请求上传文件在其他编程语言获取文件名存在双引号问题 4.8.8.53 ⏱️2023.11.07 [#I8EF1S](https://gitee.com/dotnetchina/Furion/issues/I8EF1S) + -  修复 远程请求在被请求端返回非 `200` 状态码但实际请求已处理也抛异常问题 4.8.8.14 ⏱️2023.05.12 [b14a51f](https://gitee.com/dotnetchina/Furion/commit/b14a51fd6f85a905da50729d521a2232b5c9afc1) + -  修复 远程请求 `Body` 参数为粘土对象 `Clay` 类型序列化有误 4.8.8.1 ⏱️2023.04.18 [#I6WKRZ](https://gitee.com/dotnetchina/Furion/issues/I6WKRZ) + -  修复 远程请求获取 `Cookies` 时如果包含相同 `Key` 异常问题 4.8.7.44 ⏱️2023.04.12 [#I6V3T7](https://gitee.com/dotnetchina/Furion/issues/I6V3T7) + -  修复 远程请求代理模式配置了 `WithEncodeUrl = false` 无效问题 4.8.6.4 ⏱️2023.02.16 [89639ba](https://gitee.com/dotnetchina/Furion/commit/89639ba1db8a7df750d9bca66a887e252622b219) + -  修复 由于 [#I6D64H](https://gitee.com/dotnetchina/Furion/issues/I6D64H) 导致远程请求出现 `Specified method is not supported.` 问题 4.8.5.9 ⏱️2023.02.07 [#I6DEEE](https://gitee.com/dotnetchina/Furion/issues/I6DEEE) [#I6D64H](https://gitee.com/dotnetchina/Furion/issues/I6D64H) + -  修复 ~~优化远程请求 `ReadAsStringAsync` 底层方法,尝试修复 `Error while copying content to a stream.` 错误 4.8.5.8 ⏱️2023.02.06 [#I6D64H](https://gitee.com/dotnetchina/Furion/issues/I6D64H)~~ + -  修复 远程请求配置 `WithEncodeUrl(false)` 对 `application/x-www-form-urlencoded` 请求类型无效 4.8.4 ⏱️2022.12.30 [#I682DX](https://gitee.com/dotnetchina/Furion/issues/I682DX) + +- **其他更改** + + -  调整 取消远程请求 `GET/HEAD` 不能传递 `Body` 的限制 4.8.8.39 ⏱️2023.08.02 [8113460](https://gitee.com/dotnetchina/Furion/commit/8113460ab8b23cbf392c49b79fe4eb77a89c8010) + +- **文档** + + -  新增 远程请求 `[QueryString]` 配置时间类型 `Format` 格式化文档 4.8.1.2 ⏱️2022.11.25 [!673](https://gitee.com/dotnetchina/Furion/pulls/673) + +
+
+
+ +:::important 版本说明 + +以下内容仅限 `Furion 1.16.0 +` 版本使用。 + +::: + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +## 19.1 关于远程请求 + +在互联网大数据的驱动下,平台或系统免不了需要和第三方进行数据交互,而第三方往往提供了 `RESTful API` 接口规范,这个时候就需要通过 `Http` 请求第三方接口进行数据传输交互。 + +也就是本章节所说的远程请求。 + +## 19.2 远程请求的作用 + +- 跨系统、跨设备通信 +- 实现多个系统数据传输交互 +- 跨编程语言协同开发 + +## 19.3 基础使用 + +### 19.3.1 注册服务 + +使用之前需在 `Startup.cs` 注册 `远程请求服务` + +```cs showLineNumbers {3} +public void ConfigureServices(IServiceCollection services) +{ + services.AddRemoteRequest(); +} +``` + +### 19.3.2 使用方式 + +`Furion` 提供两种方式访问发送远程请求。 + + + + +定义代理请求的 `接口` 并继承 `IHttpDispatchProxy` 接口 + +```cs showLineNumbers {1,3,6,9,12,15,18} +public interface IHttp : IHttpDispatchProxy +{ + [Get("http://furion.baiqian.ltd/get")] + Task GetXXXAsync(); + + [Post("http://furion.baiqian.ltd/post")] + Task PostXXXAsync(); + + [Put("http://furion.baiqian.ltd/put")] + Task PutXXXAsync(); + + [Delete("http://furion.baiqian.ltd/delete")] + Task DeleteXXXAsync(); + + [Patch("http://furion.baiqian.ltd/patch")] + Task PatchXXXAsync(); + + [Head("http://furion.baiqian.ltd/head")] + Task HeadXXXAsync(); +} +``` + +通过构造函数注入 `接口` + +```cs showLineNumbers {9,16} +using Furion.DynamicApiController; +using Furion.RemoteRequest.Extensions; + +namespace Furion.Application +{ + public class RemoteRequestService : IDynamicApiController + { + private readonly IHttp _http; + public RemoteRequestService(IHttp http) + { + _http = http; + } + + public async Task GetData() + { + var data = await _http.GetXXXAsync(); + } + } +} +``` + + + + +```cs showLineNumbers +var response = await "http://furion.baiqian.ltd/get".GetAsync(); + +var response = await "http://furion.baiqian.ltd/post".PostAsync(); + +var response = await "http://furion.baiqian.ltd/put".PutAsync(); + +var response = await "http://furion.baiqian.ltd/delete".DeleteAsync(); + +var response = await "http://furion.baiqian.ltd/patch".PatchAsync(); + +var response = await "http://furion.baiqian.ltd/head".HeadAsync(); +``` + +需引入 `using Furion.RemoteRequest.Extensions` 命名空间。 + + + + +## 19.4 字符串方式使用示例 + +:::important 温馨提示 + +推荐使用 《[19.5 代理方式](http.mdx#195-ihttpdispatchproxy-代理方式)》代替本小节功能。`代理方式` 能够提供更容易且更易维护的方式。 + +::: + +### 19.4.1 内置请求方式 + +```cs showLineNumbers +// 发送 Get 请求 +var response = await "http://furion.baiqian.ltd/get".GetAsync(); + +// 发送 Post 请求 +var response = await "http://furion.baiqian.ltd/post".PostAsync(); + +// 发送 Put 请求 +var response = await "http://furion.baiqian.ltd/put".PutAsync(); + +// 发送 Delete 请求 +var response = await "http://furion.baiqian.ltd/delete".DeleteAsync(); + +// 发送 Patch 请求 +var response = await "http://furion.baiqian.ltd/patch".PatchAsync(); + +// 发送 Head 请求 +var response = await "http://furion.baiqian.ltd/head".HeadAsync(); + +// 手动指定发送特定请求 +var response = await "http://furion.baiqian.ltd/post".SetHttpMethod(HttpMethod.Post) + .SendAsync(); +``` + +### 19.4.2 设置请求地址 + +```cs showLineNumbers +// 该方式在 Furion v3.0.0 已移除,多此一举了 +await "".SetRequestUrl("http://furion.baiqian.ltd/"); +``` + +### 19.4.3 设置请求方式 + +```cs showLineNumbers +await "http://furion.baiqian.ltd/post".SetHttpMethod(HttpMethod.Get); +``` + +### 19.4.4 设置地址模板 + +```cs showLineNumbers +// 字典方式 +await "http://furion.baiqian.ltd/post/{id}?name={name}&id={p.Id}".SetTemplates(new Dictionary { + { "id", 1 }, + { "name", "Furion" }, + { "p.Id", new Person { Id = 1 } } +}); + +// 对象/匿名对象方式 +await "http://furion.baiqian.ltd/post/{id}?name={name}".SetTemplates(new { + id = 1, + name = "Furion" +}); +``` + +**注:模板替换区分大小写。** + +### 19.4.5 设置请求报文头 + +```cs showLineNumbers +// 字典方式 +await "http://furion.baiqian.ltd/post".SetHeaders(new Dictionary { + { "Authorization", "Bearer 你的token"}, + { "X-Authorization", "Bearer 你的刷新token"} +}); + +// 对象/匿名对象方式 +await "http://furion.baiqian.ltd/post".SetHeaders(new { + Authorization = "Bearer 你的token" +}); +``` + +### 19.4.6 设置 `URL` 地址参数 + +```cs showLineNumbers +// 字典方式 +await "http://furion.baiqian.ltd/get".SetQueries(new Dictionary { + { "id", 1 }, + { "name", "Furion"} +}); + +// 对象/匿名对象方式 +await "http://furion.baiqian.ltd/get".SetQueries(new { + id = 1, + name = "Furion" +}); + +// Furion 4.7.3+ 新增忽略 null 值重载 +await "http://furion.baiqian.ltd/get".SetQueries(new { + id = 1, + name = "Furion", + nullValue = default(object) +}, true); // 设置 true 则忽略 null 值 +``` + +最终输出格式为:`http://furion.baiqian.ltd/get?id=1&name=Furion`。 + +### 19.4.7 设置请求客户端 + +- **全局配置方式** + +```cs showLineNumbers {1,3-4,12} +services.AddRemoteRequest(options=> +{ + // 配置 Github 基本信息 + options.AddHttpClient("github", c => + { + c.BaseAddress = new Uri("https://api.github.com/"); + c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); + c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample"); + }); +}); + +await "get".SetClient("github"); +``` + +最终生成请求地址为:`https://api.github.com/get`。 + +- **局部配置方式** + +:::important 版本说明 + +以下内容仅限 `Furion 4.3.8 +` 版本使用。 + +::: + +```cs showLineNumbers +await "http://furion.baiqian.ltd".SetClient(() => new HttpClient()); +``` + +### 19.4.8 设置 `Body` 参数 + +```cs showLineNumbers +// 传入对象 +await "http://furion.baiqian.ltd/api/user/add".SetBody(new User { Id = 1, Name = "Furion" }); + +// 配置 Content-Type +await "http://furion.baiqian.ltd/api/user/add".SetBody(new { Id = 1, Name = "Furion" }, "application/json"); + +// 设置 Encoding 编码 +await "http://furion.baiqian.ltd/api/user/add".SetBody(new User { Id = 1, Name = "Furion" }, "application/json", Encoding.UTF8); + +// 处理 application/x-www-form-urlencoded 请求 +await "http://furion.baiqian.ltd/api/user/add".SetBody(new Dictionary { + { "Id", 1 }, + { "Name", "Furion"} +}, "application/x-www-form-urlencoded"); + +// 处理 application/xml、text/xml +await "http://furion.baiqian.ltd/api/user/add".SetBody("somevalue", "application/xml"); +``` + +:::important 特别注意 + +如果请求 `Content-Type` 设置为 `application/x-www-form-urlencoded` 类型,那么底层自动将数据进行 `UrlEncode` 编码处理,无需外部处理。 + +::: + +### 19.4.9 设置 `Content-Type` + +```cs showLineNumbers +await "http://furion.baiqian.ltd/post".SetContentType("application/json"); +``` + +### 19.4.10 设置内容编码 + +```cs showLineNumbers +await "http://furion.baiqian.ltd/post".SetContentEncoding(Encoding.UTF8); +``` + +### 19.4.11 设置 `JSON` 序列化提供程序 + +`Furion` 默认情况下采用 `System.Text.Json` 进行 `JSON` 序列化处理,如需设置第三方 `JSON` 提供器,则可以通过以下配置: + +```cs showLineNumbers +// 泛型方式 +await "http://furion.baiqian.ltd/api/user/add".SetJsonSerialization(); + +// 非泛型方式 +await "http://furion.baiqian.ltd/api/user/add".SetJsonSerialization(typeof(NewtonsoftJsonSerializerProvider)); + +// 添加更多配置 +await "http://furion.baiqian.ltd/api/user/add".SetJsonSerialization(new JsonSerializerSettings { + +}); + +// 比如配置缺省的序列化选项 +await "http://furion.baiqian.ltd".SetJsonSerialization(default, new JsonSerializerOptions + { + // 中文乱码 + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }) + .GetAsAsync(); +``` + +:::important 关于 `JSON` 序列化提供器 + +如需了解更多 `JSON` 序列化知识可查阅 [23. JSON 序列化](json-serialization#2351-自定义序列化提供器) 章节 + +::: + +### 19.4.12 启用 `Body` 参数验证 + +```cs showLineNumbers +await "http://furion.baiqian.ltd/api/user/add".SetValidationState(); + +// 设置不验证 null 值 +await "http://furion.baiqian.ltd/api/user/add".SetValidationState(includeNull: true); +``` + +支持类中 `[Required]` 等完整模型验证特性。 + +### 19.4.13 请求拦截 + +```cs showLineNumbers +await "http://furion.baiqian.ltd/".OnRequesting((client, req) => { + // req 为 HttpRequestMessage 对象 + // 追加更多参数 + req.AppendQueries(new Dictionary { + { "access_token", "xxxx"} + }); +}); +``` + +**支持多次拦截** + +### 19.4.14 `HttpClient` 拦截 + +```cs showLineNumbers +await "http://furion.baiqian.ltd/".OnClientCreating(client => { + // client 为 HttpClient 对象 + client.Timeout = TimeSpan.FromSeconds(30); // 设置超时时间 +}); +``` + +**支持多次拦截** + +### 19.4.15 请求之前拦截 + +```cs showLineNumbers +await "http://furion.baiqian.ltd/".OnRequesting((client, req) => { + // req 为 HttpRequestMessage 对象 +}); +``` + +**支持多次拦截** + +### 19.4.16 成功请求拦截 + +```cs showLineNumbers +await "http://furion.baiqian.ltd/".OnResponsing((client, res) => { + // res 为 HttpResponseMessage 对象 +}); +``` + +**支持多次拦截** + +### 19.4.17 请求异常拦截 + +```cs showLineNumbers +await "http://furion.baiqian.ltd/".OnException((client, res, errors) => { + // res 为 HttpResponseMessage 对象 +}); +``` + +**支持多次拦截** + +### 19.4.18 各种返回值处理 + +`Furion` 远程请求默认提供四种返回值类型: + +- `HttpResponseMessage`:请求响应消息类型 +- `Stream`:流类型,可用来下载文件 +- `T`:泛型 `T` 类型 +- `String`:字符串类型,也就是直接将网络请求结果内容字符串化 +- `Byte[]`:字节数组类型 + +如: + +```cs showLineNumbers +// HttpResponseMessage +var res = await "http://furion.baiqian.ltd/".GetAsync(); + +// Stream,可用来下载文件 +var (stream, encoding) = await "http://furion.baiqian.ltd/".GetAsStreamAsync(); + +// T +var user = await "http://furion.baiqian.ltd/".GetAsAsync(); + +// String +var str = await "https://www.baidu.com".GetAsStringAsync(); +``` + +### 19.4.19 设置 `Byte[]/Stream` 类型/上传文件 + +:::warning `Furion 4.4.0` 以下版本 + +在 `Furion 4.4.0+` 版本移除了 `.SetBodyBytes` 方式,原因是拓展性太差,**新版本请使用 `.SetFiles` 方式**。 + +::: + +有时候我们需要上传文件,需要设置 `Content-Type` 为 `multipart/form-data` 类型,如: + +```cs showLineNumbers {3,7,10,15} +// 支持单文件,bytes 可以通过 File.ReadAllBytes(文件路径) 获取 +var res = await "http://furion.baiqian.ltd/upload".SetContentType("multipart/form-data") + .SetBodyBytes(("键", bytes, "文件名")).PostAsync(); + +// 支持多个文件 +var res = await "http://furion.baiqian.ltd/upload".SetContentType("multipart/form-data") + .SetBodyBytes(("键", bytes, "文件名"),("键", bytes, "文件名")).PostAsync(); + +// 支持单文件,Furion 4.5.8 版本支持 Stream 方式更新 +var res = await "http://furion.baiqian.ltd/upload".SetContentType("multipart/form-data") + .SetBodyBytes(("键", fileStream, "文件名")).PostAsync(); + +// 支持多个文件,Furion 4.5.8 版本支持 Stream 方式更新 +var res = await "http://furion.baiqian.ltd/upload".SetContentType("multipart/form-data") + .SetBodyBytes(("键", fileStream, "文件名"),("键", fileStream, "文件名")).PostAsync(); +``` + +:::note 关于微信上传接口 + +如果遇到微信上传出现问题,则可设置 `Content-Type` 为:`application/octet-stream`,如: + +```cs showLineNumbers +var result = await $"https://api.weixin.qq.com/wxa/img_sec_check?access_token={token}" + .SetBodyBytes(("media", bytes, Path.GetFileName(imgPath))) + .SetContentType("application/octet-stream") + .PostAsStringAsync(); +``` + +::: + +:::tip `Furion 4.4.0+` 版本 + +如果使用 `Furion 4.4.0+` 版本,请使用以下的 `.SetFiles` 替代 `.SetBodyBytes` 操作。 + +::: + +```cs showLineNumbers {3,7} +// bytes 可以通过 File.ReadAllBytes(文件路径) 获取 +var res = await "http://furion.baiqian.ltd/upload".SetContentType("multipart/form-data") + .SetFiles(HttpFile.Create("file", bytes, "image.png")).PostAsync(); + +// 支持多个文件 +var res = await "http://furion.baiqian.ltd/upload".SetContentType("multipart/form-data") + .SetFiles(HttpFile.CreateMultiple("files", (bytes, "image1.png"), (bytes, "image2.png"))).PostAsync(); +``` + +### 19.4.20 设置 `IServiceProvider` + +有时候我们需要构建一个作用域的 `IServiceProvider`,这时只需要设置即可: + +```cs showLineNumbers +var res = await "http://furion.baiqian.ltd/upload".SetRequestScoped(services); +``` + +### 19.4.21 支持模板配置 + +模板格式为:`#(配置路径)` + +```cs showLineNumbers +var res = await "#(Furion:Address)/upload".GetAsync(); +``` + +```json showLineNumbers +{ + "Furion": { + "Address": "http://furion.baiqian.ltd" + } +} +``` + +### 19.4.22 重试策略 + +在 `Furion v2.18+` 版本支持配置重试策略,如: + +```cs showLineNumbers +var res = await "http://furion.baiqian.ltd".SetRetryPolicy(3, 1000).GetAsync(); +``` + +以上代码表示请求失败重试 `3` 次,每次延迟 `1000ms` 。 + +### 19.4.23 支持 `GZip` 压缩 + +在 `Furion v3.2.0+` 版本支持`GZip` 压缩,如: + +```cs showLineNumbers +var res = await "http://furion.baiqian.ltd".WithGZip().GetAsync(); +``` + +### 19.4.24 设置 `Url` 转码 + +过去版本会对所有的 `Url` 进行 `Uri.EscapeDataString` 转码,在 `Furion v3.8.0+` 版本支持 `Url` 转码设置,如: + +```cs showLineNumbers +var res = await "http://furion.baiqian.ltd".WithEncodeUrl(false).GetAsync(); +``` + +### 19.4.25 设置 `HTTP` 版本 + +可解决一些 `HTTP` 和 `HTTPS` 请求问题。 + +```cs showLineNumbers +var res = await "http://furion.baiqian.ltd".SetHttpVersion("1.0").GetAsync(); // Furion 4.8.5.8+ 支持 +``` + +### 19.4.26 下载远程文件并保存 + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.7.32 +` 版本使用。 + +::: + +```cs showLineNumbers +await "http://furion.baiqian.ltd/img/rm1.png".GetToSaveAsync("D:/rm3.png"); +await "http://furion.baiqian.ltd/img/rm1.png".PostToSaveAsync("D:/rm3.png"); +... +``` + +## 19.5 `IHttpDispatchProxy` 代理方式 + +### 19.5.1 支持多种代理方式 + +```cs showLineNumbers +public interface IHttp : IHttpDispatchProxy +{ + // 发送 Get 请求 + [Get("http://furion.baiqian.ltd/get")] + Task GetXXXAsync(); + + // 发送 Post 请求 + [Post("http://furion.baiqian.ltd/post")] + Task PostXXXAsync(); + + // 发送 Put 请求 + [Put("http://furion.baiqian.ltd/put")] + Task PutXXXAsync(); + + // 发送 Delete 请求 + [Delete("http://furion.baiqian.ltd/delete")] + Task DeleteXXXAsync(); + + // 发送 Patch 请求 + [Patch("http://furion.baiqian.ltd/patch")] + Task PatchXXXAsync(); + + // 发送 Head 请求 + [Head("http://furion.baiqian.ltd/head")] + Task HeadXXXAsync(); +} +``` + +### 19.5.2 设置地址模板 + +```cs showLineNumbers +public interface IHttp : IHttpDispatchProxy +{ + [Get("http://furion.baiqian.ltd/get/{id}?name={name}&number={p.PersonDetail.PhonNumber}")] + Task GetXXXAsync(int id, string name, Person p); +} +``` + +**注:模板替换区分大小写。** + +### 19.5.3 设置请求报文头 + +`Furion` 框架远程请求代理模式提供三种方式设置请求报文头: + +- 支持在接口中声明 +- 支持在方法中声明 +- 支持在参数中声明 + +```cs showLineNumbers {1-2,5,9,12} +[Headers("key","value")] +[Headers("key1","value2")] // 设置多个 +public interface IHttp : IHttpDispatchProxy +{ + [Get("http://furion.baiqian.ltd/get/{id}?name={name}"), Headers("key2","value2")] + Task GetXXXAsync(int id, string name); + + [Get("http://furion.baiqian.ltd")] + Task GetXXX2Async(int id, [Headers]string token = default); + + [Get("http://furion.baiqian.ltd")] + Task GetXXX2Async(int id, string name, [Headers("别名")]string token = default); +} +``` + +--- + +**如需动态设置,可使用以下方式(添加参数拦截器):** + +```cs showLineNumbers {5} +public interface IHttp : IHttpDispatchProxy +{ + // 通过参数拦截 + [Post("http://furion.baiqian.ltd/post")] + Task PostXXXAsync(string name, [Interceptor(InterceptorTypes.Request)] Action action = default); +} +``` + +调用: + +```cs showLineNumbers {1,3,9,15} +_http.PostXXXAsync("百小僧", (client, requestMessage) => +{ + requestMessage.AppendHeaders(new Dictionary { + { "Authorization", "Bearer 你的token"}, + { "X-Authorization", "Bearer 你的刷新token"} + }); + + // 也支持对象,匿名方式 + requestMessage.AppendHeaders(new { + Authorization = "Bearer 你的token", + Others = "其他" + }); + + // 也可以使用原生 + requestMessage.Headers.TryAddWithoutValidation("Authorization", "Bearer 你的token"); + requestMessage.Headers.TryAddWithoutValidation("key", "value"); +}); +``` + +### 19.5.4 设置 `URL` 地址参数 + +```cs showLineNumbers {4,7,10,13,17,21,25} +public interface IHttp : IHttpDispatchProxy +{ + [Get("http://furion.baiqian.ltd/get/{id}?name={name}")] + Task GetXXXAsync(int id, string name); + + [Get("http://furion.baiqian.ltd/get/{p.Id}?name={p.Name}")] + Task GetXXXAsync(Person p); + + [Get("http://furion.baiqian.ltd/get")] + Task GetXXXAsync([QueryString]int id, [QueryString]string name); + + [Get("http://furion.baiqian.ltd/get")] + Task GetXXXAsync([QueryString]int id, [QueryString("别名")]string name); + + // Furion 4.8.1.4 新增 [QueryString(Format)] 配置时间类型格式化 + [Get("http://furion.baiqian.ltd/get")] + Task GetXXXAsync([QueryString(Format = "yyyy-MM-dd HH:mm:ss")] DateTime queryStartTime, [QueryString(Format = "yyyy-MM-dd HH:mm:ss")] DateTime queryEndTime); + + // Furion 4.8.1.4 新增 [QueryString(Format)] 配置时间类型格式化 + [Get("http://furion.baiqian.ltd/get")] + Task GetXXXAsync([QueryString(Format = "yyyy-MM-dd HH:mm:ss")] DateTimeOffset queryStartTime, [QueryString(Format = "yyyy-MM-dd HH:mm:ss")] DateTimeOffset queryEndTime); + + // Furion 4.7.3 新增 IgnoreNullValueQueries 配置忽略空值 + [Get("http://furion.baiqian.ltd/get", IgnoreNullValueQueries = true)] + Task GetXXXAsync([QueryString]int id, [QueryString]string name, [QueryString]string nullValue); +} +``` + +最终输出格式为:`http://furion.baiqian.ltd/get?id=1&name=Furion`。 + +:::tip 关于对象类型直接作为模板参数 + +在对接某些第三方接口的时候可能遇到一种情况,需要把对象序列化或者进行某种处理后作为 `Url` 参数,如: + +```cs showLineNumbers {1} +[Get("http://furion.baiqian.ltd/get?json={p}", WithEncodeUrl = false)] // 这里将 p 作为模板传入 +Task GetXXXAsync(Person p); +``` + +如果 `Person` 类型不做任何处理,那么最终传递的是 `Person` 的命名空间:`http://furion.baiqian.ltd/get?json=YourProject.Person`,但这并非是我们的预期。 + +这个时候我们只需要重写 `Person` 的 `ToString` 方法即可,如: + +```cs showLineNumbers {5-8} +public class Person +{ + public string Name { get; set; } + + public override string ToString() + { + return JsonSerializer.Serialize(this); // 比如这里做序列化处理 + // 如果基类中 override,可使用 return JsonSerializer.Serialize(this); + } +} +``` + +::: + +### 19.5.5 设置请求客户端 + +- **全局配置方式** + +```cs showLineNumbers {1,3-4,12,15} +services.AddRemoteRequest(options=> +{ + // 配置 Github 基本信息 + options.AddHttpClient("github", c => + { + c.BaseAddress = new Uri("https://api.github.com/"); + c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); + c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample"); + }); +}); + +[Client("github")] // 可以在接口中全局使用 +public interface IHttp : IHttpDispatchProxy +{ + [Get("get"), Client("github")] // 也可以在方法中局部使用 + Task GetXXXAsync(); +} +``` + +最终生成请求地址为:`https://api.github.com/get`。 + +- **局部配置方式** + +:::important 版本说明 + +以下内容仅限 `Furion 4.3.8 +` 版本使用。 + +::: + +```cs showLineNumbers {5,7-11} +public interface IHttp : IHttpDispatchProxy +{ + // 局部方式 + [Get("get")] + Task GetXXXAsync([Interceptor(InterceptorTypes.Initiate)]Func clientProvider); + + // 全局静态方式 + [Interceptor(InterceptorTypes.Initiate)] + static HttpClient CreateHttpClient() + { + return new HttpClient(...); + } +} +``` + +### 19.5.6 设置 `Body` 参数 + +```cs showLineNumbers {3,6,9} +public interface IHttp : IHttpDispatchProxy +{ + [Post("http://furion.baiqian.ltd/post")] + Task PostXXXAsync([Body]User user); + + [Post("http://furion.baiqian.ltd/post")] + Task PostXXXAsync([Body("application/x-www-form-urlencoded")]User user); + + [Post("http://furion.baiqian.ltd/post")] + Task PostXXXAsync([Body("application/x-www-form-urlencoded", "UTF-8")]User user); +} +``` + +### 19.5.7 设置 `JSON` 序列化提供程序 + +`Furion` 默认情况下采用 `System.Text.Json` 进行 `JSON` 序列化处理,如需设置第三方 `JSON` 提供器,则可以通过以下配置: + +```cs showLineNumbers {3,6-7} +public interface IHttp : IHttpDispatchProxy +{ + [Post("http://furion.baiqian.ltd/post"), JsonSerialization(typeof(NewtonsoftJsonSerializerProvider))] + Task PostXXXAsync([Body]User user); + + [Post("http://furion.baiqian.ltd/post"), JsonSerialization(typeof(NewtonsoftJsonSerializerProvider))] + Task PostXXXAsync([Body]User user, [JsonSerializerOptions]object jsonSerializerOptions = default); + + /// + /// 缺省序列化配置 + /// + /// + [JsonSerializerOptions] + static object GetJsonSerializerOptions() + { + // 这里也可以通过 JSON.GetSerializerOptions() 获取 Startup.cs 中的配置 + return new JsonSerializerOptions + { + + }; + } +} +``` + +`[JsonSerializerOptions]` 可以标记参数是一个 `JSON` 序列化配置参数。 + +:::important 关于 `JSON` 序列化提供器 + +如需了解更多 `JSON` 序列化知识可查阅 [23. JSON 序列化](json-serialization#2351-自定义序列化提供器) 章节 + +::: + +### 19.5.8 参数验证 + +```cs showLineNumbers {4,7} +public interface IHttp : IHttpDispatchProxy +{ + [Post("http://furion.baiqian.ltd/post")] + Task PostXXXAsync([Range(1,10)]int id, [Required, MaxLength(10)]string name); + + [Post("http://furion.baiqian.ltd/post")] + Task PostXXXAsync([Required]User user); // 对象类型支持属性配置特性验证 +} +``` + +### 19.5.9 请求拦截 + +`Furion` 远程请求代理方式提供两种拦截方式: + +- 接口静态方法拦截 +- 参数标记拦截 + +```cs showLineNumbers {5,8,18} +public interface IHttp : IHttpDispatchProxy +{ + // 通过参数拦截 + [Post("http://furion.baiqian.ltd/post")] + Task PostXXXAsync([Interceptor(InterceptorTypes.Request)] Action action = default); + + // 全局拦截,类中每一个方法都会触发 + [Interceptor(InterceptorTypes.Request)] + static void OnRequesting1(HttpClient client, HttpRequestMessage req) + { + // 追加更多参数 + req.AppendQueries(new Dictionary { + { "access_token", "xxxx"} + }); + } + + // 全局拦截,类中每一个方法都会触发 + [Interceptor(InterceptorTypes.Request)] + static void OnRequesting2(HttpClient client, HttpRequestMessage req) + { + + } +} +``` + +**支持多次拦截** + +### 19.5.10 `HttpClient` 拦截 + +`Furion` 远程请求代理方式提供两种拦截方式: + +- 接口静态方法拦截 +- 参数标记拦截 + +```cs showLineNumbers {5,8,15} +public interface IHttp : IHttpDispatchProxy +{ + // 通过参数拦截 + [Post("http://furion.baiqian.ltd/post")] + Task PostXXXAsync([Interceptor(InterceptorTypes.Client)] Action action = default); + + // 全局拦截,类中每一个方法都会触发 + [Interceptor(InterceptorTypes.Client)] + static void onClientCreating1(HttpClient client) + { + + } + + // 全局拦截,类中每一个方法都会触发 + [Interceptor(InterceptorTypes.Client)] + static void onClientCreating2(HttpClient client) + { + + } +} +``` + +**支持多次拦截** + +### 19.5.11 请求之前拦截 + +`Furion` 远程请求代理方式提供两种拦截方式: + +- 接口静态方法拦截 +- 参数标记拦截 + +```cs showLineNumbers {5,8,15} +public interface IHttp : IHttpDispatchProxy +{ + // 通过参数拦截 + [Post("http://furion.baiqian.ltd/post")] + Task PostXXXAsync([Interceptor(InterceptorTypes.Request)] Action action = default); + + // 全局拦截,类中每一个方法都会触发 + [Interceptor(InterceptorTypes.Request)] + static void OnRequest1(HttpClient client, HttpRequestMessage req) + { + + } + + // 全局拦截,类中每一个方法都会触发 + [Interceptor(InterceptorTypes.Request)] + static void OnRequest2(HttpClient client, HttpRequestMessage req) + { + + } +} +``` + +**支持多次拦截** + +### 19.5.12 成功请求拦截 + +`Furion` 远程请求代理方式提供两种拦截方式: + +- 接口静态方法拦截 +- 参数标记拦截 + +```cs showLineNumbers {5,8,15} +public interface IHttp : IHttpDispatchProxy +{ + // 通过参数拦截 + [Post("http://furion.baiqian.ltd/post")] + Task PostXXXAsync([Interceptor(InterceptorTypes.Response)] Action action = default); + + // 全局拦截,类中每一个方法都会触发 + [Interceptor(InterceptorTypes.Response)] + static void OnResponsing1(HttpClient client, HttpResponseMessage res) + { + + } + + // 全局拦截,类中每一个方法都会触发 + [Interceptor(InterceptorTypes.Response)] + static void OnResponsing2(HttpClient client, HttpResponseMessage res) + { + + } +} +``` + +**支持多次拦截** + +### 19.5.13 请求异常拦截 + +`Furion` 远程请求代理方式提供两种拦截方式: + +- 接口静态方法拦截 +- 参数标记拦截 + +```cs showLineNumbers {5,8,15} +public interface IHttp : IHttpDispatchProxy +{ + // 通过参数拦截 + [Post("http://furion.baiqian.ltd/post")] + Task PostXXXAsync([Interceptor(InterceptorTypes.Exception)] Action action = default); + + // 全局拦截,类中每一个方法都会触发 + [Interceptor(InterceptorTypes.Exception)] + static void OnException1(HttpClient client, HttpResponseMessage res, string errors) + { + + } + + // 全局拦截,类中每一个方法都会触发 + [Interceptor(InterceptorTypes.Exception)] + static void OnException2(HttpClient client, HttpResponseMessage res, string errors) + { + + } +} +``` + +**支持多次拦截** + +### 19.5.14 各种返回值处理 + +`Furion` 远程请求默认提供四种返回值类型: + +- `HttpResponseMessage`:请求响应消息类型 +- `Stream`:流类型,可用来下载文件 +- `T`:泛型 `T` 类型 +- `String`:字符串类型,也就是直接将网络请求结果内容字符串化 + +如: + +```cs showLineNumbers +public interface IHttp : IHttpDispatchProxy +{ + // HttpResponseMessage + [Post("http://furion.baiqian.ltd/post")] + Task PostXXXAsync(); + + // Stream,可用来下载文件 + [Post("http://furion.baiqian.ltd/post")] + Task PostXXXAsync(); + + // T + [Post("http://furion.baiqian.ltd/post")] + Task PostXXXAsync(); + + // String + [Post("http://furion.baiqian.ltd/post")] + Task PostXXXAsync(); +} +``` + +### 19.5.15 设置 `Byte[]/Stream` 类型/上传文件 + +:::warning `Furion 4.4.0` 以下版本 + +在 `Furion 4.4.0+` 版本移除了 `[BodyBytes]` 方式,原因是拓展性太差,**新版本请使用 `HttpFile` 方式**。 + +::: + +有时候我们需要上传文件,需要设置 `Content-Type` 为 `multipart/form-data` 类型,如: + +```cs showLineNumbers {3,4} +public interface IHttp : IHttpDispatchProxy +{ + [Post("http://furion.baiqian.ltd/upload", ContentType = "multipart/form-data")] // bytes 可以通过 File.ReadAllBytes(文件路径) 获取 + Task PostXXXAsync([BodyBytes("键","文件名")]Byte[] bytes); + + // 支持多个文件 + [Post("http://furion.baiqian.ltd/upload", ContentType = "multipart/form-data")] // bytes 可以通过 File.ReadAllBytes(文件路径) 获取 + Task PostXXXAsync([BodyBytes("键","文件名")]Byte[] bytes,[BodyBytes("键","文件名")]Byte[] bytes2); +} +``` + +:::tip `Furion 4.4.0+` 版本 + +如果使用 `Furion 4.4.0+` 版本,请使用以下的 `HttpFile` 替代 `[BodyBytes]` 操作。请求有额外参数时 `HttpFile` 必须设置 `fileName` 值。 + +::: + +```cs showLineNumbers {3-4,7-8,11-12,15-16} +public interface IHttp : IHttpDispatchProxy +{ + [Post("http://furion.baiqian.ltd/upload", ContentType = "multipart/form-data")] + Task PostXXXAsync(HttpFile file); + + + [Post("http://furion.baiqian.ltd/upload", ContentType = "multipart/form-data")] + Task PostXXXAsync(HttpFile file, [Body("multipart/form-data")]User user); + + // 支持多个文件 + [Post("http://furion.baiqian.ltd/upload", ContentType = "multipart/form-data")] + Task PostXXXAsync(HttpFile[] files); + + // 支持多个文件 + [Post("http://furion.baiqian.ltd/upload", ContentType = "multipart/form-data")] + Task PostXXXAsync(IList files); +} +``` + +### 19.5.16 支持模板配置 + +模板格式为:`#(配置路径)` + +```cs showLineNumbers +public interface IHttp : IHttpDispatchProxy +{ + [Post("#(Furion:Address)/upload")] + Task PostXXXAsync([Body]User user); +} +``` + +```json showLineNumbers +{ + "Furion": { + "Address": "http://furion.baiqian.ltd" + } +} +``` + +方法的优先级高于接口定义的优先级。 + +### 19.5.17 重试策略 + +在 `Furion v2.18+` 版本支持配置重试策略,如: + +```cs showLineNumbers +[RetryPolicy(3, 1000)] // 支持全局 +public interface IHttp : IHttpDispatchProxy +{ + [Post("http://furion.baiqian.ltd"), RetryPolicy(3, 1000)] // 支持局部 + Task PostXXXAsync([Body]User user); +} +``` + +以上代码表示请求失败重试 `3` 次,每次延迟 `1000ms` 。 + +### 19.5.18 支持 `GZip` + +在 `Furion v3.2.0+` 版本支持 `GZip`,如: + +```cs showLineNumbers +public interface IHttp : IHttpDispatchProxy +{ + [Post("http://furion.baiqian.ltd", WithGZip = true)] + Task PostXXXAsync([Body]User user); +} +``` + +### 19.5.19 设置 `Url` 转码 + +过去版本会对所有的 `Url` 进行 `Uri.EscapeDataString` 转码,在 `Furion v3.8.0+` 版本支持 `Url` 转码设置,如: + +```cs showLineNumbers +public interface IHttp : IHttpDispatchProxy +{ + [Post("http://furion.baiqian.ltd", WithEncodeUrl = false)] + Task PostXXXAsync([Body]User user); +} +``` + +### 19.5.20 设置 `HTTP` 版本 + +可解决一些 `HTTP` 和 `HTTPS` 请求问题。 + +```cs showLineNumbers +public interface IHttp : IHttpDispatchProxy +{ + [Post("http://furion.baiqian.ltd", HttpVersion = "1.1")] + Task PostXXXAsync([Body]User user); +} +``` + +## 19.6 请求客户端配置 + +`Furion` 框架也提供了多个请求客户端配置,可以为多个客户端请求配置默认请求信息,目前支持四种模式进行配置。 + +### 19.6.1 `Startup.cs` 统一配置 + +```cs showLineNumbers {4,9} +services.AddRemoteRequest(options=> +{ + // 配置默认 HttpClient + options.AddHttpClient(string.Empty, c => { + // 其他配置 + }); + + // 配置特定客户端 + options.AddHttpClient("github", c => + { + c.BaseAddress = new Uri("https://api.github.com/"); + c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); + c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample"); + }); +}) +``` + +**配置了命名客户端后,每次请求都会自动加上这些配置。** + +- 在 `代理请求` 使用 + +```cs showLineNumbers {1,7} +// 在接口定义中使用 +[Client("github")] +public interface IHttp: IHttpDispatchProxy +{ +} + +// 在方法中使用 +[Get("api/getdata"), Client("github")] +Task GetData(); + +[Put("api/getdata"), Client("facebook")] +Task GetData(); +``` + +- 在 `字符串拓展` 使用 + +```cs showLineNumbers +// 设置请求拦截 +var response = await "http://47.100.247.61/api/sysdata/categories".SetClient("github").PostAsync(); +``` + +- 在 `IHttpClientFactory` 中使用 + +```cs showLineNumbers {3,13} +public class ValuesController : Controller +{ + private readonly IHttpClientFactory _httpClientFactory; + + public ValuesController(IHttpClientFactory httpClientFactory) + { + _httpClientFactory = httpClientFactory; + } + + [HttpGet] + public async Task Get() + { + var client = _httpClientFactory.CreateClient("github"); + string result = await client.GetStringAsync("/"); + return Ok(result); + } +} +``` + +### 19.6.2 配置客户端 `Timeout` + +默认情况下,`HttpClient` 请求超时时间为 `100秒`,可根据实际情况进行设置: + +```cs showLineNumbers {4,10} +// 配置默认 HttpClient +options.AddHttpClient(string.Empty, c => +{ + c.Timeout = TimeSpan.FromMinutes(2); +}); + +// 配置特定客户端 +options.AddHttpClient("github", c => +{ + c.Timeout = TimeSpan.FromMinutes(2); +}); +``` + +### 19.6.3 配置客户端生存期 + +每次对 `IHttpClientFactory` 调用 `CreateClient` 都会返回一个新 `HttpClient` 实例。 每个命名客户端都创建一个 `HttpMessageHandler`。 工厂管理 `HttpMessageHandler` 实例的生存期。 + +`IHttpClientFactory` 将工厂创建的 `HttpMessageHandler` 实例汇集到池中,以减少资源消耗。 新建 `HttpClient` 实例时,可能会重用池中的 `HttpMessageHandler` 实例(如果生存期尚未到期的话)。 + +处理程序的默认生存期为**两分钟**。 可在每个命名客户端上重写默认值: + +```cs showLineNumbers {3,7} +// 配置默认 HttpClient +options.AddHttpClient(string.Empty, c => { ... }) + .SetHandlerLifetime(TimeSpan.FromMinutes(5)); + +// 配置特定客户端 +options.AddHttpClient("github", c => { ... }) + .SetHandlerLifetime(TimeSpan.FromMinutes(5)); +``` + +### 19.6.4 自定义 `Client` 类方式 + +我们可以按照一定的规则编写特定服务的请求客户端,如: + +```cs showLineNumbers {1,3,5} +public class GitHubClient +{ + public HttpClient Client { get; private set; } + + public GitHubClient(HttpClient httpClient) + { + httpClient.BaseAddress = new Uri("https://api.github.com/"); + httpClient.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); + httpClient.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample"); + Client = httpClient; + } +} +``` + +然后在 `Startup.cs` 中注册: + +```cs showLineNumbers +services.AddHttpClient(); +``` + +使用如下: + +```cs showLineNumbers {3,13} +public class ValuesController : Controller +{ + private readonly GitHubClient _gitHubClient;; + + public ValuesController(GitHubClient gitHubClient) + { + _gitHubClient = gitHubClient; + } + + [HttpGet] + public async Task Get() + { + string result = await _gitHubClient.Client.GetStringAsync("/"); + return Ok(result); + } +} +``` + +### 19.6.5 自定义 `Client` 类 + 接口方式 + +我们也可以定义接口,通过接口的提供具体的服务 `API` 操作,无需手动配置 `Url`,如上面的 `GetStringAsync("/")`。 + +```cs showLineNumbers {1,3,6,10,18-21} +public interface IGitHubClient +{ + Task GetData(); +} + +public class GitHubClient : IGitHubClient +{ + private readonly HttpClient _client; + + public GitHubClient(HttpClient httpClient) + { + httpClient.BaseAddress = new Uri("https://api.github.com/"); + httpClient.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); + httpClient.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample"); + _client = httpClient; + } + + public async Task GetData() + { + return await _client.GetStringAsync("/"); + } +} +``` + +然后在 `Startup.cs` 中注册: + +```cs showLineNumbers +services.AddHttpClient(); +``` + +使用: + +```cs showLineNumbers {3,13} +public class ValuesController : Controller +{ + private readonly IGitHubClient _gitHubClient;; + + public ValuesController(IGitHubClient gitHubClient) + { + _gitHubClient = gitHubClient; + } + + [HttpGet] + public async Task Get() + { + string result = await _gitHubClient.GetData(); + return Ok(result); + } +} +``` + +### 19.6.6 `HttpClient` 超时问题 + +有时候会遇到 `HttpClient` 超时问题可尝试在 `Startup.cs` 中添加以下代码: + +```cs showLineNumbers +AppContext.SetSwitch("System.Net.DisableIPv6",true); +``` + +## 19.7 `SSL/https` 证书配置 + +有时候我们请求远程接口时会遇到 `The SSL connection could not be established, see inner exception.` 这样的错误,原因是证书配置不正确问题,下面有几种解决方法。 + +### 19.7.1 使用默认 `SSL` 证书 + +在一些情况下,可直接使用默认证书即可解决问题,如: + +```cs showLineNumbers {5,7-8,13,15-16} +services.AddRemoteRequest(options=> +{ + // 默认 HttpClient 在 Furion 框架内部已经配置了该操作 + options.AddHttpClient(string.Empty) + .ConfigurePrimaryHttpMessageHandler(u => new HttpClientHandler + { + AllowAutoRedirect = true, + UseDefaultCredentials = true + }); + + // 配置特定客户端 + options.AddHttpClient("github", c => { /*其他配置*/ }) + .ConfigurePrimaryHttpMessageHandler(u => new HttpClientHandler + { + AllowAutoRedirect = true, + UseDefaultCredentials = true + }); +}); +``` + +### 19.7.2 忽略特定客户端 `SSL` 证书检查 + +```cs showLineNumbers {5,7,12,14} +services.AddRemoteRequest(options=> +{ + // 默认 HttpClient 在 Furion 框架内部已经配置了该操作 + options.AddHttpClient(string.Empty) + .ConfigurePrimaryHttpMessageHandler(u => new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (_, _, _, _) => true, + }); + + // 配置特定客户端 + options.AddHttpClient("github", c => { /*其他配置*/ }) + .ConfigurePrimaryHttpMessageHandler(u => new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (_, _, _, _) => true, + }); +}); +``` + +:::tip 关于 `HttpClientHandler` 和 `SocketsHttpHandler` + +在 `.NET6` 之后默认使用 `SocketsHttpHandler ` 作为默认底层网络通信,但比 `HttpClientHandler` 提供了更多平台无差异的功能,对 `HttpClientHandler` 的任何设置都会转发到 `SocketsHttpHandler` 中,如需使用 `SocketsHttpHandler` 配置可参考: + +```cs showLineNumbers {3,5-8} +// 忽略 SSL 不安全检查,或 https 不安全或 https 证书有误 +options.AddHttpClient(string.Empty) + .ConfigurePrimaryHttpMessageHandler(u => new SocketsHttpHandler + { + SslOptions = new SslClientAuthenticationOptions + { + RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true + } + }); +``` + +::: + +### 19.7.3 手动指定 `SSL` 证书 + +```cs showLineNumbers {5,8-13} +services.AddRemoteRequest(options=> +{ + // 配置特定客户端 + options.AddHttpClient("github", c => { /*其他配置*/ }) + .ConfigurePrimaryHttpMessageHandler(u => new HttpClientHandler + { + // 手动配置证书 + ClientCertificateOptions = ClientCertificateOption.Manual, + ClientCertificates = { + new X509Certificate2("...","..."), + new X509Certificate2("...","..."), + new X509Certificate2("...","...") + } + }); +}); +``` + +### 19.7.4 忽略所有客户端证书检查 + +:::important 版本说明 + +以下内容仅限 `Furion v3.6.6+` 版本使用。 + +::: + +```cs showLineNumbers {1,4} +services.AddRemoteRequest(options=> +{ + // 需在所有客户端注册之前注册 + options.ApproveAllCerts(); +}); +``` + +## 19.8 配置客户端请求代理 + +```cs showLineNumbers {4,8,10-11,16,18-19} +services.AddRemoteRequest(options => +{ + // 创建 Web 代理对象 + var webProxy = new WebProxy(new Uri("http://192.168.31.11:8080"), BypassOnLocal: false); + + // 默认客户端配置 + options.AddHttpClient(string.Empty) + .ConfigurePrimaryHttpMessageHandler(u => new HttpClientHandler + { + Proxy = webProxy, + UseProxy = true + }); + + // 特定客户端配置 + options.AddHttpClient("github", c => { /*其他配置*/ }) + .ConfigurePrimaryHttpMessageHandler(u => new HttpClientHandler + { + Proxy = webProxy, + UseProxy = true + }); +}); +``` + +## 19.9 关于返回值非 `200` 时忽略 `Http状态` + +`Furion` 提供了非常方便的请求并且序列化请求结果 `PostAsAsync` +在 `2.8.8` 及以下版本,当返回结果的 `Http` 状态为非 `200` 时,会直接截断。考虑到请求接口的多样性,在 `2.8.9` 及以上版本增加忽略返回 `Http` 状态,直接序列化结果的方式。 + +```cs showLineNumbers +// 请求并且序列化请求结果 +var result = await "https://api.facebook.com/" + // 如果不加 OnException,则会直接截断 + .OnException((client, res, errors)=> { + // 激活异步拦截 此处可以做记录日志操作 也可保持现状 + }) + .PostAsAsync(); +``` + +`PostAsStringAsync()` 也使用同样的 `OnException` 操作使得忽略返回 `Http` 状态,原样返回 `Http` 请求结果 + +:::important 特别说明 + +如果不加 `OnException`,则会直接截断。 +如果需要复杂的 `Http Post` 请求,建议直接使用 `PostAsync`,返回值为 `HttpResponseMessage`,可以更灵活的控制结果。 + +::: + +## 19.10 关于同步请求 + +`Furion` 框架内部默认不提供同步请求操作,建议总是使用异步的方式请求。如在不能使用异步的情况下,可自行转换为同步执行。如: + +- 字符串拓展方式: + +```cs showLineNumbers {1,4} +var result = "https://api.facebook.com".GetAsync().GetAwaiter().GetResult(); + +// 如果不考虑 Task 异常捕获,可以直接 .Result +var result = "https://api.facebook.com".GetAsync().Result; +``` + +- 代理方式 + +```cs showLineNumbers {8,11} +public interface IHttp : IHttpDispatchProxy +{ + [Get("https://api.facebook.com")] + Task GetAsync(); +} + +// 同步调用 +var result = _http.GetAsync().GetAwaiter().GetResult(); + +// 如果不考虑 Task 异常捕获,可以直接 .Result +var result = _http.GetAsync().Result; +``` + +## 19.11 静态 `Default` 方式构建 + +这种方式比字符串拓展好,避免了直接在字符串上拓展。 + +```cs showLineNumbers +await HttpRequestPart.Default().SetRequestUrl("https://www.baidu.com").GetAsStringAsync(); +``` + +## 19.12 关闭 `Http` 请求日志 + +在 `Furion` 框架底层中,`HttpClient` 对象默认通过 `IHttpClientFactory` 创建的,只要发送请求就会自动打印日志,如: + +```bash showLineNumbers {13-22} +info: 2022-10-26 11:38:16(+08:00) 星期三 L System.Logging.EventBusService[0] #1 + EventBus Hosted Service is running. +info: 2022-10-26 11:38:17(+08:00) 星期三 L Microsoft.Hosting.Lifetime[14] #1 + Now listening on: https://localhost:5001 +info: 2022-10-26 11:38:17(+08:00) 星期三 L Microsoft.Hosting.Lifetime[14] #1 + Now listening on: http://localhost:5000 +info: 2022-10-26 11:38:17(+08:00) 星期三 L Microsoft.Hosting.Lifetime[0] #1 + Application started. Press Ctrl+C to shut down. +info: 2022-10-26 11:38:17(+08:00) 星期三 L Microsoft.Hosting.Lifetime[0] #1 + Hosting environment: Development +info: 2022-10-26 11:38:17(+08:00) 星期三 L Microsoft.Hosting.Lifetime[0] #1 + Content root path: D:\Workplaces\OpenSources\Furion\samples\Furion.Web.Entry\ +info: 2022-10-26 11:39:00(+08:00) 星期三 L System.Net.Http.HttpClient.Default.LogicalHandler[100] #8 + Start processing HTTP request GET https://www.baidu.com/ +info: 2022-10-26 11:39:00(+08:00) 星期三 L System.Net.Http.HttpClient.Default.ClientHandler[100] #8 + Sending HTTP request GET https://www.baidu.com/ +info: 2022-10-26 11:39:00(+08:00) 星期三 L System.Net.Http.HttpClient.Default.ClientHandler[101] #6 + Received HTTP response headers after 288.0665ms - 200 +info: 2022-10-26 11:39:00(+08:00) 星期三 L System.Net.Http.HttpClient.Default.LogicalHandler[101] #6 + End processing HTTP request after 326.1497ms - 200 +info: 2022-10-26 11:39:04(+08:00) 星期三 L System.Net.Http.HttpClient.Default.LogicalHandler[100] #3 + Start processing HTTP request GET https://www.baidu.com/ +``` + +如需关闭只需在 `appsettings.json` 和 `appsettings.Development.json` 中添加 `System.Net.Http.HttpClient` 日志类别过滤即可,如: + +```json showLineNumbers {2,7} +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.EntityFrameworkCore": "Information", + "System.Net.Http.HttpClient": "Warning" + } + } +} +``` + +## 19.13 获取 `Cookies` + +:::important 版本说明 + +以下内容仅限 `Furion 4.7.4 +` 版本使用。 + +::: + +- 字符串方式 + +```cs showLineNumbers {2} +var response = await "http://furion.baiqian.ltd/".GetAsync(); +var cookies = response.GetCookies(); +``` + +- 代理方式 + +```cs showLineNumbers {4,8} +public interface IHttp : IHttpDispatchProxy +{ + [Get("http://furion.baiqian.ltd/")] + Task GetAsync(); +} + +var response = await _http.GetAsync(); +var cookies = response.GetCookies(); +``` + +:::important 重复 `Key` 处理 + +**由于 `Cookies` 是支持重复 `Key` 的,所以通过索引方式如 `cookies[key]` 会抛出异常**,这时可以通过 `.Lookup()` 进行分组处理然后返回新的字典集合,如: + +```cs showLineNumbers {1-2,4} +var dicCookies = cookies.ToLookup(u => u.Key, u => u.Value) + .ToDictionary(u => u.Key, u => u.First()); // 取重复第一个 .First(); + +var cookie1 = dicCookies["key"]; +``` + +::: + +## 19.14 在 `WinForm/WPF` 中使用 + +远程请求可在 `WinForm/WPF` 中使用,只需要将方法/事件标记为 `async` 即可。 + +```cs showLineNumbers {1,3} +private async void button1_Click(object sender, EventArgs e) +{ + var result = await "http://furion.baiqian.ltd".GetAsStringAsync(); +} +``` + +## 19.15 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: + +--- + +:::note 了解更多 + +想了解更多 `HttpClient` 知识可查阅 [ASP.NET Core - HTTP 请求](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/http-requests?view=aspnetcore-5.0) 章节 + +::: diff --git a/handbook/docs/httpcontext.mdx b/handbook/docs/httpcontext.mdx new file mode 100644 index 0000000000000000000000000000000000000000..af0cefdeab07440ae44b30e7d80c987c24c8adba --- /dev/null +++ b/handbook/docs/httpcontext.mdx @@ -0,0 +1,227 @@ +--- +id: httpcontext +title: 5.2 HttpContext +sidebar_label: 5.2 HttpContext +description: 和客户端交互,没它真不行 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **问题修复** + + -  修复 启用请求 `Body` 重复读且在授权之前读取导致非 `GET/HEAD/OPTION` 请求异常 4.8.7.15 ⏱️2023.03.19 [#I6NX9E](https://gitee.com/dotnetchina/Furion/issues/I6NX9E) + +
+
+
+ +## 5.2.1 关于 `HttpContext` + +在 `ASP.NET` 的时代,我们通常通过 `HttpContext` 全局静态类获取请求上下文,但在 `ASP.NET Core` 中,`HttpContext` 是一个非静态的抽象类,无法手动创建,也无法通过静态获取。 + +虽然在 `ASP.NET Core` 中无法直接获取 `HttpContext` 对象。但是微软也提供了注入 `IHttpContextAccessor` 方式获取。 + +## 5.2.2 获取 `HttpContext` + +`ASP.NET Core` 和 `Furion` 提供了多种访问 `HttpContext` 的方式。 + +### 5.2.2.1 在 `ControllerBase` 派生类中 + +在 `ControllerBase` 派生类中,我们可以直接通过 `HttpContext` 属性获取 `HttpContext` 对象。 + +```cs showLineNumbers {5-6} +public class HomeController : Controller +{ + public IActionResult Index() + { + // 在这里HttpContext 是 Controller/ControllerBase 对象的属性 + var httpContext = HttpContext; + + return View(); + } +} +``` + +### 5.2.2.2 注入 `IHttpContextAccessor` + +在 `Furion` 框架中,默认已经注册了 `IHttpContextAccessor` 服务,所以我们可以通过构造函数注入该接口获取。 + +```cs showLineNumbers {3,5} +public class AppService +{ + public AppService(IHttpContextAccessor httpContextAccessor) + { + var httpContext = httpContextAccessor.HttpContext; + } +} +``` + +### 5.2.2.3 通过 `App.HttpContext` + +`App` 静态类也提供了 `App.HttpContext` 获取 `HttpContext` 对象。 + +```cs showLineNumbers +var request = App.HttpContext.Request; +``` + +:::tip 非 `Web` 中访问 + +在 `Web` 完整的生命周期内,`App.HttpContext` 都是有效的,但在非 `Web` 中返回 `null`,避免在多线程,事件总线,定时任务等中使用。 + +::: + +## 5.2.3 `HttpContext` 拓展方法 + +`Furion` 框架基于 `HttpContext` 提供了一些常用的拓展方法。 + +### 5.2.3.1 获取当前请求的特性 `Attribute` + +下列代码通常用于授权 `Handler` 中。 + +```cs showLineNumbers +var attribute = httpContext.GetMetadata(); +``` + +:::tip `Middleware` 中间件获取特性方式 + +在 `Middleware` 中间件中获取有所区别,主要通过 `HttpContext` 的 `Features` 获取,如: + +```cs showLineNumbers +var endpointFeature = httpContext.Features.Get(); +var attribute = endpointFeature?.Endpoint?.Metadata?.GetMetadata(); +``` + +::: + +### 5.2.3.2 设置 `Swagger` 自动授权 + +**`Swagger` 默认不能记住授权信息,一旦刷新浏览器就自动清空**,所以 `Furion` 提供了该拓展,即使刷新浏览器也能保持授权状态。 + +```cs showLineNumbers {4-5} +// 检查用户登录和生成 token 代码... +// ..... + +// 之后调用该拓展,这样就可以实现 Swagger 刷新也能记住登录了 +httpContext.SigninToSwagger("你的token"); +``` + +### 5.2.3.3 退出 `Swagger` 授权 + +通过后端代码强制性让 `Swagger` 授权实现,**只针对下一次请求有效!** + +```cs showLineNumbers +httpContext.SignoutToSwagger(); +``` + +### 5.2.3.4 获取本地 IP 地址 + +```cs showLineNumbers +// ipv4 +var ipv4 = httpContext.GetLocalIpAddressToIPv4(); + +// ipv6 +var ipv6 = httpContext.GetLocalIpAddressToIPv6(); +``` + +### 5.2.3.5 获取客户端 IP 地址 + +```cs showLineNumbers +// ipv4 +var ipv4 = httpContext.GetRemoteIpAddressToIPv4(); + +// ipv6 +var ipv6 = httpContext.GetRemoteIpAddressToIPv6(); +``` + +:::tip `Nginx/IIS` 无法获取正确客户端 `IP` 问题 + +如果服务器端使用了 `nginx/iis` 等反向代理工具,可添加以下代码配置: + +```cs showLineNumbers title="Startup.cs" {1,3,5-8,12} +services.Configure(options => +{ + options.ForwardedHeaders = ForwardedHeaders.All; + + // 若上面配置无效可尝试下列代码,比如在 IIS 中 + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; + options.KnownNetworks.Clear(); + options.KnownProxies.Clear(); +}); + +// 注意在 Configure 最前面配置 +app.UseForwardedHeaders(); +``` + +::: + +### 5.2.3.6 设置响应头 `Token` + +```cs showLineNumbers +httpContext.SetTokensOfResponseHeaders("token", "刷新token"); +``` + +## 5.2.4 读取 `Body` 内容(重复读) + +:::important 版本说明 + +以下内容仅限 `Furion 4.7.9 +` 版本使用。 + +::: + +默认情况下,`ASP.NET Core` 不支持重复读取 `Body` 内容,`Furion` 框架提供了拓展方法,需要按照以下步骤操作: + +1. **在 `Startup.cs` 的 `Configure` 启用 `Body` 重复读功能**: + +- `.NET5` 版本: + +```cs showLineNumbers {1,4} +public void Configure(IApplicationBuilder app, IWebHostEnvironment env) +{ + // ... + app.EnableBuffering(); + app.UseRouting(); + // .... +} +``` + +- `.NET6+` 版本: + +```cs showLineNumbers {7} +var builder = WebApplication.CreateBuilder(args).Inject(); +// ... +var app = builder.Build(); +// ... +app.UseInject(); + +app.EnableBuffering(); +app.MapControllers(); + +app.Run(); +``` + +注意:`app.EnableBuffering()` 必须在 `app.UseRouting()` 或 `app.MapControllers()` 之前注册。 + +2. **使用 `HttpContext` 或 `HttpRequest` 拓展 `.ReadBodyContentAsync()` 即可**。 + +```cs showLineNumbers {2,5} +// HttpContext 拓展 +var body = await httpContext.ReadBodyContentAsync(); + +// HttpRequest 拓展 +var body = await httpContext.Request.ReadBodyContentAsync(); +``` + +## 5.2.5 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/idgenerator.mdx b/handbook/docs/idgenerator.mdx new file mode 100644 index 0000000000000000000000000000000000000000..84826c7b979ed2ca88f7e79a3f54c4b455fd69ce --- /dev/null +++ b/handbook/docs/idgenerator.mdx @@ -0,0 +1,103 @@ +--- +id: idgenerator +title: 27. 分布式 ID 生成 +sidebar_label: 27. 分布式 ID 生成 +--- + +## 27.1 为什么需要分布式 ID + +- `全局唯一性`: 不能出现重复的 ID 号, 既然是唯一标识, 这是最基本的要求。 + +- `趋势递增`: 在 MySQL InnoDB 引擎中使用的是聚集索引, 由于多数 RDBMS 使用 B-tree 的数据结构来存储索引数据, 在主键的选择上面我们应该尽量使用有序的主键保证写入性能。 + +- `单调递增`: 保证下一个 ID 一定大于上一个 ID, 例如事务版本号, IM 增量消息, 排序等特殊需求。 + +- `信息安全`: 如果 ID 是连续的, 恶意用户的扒取工作就非常容易做了, 直接按照顺序下载指定 URL 即可; 如果是订单号就更危险了, 竞对可以直接知道我们一天的单量。 所以在一些应用场景下, 会需要 ID 无规则, 不规则。 + +## 27.2 分布式 ID 有哪些 + +常见的分布式 ID 有 `连续 GUID`、`短 ID`、`雪花算法 ID`。 + +## 27.3 如何使用 + +### 27.3.1 `连续 GUID` 方式 + +- 静态 `IDGen` 方式 + +```cs showLineNumbers {1,4} +var guid = IDGen.NextID(); + +// 还可以配置更多参数 +var guid2 = IDGen.NextID(new SequentialGuidSettings { LittleEndianBinary16Format = true })); // SequentialGuidSettings 参数取决于你的分布式ID的实现 +``` + +:::important 特别提醒 + +如果在循环中使用 `IDGen` 静态类方式,性能最差,原因是底层不断解析服务。如果非循环中,性能等于下面两种用法。 + +::: + +- `IDistributedIDGenerator` 注入方式 **推荐** + +```cs showLineNumbers {2,6} +private readonly IDistributedIDGenerator _idGenerator; +public AppServices(IDistributedIDGenerator idGenerator) +{ + _idGenerator = idGenerator; + + var guidObject = _idGenerator.Create(); +} +``` + +- `SequentialGuidIDGenerator` 方式 + +```cs showLineNumbers +var idGen = new SequentialGuidIDGenerator(); +var guid = idGen.Create(); + +// 更多参数 +var idGen2 = new SequentialGuidIDGenerator(); +var guid2 = idGen2.Create(new SequentialGuidSettings { LittleEndianBinary16Format = true }); +``` + +### 27.3.2 `短 ID` + +`短 ID` 按道理不应该放在分布式 ID 生成这个章节,它的作用用途常用于并发不强的内部系统中,比如 `任务ID`,`Issue 编号` 等等。 + +```cs showLineNumbers {1,4} +var shortid = ShortIDGen.NextID(); // 生成一个包含数字,字母,不包含特殊符号的 8 位短id + +// 添加更多配置 +var shortid = ShortIDGen.NextID(new GenerationOptions { + UseNumbers = false, // 不包含数字 + UseSpecialCharacters = true, // 包含特殊符号 + Length = 8// 设置长度,注意:不设置次长度是随机长度!!!!!!! +}); + +// 自定义生成短 ID 参与运算字符 +string characters = "ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩ①②③④⑤⑥⑦⑧⑨⑩⑪⑫"; //whatever you want; +ShortIDGen.SetCharacters(characters); + +// 自定义随机数(for)步长 +int seed = 1939048828; +ShortIDGen.SetSeed(seed); + +// 重载所有自定义配置 +ShortIDGen.Reset(); +``` + +### 27.3.3 `雪花算法 ID` + +`Furion` 在最新的 `2.1 +` 版本移除了`雪花算法 ID` 功能,原因是: + +**目前,`雪花算法 ID` 使用频率不高,而且实现 `雪花算法 ID` 的方式也是千差万别,所以框架移除该功能,采用拓展或自集成方式。** + +## 27.4 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: + +--- diff --git a/handbook/docs/inject.mdx b/handbook/docs/inject.mdx new file mode 100644 index 0000000000000000000000000000000000000000..7ca938479b22623455e56ace9bc09337e422b448 --- /dev/null +++ b/handbook/docs/inject.mdx @@ -0,0 +1,235 @@ +--- +id: inject +title: 2.8 神奇的 Inject +sidebar_label: 2.8 神奇的 Inject +description: 一句话集成 Furion +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +:::warning 特别注意 + +所有 `.AddInject****()` 方法不能同时注册,只能取其一。 + +::: + +## 2.8.1 `Inject` 设计 + +`Inject [ɪnˈdʒekt]` 意思是 `注入,注射` 的意思,在这里意为使用最小的侵入式对应用进行机能改造。 + +`Inject()` 方法是 `Furion` 框架提供的最小侵入式的方法,可以让任何 `ASP.NET Core` Web 后端项目瞬间支持所有 `Furion` 框架特性。 + +## 2.8.2 `Inject` 方法有哪些 + +- `Inject()`:在 `Program.cs` 中注册 +- `AddInject()`:在 `Startup.cs` 的 `ConfigureServices` 中注册 +- `AddInjectBase()`:在 `Startup.cs` 的 `ConfigureServices` 中注册 +- `AddInjectWithUnifyResult()/ AddInjectWithUnifyResult()`:在 `Startup.cs` 的 `ConfigureServices` 中注册 +- `AddInjectMini()`:支持 `Minimal API` +- `UseInject()`:在 `Startup.cs` 的 `Configure` 中注册 +- `UseInjectBase()`:在 `Startup.cs` 的 `Configure` 中注册 + +## 2.8.3 在 `Program.cs` 中使用 + +### 2.8.3.1 `Inject()` 方法 + +`Inject()` 是在启动程序 `Program.cs` 中使用的,集成 `Furion` 框架唯一方法,如: + +```cs showLineNumbers {6,18} title="Furion.Web.Entry\Program.cs" +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Furion.Web.Entry +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) + { + return Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.Inject() + .UseStartup(); + }); + } + } +} +``` + +`.Inject()` 方法提供了一个可配置的选项 `InjectOptions`,可以通过该配置配置框架初始化的一些配置,如: + +```cs showLineNumbers {1} +.Inject((builder, options) => +{ + options.AssemblyName = "Furion"; // 配置外部程序集名称 + options.AutoRegisterBackgroundService = true; // 是否自动注册 BackgroundService + + options.ConfigurationScanDirectories("目录1", "目录2", ...); // 配置配置文件扫描目录 + options.IgnoreConfigurationFiles("文件1", "文件2", ...); // 配置忽略配置文件 + + // 配置配置对象 + options.ConfigureAppConfiguration((context, config) => + { + }); + + // 配置配置对象(Web) + options.ConfigureWebAppConfiguration((context, config) => + { + }); + + // 配置 ConfigureServices + options.ConfigureServices((context, services) => + { + }); + + // 配置 ConfigureServices(Web) + options.ConfigureWebServices((context, services) => + { + }); +}); +``` + +## 2.8.4 `Startup.ConfigureServices` 使用 + +### 2.8.4.1 `AddInject()` 方法 + +`AddInject()` 方法是在 `Startup.cs` 的 `ConfigureServices` 中提供最基础功能的注册。 + +`AddInject()` 包含以下基础功能注册: + +```cs showLineNumbers +services.AddSpecificationDocuments([options]) + .AddDynamicApiControllers() + .AddDataValidation() + .AddFriendlyException(); +``` + +使用如下: + +```cs showLineNumbers {3,5,7,10} +namespace Furion.Web.Entry +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddInject(); // 支持直接注册(和下面代码二选一,不能同时注册两次) + + services.AddControllers() + .AddInject(); // 支持链式注册(和上面代码二选一,不能同时注册两次) + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + // 其他代码 + } + } +} +``` + +### 2.8.4.2 `AddInjectBase()` 方法 + +`AddInjectBase()` 方法是在 `Startup.cs` 的 `ConfigureServices` 中提供最基础功能的注册。 + +`AddInjectBase()` 包含以下基础功能注册: + +```cs showLineNumbers +services.AddDataValidation() + .AddFriendlyException(); +``` + +使用同上 `AddInject()`。 + +### 2.8.4.3 `AddInjectMini()` 方法 + +:::important 版本说明 + +以下内容仅限 `Furion 3.7.6 +` 版本使用。 + +::: + +支持 `.NET6` 最新的 `Minimal API` 模式: + +```cs showLineNumbers +builder.Services.AddInjectMini(); +``` + +### 2.8.4.4 `AddInjectWithUnifyResult()` 方法 + +`AddInjectWithUnifyResult()` 方法实际上等同于: + +```cs showLineNumbers +services.AddInject([swaggerGen]) + .AddUnifyResult(); +``` + +使用同上 `AddInject()`。 + +## 2.8.5 `Startup.Configure` 使用 + +### 2.8.5.1 `UseInject()` 方法 + +`UseInject()` 方法是在 `Startup.cs` 的 `Configure` 中提供最基础功能的注册。 + +`UseInject()` 包含以下基础功能注册: + +```cs showLineNumbers +app.UseSpecificationDocuments(); +``` + +使用如下: + +```cs showLineNumbers {3,5,12} +namespace Furion.Web.Entry +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + // 其他代码 + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseInject(); // 包含最小 Swagger 注册 + } + } +} +``` + +### 2.8.5.2 `UseInjectBase()` 方法 + +`UseInjectBase()` 方法是在 `Startup.cs` 的 `Configure` 中提供最基础功能的注册。 + +`UseInjectBase()` 实际上是个空方法,为了规范化代码特意建立的。 + +使用同上 `UseInject()`。 + +## 2.8.6 配置 `Inject()` 默认注册服务 + +所有 `.Inject` 方法都可以传入委托,通过委托可以进一步去配置默认行为。如: + +```cs showLineNumbers {1} +.AddInject(options => +{ + options.ConfigureDataValidation(vail => + { + vail.GlobalEnabled = false; + }); + + // .... +}); +``` + +## 2.8.7 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/introduce.mdx b/handbook/docs/introduce.mdx new file mode 100644 index 0000000000000000000000000000000000000000..f3e9783ae5267e9bd73908ec829dc634fedd21d6 --- /dev/null +++ b/handbook/docs/introduce.mdx @@ -0,0 +1,175 @@ +--- +id: introduce +title: 1.1 介绍 +sidebar_label: 1.1 介绍 +slug: / +description: 让 .NET 开发更简单,更通用,更流行。 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + + + +
+ +[![star](https://gitee.com/dotnetchina/Furion/badge/star.svg?theme=gvp)](https://gitee.com/dotnetchina/Furion/stargazers) [![fork](https://gitee.com/dotnetchina/Furion/badge/fork.svg?theme=gvp)](https://gitee.com/dotnetchina/Furion/members) [![GitHub stars](https://img.shields.io/github/stars/MonkSoul/Furion?logo=github)](https://github.com/MonkSoul/Furion/stargazers) [![GitHub forks](https://img.shields.io/github/forks/MonkSoul/Furion?logo=github)](https://github.com/MonkSoul/Furion/network) [![GitHub license](https://img.shields.io/badge/license-MIT-orange)](https://github.com/MonkSoul/Furion/blob/main/LICENSE) [![nuget](https://img.shields.io/nuget/v/Furion.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion) + +
+ +
+ +让 .NET 开发更简单,更通用,更流行。 + +
+ + + +## 💐 序言 + +> 无私奉献不是天方夜谭,有时候,我们也可以做到。 + +## 🍕 名字的由来 + +> 故事是这样子的: +> +> 自微软宣布 `.NET 5` 平台消息之后,就琢磨着开发一个基于 `.NET 5` 平台的开发框架,想做第一个吃 `.NET 5` 螃蟹尝鲜之人。 +> +> 一开始想到了 `Lazier` 作为框架的名称,中文有 **更懒** 的意思。符合我的 “一切从简,只为了更懒” 的开发理念。 +> +> 但是 **更懒** 和 **更烂** 中文读音相近且没有特色,而且寓意也不是很好,对此换名问题苦恼了好些天。 +> +> 刚好有一次在 QQ 群中无意间刷到了群友发的 **“先知”** 单词:**“`Furion`”**,就那一刻,就认定它了! +> +> `Furion` 中文有 `先知` 的意思,恰好符合我创建框架的初衷。所以,**`Furion`** 就诞生了。 + +## 🍟 文档地址 + +- 中文文档:[http://furion.baiqian.ltd](http://furion.baiqian.ltd) + +## 🌭 开源地址 + +- Gitee:[https://gitee.com/dotnetchina/Furion](https://gitee.com/dotnetchina/Furion) +- GitHub:[https://github.com/monksoul/Furion](https://github.com/monksoul/Furion) +- NuGet:[https://www.nuget.org/packages/Furion](https://www.nuget.org/packages/Furion) + +## 🥥 框架拓展包 + +| 包类型 | 名称 | 版本 | 描述 | +| :---------------------------------------------------------------------------------------------------------------------------------------------: | ------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | --------------------------------------------------- | +| [![nuget](https://shields.io/badge/-NuGet-blue?cacheSeconds=604800)](https://www.nuget.org/packages/Furion) | Furion | [![nuget](https://img.shields.io/nuget/v/Furion.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion) | Furion 核心包 | +| [![nuget](https://shields.io/badge/-NuGet-blue?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.Pure) | Furion.Pure | [![nuget](https://img.shields.io/nuget/v/Furion.Pure.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.Pure) | Furion 纯净版包(不含 EFCore) | +| [![nuget](https://shields.io/badge/-NuGet-blue?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.Extras.Authentication.JwtBearer) | Furion.Extras.Authentication.JwtBearer | [![nuget](https://img.shields.io/nuget/v/Furion.Extras.Authentication.JwtBearer.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.Extras.Authentication.JwtBearer) | Furion Jwt 拓展包 | +| [![nuget](https://shields.io/badge/-NuGet-blue?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.Extras.DependencyModel.CodeAnalysis) | Furion.Extras.DependencyModel.CodeAnalysis | [![nuget](https://img.shields.io/nuget/v/Furion.Extras.DependencyModel.CodeAnalysis.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.Extras.DependencyModel.CodeAnalysis) | Furion CodeAnalysis 拓展包 | +| [![nuget](https://shields.io/badge/-NuGet-blue?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.Extras.ObjectMapper.Mapster) | Furion.Extras.ObjectMapper.Mapster | [![nuget](https://img.shields.io/nuget/v/Furion.Extras.ObjectMapper.Mapster.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.Extras.ObjectMapper.Mapster) | Furion Mapster 拓展包 | +| [![nuget](https://shields.io/badge/-NuGet-blue?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.Extras.DatabaseAccessor.SqlSugar) | Furion.Extras.DatabaseAccessor.SqlSugar | [![nuget](https://img.shields.io/nuget/v/Furion.Extras.DatabaseAccessor.SqlSugar.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.Extras.DatabaseAccessor.SqlSugar) | Furion SqlSugar 拓展包 | +| [![nuget](https://shields.io/badge/-NuGet-blue?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.Extras.DatabaseAccessor.Dapper) | Furion.Extras.DatabaseAccessor.Dapper | [![nuget](https://img.shields.io/nuget/v/Furion.Extras.DatabaseAccessor.Dapper.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.Extras.DatabaseAccessor.Dapper) | Furion Dapper 拓展包 | +| [![nuget](https://shields.io/badge/-NuGet-blue?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.Extras.DatabaseAccessor.MongoDB) | Furion.Extras.DatabaseAccessor.MongoDB | [![nuget](https://img.shields.io/nuget/v/Furion.Extras.DatabaseAccessor.MongoDB.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.Extras.DatabaseAccessor.MongoDB) | Furion MongoDB 拓展包 | +| [![nuget](https://shields.io/badge/-NuGet-blue?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.Extras.Logging.Serilog) | Furion.Extras.Logging.Serilog | [![nuget](https://img.shields.io/nuget/v/Furion.Extras.Logging.Serilog.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.Extras.Logging.Serilog) | Furion Serilog 拓展包 | +| [![nuget](https://shields.io/badge/-NuGet-blue?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.Xunit) | Furion.Xunit | [![nuget](https://img.shields.io/nuget/v/Furion.Xunit.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.Xunit) | Furion Xunit 单元测试拓展包 | +| [![nuget](https://shields.io/badge/-NuGet-blue?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.Pure.Xunit) | Furion.Pure.Xunit | [![nuget](https://img.shields.io/nuget/v/Furion.Pure.Xunit.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.Pure.Xunit) | Furion 纯净版包 Xunit 单元测试拓展包(不含 EFCore) | +| [![nuget](https://shields.io/badge/-NuGet-blue?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.Tools.CommandLine) | Furion.Tools.CommandLine | [![nuget](https://img.shields.io/nuget/v/Furion.Tools.CommandLine.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.Tools.CommandLine) | Furion Tools 命令行参数解析 | + +## 🍄 框架脚手架 + +#### `Furion + EFCore` + +| 模板类型 | 名称 | 版本 | 描述 | +| :--------------------------------------------------------------------------------------------------------------------------------------: | -------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------: | ---------------------- | +| [![nuget](https://shields.io/badge/-NuGet-yellow?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.Template.Mvc/) | Furion.Template.Mvc | [![nuget](https://img.shields.io/nuget/v/Furion.Template.Mvc.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.Template.Mvc/) | Mvc 模板 | +| [![nuget](https://shields.io/badge/-NuGet-yellow?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.Template.Api/) | Furion.Template.Api | [![nuget](https://img.shields.io/nuget/v/Furion.Template.Api.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.Template.Api/) | WebApi 模板 | +| [![nuget](https://shields.io/badge/-NuGet-yellow?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.Template.App/) | Furion.Template.App | [![nuget](https://img.shields.io/nuget/v/Furion.Template.App.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.Template.App/) | Mvc/WebApi 模板 | +| [![nuget](https://shields.io/badge/-NuGet-yellow?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.Template.Razor/) | Furion.Template.Razor | [![nuget](https://img.shields.io/nuget/v/Furion.Template.Razor.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.Template.Razor/) | RazorPages 模板 | +| [![nuget](https://shields.io/badge/-NuGet-yellow?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.Template.RazorWithWebApi/) | Furion.Template.RazorWithWebApi | [![nuget](https://img.shields.io/nuget/v/Furion.Template.RazorWithWebApi.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.Template.RazorWithWebApi/) | RazorPages/WebApi 模板 | +| [![nuget](https://shields.io/badge/-NuGet-yellow?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.Template.Blazor/) | Furion.Template.Blazor | [![nuget](https://img.shields.io/nuget/v/Furion.Template.Blazor.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.Template.Blazor/) | Blazor 模板 | +| [![nuget](https://shields.io/badge/-NuGet-yellow?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.Template.BlazorWithWebApi/) | Furion.Template.BlazorWithWebApi | [![nuget](https://img.shields.io/nuget/v/Furion.Template.BlazorWithWebApi.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.Template.BlazorWithWebApi/) | Blazor/WebApi 模板 | + +#### `Furion + SqlSugar` + +| 模板类型 | 名称 | 版本 | 描述 | +| :-----------------------------------------------------------------------------------------------------------------------------------------------: | ----------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | ---------------------- | +| [![nuget](https://shields.io/badge/-NuGet-yellow?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.SqlSugar.Template.Mvc/) | Furion.SqlSugar.Template.Mvc | [![nuget](https://img.shields.io/nuget/v/Furion.SqlSugar.Template.Mvc.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.SqlSugar.Template.Mvc/) | Mvc 模板 | +| [![nuget](https://shields.io/badge/-NuGet-yellow?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.SqlSugar.Template.Api/) | Furion.SqlSugar.Template.Api | [![nuget](https://img.shields.io/nuget/v/Furion.SqlSugar.Template.Api.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.SqlSugar.Template.Api/) | WebApi 模板 | +| [![nuget](https://shields.io/badge/-NuGet-yellow?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.SqlSugar.Template.App/) | Furion.SqlSugar.Template.App | [![nuget](https://img.shields.io/nuget/v/Furion.SqlSugar.Template.App.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.SqlSugar.Template.App/) | Mvc/WebApi 模板 | +| [![nuget](https://shields.io/badge/-NuGet-yellow?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.SqlSugar.Template.Razor/) | Furion.SqlSugar.Template.Razor | [![nuget](https://img.shields.io/nuget/v/Furion.SqlSugar.Template.Razor.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.SqlSugar.Template.Razor/) | RazorPages 模板 | +| [![nuget](https://shields.io/badge/-NuGet-yellow?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.SqlSugar.Template.RazorWithWebApi/) | Furion.SqlSugar.Template.RazorWithWebApi | [![nuget](https://img.shields.io/nuget/v/Furion.SqlSugar.Template.RazorWithWebApi.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.SqlSugar.Template.RazorWithWebApi/) | RazorPages/WebApi 模板 | +| [![nuget](https://shields.io/badge/-NuGet-yellow?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.SqlSugar.Template.Blazor/) | Furion.SqlSugar.Template.Blazor | [![nuget](https://img.shields.io/nuget/v/Furion.SqlSugar.Template.Blazor.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.SqlSugar.Template.Blazor/) | Blazor 模板 | +| [![nuget](https://shields.io/badge/-NuGet-yellow?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.SqlSugar.Template.BlazorWithWebApi/) | Furion.SqlSugar.Template.BlazorWithWebApi | [![nuget](https://img.shields.io/nuget/v/Furion.SqlSugar.Template.BlazorWithWebApi.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.SqlSugar.Template.BlazorWithWebApi/) | Blazor/WebApi 模板 | + +**[如何使用脚手架](http://furion.baiqian.ltd/docs/template)** + +## 🍎 框架特点 + +- 全新面貌:基于 `.NET5/6/7/8+` 平台,没有历史包袱 +- 极少依赖:框架只依赖两个第三方包 +- 极易入门:只需要一个 `Inject()` 即可完成配置 +- 极速开发:内置丰富的企业应用开发功能 +- 极其灵活:轻松面对多变复杂的需求 +- 极易维护:采用独特的架构思想,只为长久维护设计 +- 完整文档:提供完善的开发文档 +- **跨全平台:支持所有主流操作系统及 .NET 全部项目类型** + +## 🥝 功能模块 + + + +## 🥐 框架依赖 + +`Furion` 为了追求极速入门,极致性能,尽可能的不使用或减少第三方依赖。目前 `Furion` 仅集成了以下两个依赖: + +- [MiniProfiler](https://github.com/MiniProfiler/dotnet):性能分析和监听必备 +- [Swashbuckle](https://github.com/domaindrivendev/Swashbuckle.AspNetCore):`Swagger` 接口文档 + +麻雀虽小五脏俱全。`Furion` 即使只集成了这两个依赖,但是主流的 `依赖注入/控制反转`,`AOP` 面向切面编程,`事件总线`,`数据验证`,`数据库操作` 等等一个都不少。 + +## 🥗 环境要求 + +- Visual Studio 2019 16.8 + +- Visual Studio Code +- .NET 5 SDK + + +## 🥪 支持平台 + +- 运行环境 + - Windows + - Linux + - MacOS/MacOS M1 CPU + - Docker/K8S/K3S/Rancher + - ~~Xamarin/MAUI~~ +- 数据库 + - SqlServer + - Sqlite + - Azure Cosmos + - MySql + - MariaDB + - PostgreSQL + - InMemoryDatabase + - Oracle + - Firebird + - 达梦数据库 + - MongoDB +- 应用部署 + - Kestrel + - Nginx + - Jexus + - IIS + - Apache + - PM2 + - Supervisor + - 独立发布/单文件 + - 容器(Docker/K8S/K3S/Rancher/PodMan) + +## 🍖 关于性能 + +`Furion` 目前采用 `Visual Studio 2019 16.8+` 自带性能测试和 `JMeter` 进行测试,由于篇幅有限,只贴部分测试图,测试结果如下: + + + +## 🍻 贡献代码 + +`Furion` 遵循 [MIT](https://gitee.com/dotnetchina/Furion/blob/v4/LICENSE) 开源协议,欢迎大家提交 `Pull Request` 或 `Issue`。 + +如果要为项目做出贡献,请查看 [贡献指南](/docs/contribute)。感谢每一位为 `Furion` 贡献代码的朋友。 diff --git a/handbook/docs/ipc.mdx b/handbook/docs/ipc.mdx new file mode 100644 index 0000000000000000000000000000000000000000..05bd6205dfd0635cd43a2b353b95962b478a5004 --- /dev/null +++ b/handbook/docs/ipc.mdx @@ -0,0 +1,145 @@ +--- +id: ipc +title: 33. IPC 进程通信 +sidebar_label: 33. IPC 进程通信 +--- + +## 33.1 什么是 `IPC` + +> 引用百度百科 +> +> IPC(Inter-Process Communication,进程间通信)。进程间通信是指两个进程的数据之间产生交互。 + +通俗点说,`IPC` 可以实现不同应用程序间通信(交互数据)。 + +## 33.2 实现 `IPC` 通信方式 + +- 半双工 Unix 管道 +- FIFOs(命名管道) +- **消息队列**(常用模式) +- 信号量 +- **共享内存**(常用模式,`Furion` 框架默认实现方式) +- **网络 Socket**(常用模式) + +## 33.3 `IPC` 通信模式 + +`IPC` 本身指的是 `进程间` 通信,但 `Furion` 框架将内置 `进程间/内` 两种进程通信模式。 + +- `进程内通信`:`Furion` 采用 `Channel` 管道提供进程内通信 +- `进程外通信`:`Furion` 采用 `MemoryMapperFile` 共享内存方式实现进程外通信(后续版本完善) + +## 33.4 进程内通信(线程间) + +进程内通信俗称线程间通信,`Furion` 框架采用 `C#` 提供的 `Channel(管道)` + `Lazy` + `Task.Factory` 实现长时间高性能的线程间通信机制。`Channel` 管道也是目前 `.NET/C#` 实现 `生产者-订阅者` 模式最简易且最为强大的实现。 + +### 33.4.1 了解 `Channel` + +`Channel` 是在 `.NET Core 2.1+` 版本之后加入。`Channel` 底层实现是一个高效的、线程安全的队列,可以在线程之间传递数据。 + +`Channel` 的主要应用场景是 `发布/订阅、观察者模式` 中使用,如:`事件总线` 就是最好的实现方式。通过 `Channel` 实现 `生产-消费` 机制可以减少项目间的耦合,提高应用吞吐量。 + +`Furion` 框架提供了 `ChannelContext` 密封类,提供 `UnBoundedChannel` 和 `BoundedChannel` 两种管道通信模式。 + +- `UnBoundedChannel`:具有无限容量的 `Channel`, 生产者可以全速进行生产数据,但如果消费者的消费速度低于生产者,`Channel` 的资源使用会无限增加,会有服务器资源耗尽的可能。 +- `BoundedChannel`:具有有限容量的 `Channel`,`Furion` 框架默认为 `1000`,到达上限后,生产者进入等待写入直到有空闲,好处是可以控制生产的速度,控制系统资源的使用。**(推荐)** + +### 33.4.2 常规使用 + +#### 创建 `ChannelHandler` 管道处理程序 + +```cs showLineNumbers {1,10,17} +using Furion.IPCChannel; +using System; +using System.Threading.Tasks; + +namespace Furion.Core +{ + /// + /// 创建管道处理程序(处理 String 类型消息) + /// + public class MyChannelHandler : ChannelHandler + { + /// + /// 接受到管道消息后处理程序 + /// + /// + /// + public override Task InvokeAsync(string message) + { + Console.WriteLine(message); + + return Task.CompletedTask; + } + } +} +``` + +:::note + +` ChannelHandler` 泛型类型决定了你要接受那种类型的消息,不同类型消息将会自动过滤筛选。 + +::: + +#### 使用 `ChannelContext` 发送消息 + +```cs showLineNumbers {5-6} +public async Task SendAsync() +{ + for (int i = 0; i < 100; i++) + { + // 使用有限容量生产数据 + await ChannelContext.BoundedChannel.Writer.WriteAsync($"Loop {i} times."); + } +} +``` + +以上代码也可以通过 `ChannelContext.BoundedChannel.Writer.TryWrite()` 同步写入。 + +### 33.4.3 实现多订阅 + +默认情况下,`Furion` 初始化了一个长时间的 `Task` 任务进行数据检查及订阅,如需实现多订阅模式,可创建新的订阅任务即可: + +```cs showLineNumbers +var reader = ChannelContext.BoundedChannel.Reader; + +// 创建长时间线程管道读取器 +_ = Task.Factory.StartNew(async () => + { + while (await reader.WaitToReadAsync()) + { + if (!reader.TryRead(out var message)) continue; + // 默认重试 3 次(每次间隔 1s) + await Retry.Invoke(async () => await Activator.CreateInstance().InvokeAsync(message), 3, 1000, finalThrow: false); + } + }, TaskCreationOptions.LongRunning); +``` + +### 33.4.4 更多 `Channel` 知识 + +可查阅 [Dotnet Core 下的 Channel, 你用了吗?](https://www.cnblogs.com/tiger-wang/p/14068973.html) 博客教程(😃 写的不错) + +### 33.4.5 `CallContext` 方式 + +在 `Furion v2.18+` 版本提供了 `CallContext` 静态类,内部使用 `AsyncLocal` 实现,也可以实现线程间通信,如: + +```cs showLineNumbers +CallContext.SetLocalValue("name", "Furion"); +CallContext.GetLocalValue("name"); + +CallContext.SetLocalValue("count", 1); +CallContext.GetLocalValue("count"); +``` + +## 34.5 进程外通信(共享内存) + +`Furion` 目前暂未提供的进程外通信功能,将在后续版本实现(主要是模块设计还未想好,技术已实现)。 + +主要是通过 `MemoryMapperFile` 实现共享内存达到进程外通信功能,[了解更多 MemoryMapperFile](https://docs.microsoft.com/zh-cn/dotnet/api/system.io.memorymappedfiles.memorymappedfile?view=net-5.0) + +## 33.6 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/job-old.mdx b/handbook/docs/job-old.mdx new file mode 100644 index 0000000000000000000000000000000000000000..d68483fc4c39c3ca361819e4d4f4555c35f1e1c6 --- /dev/null +++ b/handbook/docs/job-old.mdx @@ -0,0 +1,890 @@ +--- +id: job-old +title: 26. 定时任务/后台任务 +sidebar_label: 26. 定时任务/后台任务 +--- + +:::warning 4.8.0+ 版本说明 + +**在 `Furion 4.8.0+` 版本采用 [Sundial](https://gitee.com/dotnetchina/Sundial) 定时任务替换原有的 `TaskScheduler`**,**😶[查看新文档](/docs/job)** + +旧版本将支持到 `2022年12月31日`,之后旧版本代码从框架中移除,请尽快使用新版本替代。 + +::: + +:::important 版本说明 + +以下内容仅限 `Furion 2.0.0 +` 版本使用。 + +::: + +:::tip IIS 部署说明 + +由于 IIS 有回收的机制,所以定时任务应该采用独立部署,不然经常出现不能触发的情况。查看【[Worker Service 章节](./process-service.mdx)】 + +::: + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +## 26.1 关于定时任务 + +顾名思义,定时任务就是在特定的时间或符合某种时间规律执行的任务。通常定时任务有四种时间调度方式: + +- `缓隔时间` 方式:延迟多少时间后调配任务,这种方式任务只会被调用一次。 +- `间隔时间` 方式:每隔一段固定时间调配任务,无间断调用任务。 +- `Cron 表达式` 方法:通过 `Cron` 表达式计算下一次执行时间进行调配任务,可以配置特定时间范围内执行,也可以无间断执行。 +- `自定义下次执行时间`:可以通过各种逻辑运算返回下一次执行时间 + +## 26.2 如何实现 + +`Furion` 框架提供了两种方式实现定时任务: + +- `SpareTime` 静态类:`SpareTime` 静态类提供 `SpareTime.Do([options])` 方式调用。 +- `ISpareTimeWorker` 依赖方式:通过自定义类实现 `ISpareTimeWorker` 接口并编写一定规则的方法即可。**需要在 `Startup.cs` 中注册 `services.AddTaskScheduler()`** + +## 26.3 缓隔方式使用 + +### 26.3.1 特定时间后执行 + +这里演示 `3s` 后执行 + +```cs showLineNumbers {5} +Console.WriteLine("当前时间:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + +// timer 是定时器的对象,包含定时器相关信息 +// count 表示执行次数,这里只有一次 +SpareTime.DoOnce(3000, (timer, count) => { + Console.WriteLine("现在时间:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); +}); +``` + +### 26.3.2 配置任务信息 + +```cs showLineNumbers {3} +SpareTime.DoOnce(3000, (timer, count) => { + Console.WriteLine("现在时间:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); +}, "jobName", "描述一下这个任务是干什么的"); +``` + +`jobName` 标识任务的唯一标识,通过这个标识可以启动、暂停、销毁任务。 + +### 26.3.3 手动启动执行 + +默认情况下,任务初始化后就立即启动,等待符合的时间就执行,有些时候我们仅仅想初始化时间,不希望立即执行,只需要配置 `startNow` 即可: + +```cs showLineNumbers {3,6} +SpareTime.DoOnce(3000, (timer, count) => { + Console.WriteLine("现在时间:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); +},"jobName", startNow: false); + +// 手动启动执行 +SpareTime.Start("jobName"); +``` + +### 26.3.4 模拟后台执行 + +有些时候,我们只需要开启新线程去执行一个任务,比如发短信,发邮件,无需配置。 + +```cs showLineNumbers {2} +// 此方法无需主线程等待即可返回,可大大提高性能 +SpareTime.DoIt(() => { + // 这里发送短信,发送邮件或记录访问记录 +}); +``` + +还可以指定多长时间后触发,建议 `10-1000` 毫秒之间: + +```cs showLineNumbers {3} +SpareTime.DoIt(() => { + // 发送短信 +}, 100); +``` + +### 26.3.5 `ISpareTimeWorker` 方式 + +```cs showLineNumbers {1,8} +public class JobWorker : ISpareTimeWorker +{ + /// + /// 3s 后执行 + /// + /// + /// + [SpareTime(3000, "jobName", DoOnce = true, StartNow = true)] + public void DoSomething(SpareTimer timer, long count) + { + Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + } + + /// + /// 3s 后执行(支持异步) + /// + /// + /// + [SpareTime(3000, "jobName", DoOnce = true, StartNow = true)] + public async Task DoSomethingAsync(SpareTimer timer, long count) + { + Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + await Task.CompletedTask; + } +} +``` + +**需要在 `Startup.cs` 中注册 `services.AddTaskScheduler()`** + +## 26.4 间隔方式使用 + +### 26.4.1 每隔一段时间执行 + +```cs showLineNumbers {2} +// 每隔 1s 执行 +SpareTime.Do(1000, (timer, count) => { + Console.WriteLine("现在时间:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + Console.WriteLine($"一共执行了:{count} 次"); +}); +``` + +### 26.4.2 配置任务信息 + +```cs showLineNumbers {1,4} +SpareTime.Do(1000, (timer, count) => { + Console.WriteLine("现在时间:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + Console.WriteLine($"一共执行了:{count} 次"); +}, "jobName", "这是一个计时器任务"); +``` + +### 26.4.3 手动启动执行 + +```cs showLineNumbers {1,4,6} +SpareTime.Do(1000, (timer, count) => { + Console.WriteLine("现在时间:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + Console.WriteLine($"一共执行了:{count} 次"); +}, "jobName", startNow:false); + +SpareTime.Start("jobName"); +``` + +### 26.4.4 `ISpareTimeWorker` 方式 + +```cs showLineNumbers {1,8} +public class JobWorker : ISpareTimeWorker +{ + /// + /// 每隔 3s 执行 + /// + /// + /// + [SpareTime(3000, "jobName", StartNow = true)] + public void DoSomething(SpareTimer timer, long count) + { + Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + Console.WriteLine($"一共执行了:{count} 次"); + } +} +``` + +**需要在 `Startup.cs` 中注册 `services.AddTaskScheduler()`** + +## 26.5 `Cron` 表达式使用 + +### 26.5.1 什么是 `Cron` 表达式 + +Cron 表达式是一个字符串,字符串以 `5` 或 `6` 个空格隔开,分为 6 或 7 个域,每一个域代表一个含义,Cron 有如下两种语法格式: + +(1) Seconds Minutes Hours DayofMonth Month DayofWeek Year + +(2)Seconds Minutes Hours DayofMonth Month DayofWeek + +Cron 从左到右(用空格隔开):`秒` `分` `小时` `月份中的日期` `月份` `星期中的日期` `年份` + +| 字段 | 允许值 | 允许的特殊字符 | +| ---------------------- | ------------------------------------------- | --------------------------- | +| 秒(Seconds) | `0~59` 的整数 | `, - \* /` 四个字符 | +| 分(Minutes) | `0~59` 的整数 | `, - \* /` 四个字符 | +| 小时(Hours) | `0~23` 的整数 | `, - \* /` 四个字符 | +| 日期(DayofMonth) | `1~31` 的整数(但是你需要考虑平闰月的天数) | `,- \* ? / L W C` 八个字符 | +| 月份(Month) | `1~12` 的整数或者 `JAN-DEC` | `, - \* /` 四个字符 | +| 星期(DayofWeek) | `1~7` 的整数或者 `SUN-SAT (1=SUN)` | `, - \* ? / L C #` 八个字符 | +| 年(可选,留空)(Year) | `1970~2099` | `, - \* /` 四个字符 | + +每一个域都使用数字,但还可以出现如下特殊字符,它们的含义是: + +(1)`_`:表示匹配该域的任意值。假如在 `Minutes` 域使用 `\_`, 即表示每分钟都会触发事件。 + +(2)`?`:只能用在 `DayofMonth` 和 `DayofWeek` 两个域。它也匹配域的任意值,但实际不会。因为 `DayofMonth` 和 `DayofWeek` 会相互影响。例如想在每月的 `20` 日触发调度,不管 `20` 日到底是星期几,则只能使用如下写法: `13 13 15 20 _ ?`, 其中最后一位只能用`?`,而不能使用\_,如果使用\*表示不管星期几都会触发,实际上并不是这样。 + +(3)`-`:表示范围。例如在 `Minutes` 域使用 `5-20`,表示从 `5` 分到 `20` 分钟每分钟触发一次 + +(4)`/`:表示起始时间开始触发,然后每隔固定时间触发一次。例如在 `Minutes` 域使用 `5/20`,则意味着 `5` 分钟触发一次,而 `25,45` 等分别触发一次. + +(5)`,`:表示列出枚举值。例如:在 `Minutes` 域使用 `5,20`,则意味着在 `5` 和 `20` 分每分钟触发一次。 + +(6)`L`:表示最后,只能出现在 `DayofWeek` 和 `DayofMonth` 域。如果在 `DayofWeek` 域使用 `5L`,意味着在最后的一个星期四触发。 + +(7)`W`:表示有效工作日(周一到周五) 只能出现在 `DayofMonth` 域,系统将在离指定日期的最近的有效工作日触发事件。例如:在 `DayofMonth` 使用 `5W`,如果 `5` 日是星期六,则将在最近的工作日:星期五,即 `4` 日触发。如果 `5` 日是星期天,则在 `6` 日(周一)触发;如果 `5` 日在星期一到星期五中的一天,则就在 `5` 日触发。另外一点,`W` 的最近寻找不会跨过月份 。 + +(8)`LW`:这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五。 + +(9)`#`:用于确定每个月第几个星期几,只能出现在 `DayofMonth` 域。例如在 `4#2`,表示某月的第二个星期三。 + +### 26.5.2 常见 `Cron` 表达式 + +| 表达式 | 表达式代表含义 | 格式化 | +| ------------------------ | ---------------------------------------------------------- | --------------------------- | +| `* * * * *` | 每分钟 | `CronFormat.Standard` | +| `*/1 * * * *` | 每分钟 | `CronFormat.Standard` | +| `0 0/1 * * * ?` | 每分钟 | `CronFormat.IncludeSeconds` | +| `0 0 * * * ?` | 每小时 | `CronFormat.IncludeSeconds` | +| `0 0 0/1 * * ?` | 每小时 | `CronFormat.IncludeSeconds` | +| `0 23 ? * MON-FRI` | 晚上 11:00,周一至周五 | `CronFormat.Standard` | +| `* * * * * *` | 每秒 | `CronFormat.IncludeSeconds` | +| `*/45 * * * * *` | 每 45 秒 | `CronFormat.IncludeSeconds` | +| `*/5 * * * *` | 每 5 分钟 | `CronFormat.Standard` | +| `0 0/10 * * * ?` | 每 10 分钟 | `CronFormat.IncludeSeconds` | +| `0 */5 * * * *` | 每 5 分钟 | `CronFormat.IncludeSeconds` | +| `30 11 * * 1-5` | 周一至周五上午 11:30 | `CronFormat.Standard` | +| `30 11 * * *` | 11:30 | `CronFormat.Standard` | +| `0-10 11 * * *` | 上午 11:00 至 11:10 之间的每一分钟 | `CronFormat.Standard` | +| `* * * 3 *` | 每分钟,只在 3 月份 | `CronFormat.Standard` | +| `* * * 3,6 *` | 每分钟,只在 3 月和 6 月 | `CronFormat.Standard` | +| `30 14,16 * * *` | 下午 02:30 分和 04:30 分 | `CronFormat.Standard` | +| `30 6,14,16 * * *` | 早上 06:30,下午 02:30 和 04:30 | `CronFormat.Standard` | +| `46 9 * * 1` | 早上 09:46,只在星期一 | `CronFormat.Standard` | +| `23 12 15 * *` | 下午 12:23,在本月的第 15 天 | `CronFormat.Standard` | +| `23 12 * JAN *` | 下午 12:23,只在 1 月份 | `CronFormat.Standard` | +| `23 12 ? JAN *` | 下午 12:23,只在 1 月份 | `CronFormat.Standard` | +| `23 12 * JAN-FEB *` | 下午 12:23,1 月至 2 月 | `CronFormat.Standard` | +| `23 12 * JAN-MAR *` | 下午 12:23,1 月至 3 月 | `CronFormat.Standard` | +| `23 12 * * SUN` | 下午 12:23,仅在星期天 | `CronFormat.Standard` | +| `*/5 15 * * MON-FRI` | 每 5 分钟,下午 0:00 至 03:59,周一至周五 | `CronFormat.Standard` | +| `* * * * MON#3` | 每分钟,在月的第三个星期一 | `CronFormat.Standard` | +| `* * * * 4L` | 每一分钟,在本月的最后一天 | `CronFormat.Standard` | +| `*/5 * L JAN *` | 每月一次每月 5 分钟,只在 1 月份 | `CronFormat.Standard` | +| `30 02 14 * * *` | 下午在 02:02:30 | `CronFormat.IncludeSeconds` | +| `5-10 * * * * *` | 每分钟的 5-10 秒 | `CronFormat.IncludeSeconds` | +| `5-10 30-35 10-12 * * *` | 10:00 至 12:00 之间的每分钟 5-10 秒,每小时 30-35 分钟 | `CronFormat.IncludeSeconds` | +| `30 */5 * * * *` | 每分钟的 30 秒,每五分钟 | `CronFormat.IncludeSeconds` | +| `0 30 10-13 ? * WED,FRI` | 每小时的 30 分钟,下午 10:00 至 01:00 之间,仅在周三和周五 | `CronFormat.IncludeSeconds` | +| `10 0/5 * * * ?` | 每分钟的 10 秒,每 05 分钟 | `CronFormat.IncludeSeconds` | +| `0 0 6 1/1 * ?` | 下午 06:00 | `CronFormat.IncludeSeconds` | +| `0 5 0/1 * * ?` | 一个小时的 05 分 | `CronFormat.IncludeSeconds` | +| `0 0 L * *` | 每月最后一天上午 00:00 | `CronFormat.Standard` | +| `0 0 L-1 * *` | 每月最后一天的凌晨 00:00 | `CronFormat.Standard` | +| `0 0 3W * *` | 每月第 3 个工作日上午 00:00 | `CronFormat.Standard` | +| `0 0 LW * *` | 在每月的最后一个工作日,上午 00:00 | `CronFormat.Standard` | +| `0 0 * * 2L` | 本月最后一个星期二上午 00:00 | `CronFormat.Standard` | +| `0 0 * * 6#3` | 每月第三个星期六上午 00:00 | `CronFormat.Standard` | +| `0 0 ? 1 MON#1` | 1 月第一个星期一上午 00:00 | `CronFormat.Standard` | +| `0 0 3 * * ?` | 每天几点执行一次 | `CronFormat.IncludeSeconds` | + +### 26.5.3 在线生成 `Cron` 表达式 + +[https://cron.qqe2.com/](https://cron.qqe2.com/) + +### 26.5.4 `Macro` 标识符 + +为了方便定义 `Cron` 表达式,`Furion` 框架也提供了非常方便的占位符实现常用的时间格式: + +| 占位符 | 对应表达式 | 占位符代表含义 | +| --------------- | ------------- | ------------------------------ | +| `@every_second` | `* * * * * *` | 一秒钟跑一次 | +| `@every_minute` | `* * * * *` | 在分钟开始时每分钟运行一次 | +| `@hourly` | `0 * * * *` | 在小时开始时每小时运行一次 | +| `@daily` | `0 0 * * *` | 每天午夜运行一次 | +| `@midnight` | `0 0 * * *` | 每天午夜运行一次 | +| `@weekly` | `0 0 * * 0` | 周日上午午夜每周运行一次 | +| `@monthly` | `0 0 1 * *` | 每月在每月第一天的午夜运行一次 | +| `@yearly` | `0 0 1 1 *` | 每年 1 月 1 日午夜运行一次 | +| `@annually` | `0 0 1 1 *` | 每年 1 月 1 日午夜运行一次 | + +### 26.5.5 使用 `Cron` 表达式 + +```cs showLineNumbers {2,5} +// 每隔 1s 执行 +SpareTime.Do("* * * * * *", (timer, count) => { + Console.WriteLine("现在时间:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + Console.WriteLine($"一共执行了:{count} 次"); +}, cronFormat: CronFormat.IncludeSeconds); +``` + +:::important 关于 CronFormat + +默认情况下,`Furion` 框架未启用对 `秒` 的支持,如需开启,则设置 `cronFormat: CronFormat.IncludeSeconds` 即可。默认值是 `cronFormat: CronFormat.Standard` + +::: + +### 26.5.6 使用 `Macro` 占位符 + +```cs showLineNumbers {2} +// 每隔 1s 执行 +SpareTime.Do("@every_second", (timer, count) => { + Console.WriteLine("现在时间:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + Console.WriteLine($"一共执行了:{count} 次"); +}); +``` + +### 26.5.7 配置任务信息 + +```cs showLineNumbers {4} +SpareTime.Do("* * * * *", (timer, count) => { + Console.WriteLine("现在时间:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + Console.WriteLine($"一共执行了:{count} 次"); +}, "cronName", "每分钟执行一次"); +``` + +### 26.5.8 手动启动执行 + +```cs showLineNumbers {4,6} +SpareTime.Do("* * * * *", (timer, count) => { + Console.WriteLine("现在时间:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + Console.WriteLine($"一共执行了:{count} 次"); +}, "cronName", "每分钟执行一次", startNow: false); + +SpareTime.Start("cronName"); +``` + +### 26.5.9 `ISpareTimeWorker` 方式 + +```cs showLineNumbers {1,8} +public class JobWorker : ISpareTimeWorker +{ + /// + /// 每分钟执行 + /// + /// + /// + [SpareTime("* * * * *", "jobName", StartNow = true)] + public void DoSomething(SpareTimer timer, long count) + { + Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + Console.WriteLine($"一共执行了:{count} 次"); + } + + /// + /// 每分钟执行(支持异步) + /// + /// + /// + [SpareTime("* * * * *", "jobName", StartNow = true)] + public async Task DoSomethingAsync(SpareTimer timer, long count) + { + Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + Console.WriteLine($"一共执行了:{count} 次"); + await Task.CompletedTask; + } +} +``` + +**需要在 `Startup.cs` 中注册 `services.AddTaskScheduler()`** + +## 26.6 自定义下次执行时间 + +有些时候我们需要进行一些业务逻辑,比如数据库查询等操作返回下一次执行时间,这个时候我们可以通过高级自定义方式。 + +### 26.6.1 高级自定义间隔方式 + +```cs showLineNumbers {2,4-5,7} +SpareTime.Do(()=>{ + // 这里可以查询数据库或进行或进行任何业务逻辑 + + if(符合逻辑){ + return 1000; // 每秒执行 + } + else return -1; // 不符合逻辑取消任务 +}, +(timer,count)=>{ + Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + Console.WriteLine($"一共执行了:{count} 次"); +}); +``` + +:::important 配置是否持续检查 + +默认情况下,该自定义会在返回 `小于或等于0` 时终止任务的执行。但是我们希望该任务不要终止,只要符合条件都一直执行,只需要配置 `cancelInNoneNextTime: false` 即可 + +::: + +### 26.6.2 高级自定义 `Cron` 表达式 + +```cs showLineNumbers {2,4-5,7} +SpareTime.Do(()=>{ + // 这里可以查询数据库或进行或进行任何业务逻辑 + + if(符合逻辑){ + return DateTimeOffset.Now.AddMinutes(10); // 十分钟后再执行 + } + else return null; // 不符合逻辑取消任务 +}, +(timer,count) => { + Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + Console.WriteLine($"一共执行了:{count} 次"); +}); +``` + +:::important 配置是否持续检查 + +默认情况下,该自定义会在返回 `null` 时终止任务的执行。但是我们希望该任务不要终止,只要符合条件都一直执行,只需要配置 `cancelInNoneNextTime: false` 即可,如: + +```cs showLineNumbers {2,4-5,7,12} +SpareTime.Do(()=>{ + // 这里可以查询数据库或进行或进行任何业务逻辑 + + if(符合逻辑){ + return SpareTime.GetCronNextOccurrence("cron 表达式"); + } + else return null; // 不符合逻辑继续检查 +}, +(timer,count) => { + Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + Console.WriteLine($"一共执行了:{count} 次"); +}, cancelInNoneNextTime: false); +``` + +::: + +## 26.7 `ISpareTimeWorker` 说明 + +除了上面的 `SpareTime` 静态类方式,`Furion` 框架还提供了 `ISpareTimeWorker` 方式,使用该方式非常简单,只需要自定义一个**公开且非抽象非静态**类并实现 `ISpareTimeWorker` 即可。 + +在该类中定义的任务方法需满足以下规则: + +- 必须是**公开且实例方法** +- 该方法必须返回 `void` 且提供 `SpareTimer` 和 `long` 两个参数 +- 必须贴有 `[SpareTime]` 特性 + +如: + +```cs showLineNumbers {1,4-5,12-13,20-21,28-30,37-38,45} +public class JobWorker : ISpareTimeWorker +{ + // 每隔一秒执行,且立即启动 + [SpareTime(1000, "jobName1", StartNow = true)] + public void DoSomething1(SpareTimer timer, long count) + { + Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + Console.WriteLine($"一共执行了:{count} 次"); + } + + // 每分钟执行,且立即启动 + [SpareTime("* * * * *", "jobName2", StartNow = true)] + public void DoSomething2(SpareTimer timer, long count) + { + Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + Console.WriteLine($"一共执行了:{count} 次"); + } + + // 每秒执行,且等待启动 + [SpareTime("* * * * * *", "jobName3",CronFormat = CronFormat.IncludeSeconds, StartNow = false)] + public void DoSomething3(SpareTimer timer, long count) + { + Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + Console.WriteLine($"一共执行了:{count} 次"); + } + + // 每秒执行一次,每分钟也执行一次 + [SpareTime(1000, "jobName4", StartNow = true)] + [SpareTime("* * * * *", "jobName5", StartNow = true)] + public void DoSomething4(SpareTimer timer, long count) + { + Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + Console.WriteLine($"一共执行了:{count} 次"); + } + + // 只执行一次 + [SpareTime(1000, "jobName5", StartNow = true, DoOnce = true)] + public void DoSomething5(SpareTimer timer, long count) + { + Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + Console.WriteLine($"一共执行了:{count} 次"); + } + + // 读取配置文件,通过 #(配置路径) + [SpareTime("#(MyJob:Time)", "jobName6", StartNow = true, DoOnce = true)] + public void DoSomething5(SpareTimer timer, long count) + { + Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + Console.WriteLine($"一共执行了:{count} 次"); + } + + // 支持异步 + [SpareTime(1000, "jobName1", StartNow = true)] + public async Task DoSomethingAsync(SpareTimer timer, long count) + { + Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + Console.WriteLine($"一共执行了:{count} 次"); + await Task.CompletedTask; + } +} +``` + +:::warning 关于依赖注入 + +`ISpareTimeWorker` 接口主要是用来查找定时器对象的,也就是它的实现类并未提供依赖注入功能,所以在实现类并不支持构造函数注入依赖项。 + +::: + +### 26.7.1 `[SpareTime]` 特性 + +`[SpareTime]` 支持以下配置属性 + +- `Interval`:间隔时间, `double` 类型 +- `CronExpression`:`Cron` 表达式,`string` 类型 +- `WorkerName`:任务唯一标识,`string` 类型,`必填` +- `Description`:任务描述,`string` 类型 +- `DoOnce`:是否只执行一次,`bool` 类型,默认 `false` +- `StartNow`:是否立即启动,默认 `false` +- `CronFormat`:`Cron` 表达式格式化方式,`CronFormat` 枚举类型,默认 `CronFormat.Standard` +- `ExecuteType`:配置任务执行方式,`SpareTimeExecuteTypes` 枚举类型,默认 `SpareTimeExecuteTypes.Parallel` + +## 26.8 `SpareTime` 静态类 + +`SpareTime` 静态类提供了一些方法方便初始化和管理任务 + +### 26.8.1 初始化任务 + +```cs showLineNumbers +// 开启间隔任务 +SpareTime.Do(interval, [options]); + +// 开启 Cron 表达式任务 +SpareTime.Do(expression, [options]); + +// 只执行一次任务 +SpareTime.DoOnce(interval, [options]); + +// 实现自定义任务 +SpareTime.Do(()=>{ + return DateTime.Now.AddMinutes(10); +},[options]); +``` + +### 26.8.2 实现后台执行 + +```cs showLineNumbers +// 实现后台执行 +SpareTime.DoIt(()=>{}); +``` + +### 26.8.3 开始一个任务 + +```cs showLineNumbers +SpareTime.Start("任务标识"); +``` + +### 26.8.4 暂停一个任务 + +```cs showLineNumbers +SpareTime.Stop("任务标识"); +// 还可以标记一个任务执行失败 +SpareTime.Stop("任务标识", true); +``` + +### 26.8.5 取消一个任务 + +```cs showLineNumbers +SpareTime.Cancel("任务名称"); +``` + +### 26.8.6 销毁所有任务 + +```cs showLineNumbers +SpareTime.Dispose(); +``` + +### 26.8.7 获取所有任务 + +```cs showLineNumbers +var workers = SpareTime.GetWorkers(); +``` + +### 26.8.8 获取单个任务 + +```cs showLineNumbers +var worker = SpareTime.GetWorker("workerName"); +``` + +### 26.8.9 解析 `Cron` 表达式 + +```cs showLineNumbers +var nextTime = SpareTime.GetCronNextOccurrence("* * * * *"); +``` + +## 26.9 `并行`和`串行`执行方式 + +`Furion` 框架提供了任务两种执行方式 `并行` 和 `串行`: + +- `并行`:无需等待上一次任务完成,**默认值** +- `串行`:需等待上一次任务完成 + +### 26.9.1 `SpareTime` 静态方式指定 + +```cs showLineNumbers {3,5} +SpareTime.Do(1000, (t, i) => +{ + Thread.Sleep(5000); // 模拟执行耗时任务 + Console.WriteLine($"{t.WorkerName} -{t.Description} - {DateTime.Now:yyyy-MM-dd HH:mm:ss} - {i}"); +}, "serialName", "模拟串行任务", executeType: SpareTimeExecuteTypes.Serial); +``` + +### 26.9.2 `ISpareTimeWorker` 方式 + +```cs showLineNumbers {1} +[SpareTime(1000, "jobName1", StartNow = true, ExecuteType = SpareTimeExecuteTypes.Serial)] +public void DoSomething1(SpareTimer timer, long count) +{ + Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + Console.WriteLine($"一共执行了:{count} 次"); +} +``` + +## 26.10 任务异常处理 + +有些时候我们可能在执行任务过程中出现异常,`Furion` 也提供了属性判断是否有异常和异常信息,方便记录到日志中,如: + +```cs showLineNumbers {4-7,11} +SpareTime.Do(1000, (t, c) => +{ + // 判断是否有异常 + if (t.Exception.Any()) + { + Console.WriteLine(t.Exception.Values.LastOrDefault()?.Message); + } + // 执行第三次抛异常 + if (c > 2) + { + throw Oops.Oh("抛异常" + c); + } + else + { + Console.WriteLine($"{t.WorkerName} -{t.Description} - {DateTime.Now:yyyy-MM-dd HH:mm:ss} - {c}"); + } +}, "exceptionJob"); +``` + +:::warning 特别说明 + +如果一个任务连续错误次数达 `10次` 则任务将自动停止,并标记任务状态为 `Failed`。 + +::: + +## 26.11 如何在任务中解析对象 + +有些时候我们需要在任务中进行数据库操作或解析服务,这时候我们只需要创建一个新的作用域即可 + +### 26.11.1 `SpareTime` 静态类中 + +```cs showLineNumbers {2} +SpareTime.Do(1000, (timer,count) => { + Scoped.Create((_, scope) => + { + var services = scope.ServiceProvider; + + // 获取数据库上下文 + var dbContext = Db.GetDbContext(services); + + // 获取仓储 + var respository = Db.GetRepository(services); + + // 解析其他服务 + var otherService = services.GetService(); + var otherService2 = App.GetService(services); + }); +}, "任务标识"); +``` + +### 26.11.2 `ISpareTimeWorker` 方式 + +```cs showLineNumbers {4} +[SpareTime(1000, "jobName1", StartNow = true, ExecuteType = SpareTimeExecuteTypes.Serial)] +public void DoSomething1(SpareTimer timer, long count) +{ + Scoped.Create((_, scope) => + { + var services = scope.ServiceProvider; + + // 获取数据库上下文 + var dbContext = Db.GetDbContext(services); + + // 获取仓储 + var respository = Db.GetRepository(services); + + // 解析其他服务 + var otherService = services.GetService(); + var otherService2 = App.GetService(services); + }); +} +``` + +:::important 数据库操作注意 + +如果作用域中对**数据库有任何变更操作**,需手动调用 `SaveChanges` 或带 `Now` 结尾的方法。也可以使用 `Scoped.CreateUow(handler)` 代替。 + +::: + +## 26.12 在 `BackgroundService` 中使用 + +`BackgroundService` 是 `.NET Core 3` 之后提供的轻量级后台任务,同时可以发布到 `Windows` 服务和 `Linux` 守护进程中。 + +### 26.12.1 间隔执行方式 + +```cs showLineNumbers {7,18,20,30} +namespace WorkerService; + +public class Worker : BackgroundService +{ + private readonly ILogger _logger; + + private const int delay = 1000; + + public Worker(ILogger logger) + { + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + var taskFactory = new TaskFactory(System.Threading.Tasks.TaskScheduler.Current); + + await taskFactory.StartNew(async () => + { + // 你的业务代码写到这里面 + + _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); + + await Task.CompletedTask; + + }, stoppingToken); + + await Task.Delay(delay, stoppingToken); + } + } +} +``` + +### 26.12.2 `Cron` 表达式执行方式 + +```cs showLineNumbers {9,14,21,23,32} +using Furion.TimeCrontab; + +namespace WorkerService; + +public class Worker : BackgroundService +{ + private readonly ILogger _logger; + + private readonly Crontab _crontab; + + public Worker(ILogger logger) + { + _logger = logger; + _crontab = Crontab.Parse("* * * * * *", CronStringFormat.WithSeconds); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + var taskFactory = new TaskFactory(System.Threading.Tasks.TaskScheduler.Current); + + await taskFactory.StartNew(async () => + { + // 你的业务代码写到这里面 + + _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); + + await Task.CompletedTask; + }, stoppingToken); + + await Task.Delay(_crontab.GetSleepMilliseconds(DateTime.UtcNow), stoppingToken); + } + } +} +``` + +## 26.13 定时任务监听器 + +在 `Furion v2.18+` 版本之后新增了定时任务监听器 `ISpareTimeListener`,通过监听器可以实现所有定时任务的状态。如,创建一个 `单例` 的监听器 `SpareTimeListener`: + +```cs showLineNumbers {8,15} +using Furion.DependencyInjection; +using Furion.TaskScheduler; +using System; +using System.Threading.Tasks; + +namespace Furion.Core +{ + public class SpareTimeListener : ISpareTimeListener, ISingleton + { + /// + /// 监听所有任务 + /// + /// + /// + public Task OnListener(SpareTimerExecuter executer) + { + switch (executer.Status) + { + // 执行开始通知 + case 0: + Console.WriteLine($"{executer.Timer.WorkerName} 任务开始通知"); + break; + // 任务执行之前通知 + case 1: + Console.WriteLine($"{executer.Timer.WorkerName} 执行之前通知"); + break; + // 执行成功通知 + case 2: + Console.WriteLine($"{executer.Timer.WorkerName} 执行成功通知"); + break; + // 任务执行失败通知 + case 3: + Console.WriteLine($"{executer.Timer.WorkerName} 执行失败通知"); + break; + // 任务执行停止通知 + case -1: + Console.WriteLine($"{executer.Timer.WorkerName} 执行停止通知"); + break; + // 任务执行取消通知 + case -2: + Console.WriteLine($"{executer.Timer.WorkerName} 执行取消通知"); + break; + default: + break; + } + + return Task.CompletedTask; + } + } +} +``` + +### 26.13.1 `SpareTimerExecuter` 属性说明 + +- `Timer`:`SpareTimer` 定时器对象 +- `Status`:监听状态 + - `0`:任务开始 + - `1`:执行之前 + - `2`:执行成功 + - `3`:执行失败 + - `-1`:任务停止 + - `-2`:任务取消 + +## 26.14 IIS 部署回收设置 + +如果在项目中使用了定时任务且部署到 `IIS` 中,那么需要设置 `IIS` 禁止回收,[点击查看 `IIS` 回收问题解决方案](/docs/deploy-iis#3415-iis-%E5%9B%9E%E6%94%B6%E9%97%AE%E9%A2%98%E5%92%8C%E9%85%8D%E7%BD%AE) + +:::warning 部署建议 + +建议定时任务采用 `Worker Service` 独立部署方式,不应依托 `Web` 项目进程中。[查看【 Worker Service】章节](/docs/process-service.mdx) + +::: + +## 26.15 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/job.mdx b/handbook/docs/job.mdx new file mode 100644 index 0000000000000000000000000000000000000000..c4367971700a8944afc2fd9169c9d69ca7a3d3da --- /dev/null +++ b/handbook/docs/job.mdx @@ -0,0 +1,6574 @@ +--- +id: job +title: 26.1 调度作业 +sidebar_label: 26.1 调度作业 +--- + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **新特性** + + -  新增 定时任务 `Http` 作业请求头 `Headers` 和作业分组 `Group` 和描述 `Description` 支持 4.8.8.46 ⏱️2023.10.09 [#I85Z7S](https://gitee.com/dotnetchina/Furion/issues/I85Z7S) + -  新增 定时任务看板列表支持作业分组名排序 4.8.8.43 ⏱️2023.09.14 [#I7YQ9V](https://gitee.com/dotnetchina/Furion/issues/I7YQ9V) + -  新增 定时任务作业计划 `OnChanged` 事件处理 4.8.8.29 ⏱️2023.06.25 [e4c4cf1](https://gitee.com/dotnetchina/Furion/commit/e4c4cf1d418f3cc2291eca7d7dd1c8b62d17b0e9) + -  新增 定时任务支持二级虚拟目录 `VisualPath` 配置部署 4.8.8.20 ⏱️2023.05.18 [#I740IA](https://gitee.com/dotnetchina/Sundial/issues/I740IA) + -  新增 定时任务作业处理程序工厂 `IJobFactory` 支持 4.8.8.13 ⏱️2023.05.08 [ad58dd3](https://gitee.com/dotnetchina/Furion/commit/ad58dd3141ed40e58cd486895ac6c1f21803797c) + -  新增 定时任务 `Schedular.CompileCSharpClassCode(code)` 支持动态编译作业处理程序代码 4.8.8.7 ⏱️2023.04.30 [fe1e8a1](https://gitee.com/dotnetchina/Furion/commit/fe1e8a1768c7020477684689b35a2a1349ec2b01) + -  新增 定时任务支持配置 `IJob` 执行异常 `FallbackAsync` 回退策略 4.8.8.6 ⏱️2023.04.25 [7671489](https://gitee.com/dotnetchina/Furion/commit/7671489a46ec7c957e92b7fbf9836e27f9077e24) + -  新增 定时任务支持在非 `IOC/DI` 项目类型中使用 4.8.8.5 ⏱️2023.04.24 [#I6YJNB](https://gitee.com/dotnetchina/Sundial/issues/I6YJNB) + -  新增 定时任务看板支持自定义刷新频率 `SyncRate` 功能 4.8.7.43 ⏱️2023.04.12 [703b465](https://gitee.com/dotnetchina/Furion/commit/703b465f41510d86976d325cd31d7f8eba3a31ec) + -  新增 定时任务看板支持完全自定义 `RequestPath` 入口地址功能 4.8.7.34 ⏱️2023.04.04 [24736f6](https://gitee.com/dotnetchina/Furion/commit/24736f6421dd5aa90289fbb9bc519e6ef55e667f) + -  新增 **定时任务一系列 `.AlterTo` 修改作业触发器触发时间便捷方法** 4.8.7.31 ⏱️2023.03.31 [0349017](https://gitee.com/dotnetchina/Furion/commit/0349017902835bed91041fb3ea1ee987b0a81bbb) + -  新增 定时任务看板 `UI` 作业列表 `最近执行时间` 列和优化显示效果 4.8.7.12 ⏱️2023.03.15 [26462a8](https://gitee.com/dotnetchina/Furion/commit/26462a84e553e39ce4cddd5128833ff732c85f3e) [cb5dd17](https://gitee.com/dotnetchina/Furion/commit/cb5dd17969244987b847fcd96825d28b243a5b9f) + -  新增 定时任务作业计划/工厂立即执行 `RunJob` 方法 4.8.7.11 ⏱️2023.03.15 [#I6LD9X](https://gitee.com/dotnetchina/Furion/issues/I6LD9X) + -  新增 定时任务看板 `UI` 提供立即执行功能 4.8.7.11 ⏱️2023.03.15 [#I6LD9X](https://gitee.com/dotnetchina/Furion/issues/I6LD9X) + -  新增 定时任务作业执行上下文 `JobExecutionContext` 服务提供器 `ServiceProvider` 属性 4.8.7.10 ⏱️2023.03.14 [02586f8](https://gitee.com/dotnetchina/Furion/commit/02586f83edb4f98e4801ae65080c2d6aa5545763) + -  新增 **定时任务 `HTTP` 作业,支持定时请求互联网 `URL` 地址** 4.8.7.7 ⏱️2023.03.11 [01d4466](https://gitee.com/dotnetchina/Furion/commit/01d446620c20e373f198195797896d8c96feeb15) + +
+ 查看变化 +
+ +```cs showLineNumbers {3} +services.AddSchedule(options => +{ + options.AddHttpJob(request => + { + request.RequestUri = "https://www.chinadot.net"; + request.HttpMethod = HttpMethod.Get; + // request.Body = "{}"; // 设置请求报文体 + }, Triggers.PeriodSeconds(5)); +}); +``` + +
+
+ +- -  新增 **定时任务作业触发器 `Trigger` 执行结果 `Result` 和执行耗时 `ElapsedTime` 属性** 4.8.7.7 ⏱️2023.03.11 [01d4466](https://gitee.com/dotnetchina/Furion/commit/01d446620c20e373f198195797896d8c96feeb15) +- -  新增 **定时任务作业看板支持查看作业触发器执行结果 `Result` 和执行耗时 `ElapsedTime` 属性** 4.8.7.7 ⏱️2023.03.11 [01d4466](https://gitee.com/dotnetchina/Furion/commit/01d446620c20e373f198195797896d8c96feeb15) +- -  新增 定时任务休眠时长和唤醒时机日志输出 4.8.7.6 ⏱️2023.03.08 [#I6LANE](https://gitee.com/dotnetchina/Furion/issues/I6LANE) +- -  新增 定时任务 `IScheduler.[Try]UpdateDetail(builder => {})` 和 `IScheduler.[Try]UpdateTrigger(triggerId, builder => {})` 重载方法 4.8.6 ⏱️2023.02.08 [6e43a54](https://gitee.com/dotnetchina/Furion/commit/6e43a542f1a7e10bf996c6e0179a40d434d8682e) + +
+ 查看变化 +
+ +- 更新作业信息 + +```cs showLineNumbers {2,8} +// 返回 ScheduleResult 类型 +var scheduleResult = Scheduler.TryUpdateDetail(jobBuilder => +{ + jobBuilder.SetDescription("~~~"); +}, out var jobDetail); + +// 无返回值 +scheduler.UpdateDetail(jobBuilder => +{ + jobBuilder.SetDescription("~~~"); +}); +``` + +- 更新作业触发器 + +```cs showLineNumbers {2,8} +// 返回 ScheduleResult 类型 +var scheduleResult = scheduler.TryUpdateTrigger("triggerId", triggerBuilder => +{ + triggerBuilder.SetDescription("~~"); +}, out var trigger); + +// 无返回值 +scheduler.UpdateTrigger("triggerId", triggerBuilder => +{ + triggerBuilder.SetDescription("~~"); +}); +``` + +
+
+ +- -  新增 定时任务 `Dashboard` 可自定义入口地址 `/schedule` 4.8.5.6 ⏱️2023.02.02 [c5639f5](https://gitee.com/dotnetchina/Furion/commit/c5639f5b8d1ae5164bf812540aeb98f90487e855) +- -  新增 定时任务执行上下文 `RunId` 属性,用于标识单次作业触发器执行 4.8.5.1 ⏱️2023.01.30 [1aac470](https://gitee.com/dotnetchina/Furion/commit/1aac4707d270cb0fe66eab67490d946029a9e41d) +- -  新增 定时任务 `Dashboard` 查看作业触发器最近运行记录功能 4.8.4.3 ⏱️2023.01.03 [e7d24d8](https://gitee.com/dotnetchina/Furion/commit/e7d24d84bcc448b0a13f8e0b76328669261af44b) +- -  新增 定时任务作业触发器 `trigger.GetTimelines()` 获取最近 `10` 条运行记录列表 4.8.4.3 ⏱️2023.01.03 [e7d24d8](https://gitee.com/dotnetchina/Furion/commit/e7d24d84bcc448b0a13f8e0b76328669261af44b) +- -  新增 **定时任务 `Dashboard` 看板** 4.8.4 ⏱️2022.12.30 [d3f9669](https://gitee.com/dotnetchina/Furion/commit/d3f966921dfa757c12c2bd071fb19fc166a2f24e) +- -  新增 定时任务 `IScheduler.GetEnumerable()` 方法,可将作业计划转换成可枚举字典 4.8.4 ⏱️2022.12.30 [4d5235c](https://gitee.com/dotnetchina/Furion/commit/4d5235c37a9ef5e66e92847e65ef9786bcd7387c) +- -  新增 定时任务配置选项 `options.JobDetail.LogEnabled` 配置,可自动输出执行日志 4.8.3.7 ⏱️2022.12.14 [58d2c20](https://gitee.com/dotnetchina/Furion/commit/58d2c20de05dc458b206863f7814e89866e7522b) +- -  新增 **定时任务 `IScheduler` 对象每次操作后自动刷新和提供手动刷新 `Reload()` 方法** 4.8.3.3 ⏱️2022.12.09 [#I65EQ1](https://gitee.com/dotnetchina/Furion/issues/I65EQ1#note_15047484_link) +- -  新增 定时任务间隔分钟作业触发器 `Triggers.PeriodMinutes(5)` 和 `[PeriodMinutes(5)]` 特性 4.8.2.8 ⏱️2022.12.01 [8e1f06f](https://gitee.com/dotnetchina/Furion/commit/8e1f06fa2161ee2bf8bcea29af8aaa5a60ef9db9) +- -  新增 定时任务工作日作业触发器 `Triggers.Workday()` 和 `[Workday]` 特性 4.8.2.6 ⏱️2022.11.30 [28b2d20](https://gitee.com/dotnetchina/Furion/commit/28b2d20b3f6034a4cdf5827576c34412315fbb15) +- -  新增 定时任务作业校对功能,可对误差进行校正 4.8.2.6 ⏱️2022.11.30 [f725a25](https://gitee.com/dotnetchina/Furion/commit/f725a252e6a89f9dea6489d4e54452077b1935e5) +- -  新增 **定时任务 `Triggers` 所有带 `At` 的 `Cron` 表达式触发器构建器及特性** 4.8.2.5 ⏱️2022.11.29 [#I63PLR](https://gitee.com/dotnetchina/Sundial/issues/I63PLR) +- -  新增 定时任务批量添加 `SchedulerBuilder` 作业功能 4.8.2.4 ⏱️2022.11.29 [5faa67b](https://gitee.com/dotnetchina/Furion/commit/5faa67b7817459cb0ee0add86a6e53c17ff51a05) +- -  新增 定时任务 `BuildSqlType` 配置,可设置生成不同数据库类型的 `SQL` 语句 4.8.2.3 ⏱️2022.11.29 [293f9bc](https://gitee.com/dotnetchina/Furion/commit/293f9bce34fc4f70eacae1043ed697d31da88409) [!675](https://gitee.com/dotnetchina/Furion/pulls/675) +- -  新增 `JobDetail` 和 `Trigger` 自定义 `ConvertToSQL` 输出 `SQL` 配置 4.8.2 ⏱️2022.11.27 [0bb9d8f](https://gitee.com/dotnetchina/Furion/commit/0bb9d8f1f3606af145b44c2984b87fdc020f02e1) +- -  新增 **作业触发器 `ResetOnlyOnce` 属性,支持只运行一次的作业重新启动服务重复执行** 4.8.1.5 ⏱️2022.11.25 [a8be728](https://gitee.com/dotnetchina/Furion/commit/a8be728eac986ebc5f44718b08c67aaee8b89dc6) +- -  新增 动态作业处理程序委托支持 4.8.1.8 ⏱️2022.11.27 [e02266c](https://gitee.com/dotnetchina/Furion/commit/e02266c44187dbc2b416abe1ca4112ce13c89180) + +- **突破性变化** + + -  移除 **定时任务看板 `SyncRate` 配置,前后端采用最新的 `SSE` 推送技术替代** 4.8.8.29 ⏱️2023.06.25 [e4c4cf1](https://gitee.com/dotnetchina/Furion/commit/e4c4cf1d418f3cc2291eca7d7dd1c8b62d17b0e9) + -  调整 **定时任务动态作业 `DynamicJob` 委托/方法签名** 4.8.7.10 ⏱️2023.03.14 [6d56b53](https://gitee.com/dotnetchina/Furion/commit/6d56b531f34c9d616202a6f53c31a10974065d56) + +
+ 查看变化 +
+ +减少记忆负担,统一动态作业和普通作业的 `ExecuteAsync` 方法签名,故做出调整。 + +由: + +```cs showLineNumbers {1,3} +options.AddJob((serviceProvider, context, stoppingToken) => +{ + serviceProvider.GetLogger().LogInformation($"{context}"); + return Task.CompletedTask; +}, Triggers.PeriodSeconds(5)); +``` + +调整为: + +```cs showLineNumbers {1,3} +options.AddJob((context, stoppingToken) => +{ + context.ServiceProvider.GetLogger().LogInformation($"{context}"); + return Task.CompletedTask; +}, Triggers.PeriodSeconds(5)); +``` + +
+
+ +- -  调整 **定时任务底层所有代码,日志,注释,文档** 4.8.1.10 ⏱️2022.12.05 + +- **问题修复** + + -  修复 定时任务设置触发器 `Result` 后作业执行异常不能重置问题 4.9.1.7 ⏱️2023.11.24 [147215f](https://gitee.com/dotnetchina/Furion/commit/147215f1631f58fca900f17cca5695f9431555e5) + -  修复 定时任务高频作业下持久化操作出现阻塞卡问题 4.8.8.51 ⏱️2023.11.06 [f1d0b4a](https://gitee.com/dotnetchina/Furion/commit/f1d0b4a9d7d65d5263109d5370b8d87705f4178b) + -  修复 定时任务看板中间件 `SSE` 请求不是长连接导致连接频繁初始化销毁 4.8.8.49 ⏱️2023.10.26 [1997f1b](https://gitee.com/dotnetchina/Furion/commit/1997f1b99043eb80accac4e6a0c60c4e33d77183) + -  修复 定时任务因上一版本修改 [4e2615b](https://gitee.com/dotnetchina/Furion/commit/4e2615b00da0b2db756e4084be882c0362c442f5) 导致自定义作业触发器异常问题 4.8.8.36 ⏱️2023.07.06 [#I7J59D](https://gitee.com/dotnetchina/Furion/issues/I7J59D) + -  修复 定时任务因上一版本修改 [4e2615b](https://gitee.com/dotnetchina/Furion/commit/4e2615b00da0b2db756e4084be882c0362c442f5) 导致 `Cron` 解析异常问题 4.8.8.32 ⏱️2023.06.28 [#I7GQ5I](https://gitee.com/dotnetchina/Furion/issues/I7GQ5I) + -  修复 定时任务设置额外数据不支持 `long/int64` 类型参数问题 4.8.8.31 ⏱️2023.06.28 [4e2615b](https://gitee.com/dotnetchina/Furion/commit/4e2615b00da0b2db756e4084be882c0362c442f5) + -  修复 定时任务休眠毫秒数大于 `int.MaxValue` 时出现 `ArgumentOutOfRangeException` 4.8.8.27 ⏱️2023.06.21 [#I7F6ZT](https://gitee.com/dotnetchina/Furion/issues/I7F6ZT) + -  修复 定时任务通过作业 `Id` 删除作业不能删除作业触发器问题 4.8.7.35 ⏱️2023.04.05 [312ca35](https://gitee.com/dotnetchina/Furion/commit/312ca357cca1e59e1b6cc67ec499bf512f79dd0a) + -  修复 定时任务作业状态为 `积压:0` 和 `归档:6` 时调用立即执行后不能恢复上一次状态 4.8.7.18 ⏱️2023.03.21 [6f5aae8](https://gitee.com/dotnetchina/Furion/commit/6f5aae8dd1169b7111ff6801111691764b03ba29) + -  修复 定时任务更新作业 `null` 值默认被跳过问题 4.8.7.17 ⏱️2023.03.20 [#I6OHO4](https://gitee.com/dotnetchina/Furion/issues/I6OHO4) + -  修复 定时任务生成 `SQL` 语句没有处理 `'` 转义问题 4.8.7.15 ⏱️2023.03.19 [#I6NXKA](https://gitee.com/dotnetchina/Furion/issues/I6NXKA) + -  修复 定时任务服务在停止进程时会卡住 `30秒` 问题 4.8.7.8 ⏱️2023.03.13 [#I6MI9I](https://gitee.com/dotnetchina/Furion/issues/I6MI9I) [#I6MHOU](https://gitee.com/dotnetchina/Furion/issues/I6MHOU) + -  修复 定时任务看板删除不存在的作业触发器出现空异常 4.8.7.7 ⏱️2023.03.11 [01d4466](https://gitee.com/dotnetchina/Furion/commit/01d446620c20e373f198195797896d8c96feeb15) + -  修复 定时任务 `StartAll` 出现个别作业显示 `无触发时间` 的状态 4.8.4.14 ⏱️2023.01.12 [#I6A08X](https://gitee.com/dotnetchina/Furion/issues/I6A08X) + -  修复 定时任务停止作业触发器后运行记录不能写入最新记录问题 4.8.4.8 ⏱️2023.01.05 [d4c553f](https://gitee.com/dotnetchina/Furion/commit/d4c553fb9b0037ab29942ccc2a25a386fc28c1db) + -  修复 定时任务使用 `Furion.Pure` 包访问 `Dashboard` 出现 `404` 问题 4.8.4.2 ⏱️2023.01.02 [21977b7](https://gitee.com/dotnetchina/Furion/commit/21977b70ef84fd674bb306ab86ef032f7f28c7a7) + -  修复 定时任务通过 `scheduler.RemoveTrigger(triggerId)` 报异常问题 4.8.3.3 ⏱️2022.12.09 [#I65EQ1](https://gitee.com/dotnetchina/Furion/issues/I65EQ1#note_15047484_link) + -  修复 定时任务作业触发器配置了 `EndTime` 和 `StartTime` 之后 `Status` 没有对应上 4.8.3.1 ⏱️2022.12.09 [52a5506](https://gitee.com/dotnetchina/Furion/commit/52a5506c2fda7c31df01b2e90af7ad6b0c5f94aa) + -  修复 定时任务通过 `scheduler.AddTrigger(triggerBuilder)` 无效的问题 4.8.3.1 ⏱️2022.12.09 [#I65EQ1](https://gitee.com/dotnetchina/Furion/issues/I65EQ1) + -  修复 作业拥有多个触发器时暂停作业后依然存在个别未暂停的清空(并发问题) 4.8.2.12 ⏱️2022.12.07 [#I655W9](https://gitee.com/dotnetchina/Furion/issues/I655W9) + -  修复 作业触发器不符合下一次执行规律但 `NextRunTime` 不为 `null` 情况 4.8.1.5 ⏱️2022.11.25 [a8be728](https://gitee.com/dotnetchina/Furion/commit/a8be728eac986ebc5f44718b08c67aaee8b89dc6) + -  修复 运行时启动/暂停作业无效问题 4.8.1.6 ⏱️2022.11.25 [#I6368M](https://gitee.com/dotnetchina/Furion/issues/I6368M) + -  修复 定时任务生成的 `SQL` 语句不支持 `MySQL` 问题 4.8.1.7 ⏱️2022.11.26 [#I638ZC](https://gitee.com/dotnetchina/Furion/issues/I638ZC) + +- **其他更改** + + -  调整 定时任务 `GC` 回收逻辑,避免高频添加作业导致 `尾延迟` 问题 4.8.8.3 ⏱️2023.04.21 [#I6XIV8](https://gitee.com/dotnetchina/Furion/issues/I6XIV8) + -  调整 定时任务日志设计,减少不必要的日志输出 4.8.8.3 ⏱️2023.04.21 [#I6XI2L](https://gitee.com/dotnetchina/Furion/issues/I6XI2L) + -  调整 定时任务动态委托作业持久化逻辑,采用不触发持久化操作 4.8.7.36 ⏱️2023.04.06 [7bb58b6](https://gitee.com/dotnetchina/Furion/commit/7bb58b64407f899d5f7f128da64fa972cf4df61b) + -  调整 定时任务 `Http` 作业 `HttpMethod` 属性拼写错成 `HttpMedhod` 4.8.7.24 ⏱️2023.03.28 [!756](https://gitee.com/dotnetchina/Furion/pulls/756) + -  调整 定时任务配置选项 `BuilSqlType` 属性命为 `BuildSqlType` 4.8.7.11 ⏱️2023.03.15 [92117b8](https://gitee.com/dotnetchina/Furion/commit/92117b842f7f8bdeb983bf3dac510f713d8410c2) + -  调整 **定时任务查看作业触发器运行记录由保存 `10条` 改为 `5条`** 4.8.7.7 ⏱️2023.03.07 [01d4466](https://gitee.com/dotnetchina/Furion/commit/01d446620c20e373f198195797896d8c96feeb15) + -  调整 定时任务调度器时间精度,控制持续执行一年误差在 `100ms` 以内 4.8.2.9 ⏱️2022.12.01 [334d089](https://gitee.com/dotnetchina/Furion/commit/334d08989503bacd8bf2abb2cc87cf2031dc9da6) + -  调整 定时任务作业计划工厂 `GetNextRunJobs()` 方法逻辑 4.8.2.7 ⏱️2022.11.30 [#I63VS2](https://gitee.com/dotnetchina/Furion/issues/I63VS2) + +- **文档** + + -  新增 作业执行器实现超时文档 4.8.3.8 ⏱️2022.12.20 + -  新增 作业触发器 `ResetOnlyOnce` 文档 4.8.1.5 ⏱️2022.11.25 [a8be728](https://gitee.com/dotnetchina/Furion/commit/a8be728eac986ebc5f44718b08c67aaee8b89dc6) + -  新增 **通过 `Roslyn` 动态编译代码创建 `IJob` 类型文档** 4.8.1.5 ⏱️2022.11.25 [2c5e5be](https://gitee.com/dotnetchina/Furion/commit/2c5e5befc7a335d6ef0e75eea0061aee1e4dd061) + -  新增 自定义 `JobDetail` 和 `Trigger` 输出 `SQL` 文档 4.8.2 ⏱️2022.11.27 [0bb9d8f](https://gitee.com/dotnetchina/Furion/commit/0bb9d8f1f3606af145b44c2984b87fdc020f02e1) + +
+
+
+ +:::warning 4.8.0 以下版本说明 + +**在 `Furion 4.8.0+` 版本采用 [Sundial](https://gitee.com/dotnetchina/Sundial) 定时任务替换原有的 `TaskScheduler`**,[查看旧文档](/docs/job-old) + +::: + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.0 +` 版本使用。 + +::: + +import useBaseUrl from "@docusaurus/useBaseUrl"; +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +## 26.1.1 关于调度作业 + +调度作业又称定时任务,顾名思义,定时任务就是在特定的时间或符合某种时间规律自动触发并执行任务。 + + + +### 26.1.1.1 使用场景 + +定时任务的应用场景非常广,几乎是每一个软件系统必备功能: + +- 叫你起床的闹钟 +- 日历日程提醒 +- 生日纪念日提醒 +- 定时备份数据库 +- 定时清理垃圾数据 +- 定时发送营销信息,邮件 +- 定时上线产品,比如预售产品,双十一活动 +- 定时发送优惠券 +- 定时发布,实现 Devops 功能,如 Jenkins +- 定时爬虫抓数据 +- 定时导出报表,历史统计,考勤统计 +- ... + +## 26.1.2 快速入门 + +1. 定义作业处理程序 `MyJob`: + +```cs showLineNumbers {1,9,11} +public class MyJob : IJob +{ + private readonly ILogger _logger; + public MyJob(ILogger logger) + { + _logger = logger; + } + + public Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + _logger.LogInformation($"{context}"); + return Task.CompletedTask; + } +} +``` + +2. 在 `Startup.cs` 注册 `Schedule` 服务: + +```cs showLineNumbers {1,3-4} +services.AddSchedule(options => +{ + // 注册作业,并配置作业触发器 + options.AddJob(Triggers.Secondly()); // 表示每秒执行 +}); +``` + +3. 查看作业执行结果 + +```bash showLineNumbers {6,8,10,12,14,16,18} +info: 2022-12-02 16:51:33.5032989 +08:00 星期五 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is running. +info: 2022-12-02 16:51:33.5180669 +08:00 星期五 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is preloading... +info: 2022-12-02 16:51:34.1452041 +08:00 星期五 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended to the schedule. +info: 2022-12-02 16:51:34.1541701 +08:00 星期五 L System.Logging.ScheduleService[0] #1 + The scheduler of successfully appended to the schedule. +warn: 2022-12-02 16:51:34.1748401 +08:00 星期五 L System.Logging.ScheduleService[0] #1 + Schedule hosted service preload completed, and a total of <1> schedulers are appended. +info: 2022-12-02 16:51:35.0712571 +08:00 星期五 L MyJob[0] #4 + [C] * * * * * * 1ts 2022-12-02 16:51:35.000 -> 2022-12-02 16:51:36.000 +info: 2022-12-02 16:51:36.0317375 +08:00 星期五 L MyJob[0] #14 + [C] * * * * * * 2ts 2022-12-02 16:51:36.000 -> 2022-12-02 16:51:37.000 +info: 2022-12-02 16:51:37.0125007 +08:00 星期五 L MyJob[0] #9 + [C] * * * * * * 3ts 2022-12-02 16:51:37.000 -> 2022-12-02 16:51:38.000 +info: 2022-12-02 16:51:38.0179920 +08:00 星期五 L MyJob[0] #8 + [C] * * * * * * 4ts 2022-12-02 16:51:38.000 -> 2022-12-02 16:51:39.000 +``` + +`JobExecutionContext` 重写了 `ToString()` 方法并提供以下几种格式: + +```bash showLineNumbers {2,5} +# 持续运行格式 +<作业Id> 作业描述 [并行C/串行S] <作业Id 触发器Id> 触发器字符串 触发器描述 触发次数ts 触发时间 -> 下一次触发时间 + +# 触发停止格式 +<作业Id> 作业描述 [并行C/串行S] <作业Id 触发器Id> 触发器字符串 触发器描述 触发次数ts 触发时间 [触发器终止状态] +``` + +### 26.1.2.1 指定作业 `Id` + +默认情况下,不指定作业 `Id` 会自动生成 `job[编号]`。 + +```cs showLineNumbers {3} +services.AddSchedule(options => +{ + options.AddJob("myjob", Triggers.Secondly()); +}); +``` + +查看作业执行结果: + +```bash showLineNumbers {6,8,12,14,16} +info: 2022-12-02 17:15:43.3024818 +08:00 星期五 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is running. +info: 2022-12-02 17:15:43.3107918 +08:00 星期五 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is preloading... +info: 2022-12-02 17:15:43.9498664 +08:00 星期五 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended to the schedule. +info: 2022-12-02 17:15:43.9532894 +08:00 星期五 L System.Logging.ScheduleService[0] #1 + The scheduler of successfully appended to the schedule. +warn: 2022-12-02 17:15:43.9941565 +08:00 星期五 L System.Logging.ScheduleService[0] #1 + Schedule hosted service preload completed, and a total of <1> schedulers are appended. +info: 2022-12-02 17:15:44.1230353 +08:00 星期五 L MyJob[0] #6 + [C] * * * * * * 1ts 2022-12-02 17:15:44.000 -> 2022-12-02 17:15:45.000 +info: 2022-12-02 17:15:45.0854893 +08:00 星期五 L MyJob[0] #9 + [C] * * * * * * 2ts 2022-12-02 17:15:45.000 -> 2022-12-02 17:15:46.000 +info: 2022-12-02 17:15:46.0100813 +08:00 星期五 L MyJob[0] #13 + [C] * * * * * * 3ts 2022-12-02 17:15:46.000 -> 2022-12-02 17:15:47.000 +``` + +### 26.1.2.2 多个作业触发器 + +有时候,一个作业支持多种触发时间,比如 `每分钟` 执行一次,每 `5秒` 执行一次,每分钟第 `3/7/8秒` 执行一次。 + +```cs showLineNumbers {3-5} +services.AddSchedule(options => +{ + options.AddJob(Triggers.Minutely() // 每分钟开始 + , Triggers.Period(5000) // 每 5 秒,还支持 Triggers.PeriodSeconds(5),Triggers.PeriodMinutes(5),Triggers.PeriodHours(5) + , Triggers.Cron("3,7,8 * * * * ?", CronStringFormat.WithSeconds)); // 每分钟第 3/7/8 秒 +}); +``` + +查看作业执行结果: + +```cs showLineNumbers {6,8,10,12,16,18,20,24,26} +info: 2022-12-02 17:18:53.3593518 +08:00 星期五 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is running. +info: 2022-12-02 17:18:53.3663583 +08:00 星期五 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is preloading... +info: 2022-12-02 17:18:54.0381456 +08:00 星期五 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended to the schedule. +info: 2022-12-02 17:18:54.0708796 +08:00 星期五 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended to the schedule. +info: 2022-12-02 17:18:54.0770193 +08:00 星期五 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended to the schedule. +info: 2022-12-02 17:18:54.0800017 +08:00 星期五 L System.Logging.ScheduleService[0] #1 + The scheduler of successfully appended to the schedule. +warn: 2022-12-02 17:18:54.1206816 +08:00 星期五 L System.Logging.ScheduleService[0] #1 + Schedule hosted service preload completed, and a total of <1> schedulers are appended. +info: 2022-12-02 17:18:59.0040452 +08:00 星期五 L MyJob[0] #9 + [C] 5000ms 1ts 2022-12-02 17:18:58.927 -> 2022-12-02 17:19:03.944 +info: 2022-12-02 17:19:00.0440142 +08:00 星期五 L MyJob[0] #15 + [C] * * * * * 1ts 2022-12-02 17:19:00.000 -> 2022-12-02 17:20:00.000 +info: 2022-12-02 17:19:03.0149075 +08:00 星期五 L MyJob[0] #6 + [C] 3,7,8 * * * * ? 1ts 2022-12-02 17:19:03.000 -> 2022-12-02 17:19:07.000 +info: 2022-12-02 17:19:03.9519350 +08:00 星期五 L MyJob[0] #15 + [C] 5000ms 2ts 2022-12-02 17:19:03.944 -> 2022-12-02 17:19:08.919 +info: 2022-12-02 17:19:07.0116797 +08:00 星期五 L MyJob[0] #4 + [C] 3,7,8 * * * * ? 2ts 2022-12-02 17:19:07.000 -> 2022-12-02 17:19:08.000 +info: 2022-12-02 17:19:08.0078132 +08:00 星期五 L MyJob[0] #15 + [C] 3,7,8 * * * * ? 3ts 2022-12-02 17:19:08.000 -> 2022-12-02 17:20:03.000 +info: 2022-12-02 17:19:08.9298393 +08:00 星期五 L MyJob[0] #14 + [C] 5000ms 3ts 2022-12-02 17:19:08.919 -> 2022-12-02 17:19:13.897 +info: 2022-12-02 17:19:13.9056247 +08:00 星期五 L MyJob[0] #8 + [C] 5000ms 4ts 2022-12-02 17:19:13.897 -> 2022-12-02 17:19:18.872 +info: 2022-12-02 17:19:18.8791123 +08:00 星期五 L MyJob[0] #12 + [C] 5000ms 5ts 2022-12-02 17:19:18.872 -> 2022-12-02 17:19:23.846 +``` + +### 26.1.2.3 `串行` 执行 + +默认情况下,作业采用 `并行` 执行方式,也就是不会等待上一次作业执行完成,只要触发时间到了就自动执行,但一些情况下,我们可能希望等待上一次作业完成再执行,如: + +```cs showLineNumbers {3} +services.AddSchedule(options => +{ + options.AddJob(concurrent: false, Triggers.Secondly()); // 串行,每秒执行 +}); +``` + +```cs showLineNumbers {12} +public class MyJob : IJob +{ + private readonly ILogger _logger; + public MyJob(ILogger logger) + { + _logger = logger; + } + + public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + _logger.LogInformation($"{context.JobId} {context.TriggerId} {context.OccurrenceTime} {context.Trigger}"); + await Task.Delay(2000, stoppingToken); // 这里模拟耗时操作,比如耗时2秒 + } +} +``` + +查看作业执行结果: + +```cs showLineNumbers {6,8,12,14,16,18,20} +info: 2022-12-02 17:23:27.3726863 +08:00 星期五 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is running. +info: 2022-12-02 17:23:27.3830366 +08:00 星期五 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is preloading... +info: 2022-12-02 17:23:27.9083148 +08:00 星期五 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended to the schedule. +info: 2022-12-02 17:23:27.9184699 +08:00 星期五 L System.Logging.ScheduleService[0] #1 + The scheduler of successfully appended to the schedule. +warn: 2022-12-02 17:23:27.9740028 +08:00 星期五 L System.Logging.ScheduleService[0] #1 + Schedule hosted service preload completed, and a total of <1> schedulers are appended. +info: 2022-12-02 17:23:28.0638789 +08:00 星期五 L MyJob[0] #9 + [S] * * * * * * 1ts 2022-12-02 17:23:28.000 -> 2022-12-02 17:23:29.000 +warn: 2022-12-02 17:23:29.1119269 +08:00 星期五 L System.Logging.ScheduleService[0] #9 + 12/02/2022 17:23:29: The trigger of job failed to execute as scheduled due to blocking. +warn: 2022-12-02 17:23:30.0090551 +08:00 星期五 L System.Logging.ScheduleService[0] #9 + 12/02/2022 17:23:30: The trigger of job failed to execute as scheduled due to blocking. +info: 2022-12-02 17:23:31.0121694 +08:00 星期五 L MyJob[0] #9 + [S] * * * * * * 2ts 2022-12-02 17:23:31.000 -> 2022-12-02 17:23:32.000 +warn: 2022-12-02 17:23:32.0243646 +08:00 星期五 L System.Logging.ScheduleService[0] #9 + 12/02/2022 17:23:32: The trigger of job failed to execute as scheduled due to blocking. +``` + +:::caution `串行` 执行规则说明 + +`串行` 执行如果遇到上一次作业还未完成那么它会等到下一次触发时间到了再执行,以此重复。 + +::: + +默认情况下,使用 `串行` 执行但因为耗时导致**触发时间到了但实际未能执行**会默认输出 `warn` 警告日志,如需关闭只需要: + +```cs showLineNumbers {3} +services.AddSchedule(options => +{ + options.LogEnabled = false; + options.AddJob(concurrent: false, Triggers.Secondly()); // 每秒执行 +}); +``` + +查看作业执行结果: + +```bash showLineNumbers {2,4,6,8,10} +info: 2022-12-02 17:27:13.1136450 +08:00 星期五 L MyJob[0] #12 + [S] * * * * * * 1ts 2022-12-02 17:27:13.000 -> 2022-12-02 17:27:14.000 +info: 2022-12-02 17:27:16.0092433 +08:00 星期五 L MyJob[0] #8 + [S] * * * * * * 2ts 2022-12-02 17:27:16.000 -> 2022-12-02 17:27:17.000 +info: 2022-12-02 17:27:19.0092363 +08:00 星期五 L MyJob[0] #6 + [S] * * * * * * 3ts 2022-12-02 17:27:19.000 -> 2022-12-02 17:27:20.000 +info: 2022-12-02 17:27:22.0183594 +08:00 星期五 L MyJob[0] #9 + [S] * * * * * * 4ts 2022-12-02 17:27:22.000 -> 2022-12-02 17:27:23.000 +info: 2022-12-02 17:27:25.0152323 +08:00 星期五 L MyJob[0] #4 + [S] * * * * * * 5ts 2022-12-02 17:27:25.000 -> 2022-12-02 17:27:26.000 +``` + +### 26.1.2.4 打印作业完整信息 + +框架提供了四种方式打印作业完整信息。 + +- **第一种:输出完整的作业 `JSON` 信息:`context.ConvertToJSON()`** + +```cs showLineNumbers {11} +public class MyJob : IJob +{ + private readonly ILogger _logger; + public MyJob(ILogger logger) + { + _logger = logger; + } + + public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + _logger.LogInformation(context.ConvertToJSON()); + await Task.CompletedTask; + } +} +``` + +查看作业打印结果: + +```json showLineNumbers {3,14} +info: 2022-12-02 18:00:59.4140802 +08:00 星期五 L MyJob[0] #13 + { + "jobDetail": { + "jobId": "job1", + "groupName": null, + "jobType": "MyJob", + "assemblyName": "ConsoleApp32", + "description": null, + "concurrent": true, + "includeAnnotations": false, + "properties": "{}", + "updatedTime": "2022-12-02 18:00:59.390" + }, + "trigger": { + "triggerId": "job1_trigger1", + "jobId": "job1", + "triggerType": "Furion.Schedule.PeriodSecondsTrigger", + "assemblyName": "Furion", + "args": "[5]", + "description": null, + "status": 2, + "startTime": null, + "endTime": null, + "lastRunTime": "2022-12-02 18:00:59.326", + "nextRunTime": "2022-12-02 18:01:04.358", + "numberOfRuns": 1, + "maxNumberOfRuns": 0, + "numberOfErrors": 0, + "maxNumberOfErrors": 0, + "numRetries": 0, + "retryTimeout": 1000, + "startNow": true, + "runOnStart": false, + "resetOnlyOnce": true, + "result": null, + "elapsedTime": 100, + "updatedTime": "2022-12-02 18:00:59.390" + } + } +``` + +- **第二种:输出单独的作业 `JSON` 信息:`jobDetail.ConvertToJSON()` 或 `trigger.ConvertToJSON()`** + +```cs showLineNumbers {11-12} +public class MyJob : IJob +{ + private readonly ILogger _logger; + public MyJob(ILogger logger) + { + _logger = logger; + } + + public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + _logger.LogInformation(context.JobDetail.ConvertToJSON()); + _logger.LogInformation(context.Trigger.ConvertToJSON(NamingConventions.UnderScoreCase)); // 支持三种属性名输出规则 + + await Task.CompletedTask; + } +} +``` + +查看作业打印结果: + +```json showLineNumbers {2-12,14-37} +info: 2022-12-02 18:02:10.7923360 +08:00 星期五 L MyJob[0] #8 + { + "jobId": "job1", + "groupName": null, + "jobType": "MyJob", + "assemblyName": "ConsoleApp32", + "description": null, + "concurrent": true, + "includeAnnotations": false, + "properties": "{}", + "updatedTime": "2022-12-02 18:02:10.774" + } +info: 2022-12-02 18:02:10.8008708 +08:00 星期五 L MyJob[0] #8 + { + "trigger_id": "job1_trigger1", + "job_id": "job1", + "trigger_type": "Furion.Schedule.PeriodSecondsTrigger", + "assembly_name": "Furion", + "args": "[5]", + "description": null, + "status": 2, + "start_time": null, + "end_time": null, + "last_run_time": "2022-12-02 18:02:10.727", + "next_run_time": "2022-12-02 18:02:15.733", + "number_of_runs": 1, + "max_number_of_runs": 0, + "number_of_errors": 0, + "max_number_of_errors": 0, + "num_retries": 0, + "retry_timeout": 1000, + "start_now": true, + "run_on_start": false, + "reset_only_once": true, + "result": null, + "elapsed_time": 100, + "updated_time": "2022-12-02 18:02:10.774" + } +``` + +- **第三种:输出单独的作业 `SQL` 信息:`jobDetail.ConvertToSQL()` 或 `trigger.ConvertToSQL()`** + +```cs showLineNumbers {11-12,14-16} +public class MyJob : IJob +{ + private readonly ILogger _logger; + public MyJob(ILogger logger) + { + _logger = logger; + } + + public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + var jobDetail = context.JobDetail; + var trigger = context.Trigger; + + _logger.LogInformation(jobDetail.ConvertToSQL("作业信息表名", PersistenceBehavior.Appended)); // 输出新增语句 + _logger.LogInformation(trigger.ConvertToSQL("作业触发器表名", PersistenceBehavior.Removed, NamingConventions.Pascal)); // 输出删除语句 + _logger.LogInformation(trigger.ConvertToSQL("作业触发器表名", PersistenceBehavior.Updated, NamingConventions.UnderScoreCase)); // 输出更新语句 + + await Task.CompletedTask; + } +} +``` + +查看作业打印结果: + +```sql showLineNumbers {2,25,28} +info: 2022-12-02 18:03:11.8543760 +08:00 星期五 L MyJob[0] #13 + INSERT INTO 作业信息表名( + jobId, + groupName, + jobType, + assemblyName, + description, + concurrent, + includeAnnotations, + properties, + updatedTime + ) + VALUES( + 'job1', + NULL, + 'MyJob', + 'ConsoleApp32', + NULL, + 1, + 0, + '{}', + '2022-12-02 18:03:11.836' + ); +info: 2022-12-02 18:03:11.8636268 +08:00 星期五 L MyJob[0] #13 + DELETE FROM 作业触发器表名 + WHERE TriggerId = 'job1_trigger1' AND JobId = 'job1'; +info: 2022-12-02 18:03:11.8669134 +08:00 星期五 L MyJob[0] #13 + UPDATE 作业触发器表名 + SET + trigger_id = 'job1_trigger1', + job_id = 'job1', + trigger_type = 'Furion.Schedule.PeriodSecondsTrigger', + assembly_name = 'Furion', + args = '[5]', + description = NULL, + status = 2, + start_time = NULL, + end_time = NULL, + last_run_time = '2022-12-02 18:03:11.778', + next_run_time = '2022-12-02 18:03:16.794', + number_of_runs = 1, + max_number_of_runs = 0, + number_of_errors = 0, + max_number_of_errors = 0, + num_retries = 0, + retry_timeout = 1000, + start_now = 1, + run_on_start = 0, + reset_only_once = 1, + result = NULL, + elapsed_time = 100, + updated_time = '2022-12-02 18:03:11.836' + WHERE trigger_id = 'job1_trigger1' AND job_id = 'job1'; +``` + +- **第四种:输出单独的作业 `Monitor` 信息:`jobDetail.ConvertToMonitor()` 或 `trigger.ConvertToMonitor()`** + +```cs showLineNumbers {11-12} +public class MyJob : IJob +{ + private readonly ILogger _logger; + public MyJob(ILogger logger) + { + _logger = logger; + } + + public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + _logger.LogInformation(context.JobDetail.ConvertToMonitor()); + _logger.LogInformation(context.Trigger.ConvertToMonitor()); + + await Task.CompletedTask; + } +} +``` + +查看作业打印结果: + +```bash showLineNumbers {2,16} +info: 2022-12-02 18:04:06.2833095 +08:00 星期五 L MyJob[0] #8 + ┏━━━━━━━━━━━ JobDetail ━━━━━━━━━━━ + ┣ MyJob + ┣ + ┣ jobId: job1 + ┣ groupName: + ┣ jobType: MyJob + ┣ assemblyName: ConsoleApp32 + ┣ description: + ┣ concurrent: True + ┣ includeAnnotations: False + ┣ properties: {} + ┣ updatedTime: 2022-12-02 18:04:06.254 + ┗━━━━━━━━━━━ JobDetail ━━━━━━━━━━━ +info: 2022-12-02 18:04:06.2868205 +08:00 星期五 L MyJob[0] #8 + ┏━━━━━━━━━━━ Trigger ━━━━━━━━━━━ + ┣ Furion.Schedule.PeriodSecondsTrigger + ┣ + ┣ triggerId: job1_trigger1 + ┣ jobId: job1 + ┣ triggerType: Furion.Schedule.PeriodSecondsTrigger + ┣ assemblyName: Furion + ┣ args: [5] + ┣ description: + ┣ status: Running + ┣ startTime: + ┣ endTime: + ┣ lastRunTime: 2022-12-02 18:04:06.189 + ┣ nextRunTime: 2022-12-02 18:04:11.212 + ┣ numberOfRuns: 1 + ┣ maxNumberOfRuns: 0 + ┣ numberOfErrors: 0 + ┣ maxNumberOfErrors: 0 + ┣ numRetries: 0 + ┣ retryTimeout: 1000 + ┣ startNow: True + ┣ runOnStart: False + ┣ resetOnlyOnce: True + ┣ result: + ┣ elapsedTime: 100 + ┣ updatedTime: 2022-12-02 18:04:06.254 + ┗━━━━━━━━━━━ Trigger ━━━━━━━━━━━ +``` + +### 26.1.2.5 运行时(动态)操作作业 + +有时候,我们需要在运行时对作业动态的增加,更新,删除等操作,如动态添加作业: + +1. 注册 `services.AddSchedule()` 服务 + +```cs showLineNumbers {2,5} +// 可以完全动态操作,只需要注册服务即可 +services.AddSchedule(); + +// 也可以部分静态,部分动态注册 +services.AddSchedule(options => +{ + options.AddJob(concurrent: false, Triggers.PeriodSeconds(5)); +}); +``` + +2. 注入 `ISchedulerFactory` 服务 + +```cs showLineNumbers {4,11} +public class YourService: IYourService +{ + private readonly ISchedulerFactory _schedulerFactory; + public YourService(ISchedulerFactory schedulerFactory) + { + _schedulerFactory = schedulerFactory; + } + + public void AddJob() + { + _schedulerFactory.AddJob("动态作业 Id", Triggers.Secondly()); + } +} +``` + +3. 查看作业执行结果 + +```bash showLineNumbers {6,8,12,14,16,18,22,24} +info: 2022-12-02 18:07:33.7799062 +08:00 星期五 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is running. +info: 2022-12-02 18:07:33.7971487 +08:00 星期五 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is preloading... +info: 2022-12-02 18:07:33.8751390 +08:00 星期五 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended to the schedule. +info: 2022-12-02 18:07:33.8805159 +08:00 星期五 L System.Logging.ScheduleService[0] #1 + The scheduler of successfully appended to the schedule. +warn: 2022-12-02 18:07:33.9013656 +08:00 星期五 L System.Logging.ScheduleService[0] #1 + Schedule hosted service preload completed, and a total of <1> schedulers are appended. +info: 2022-12-02 18:07:38.9241031 +08:00 星期五 L MyJob[0] #9 + [C] 5s 1ts 2022-12-02 18:07:38.813 -> 2022-12-02 18:07:43.863 +info: 2022-12-02 18:07:43.0865787 +08:00 星期五 L System.Logging.ScheduleService[0] #16 + The <动态作业 Id_trigger1> trigger for scheduler of <动态作业 Id> successfully appended to the schedule. +warn: 2022-12-02 18:07:43.0894163 +08:00 星期五 L System.Logging.ScheduleService[0] #16 + Schedule hosted service cancels hibernation and GC.Collect(). +info: 2022-12-02 18:07:43.1129824 +08:00 星期五 L System.Logging.ScheduleService[0] #16 + The scheduler of <动态作业 Id> successfully appended to the schedule. +info: 2022-12-02 18:07:43.8810686 +08:00 星期五 L MyJob[0] #17 + [C] 5s 2ts 2022-12-02 18:07:43.863 -> 2022-12-02 18:07:48.848 +info: 2022-12-02 18:07:44.0104025 +08:00 星期五 L MyJob[0] #16 + <动态作业 Id> [C] <动态作业 Id 动态作业 Id_trigger1> * * * * * * 1ts 2022-12-02 18:07:44.000 -> 2022-12-02 18:07:45.000 +info: 2022-12-02 18:07:45.0092441 +08:00 星期五 L MyJob[0] #8 + <动态作业 Id> [C] <动态作业 Id 动态作业 Id_trigger1> * * * * * * 2ts 2022-12-02 18:07:45.000 -> 2022-12-02 18:07:46.000 +``` + +### 26.1.2.6 作业触发器特性 + +默认情况下,框架不会扫描 `IJob` 实现类的作业触发器特性,但可以设置作业的 `IncludeAnnotations` 进行启用。 + +1. 启用 `IncludeAnnotations` 扫描 + +```cs showLineNumbers {3,7,10} +services.AddSchedule(options => +{ + options.AddJob(JobBuilder.Create().SetIncludeAnnotations(true) + , Triggers.PeriodSeconds(5)); // 这里可传可不传,传了则会自动载入特性和这里配置的作业触发器 + + // 还可以更简单~~ + options.AddJob(typeof(MyJob).ScanToBuilder()); + + // 还可以批量新增 Furion 4.8.2.4+ + options.AddJob(App.EffectiveTypes.ScanToBuilders()); +}); +``` + +2. 在 `MyJob` 中添加多个作业触发器特性 + +```cs showLineNumbers {1-2} +[Minutely] +[Cron("3,7,8 * * * * ?", CronStringFormat.WithSeconds)] +public class MyJob : IJob +{ + private readonly ILogger _logger; + public MyJob(ILogger logger) + { + _logger = logger; + } + + public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + _logger.LogInformation($"{context}"); + await Task.CompletedTask; + } +} +``` + +3. 查看作业执行结果 + +```bash showLineNumbers {6,8,10,12,16,18,20,24,26} +info: 2022-12-02 18:12:56.4199663 +08:00 星期五 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is running. +info: 2022-12-02 18:12:56.4287962 +08:00 星期五 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is preloading... +info: 2022-12-02 18:12:56.6149505 +08:00 星期五 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended to the schedule. +info: 2022-12-02 18:12:56.6205117 +08:00 星期五 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended to the schedule. +info: 2022-12-02 18:12:56.6266132 +08:00 星期五 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended to the schedule. +info: 2022-12-02 18:12:56.6291006 +08:00 星期五 L System.Logging.ScheduleService[0] #1 + The scheduler of successfully appended to the schedule. +warn: 2022-12-02 18:12:56.6454334 +08:00 星期五 L System.Logging.ScheduleService[0] #1 + Schedule hosted service preload completed, and a total of <1> schedulers are appended. +info: 2022-12-02 18:13:00.0842828 +08:00 星期五 L MyJob[0] #15 + [C] * * * * * 1ts 2022-12-02 18:13:00.000 -> 2022-12-02 18:14:00.000 +info: 2022-12-02 18:13:01.5260220 +08:00 星期五 L MyJob[0] #16 + [C] 5s 1ts 2022-12-02 18:13:01.494 -> 2022-12-02 18:13:06.492 +info: 2022-12-02 18:13:03.0076111 +08:00 星期五 L MyJob[0] #6 + [C] 3,7,8 * * * * ? 1ts 2022-12-02 18:13:03.000 -> 2022-12-02 18:13:07.000 +info: 2022-12-02 18:13:06.4954400 +08:00 星期五 L MyJob[0] #13 + [C] 5s 2ts 2022-12-02 18:13:06.492 -> 2022-12-02 18:13:11.463 +info: 2022-12-02 18:13:07.0180453 +08:00 星期五 L MyJob[0] #6 + [C] 3,7,8 * * * * ? 2ts 2022-12-02 18:13:07.000 -> 2022-12-02 18:13:08.000 +info: 2022-12-02 18:13:08.0114292 +08:00 星期五 L MyJob[0] #13 + [C] 3,7,8 * * * * ? 3ts 2022-12-02 18:13:08.000 -> 2022-12-02 18:14:03.000 +info: 2022-12-02 18:13:11.4774564 +08:00 星期五 L MyJob[0] #16 + [C] 5s 3ts 2022-12-02 18:13:11.463 -> 2022-12-02 18:13:16.445 +``` + +### 26.1.2.7 `HTTP` 请求作业 + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.7.7 +` 版本使用。 + +::: + +`HTTP` 请求作业通常用于定时请求/访问互联网地址。 + +```cs showLineNumbers {3} +services.AddSchedule(options => +{ + options.AddHttpJob(request => + { + request.RequestUri = "https://www.chinadot.net"; + request.HttpMethod = HttpMethod.Get; + // request.Body = "{}"; // 设置请求报文体 + // request.Headers.Add("framework", "Furion"); // Furion 4.8.8.46+ 支持 + // request.GroupName = "group"; // Furion 4.8.8.46+ 支持 + // request.Description = "作业请求描述"; // Furion 4.8.8.46+ 支持 + }, Triggers.PeriodSeconds(5)); +}); +``` + +:::important `System.Net.Http.IHttpClientFactory` 错误 + +如遇 `Unable to resolve service for type 'System.Net.Http.IHttpClientFactory' while attempting to activate 'Furion.Schedule.HttpJob'.` 错误,请先注册 `servces.AddHttpClient()` 服务。 + +::: + +作业执行日志如下: + +```bash showLineNumbers {13-26} +info: 2023-03-11 11:05:36.3616747 +08:00 星期六 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is running. +info: 2023-03-11 11:05:36.3652411 +08:00 星期六 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is preloading... +info: 2023-03-11 11:05:36.5172940 +08:00 星期六 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended to the schedule. +info: 2023-03-11 11:05:36.5189296 +08:00 星期六 L System.Logging.ScheduleService[0] #1 + The scheduler of successfully appended to the schedule. +warn: 2023-03-11 11:05:36.5347816 +08:00 星期六 L System.Logging.ScheduleService[0] #1 + Schedule hosted service preload completed, and a total of <1> schedulers are appended. +warn: 2023-03-11 11:05:41.5228138 +08:00 星期六 L System.Logging.ScheduleService[0] #15 + Schedule hosted service will sleep <4970> milliseconds and be waked up at <2023-03-11 11:05:46.486>. +info: 2023-03-11 11:05:41.5542865 +08:00 星期六 L System.Net.Http.HttpClient.HttpJob.LogicalHandler[100] #9 + Start processing HTTP request GET https://www.chinadot.net/ +info: 2023-03-11 11:05:41.5589056 +08:00 星期六 L System.Net.Http.HttpClient.HttpJob.ClientHandler[100] #9 + Sending HTTP request GET https://www.chinadot.net/ +info: 2023-03-11 11:05:44.1305461 +08:00 星期六 L System.Net.Http.HttpClient.HttpJob.ClientHandler[101] #8 + Received HTTP response headers after 2566.7836ms - 200 +info: 2023-03-11 11:05:44.1343977 +08:00 星期六 L System.Net.Http.HttpClient.HttpJob.LogicalHandler[101] #8 + End processing HTTP request after 2584.2327ms - 200 +info: 2023-03-11 11:05:48.6475959 +08:00 星期六 L System.Logging.ScheduleService[0] #4 + Received HTTP response body with a length of <63639> output as follows - 200 + + dotNET China | 让 .NET 开发更简单,更通用,更流行 + ...... + +``` + +### 26.1.2.8 委托方式作业 + +有时我们需要快速开启新的定时作业**但不考虑后续持久化存储(如数据库存储)**,这时可以使用委托作业方式,如: + +```cs showLineNumbers {3-7} +services.AddSchedule(options => +{ + // 和 IJob 的 ExecuteAsync 方法签名一致 + options.AddJob((context, stoppingToken) => + { + // 可通过 context.ServiceProvider 解析服务;框架提供了 .GetLogger() 拓展方法输出日志 + context.ServiceProvider.GetLogger().LogInformation($"{context}"); + return Task.CompletedTask; + }, Triggers.PeriodSeconds(5)); +}); +``` + +作业执行日志如下: + +```bash showLineNumbers {6,8,12,14} +info: 2023-03-21 14:22:34.1910781 +08:00 星期二 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is running. +info: 2023-03-21 14:22:34.1967420 +08:00 星期二 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is preloading... +info: 2023-03-21 14:22:34.6163320 +08:00 星期二 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended to the schedule. +info: 2023-03-21 14:22:34.6195112 +08:00 星期二 L System.Logging.ScheduleService[0] #1 + The scheduler of successfully appended to the schedule. +warn: 2023-03-21 14:22:34.6398162 +08:00 星期二 L System.Logging.ScheduleService[0] #1 + Schedule hosted service preload completed, and a total of <1> schedulers are appended. +info: 2023-03-21 14:22:39.7171392 +08:00 星期二 L System.Logging.DynamicJob[0] #9 + [C] 5s 1ts 2023-03-21 14:22:39.575 -> 2023-03-21 14:22:44.623 +info: 2023-03-21 14:22:44.6986483 +08:00 星期二 L System.Logging.DynamicJob[0] #9 + [C] 5s 2ts 2023-03-21 14:22:44.623 -> 2023-03-21 14:22:49.657 +``` + +### 26.1.2.9 非 `IOC/DI` 项目中使用 + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.8.5 +` 版本使用。 + +::: + +在一些不支持依赖注入的项目类型如 `Console、WinForm、WPF` 中,可以通过以下方式使用: + +- 方式一:无需获取其他服务对象 + +```cs showLineNumbers {1,2,6-7} +_ = new ServiceCollection() + .AddSchedule(options => + { + options.AddJob(Triggers.Period(5000)); + }) + .GetScheduleHostedService() + .StartAsync(new CancellationTokenSource().Token); +``` + +- 方式二:需要后续解析服务 + +```cs showLineNumbers {2,7,10-11,14} +// 注册服务并构建 +IServiceProvider services = new ServiceCollection() + .AddSchedule(options => + { + options.AddJob(Triggers.Period(5000)); + }) + .BuildServiceProvider(); + +// 启动作业调度主机服务 +services.GetScheduleHostedService() + .StartAsync(new CancellationTokenSource().Token); + +// 解析作业计划工厂 +var schedulerFactory = services.GetService(); +``` + +:::tip 小知识 + +只需要将 `services` 对象用类的静态属性存储起来即可,如: + +```cs showLineNumbers {3} +public class DI +{ + public static IServiceProvider Services {get; set;} +} +``` + +之后通过 `DI.Services = services;` 即可,后续便可以通过 `DI.Services.GetService()` 解析服务。 + +::: + +## 26.1.3 作业信息 `JobDetail` 及构建器 + +### 26.1.3.1 关于作业信息 + +框架提供了 `JobDetail` 类型来描述作业信息,`JobDetail` 类型提供以下**只读属性**: + +| 属性名 | 属性类型 | 默认值 | 说明 | +| -------------------- | ----------- | ------- | ------------------------------------------------------------------------ | +| `JobId` | `string` | | 作业 `Id` | +| `GroupName` | `string` | | 作业组名称 | +| `JobType` | `string` | | 作业处理程序类型,存储的是类型的 `FullName` | +| `AssemblyName` | `string` | | 作业处理程序类型所在程序集,存储的是程序集 `Name` | +| `Description` | `string` | | 描述信息 | +| `Concurrent` | `bool` | `true` | 作业执行方式,如果设置为 `false`,那么使用 `串行` 执行,否则 `并行` 执行 | +| `IncludeAnnotations` | `bool` | `false` | 是否扫描 `IJob` 实现类 `[Trigger]` 特性触发器 | +| `Properties` | `string` | `"{}"` | 作业信息额外数据,由 `Dictionary` 序列化成字符串存储 | +| `UpdatedTime` | `DateTime?` | | 作业更新时间 | + +### 26.1.3.2 关于作业信息构建器 + +作业信息 `JobDetail` 是作业调度模块提供运行时的**只读类型**,那么我们该如何创建或变更 `JobDetail` 对象呢? + +`JobBuilder` 是作业调度模块提供可用来生成运行时 `JobDetail` 的类型,这样做的好处可避免外部直接修改运行时 `JobDetail` 数据,还能实现任何修改动作监听,也能避免多线程抢占情况。 + +作业调度模块提供了多种方式用来创建 `JobBuilder` 对象。 + +1. **通过 `Create` 静态方法创建** + +```cs showLineNumbers {2,5,8,11,14} +// 根据作业 Id 创建 +var jobBuilder = JobBuilder.Create("job1"); + +// 根据 IJob 实现类类型创建 +var jobBuilder = JobBuilder.Create(); + +// 根据程序集名称和类型完全限定名(FullName)创建 +var jobBuilder = JobBuilder.Create("YourProject", "YourProject.MyJob"); + +// 根据 Type 类型创建 +var jobBuilder = JobBuilder.Create(typeof(MyJob)); + +// 通过委托创建动态作业 +var jobBuilder = JobBuilder.Create((context, stoppingToken) => +{ + context.ServiceProvider.GetLogger().LogInformation($"{context}"); + return Task.CompletedTask; +}); +``` + +2. **通过 `JobDetail` 类型创建** + +这种方式常用于在运行时更新作业信息。 + +```cs showLineNumbers +var jobBuilder = JobBuilder.From(jobDetail); + +//也可以通过以下方式 +var jobBuilder = jobDetail.GetBuilder(); +``` + +3. **通过 `JSON` 字符串创建** + +该方式非常灵活,可从配置文件,`JSON` 字符串,或其他能够返回 `JSON` 字符串的地方创建。 + +```cs showLineNumbers {2-12} +var jobBuilder = JobBuilder.From(@"{ + ""jobId"": ""job1"", + ""groupName"": null, + ""jobType"": ""MyJob"", + ""assemblyName"": ""ConsoleApp13"", + ""description"": null, + ""concurrent"": true, + ""includeAnnotations"": false, + ""properties"": ""{}"", + ""updatedTime"": null +}"); +``` + +如果使用的是 `.NET7`,可使用 `"""` 避免转义,如: + +```cs showLineNumbers {2-12} +var jobBuilder = JobBuilder.From(""" +{ + "jobId": "job1", + "groupName": null, + "jobType": "MyJob", + "assemblyName": "ConsoleApp13", + "description": null, + "concurrent": true, + "includeAnnotations": false, + "properties": "{}", + "updatedTime": "2022-12-02 18:00:59.390" +} +"""); +``` + +:::important 关于属性名匹配规则 + +支持 `CamelCase(驼峰命名法)`,`Pascal(帕斯卡命名法)` 命名方式。 + +**不支持 `UnderScoreCase(下划线命名法)`** ,如 `"include_annotations": true` + +::: + +4. **还可以通过 `Clone` 静态方法从一个 `JobBuilder` 创建** + +```cs showLineNumbers +var jobBuilder = JobBuilder.Clone(fromJobBuilder); +``` + +:::important 克隆说明 + +克隆操作只会克隆 `AssemblyName`,`JobType`,`GroupName`,`Description`,`Concurrent`,`IncludeAnnotations`,`Properties`,`DynamicExecuteAsync`(动态作业)。 + +- **不会克隆 `JobId`,`UpdatedTime`。** + +::: + +5. **还可以通过 `LoadFrom` 实例方法填充当前的 `JobBuilder`** + +比如可以传递匿名类型,类类型,字典 `Dictionary` 类型: + +```cs showLineNumbers {2,9,14,17,22,25} +// 会覆盖所有相同的值 +jobBuilder.LoadFrom(new +{ + Description = "我是描述", + Concurrent = false +}); + +// 支持多个填充,还可以配置跳过 null 值覆盖 +jobBuilder.LoadFrom(new +{ + Description = "我是另外一个描述", + Concurrent = false, + IncludeAnnotations = default(object) // 会跳过赋值 +}, ignoreNullValue: true); + +// 支持忽略特定属性名映射 +jobBuilder.LoadFrom(new +{ + Description = "我是另外一个描述", + Concurrent = false, + IncludeAnnotations = default(object) // 会跳过赋值 +}, ignorePropertyNames: new[]{ "description" }); + +// 支持字典类型 +jobBuilder.LoadFrom(new Dictionary +{ + {"Description", "这是新的描述" }, + {"include_annotations", false }, + {"updatedTime", DateTime.Now } +}); +``` + +:::important 关于属性名匹配规则 + +支持 `CamelCase(驼峰命名法)`,`Pascal(帕斯卡命名法)` 和 `UnderScoreCase(下划线命名法)` 命名方式。 + +::: + +### 26.1.3.3 设置作业信息构建器 + +`JobBuilder` 提供了和 `JobDetail` 完全匹配的 `Set[属性名]` 方法来配置作业信息各个属性,如: + +```cs showLineNumbers {3,20} +services.AddSchedule(options => +{ + var jobBuilder = JobBuilder.Create() + .SetJobId("job1") // 作业 Id + .SetGroupName("group1") // 作业组名称 + .SetJobType("Furion.Application", "Furion.Application.MyJob") // 作业类型,支持多个重载 + .SetJobType() // 作业类型,支持多个重载 + .SetJobType(typeof(MyJob)) // 作业类型,支持多个重载 + .SetDescription("这是一段描述") // 作业描述 + .SetConcurrent(false) // 并行还是串行方式,false 为 串行 + .SetIncludeAnnotations(true) // 是否扫描 IJob 类型的触发器特性,true 为 扫描 + .SetProperties("{}") // 作业额外数据 Dictionary 类型序列化,支持多个重载 + .SetProperties(new Dictionary { { "name", "Furion" } }) // 作业类型额外数据,支持多个重载,推荐!!! + .SetDynamicExecuteAsync((context, stoppingToken) => { + context.ServiceProvider.GetLogger().LogInformation($"{context}"); + return Task.CompletedTask; + }) // 动态委托处理程序,一旦设置了此委托,那么优先级将大于 MyJob 的 ExecuteAsync + ; + + options.AddJob(jobBuilder, Triggers.PeriodSeconds(5)); +}); +``` + +### 26.1.3.4 作业信息/构建器额外数据 + +有时候我们需要在作业运行的时候添加一些额外数据,或者实现多个触发器共享数据,经常用于 `串行` 执行中(`并行` 也同样工作),后面一个触发器需等待前一个触发器完成。 + +```cs showLineNumbers {13-14,16} +public class MyJob : IJob +{ + private readonly ILogger _logger; + public MyJob(ILogger logger) + { + _logger = logger; + } + + public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + var jobDetail = context.JobDetail; + + var count = jobDetail.GetProperty("count"); + jobDetail.AddOrUpdateProperty("count", count + 1); // 递增 count + + _logger.LogInformation($"count: {count} {context}"); + + await Task.CompletedTask; + } +} +``` + +查看作业运行日志: + +```bash showLineNumbers {12,14,16,18} +info: 2022-12-03 23:16:46.5150228 +08:00 星期六 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is running. +info: 2022-12-03 23:16:46.5197497 +08:00 星期六 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is preloading... +info: 2022-12-03 23:16:46.6987703 +08:00 星期六 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended to the schedule. +info: 2022-12-03 23:16:46.7003295 +08:00 星期六 L System.Logging.ScheduleService[0] #1 + The scheduler of successfully appended to the schedule. +warn: 2022-12-03 23:16:46.7248216 +08:00 星期六 L System.Logging.ScheduleService[0] #1 + Schedule hosted service preload completed, and a total of <1> schedulers are appended. +info: 2022-12-03 23:16:51.7013640 +08:00 星期六 L MyJob[0] #8 + count: 0 [C] 5s 1ts 2022-12-03 23:16:51.663 -> 2022-12-03 23:16:56.656 +info: 2022-12-03 23:16:56.6768044 +08:00 星期六 L MyJob[0] #9 + count: 1 [C] 5s 2ts 2022-12-03 23:16:56.656 -> 2022-12-03 23:17:01.635 +info: 2022-12-03 23:17:01.6454604 +08:00 星期六 L MyJob[0] #8 + count: 2 [C] 5s 3ts 2022-12-03 23:17:01.635 -> 2022-12-03 23:17:06.608 +info: 2022-12-03 23:17:06.6247917 +08:00 星期六 L MyJob[0] #6 + count: 3 [C] 5s 4ts 2022-12-03 23:17:06.608 -> 2022-12-03 23:17:11.586 +``` + +作业调度模块为 `JobDetail` 和 `JobBuilder` 提供了多个方法操作额外数据: + +```cs showLineNumbers {2,5,8,11,14,17,20,23} +// 查看所有额外数据 +var properties = jobDetail.GetProperties(); + +// 查看单个额外数据,返回 object +var value = jobBuilder.GetProperty("key"); + +// 查看单个额外数据泛型 +var value = jobDetail.GetProperty("key"); + +// 添加新的额外数据,支持链式操作,如果键已存在,则跳过 +jobDetail.AddProperty("key", "Furion").AddProperty("key1", 2); + +// 添加或更新额外数据,支持链式操作,不存在则新增,存在则替换,推荐 +jobDetail.AddOrUpdateProperty("key", "Furion").AddOrUpdateProperty("key1", 2); + +// 还可以通过委托的方式:如果键不存在则插入 count = newValue,否则更新为 value(旧值)+1 +jobDetail.AddOrUpdateProperty("count", newValue, value => value + 1); + +// 删除某个额外数据,支持链式操作,如果 key 不存在则跳过 +jobDetail.RemoveProperty("key").RemoveProperty("key1"); + +// 清空所有额外数据 +jobDetail.ClearProperties(); +``` + +:::important 作业额外数据类型支持 + +作业额外数据每一项的值只支持 `int32`,`int64`,`string`,`bool`,`null` 或它们组成的数组类型。 + +::: + +### 26.1.3.5 作业信息特性 + +作业信息特性 `[JobDetail]` 是为了方便运行时或启动时快速创建作业计划构建器而提供的,可在启动时或运行时通过以下方式创建,如: + +```cs showLineNumbers {1} +[JobDetail("job1", "这是一段描述")] +[PeriodSeconds(5, TriggerId = "trigger1")] +public class MyJob : IJob +{ +} +``` + +- **启动 `IncludeAnnotations` 属性自动填充** + +```cs showLineNumbers {4} +services.AddSchedule(options => +{ + options.AddJob(JobBuilder.Create() + .SetIncludeAnnotations(true)); // 此时 [JobDetail] 配置的非空属性将自动复制给 JobBuilder,[PeriodSeconds] 也会自动创建 TriggerBuilder +}); +``` + +- **手动扫描并创建作业计划构建器** + +```cs showLineNumbers +var schedulerBuilder = typeof(MyJob).ScanToBuilder(); +``` + +- **通过程序集类型扫描批量创建作业计划构建器** + +也可以用于作业持久化 `Preload` 初始化时使用: + +```cs showLineNumbers {1,4-5,8} +public IEnumerable Preload() +{ + // 扫描所有类型并创建 + return App.EffectiveTypes.Where(t => t.IsJobType()) + .Select(t => t.ScanToBuilder()); + + // 还可以更简单~~ + return App.EffectiveTypes.ScanToBuilders(); +} +``` + +--- + +**作业信息特性还提供了多个属性配置**,如: + +- `JobId`:作业信息 Id,`string` 类型 +- `GroupName`:作业组名称,`string` 类型 +- `Description`:描述信息,`string` 类型 +- `Concurrent`:是否采用并行执行,`bool` 类型,如果设置为 `false`,那么使用 `串行` 执行 + +使用如下: + +```cs showLineNumbers {1-6} +[JobDetail("jobId")] // 仅作业 Id +[JobDetail("jobId", "这是一段描述")] // 描述 +[JobDetail("jobId", false)] // 串行 +[JobDetail("jobId", false, "这是一段描述")] // 串行 + 描述 +[JobDetail("jobId", Concurrent = false, Description = "这是一段描述")] +[JobDetail("jobId", Concurrent = false, Description = "这是一段描述", GroupName = "分组名")] +public class MyJob : IJob +{ + // .... +} +``` + +### 26.1.3.6 多种格式字符串输出 + +`JobDetail` 和 `JobBuilder` 都提供了多种将自身转换成特定格式的字符串。 + +1. **转换成 `JSON` 字符串** + +```cs showLineNumbers +var json = jobDetail.ConvertToJSON(); +``` + +字符串打印如下: + +```json showLineNumbers +{ + "jobId": "job1", + "groupName": null, + "jobType": "MyJob", + "assemblyName": "ConsoleApp13", + "description": null, + "concurrent": true, + "includeAnnotations": false, + "properties": "{}", + "updatedTime": "2022-12-04 11:51:00.483" +} +``` + +2. **转换成 `SQL` 字符串** + +```cs showLineNumbers {2,6,9,13,16,20} +// 输出新增 SQL,使用 CamelCase 属性命名 +var insertSql = jobDetail.ConvertToSQL("tbName" + , PersistenceBehavior.Appended + , NamingConventions.CamelCase); +// 更便捷拓展 +var insertSql = jobDetail.ConvertToInsertSQL("tbName", NamingConventions.CamelCase); + +// 输出删除 SQL,使用 Pascal 属性命名 +var deleteSql = jobDetail.ConvertToSQL("tbName" + , PersistenceBehavior.Removed + , NamingConventions.Pascal); +// 更便捷拓展 +var deleteSql = jobDetail.ConvertToDeleteSQL("tbName", NamingConventions.Pascal); + +// 输出更新 SQL,使用 UnderScoreCase 属性命名 +var updateSql = jobDetail.ConvertToSQL("tbName" + , PersistenceBehavior.Updated + , NamingConventions.UnderScoreCase); +// 更便捷拓展 +var updateSql = jobDetail.ConvertToUpdateSQL("tbName", NamingConventions.UnderScoreCase); +``` + +字符串打印如下: + +```sql showLineNumbers {2,25,28} +-- 新增语句 +INSERT INTO tbName( + jobId, + groupName, + jobType, + assemblyName, + description, + concurrent, + includeAnnotations, + properties, + updatedTime +) +VALUES( + 'job1', + NULL, + 'MyJob', + 'ConsoleApp13', + NULL, + 1, + 0, + '{}', + '2022-12-04 11:53:05.489' +); +-- 删除语句 +DELETE FROM tbName +WHERE JobId = 'job1'; +-- 更新语句 +UPDATE tbName +SET + job_id = 'job1', + group_name = NULL, + job_type = 'MyJob', + assembly_name = 'ConsoleApp13', + description = NULL, + concurrent = 1, + include_annotations = 0, + properties = '{}', + updated_time = '2022-12-04 11:53:05.489' +WHERE job_id = 'job1'; +``` + +3. **转换成 `Monitor` 字符串** + +```cs showLineNumbers +var monitor = jobDetail.ConvertToMonitor(); +``` + +字符串打印如下: + +```bash showLineNumbers +┏━━━━━━━━━━━ JobDetail ━━━━━━━━━━━ +┣ MyJob +┣ +┣ jobId: job1 +┣ groupName: +┣ jobType: MyJob +┣ assemblyName: ConsoleApp13 +┣ description: +┣ concurrent: True +┣ includeAnnotations: False +┣ properties: {} +┣ updatedTime: 2022-12-04 11:55:11.186 +┗━━━━━━━━━━━ JobDetail ━━━━━━━━━━━ +``` + +4. **简要字符串输出** + +```cs showLineNumbers +var str = jobDetail.ToString(); +``` + +字符串打印如下: + +```bash showLineNumbers + 这是一段描述 [C] +``` + +### 26.1.3.7 自定义 `SQL` 输出配置 + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.2 +` 版本使用。 + +::: + +```cs showLineNumbers {1,3,8,13,18} +services.AddSchedule(options => +{ + options.JobDetail.ConvertToSQL = (tableName, columnNames, jobDetail, behavior, naming) => + { + // 生成新增 SQL + if (behavior == PersistenceBehavior.Appended) + { + return jobDetail.ConvertToInsertSQL(tableName, naming); + } + // 生成更新 SQL + else if (behavior == PersistenceBehavior.Updated) + { + return jobDetail.ConvertToUpdateSQL(tableName, naming); + } + // 生成删除 SQL + else if (behavior == PersistenceBehavior.Removed) + { + return jobDetail.ConvertToDeleteSQL(tableName, naming); + } + + return string.Empty; + }; +}); +``` + +- `ConvertToSQL` 委托参数说明 + - `tableName`:数据库表名称,`string` 类型 + - `columnNames`:数据库列名:`string[]` 类型,只能通过 `索引` 获取 + - `jobDetail`:作业信息 `JobDetail` 对象 + - `behavior`:持久化 `PersistenceBehavior` 类型,用于标记 `新增`,`更新` 还是 `删除` 操作 + - `naming`:命名法 `NamingConventions` 类型,包含 `CamelCase(驼峰命名法)`,`Pascal(帕斯卡命名法)` 和 `UnderScoreCase(下划线命名法)` + +:::caution 注意事项 + +如果在该自定义 `SQL` 输出方法中调用 `jobDetail.ConvertToSQL(..)` 会导致死循环。 + +::: + +### 26.1.3.8 启用作业执行日志输出 + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.3.7 +` 版本使用。 + +::: + +通常我们需要在 `IJob` 实现类中输出作业触发日志,如 `_logger.LogInformation($"{context}");` + +```cs showLineNumbers {11} +public class MyJob : IJob +{ + private readonly ILogger _logger; + public MyJob(ILogger logger) + { + _logger = logger; + } + + public Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + _logger.LogInformation($"{context}"); + return Task.CompletedTask; + } +} +``` + +**但这样的 `范式代码` 几乎每一个 `IJob` 实现类都可能输出**,所以在 `Furion 4.8.3.7+` 版本提供了更便捷的配置,无需每一个 `IJob` 编写 `_logger.LogInformation($"{context}");`。 + +配置启用如下: + +```cs showLineNumbers {3} +services.AddSchedule(options => +{ + options.JobDetail.LogEnabled = true; // 默认 false +}); +``` + +之后 `MyJob` 可以更加精简了,日志类别自动设置为 `MyJob` 类型完整限定名。 + +```cs showLineNumbers {5} +public class MyJob : IJob +{ + public Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + // 这里写业务逻辑即可,无需调用 _logger.LogInformation($"{context}"); + return Task.CompletedTask; + } +} +``` + +作业执行日志如下: + +```bash showLineNumbers {1,3,5} +info: 2022-12-14 11:56:12.3963326 +08:00 星期三 L Furion.Application.MyJob[0] #4 + [C] 5s 1ts 2022-12-14 11:56:08.361 -> 2022-12-14 11:56:13.366 +info: 2022-12-14 11:56:13.4100745 +08:00 星期三 L Furion.Application.MyJob[0] #6 + [C] 5s 2ts 2022-12-14 11:56:13.366 -> 2022-12-14 11:56:18.376 +info: 2022-12-14 11:56:18.3931380 +08:00 星期三 L Furion.Application.MyJob[0] #9 + [C] 5s 3ts 2022-12-14 11:56:18.376 -> 2022-12-14 11:56:23.360 +``` + +## 26.1.4 作业处理程序 `IJob` + +作业处理程序是作业符合触发时间执行的业务逻辑代码,通常由程序员编写,作业处理程序需实现 `IJob` 接口。 + +### 26.1.4.1 如何定义 + +```cs showLineNumbers {1,3,5} +public class MyJob : IJob +{ + public Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + // your code... + } +} +``` + +### 26.1.4.2 `JobExecutingContext` 上下文 + +`JobExecutingContext` 上下文作为 `ExecuteAsync` 方法的第一个参数,包含以下运行时信息: + +- **`JobExecutingContext` 属性列表** + - `JobId`:作业 `Id` + - `TriggerId`:当前触发器 `Id` + - `JobDetail`:作业信息 + - `Trigger`:作业触发器 + - `OccurrenceTime`:作业计划触发时间,最准确的记录时间 + - `ExecutingTime`:实际执行时间(可能存在误差) + - `RunId`:本次作业执行唯一 `Id`,`Furion 4.8.5.1+` 提供 + - `Result`:设置/读取本次作业执行结果,`Furion 4.8.7.7+` 提供 + - `ServiceProvider`:服务提供器,`Furion 4.8.7.10+` 提供 +- **`JobExecutingContext` 方法列表** + - `.ConvertToJSON(naming)`:将上下文转换成 `JSON` 字符串 + - `.ToString()`:输出为字符串 + +### 26.1.4.3 作业处理程序实例 + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.8.13 +` 版本使用。 + +::: + +默认情况下,作业处理程序会在作业触发器符合触发条件下通过 `ActivatorUtilities.CreateInstance` 动态创建,也就是**每次触发都会创建新的 `IJob` 实例**,如: + +```cs showLineNumbers +var jobHandler = ActivatorUtilities.CreateInstance(_serviceProvider, jobType); +``` + +其中 `_serviceProvider` 是**单例服务提供器**,所以 `IJob` 实现类只能通过构造函数注入 `单例服务`。如果没有范围作用域服务的需求,那么可以将 `IJob` 注册为单例服务,**这样就可以避免每次重复创建 `IJob` 实例,对性能和减少内存占用有不小优化。** 如: + +```cs showLineNumbers +services.AddSingleton(); +``` + +**如果希望能够在构造函数注入范围作用域或瞬时作用域**,可实现 `IJobFactory` 接口,如: + +```cs showLineNumbers {6,8-10,12-13} +using Furion.Schedule; +using Microsoft.Extensions.DependencyInjection; + +namespace Furion.Application; + +public class JobFactory : IJobFactory +{ + public IJob CreateJob(IServiceProvider serviceProvider, JobFactoryContext context) + { + return ActivatorUtilities.CreateInstance(serviceProvider, context.JobType) as IJob; + + // 如果通过 services.AddSingleton(); 或 serivces.AddScoped(); 或 services.AddTransient 可通过下列方式 + // return serviceProvider.GetRequiredService(context.JobType) as IJob; + } +} +``` + +之后注册 `JobFactory` 即可,如: + +```cs showLineNumbers {4} +services.AddSchedule(options => +{ + // 添加作业处理程序工厂 + options.AddJobFactory(); +}); +``` + +这样作业就可以注入范围和瞬时服务了。 + +### 26.1.4.4 依赖注入 + +实现 `IJob` 的作业处理程序类型默认注册为 `单例`,**那么只要是单例的服务,皆可以通过构造函数注入**,如:`ILogger<>`,`IConfiguration`。 + +```cs showLineNumbers {3-4,6-7} +public class MyJob : IJob +{ + private readonly ILogger _logger; + private readonly IConfiguration _configuration; + + public MyJob(ILogger logger + , IConfiguration configuration) + { + _logger = logger; + _configuration = configuration; + } + + public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + _logger.LogInformation($"{context} {_configuration["key"]}"); + await Task.CompletedTask; + } +} +``` + +- **如果是非 `单例` 的接口,如 `瞬时` 或 `范围` 服务,可通过 `IServiceScopeFactory` 创建** + +:::tip 推荐 `IJobFactory` 方式 + +`Furion 4.8.8.13+` 版本可以通过上一小节 `IJobFactory` 统一实现。**推荐**。 + +::: + +```cs showLineNumbers {5,9,18-19} +public class MyJob : IJob +{ + private readonly ILogger _logger; + private readonly IConfiguration _configuration; + private readonly IServiceScopeFactory _scopeFactory; + + public MyJob(ILogger logger + , IConfiguration configuration + , IServiceScopeFactory scopeFactory) + { + _logger = logger; + _configuration = configuration; + _schedulerFactory = scopeFactory; + } + + public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + using var serviceScope = _scopeFactory.CreateScope(); + var repository = serviceScope.ServiceProvider.GetService>(); + + _logger.LogInformation($"{context} {_configuration["key"]}"); + await Task.CompletedTask; + } +} +``` + +- **针对高频定时任务,比如每秒执行一次,或者更频繁的任务** + +:::tip 推荐 `IJobFactory` 方式 + +`Furion 4.8.8.13+` 版本可以通过上一小节 `IJobFactory` 统一实现。**推荐**。 + +::: + +为了避免频繁创建作用域和销毁作用域,可创建长范围的作用域。 + +```cs showLineNumbers {1,5,13,18,25,27} +public class MyJob : IJob, IDisposable +{ + private readonly ILogger _logger; + private readonly IConfiguration _configuration; + private readonly IServiceScope _serviceScope; + + public MyJob(ILogger logger + , IConfiguration configuration + , IServiceScopeFactory scopeFactory) + { + _logger = logger; + _configuration = configuration; + _serviceScope = scopeFactory.CreateScope(); + } + + public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + var repository = _serviceScope.ServiceProvider.GetService>(); + var user = await repository.GetAsync(1); + + _logger.LogInformation($"{context} {_configuration["key"]}"); + await Task.CompletedTask; + } + + public void Dispose() + { + _serviceScope?.Dispose(); + } +} +``` + +### 26.1.4.5 动态作业 `DynamicJob` + +框架提供了便捷的动态作业 `DynamicJob` 类型,可通过 `Func` 委托传入,无需创建 `IJob` 实现类型。 + +框架还为 `JobExecutionContext` 属性 `ServiceProvder` 提供了 `.GetLogger()` 拓展方法,方便快速获取 `ILogger` 日志对象实例。 + +```cs showLineNumbers {2,9,18,26} +// 通过 JobBuilder 创建 +var jobBuilder = JobBuilder.Create((context, stoppingToken) => +{ + context.ServiceProvider.GetLogger().LogInformation($"{context}"); + return Task.CompletedTask; +}); + +// 通过 jobBuilder 方法 SetDynamicExecuteAsync 创建 +jobBuilder.SetDynamicExecuteAsync((context, stoppingToken) => +{ + context.ServiceProvider.GetLogger().LogInformation($"{context}"); + return Task.CompletedTask; +}); + +// 通过 AddJob 创建 +service.AddSchedule(options => +{ + options.AddJob((context, stoppingToken) => + { + context.ServiceProvider.GetLogger().LogInformation($"{context}"); + return Task.CompletedTask; + }, Triggers.PeriodSeconds(5)); +}); + +// 通过 ISchedulerFactory 创建 +_schedulerFactory.AddJob((context, stoppingToken) => +{ + context.ServiceProvider.GetLogger().LogInformation($"{context}"); + return Task.CompletedTask; +}, Triggers.PeriodSeconds(5)); +``` + +动态作业执行结果: + +```bash showLineNumbers {6,8,10,11,13} +info: 2022-12-04 12:26:18.6562296 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is running. +info: 2022-12-04 12:26:18.6618404 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is preloading... +info: 2022-12-04 12:26:18.8727764 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended to the schedule. +info: 2022-12-04 12:26:18.8745765 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + The scheduler of successfully appended to the schedule. +warn: 2022-12-04 12:26:18.9013540 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + Schedule hosted service preload completed, and a total of <1> schedulers are appended. +info: 2022-12-04 12:26:23.8753926 +08:00 星期日 L System.Logging.DynamicJob[0] #6 + [C] 5s 1ts 2022-12-04 12:26:23.837 -> 2022-12-04 12:26:28.835 +info: 2022-12-04 12:26:28.8686474 +08:00 星期日 L System.Logging.DynamicJob[0] #6 + [C] 5s 2ts 2022-12-04 12:26:28.835 -> 2022-12-04 12:26:33.823 +info: 2022-12-04 12:26:33.8531796 +08:00 星期日 L System.Logging.DynamicJob[0] #13 + [C] 5s 3ts 2022-12-04 12:26:33.823 -> 2022-12-04 12:26:38.820 +``` + +:::tip 动态作业和普通作业的区别 + +- 动态作业处理程序类型是:`DynamicJob` 类型 +- 动态作业提供的 `.GetLogger()` 拓展输出日志类别是:`System.Logging.DynamicJob` +- 如果普通作业同时设置了 `SetJobType` 和 `SetDynamicExecuteAsync`,那么优先作为动态作业执行。 +- **动态作业无法将 `Func<..>` 进行序列化持久化存储** + +::: + +### 26.1.4.6 使用 `Roslyn` 动态创建 + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.8.7 +` 版本使用。 + +::: + +按照程序开发的正常思维,理应先在代码中创建作业处理程序类型,但我们可以借助 `Roslyn` 动态编译 `C#` 代码。 + +1. **根据字符串创建 `IJob` 类型** + +```cs showLineNumbers {2,9,11,29} +// 调用 Schedular 静态类提供的 CompileCSharpClassCode 方法 +var jobAssembly = Schedular.CompileCSharpClassCode(@" +using Furion.Schedule; +using Microsoft.Extensions.Logging; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace YourProject; + +public class MyJob : IJob +{ + private readonly ILogger _logger; + + public MyJob(ILogger logger) + { + _logger = logger; + } + + public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + _logger.LogInformation($""我是 Roslyn 方式创建的:{context}""); + await Task.CompletedTask; + } +} +"); + +// 生成运行时 MyJob 类型 +var jobType = jobAssembly.GetType("YourProject.MyJob"); +``` + +2. **注册作业** + +```cs showLineNumbers {4,9} +// 可以在启动的时候添加 +services.AddSchedule(options => +{ + options.AddJob(jobType + , Triggers.PeriodSeconds(5)); +}); + +// 也可以完全在运行时添加(常用) +_schedulerFactory.AddJob(jobType + , Triggers.PeriodSeconds(5)); +``` + +查看作业执行日志: + +```bash showLineNumbers {6,8,10,12,14,16} +info: 2022-12-04 12:38:00.6249410 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is running. +info: 2022-12-04 12:38:00.6294089 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is preloading... +info: 2022-12-04 12:38:00.7496005 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended to the schedule. +info: 2022-12-04 12:38:00.7514579 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + The scheduler of successfully appended to the schedule. +warn: 2022-12-04 12:38:00.7836777 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + Schedule hosted service preload completed, and a total of <1> schedulers are appended. +info: 2022-12-04 12:38:05.7389682 +08:00 星期日 L YourProject.MyJob[0] #6 + 我是 Roslyn 方式创建的: [C] 5s 1ts 2022-12-04 12:38:05.713 -> 2022-12-04 12:38:10.692 +info: 2022-12-04 12:38:10.7108416 +08:00 星期日 L YourProject.MyJob[0] #11 + 我是 Roslyn 方式创建的: [C] 5s 2ts 2022-12-04 12:38:10.692 -> 2022-12-04 12:38:15.673 +info: 2022-12-04 12:38:15.6925578 +08:00 星期日 L YourProject.MyJob[0] #11 + 我是 Roslyn 方式创建的: [C] 5s 3ts 2022-12-04 12:38:15.673 -> 2022-12-04 12:38:20.656 +``` + +**惊不惊喜,意外意外~**。 + +:::tip 小知识 + +通过 `Roslyn` 的方式支持创建 `IJob`,`JobDetail`,`Trigger`,`Scheduler` 哦,自行测试。😊 + +::: + +### 26.1.4.7 作业执行异常处理 + +正常情况下,程序员应该保证作业执行程序总是稳定运行,但有时候会出现一些不可避免的意外导致出现异常,如网络异常等。 + +下面给出模拟出现异常和常见的处理方式例子: + +```cs showLineNumbers {3} +services.AddSchedule(options => +{ + options.AddJob(Triggers.PeriodSeconds(3)); +}); +``` + +```cs showLineNumbers {13-16} +public class MyJob : IJob +{ + private readonly ILogger _logger; + public MyJob(ILogger logger) + { + _logger = logger; + } + + public Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + _logger.LogInformation($"{context}"); + + // 模拟异常 + var num = 10; + var n = 0; + var c = num / n; + + return Task.CompletedTask; + } +} +``` + +输出日志如下: + +```bash showLineNumbers {11-13,23-25} +info: 2023-04-22 22:18:04.2149071 +08:00 星期六 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is running. +info: 2023-04-22 22:18:04.2189082 +08:00 星期六 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is preloading... +info: 2023-04-22 22:18:04.3216571 +08:00 星期六 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended to the schedule. +info: 2023-04-22 22:18:04.3230110 +08:00 星期六 L System.Logging.ScheduleService[0] #1 + The scheduler of successfully appended to the schedule. +warn: 2023-04-22 22:18:04.3521056 +08:00 星期六 L System.Logging.ScheduleService[0] #1 + Schedule hosted service preload completed, and a total of <1> schedulers are appended. +info: 2023-04-22 22:18:07.3782666 +08:00 星期六 L MyJob[0] #17 + [C] 3s 1ts 2023-04-22 22:18:07.288 -> 2023-04-22 22:18:10.308 +fail: 2023-04-22 22:18:07.6652239 +08:00 星期六 L System.Logging.ScheduleService[0] #17 + Error occurred executing [C] 3s 1ts 2023-04-22 22:18:07.288 -> 2023-04-22 22:18:10.308. + ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + System.DivideByZeroException: Attempted to divide by zero. + at MyJob.ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) in C:\Users\MonkSoul\source\repos\ConsoleApp3\Program.cs:line 29 + at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass24_3.<b__3>d.MoveNext() in C:\Workplaces\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 233 + --- End of stack trace from previous location --- + at Furion.FriendlyException.Retry.InvokeAsync(Func`1 action, Int32 numRetries, Int32 retryTimeout, Boolean finalThrow, Type[] exceptionTypes, Func`2 fallbackPolicy, Action`2 retryAction) in C:\Workplaces\Furion\framework\Furion\FriendlyException\Retry.cs:line 79 + at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass24_2.<b__2>d.MoveNext() in C:\Workplaces\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 231 + ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +info: 2023-04-22 22:18:10.3507729 +08:00 星期六 L MyJob[0] #8 + [C] 3s 2ts 2023-04-22 22:18:10.308 -> 2023-04-22 22:18:13.318 +fail: 2023-04-22 22:18:10.4292529 +08:00 星期六 L System.Logging.ScheduleService[0] #8 + Error occurred executing [C] 3s 2ts 2023-04-22 22:18:10.308 -> 2023-04-22 22:18:13.318. + ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + System.DivideByZeroException: Attempted to divide by zero. + at MyJob.ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) in C:\Users\MonkSoul\source\repos\ConsoleApp3\Program.cs:line 29 + at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass24_3.<b__3>d.MoveNext() in C:\Workplaces\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 233 + --- End of stack trace from previous location --- + at Furion.FriendlyException.Retry.InvokeAsync(Func`1 action, Int32 numRetries, Int32 retryTimeout, Boolean finalThrow, Type[] exceptionTypes, Func`2 fallbackPolicy, Action`2 retryAction) in C:\Workplaces\Furion\framework\Furion\FriendlyException\Retry.cs:line 79 + at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass24_2.<b__2>d.MoveNext() in C:\Workplaces\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 231 + ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +``` + +从上面执行日志可以看出,作业执行出现异常只会影响当前触发时间的执行,但不会影响下一次执行。出现这种情况通常配置 **重试策略** 确保每次作业处理程序可能执行成功,如重试 `3` 次,如: + +```cs showLineNumbers {4} +services.AddSchedule(options => +{ + options.AddJob(Triggers.PeriodSeconds(3) + .SetNumRetries(3)); // 重试三次 +}); +``` + +输出日志如下: + +```bash showLineNumbers {11-23} +info: 2023-04-22 22:25:00.7244392 +08:00 星期六 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is running. +info: 2023-04-22 22:25:00.7293195 +08:00 星期六 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is preloading... +info: 2023-04-22 22:25:00.8796238 +08:00 星期六 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended to the schedule. +info: 2023-04-22 22:25:00.8852651 +08:00 星期六 L System.Logging.ScheduleService[0] #1 + The scheduler of successfully appended to the schedule. +warn: 2023-04-22 22:25:00.9348100 +08:00 星期六 L System.Logging.ScheduleService[0] #1 + Schedule hosted service preload completed, and a total of <1> schedulers are appended. +info: 2023-04-22 22:25:03.9357047 +08:00 星期六 L MyJob[0] #12 + [C] 3s 1ts 2023-04-22 22:25:03.840 -> 2023-04-22 22:25:06.888 +warn: 2023-04-22 22:25:04.0147234 +08:00 星期六 L System.Logging.ScheduleService[0] #12 + Retrying 1/3 times for [C] 3s 1ts 2023-04-22 22:25:03.840 -> 2023-04-22 22:25:06.888 +info: 2023-04-22 22:25:05.0243650 +08:00 星期六 L MyJob[0] #12 + [C] 3s 1ts 2023-04-22 22:25:03.840 -> 2023-04-22 22:25:06.888 +warn: 2023-04-22 22:25:05.0963359 +08:00 星期六 L System.Logging.ScheduleService[0] #12 + Retrying 2/3 times for [C] 3s 1ts 2023-04-22 22:25:03.840 -> 2023-04-22 22:25:06.888 +info: 2023-04-22 22:25:06.1100662 +08:00 星期六 L MyJob[0] #12 + [C] 3s 1ts 2023-04-22 22:25:03.840 -> 2023-04-22 22:25:06.888 +warn: 2023-04-22 22:25:06.1785087 +08:00 星期六 L System.Logging.ScheduleService[0] #12 + Retrying 3/3 times for [C] 3s 1ts 2023-04-22 22:25:03.840 -> 2023-04-22 22:25:06.888 +fail: 2023-04-22 22:25:07.3754596 +08:00 星期六 L System.Logging.ScheduleService[0] #16 + Error occurred executing [C] 3s 2ts 2023-04-22 22:25:03.840 -> 2023-04-22 22:25:09.884. + ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + System.DivideByZeroException: Attempted to divide by zero. + at MyJob.ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) in C:\Users\MonkSoul\source\repos\ConsoleApp3\Program.cs:line 30 + at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass24_3.<b__3>d.MoveNext() in C:\Workplaces\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 233 + --- End of stack trace from previous location --- + at Furion.FriendlyException.Retry.InvokeAsync(Func`1 action, Int32 numRetries, Int32 retryTimeout, Boolean finalThrow, Type[] exceptionTypes, Func`2 fallbackPolicy, Action`2 retryAction) in C:\Workplaces\Furion\framework\Furion\FriendlyException\Retry.cs:line 91 + at Furion.FriendlyException.Retry.InvokeAsync(Func`1 action, Int32 numRetries, Int32 retryTimeout, Boolean finalThrow, Type[] exceptionTypes, Func`2 fallbackPolicy, Action`2 retryAction) in C:\Workplaces\Furion\framework\Furion\FriendlyException\Retry.cs:line 102 + at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass24_2.<b__2>d.MoveNext() in C:\Workplaces\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 231 + ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +``` + +:::tip 全局配置**重试策略** + +推荐使用 [【26.1.9 作业执行器 `IJobExecutor`】](./job#2619-作业执行器-ijobexecutor) 配置全局异常重试策略。 + +::: + +### 26.1.4.8 作业执行异常回退策略 + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.8.6 +` 版本使用。 + +::: + +作业处理程序执行异常除了配置 `重试次数` 或配置 `全局异常重试策略` 以外,还可以实现 `IJob.FallbackAsync` 进行回退配置。 + +```cs showLineNumbers {13-17,22,24} +public class TestJob : IJob +{ + private readonly ILogger _logger; + public TestJob(ILogger logger) + { + _logger = logger; + } + + public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + _logger.LogWarning($"{context}"); + + // 模拟运行第三次出异常 + if (context.Trigger.NumberOfRuns == 3) + { + throw new Exception("假装出错"); + } + + await Task.CompletedTask; + } + + public Task FallbackAsync(JobExecutedContext context, CancellationToken stoppingToken) + { + Console.WriteLine("调用了回退"); + return Task.CompletedTask; + } +} +``` + +输出日志如下: + +```bash showLineNumbers {22,30-31} +info: 2023-04-25 17:19:06.5448438 +08:00 星期二 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is running. +info: 2023-04-25 17:19:06.5523770 +08:00 星期二 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is preloading... +info: 2023-04-25 17:19:07.1156318 +08:00 星期二 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended to the schedule. +info: 2023-04-25 17:19:07.1293994 +08:00 星期二 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended to the schedule. +info: 2023-04-25 17:19:07.1360332 +08:00 星期二 L System.Logging.ScheduleService[0] #1 + The scheduler of successfully appended to the schedule. +warn: 2023-04-25 17:19:07.1614880 +08:00 星期二 L System.Logging.ScheduleService[0] #1 + Schedule hosted service preload completed, and a total of <1> schedulers are appended. +warn: 2023-04-25 17:19:11.1565118 +08:00 星期二 L Furion.Application.TestJob[0] #9 + [C] 4s 1ts 2023-04-25 17:19:11.067 -> 2023-04-25 17:19:15.092 +warn: 2023-04-25 17:19:15.1275434 +08:00 星期二 L Furion.Application.TestJob[0] #18 + [C] 4s 2ts 2023-04-25 17:19:15.092 -> 2023-04-25 17:19:19.094 +warn: 2023-04-25 17:19:19.1006636 +08:00 星期二 L Furion.Application.TestJob[0] #17 + [C] 4s 3ts 2023-04-25 17:19:19.094 -> 2023-04-25 17:19:23.067 +fail: 2023-04-25 17:19:19.2554424 +08:00 星期二 L System.Logging.ScheduleService[0] #17 + Error occurred executing in [C] 4s 3ts 2023-04-25 17:19:19.094 -> 2023-04-25 17:19:23.067. + ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + System.Exception: 假装出错 + at Furion.Application.TestJob.ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) in D:\Workplaces\OpenSources\Furion\samples\Furion.Application\TestJob.cs:line 22 + at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass24_3.<b__3>d.MoveNext() in D:\Workplaces\OpenSources\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 233 + --- End of stack trace from previous location --- + at Furion.FriendlyException.Retry.InvokeAsync(Func`1 action, Int32 numRetries, Int32 retryTimeout, Boolean finalThrow, Type[] exceptionTypes, Func`2 fallbackPolicy, Action`2 retryAction) in D:\Workplaces\OpenSources\Furion\framework\Furion\FriendlyException\Retry.cs:line 79 + at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass24_2.<b__2>d.MoveNext() in D:\Workplaces\OpenSources\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 231 + ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +info: 2023-04-25 17:19:19.2589045 +08:00 星期二 L System.Logging.ScheduleService[0] #17 + Fallback called in [C] 4s 3ts 2023-04-25 17:19:19.094 -> 2023-04-25 17:19:23.067. +调用了回退 +warn: 2023-04-25 17:19:23.0840895 +08:00 星期二 L Furion.Application.TestJob[0] #14 + [C] 4s 4ts 2023-04-25 17:19:23.067 -> 2023-04-25 17:19:27.050 +``` + +如果 `FallbackAsync` 发生二次异常,如: + +```cs showLineNumbers {1,5} +public Task FallbackAsync(JobExecutedContext context, CancellationToken stoppingToken) +{ + Console.WriteLine("调用了回退"); + + throw new Exception("回退了我还是出异常~"); + return Task.CompletedTask; +} +``` + +输出日志将合并前面所有异常并输出: + +```bash showLineNumbers {30-31,33,37,45} +info: 2023-04-25 17:24:46.0348224 +08:00 星期二 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is running. +info: 2023-04-25 17:24:46.0392736 +08:00 星期二 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is preloading... +info: 2023-04-25 17:24:46.4677115 +08:00 星期二 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended to the schedule. +info: 2023-04-25 17:24:46.4847108 +08:00 星期二 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended to the schedule. +info: 2023-04-25 17:24:46.4936590 +08:00 星期二 L System.Logging.ScheduleService[0] #1 + The scheduler of successfully appended to the schedule. +warn: 2023-04-25 17:24:46.6097957 +08:00 星期二 L System.Logging.ScheduleService[0] #1 + Schedule hosted service preload completed, and a total of <1> schedulers are appended. +warn: 2023-04-25 17:24:50.4988840 +08:00 星期二 L Furion.Application.TestJob[0] #17 + [C] 4s 1ts 2023-04-25 17:24:50.419 -> 2023-04-25 17:24:54.436 +warn: 2023-04-25 17:24:54.4704187 +08:00 星期二 L Furion.Application.TestJob[0] #15 + [C] 4s 2ts 2023-04-25 17:24:54.436 -> 2023-04-25 17:24:58.436 +warn: 2023-04-25 17:24:58.4441477 +08:00 星期二 L Furion.Application.TestJob[0] #15 + [C] 4s 3ts 2023-04-25 17:24:58.436 -> 2023-04-25 17:25:02.411 +fail: 2023-04-25 17:24:58.5704807 +08:00 星期二 L System.Logging.ScheduleService[0] #15 + Error occurred executing in [C] 4s 3ts 2023-04-25 17:24:58.436 -> 2023-04-25 17:25:02.411. + ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + System.Exception: 假装出错 + at Furion.Application.TestJob.ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) in D:\Workplaces\OpenSources\Furion\samples\Furion.Application\TestJob.cs:line 22 + at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass24_3.<b__3>d.MoveNext() in D:\Workplaces\OpenSources\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 233 + --- End of stack trace from previous location --- + at Furion.FriendlyException.Retry.InvokeAsync(Func`1 action, Int32 numRetries, Int32 retryTimeout, Boolean finalThrow, Type[] exceptionTypes, Func`2 fallbackPolicy, Action`2 retryAction) in D:\Workplaces\OpenSources\Furion\framework\Furion\FriendlyException\Retry.cs:line 79 + at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass24_2.<b__2>d.MoveNext() in D:\Workplaces\OpenSources\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 231 + ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +info: 2023-04-25 17:24:58.5737508 +08:00 星期二 L System.Logging.ScheduleService[0] #15 + Fallback called in [C] 4s 3ts 2023-04-25 17:24:58.436 -> 2023-04-25 17:25:02.411. +调用了回退 +fail: 2023-04-25 17:24:58.5929688 +08:00 星期二 L System.Logging.ScheduleService[0] #15 + Fallback called error in [C] 4s 3ts 2023-04-25 17:24:58.436 -> 2023-04-25 17:25:02.411. + ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + System.AggregateException: One or more errors occurred. (Error occurred executing in [C] 4s 3ts 2023-04-25 17:24:58.436 -> 2023-04-25 17:25:02.411.) (回退了我还是出异常~) + ---> System.InvalidOperationException: Error occurred executing in [C] 4s 3ts 2023-04-25 17:24:58.436 -> 2023-04-25 17:25:02.411. + ---> System.Exception: 假装出错 + at Furion.Application.TestJob.ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) in D:\Workplaces\OpenSources\Furion\samples\Furion.Application\TestJob.cs:line 22 + at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass24_3.<b__3>d.MoveNext() in D:\Workplaces\OpenSources\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 233 + --- End of stack trace from previous location --- + at Furion.FriendlyException.Retry.InvokeAsync(Func`1 action, Int32 numRetries, Int32 retryTimeout, Boolean finalThrow, Type[] exceptionTypes, Func`2 fallbackPolicy, Action`2 retryAction) in D:\Workplaces\OpenSources\Furion\framework\Furion\FriendlyException\Retry.cs:line 79 + at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass24_2.<b__2>d.MoveNext() in D:\Workplaces\OpenSources\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 231 + --- End of inner exception stack trace --- + --- End of inner exception stack trace --- + ---> (Inner Exception #1) System.Exception: 回退了我还是出异常~ + at Furion.Application.TestJob.FallbackAsync(JobExecutedContext context, CancellationToken stoppingToken) in D:\Workplaces\OpenSources\Furion\samples\Furion.Application\TestJob.cs:line 32 + at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass24_2.<b__2>d.MoveNext() in D:\Workplaces\OpenSources\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 309<--- + ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +warn: 2023-04-25 17:25:02.4212180 +08:00 星期二 L Furion.Application.TestJob[0] #15 + [C] 4s 4ts 2023-04-25 17:25:02.411 -> 2023-04-25 17:25:06.388 +``` + +### 26.1.4.9 作业调度器被取消处理 + +一般情况下,作业调度器意外关闭或手动关闭,但作业处理程序异步操作还未处理完成,这个时候我们可以选择取消还是继续执行,如果选择取消: + +```cs showLineNumbers {15,19,23,31} +public class MyJob : IJob +{ + private readonly ILogger _logger; + public MyJob(ILogger logger) + { + _logger = logger; + } + + public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + _logger.LogInformation($"{context}"); + + try + { + await SomeMethodAsync(stoppingToken); + } + catch (TaskCanceledException) + { + _logger.LogWarning("作业被取消了~"); + } + catch (AggregateException ex) when (ex.InnerExceptions.Count == 1 && ex.InnerExceptions[0] is TaskCanceledException) + { + _logger.LogWarning("作业被取消了~"); + } + catch {} + } + + private async Task SomeMethodAsync(CancellationToken stoppingToken) + { + // 模拟耗时 + await Task.Delay(1000 * 60 * 60, stoppingToken); + } +} +``` + +这样当作业调度器被关闭时,`SomeMethodAsync` 如果未处理完成也会取消操作。 + +```bash showLineNumbers {14,16,18,20} +info: 2022-12-04 12:49:00.2636929 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is running. +info: 2022-12-04 12:49:00.2686096 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is preloading... +info: 2022-12-04 12:49:00.4252737 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended to the schedule. +info: 2022-12-04 12:49:00.4266075 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + The scheduler of successfully appended to the schedule. +warn: 2022-12-04 12:49:00.4468654 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + Schedule hosted service preload completed, and a total of <1> schedulers are appended. +info: 2022-12-04 12:49:05.4397629 +08:00 星期日 L MyJob[0] #4 + [C] 5s 1ts 2022-12-04 12:49:05.390 -> 2022-12-04 12:49:10.393 +info: 2022-12-04 12:49:08.6301592 +08:00 星期日 L Microsoft.Hosting.Lifetime[0] #14 + Application is shutting down... +warn: 2022-12-04 12:49:08.7247004 +08:00 星期日 L MyJob[0] #6 + 作业被取消了~ +warn: 2022-12-04 12:49:10.4257861 +08:00 星期日 L System.Logging.ScheduleService[0] #6 + Schedule hosted service cancels hibernation and GC.Collect(). +crit: 2022-12-04 12:49:10.4360088 +08:00 星期日 L System.Logging.ScheduleService[0] #6 + Schedule hosted service is stopped. +``` + +### 26.1.4.10 `HTTP` 请求作业 + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.7.7 +` 版本使用。 + +::: + +`HTTP` 请求作业通常用于定时请求/访问互联网地址。 + +```cs showLineNumbers {3} +services.AddSchedule(options => +{ + options.AddHttpJob(request => + { + request.RequestUri = "https://www.chinadot.net"; + request.HttpMethod = HttpMethod.Get; + // request.Body = "{}"; // 设置请求报文体 + // request.Headers.Add("framework", "Furion"); // Furion 4.8.8.46+ 支持 + // request.GroupName = "group"; // Furion 4.8.8.46+ 支持 + // request.Description = "作业请求描述"; // Furion 4.8.8.46+ 支持 + }, Triggers.PeriodSeconds(5)); +}); +``` + +作业执行日志如下: + +```bash showLineNumbers {13-26} +info: 2023-03-11 11:05:36.3616747 +08:00 星期六 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is running. +info: 2023-03-11 11:05:36.3652411 +08:00 星期六 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is preloading... +info: 2023-03-11 11:05:36.5172940 +08:00 星期六 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended to the schedule. +info: 2023-03-11 11:05:36.5189296 +08:00 星期六 L System.Logging.ScheduleService[0] #1 + The scheduler of successfully appended to the schedule. +warn: 2023-03-11 11:05:36.5347816 +08:00 星期六 L System.Logging.ScheduleService[0] #1 + Schedule hosted service preload completed, and a total of <1> schedulers are appended. +warn: 2023-03-11 11:05:41.5228138 +08:00 星期六 L System.Logging.ScheduleService[0] #15 + Schedule hosted service will sleep <4970> milliseconds and be waked up at <2023-03-11 11:05:46.486>. +info: 2023-03-11 11:05:41.5542865 +08:00 星期六 L System.Net.Http.HttpClient.HttpJob.LogicalHandler[100] #9 + Start processing HTTP request GET https://www.chinadot.net/ +info: 2023-03-11 11:05:41.5589056 +08:00 星期六 L System.Net.Http.HttpClient.HttpJob.ClientHandler[100] #9 + Sending HTTP request GET https://www.chinadot.net/ +info: 2023-03-11 11:05:44.1305461 +08:00 星期六 L System.Net.Http.HttpClient.HttpJob.ClientHandler[101] #8 + Received HTTP response headers after 2566.7836ms - 200 +info: 2023-03-11 11:05:44.1343977 +08:00 星期六 L System.Net.Http.HttpClient.HttpJob.LogicalHandler[101] #8 + End processing HTTP request after 2584.2327ms - 200 +info: 2023-03-11 11:05:48.6475959 +08:00 星期六 L System.Logging.ScheduleService[0] #4 + Received HTTP response body with a length of <63639> output as follows - 200 + + dotNET China | 让 .NET 开发更简单,更通用,更流行 + ...... + +``` + +--- + +**❤️ 如何自定义 `HTTP` 作业** + +默认情况下,`Furion` 框架提供有限的 `HTTP` 配置参数,如果不能满足可自行定义。 + +1. 自定义 `Http` 参数类:`MyHttpJobMessage` + +```cs showLineNumbers {6} +namespace YourProject.Core; + +/// +/// HTTP 作业消息 +/// +public class MyHttpJobMessage +{ + /// + /// 请求地址 + /// + public string RequestUri { get; set; } + + /// + /// 请求方法 + /// + public HttpMethod HttpMethod { get; set; } = HttpMethod.Get; + + /// + /// 请求报文体 + /// + public string Body { get; set; } +} +``` + +2. 自定义 `Http` 作业处理程序:`MyHttpJob` + +```cs showLineNumbers {4,9,39,48,66-67,70,73,79} +/// +/// HTTP 请求作业处理程序 +/// +public class MyHttpJob : IJob // 也可以继承内部的 HttpJob 类 +{ + /// + /// 创建工厂 + /// + private readonly IHttpClientFactory _httpClientFactory; + + /// + /// 日志服务 + /// + private readonly ILogger _logger; + + /// + /// 构造函数 + /// + /// 创建工厂 + /// 日志服务 + public MyHttpJob(IHttpClientFactory httpClientFactory + , ILogger logger) + { + _httpClientFactory = httpClientFactory; + _logger = logger; + } + + /// + /// 具体处理逻辑 + /// + /// 作业执行前上下文 + /// 取消任务 Token + /// + public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + var jobDetail = context.JobDetail; + + // 解析 HTTP 请求参数,键名称为类名 + var httpJobMessage = Penetrates.Deserialize(jobDetail.GetProperty(nameof(MyHttpJob))); + + // 空检查 + if (httpJobMessage == null || string.IsNullOrWhiteSpace(httpJobMessage.RequestUri)) + { + return; + } + + // 创建请求客户端 + using var httpClient = _httpClientFactory.CreateClient(); // CreateClient 可以传入一个字符串进行全局配置 Client + + // 添加请求报文头 User-Agent + httpClient.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.81 Safari/537.36 Edg/104.0.1293.47"); + + // 创建请求对象 + var httpRequestMessage = new HttpRequestMessage(httpJobMessage.HttpMethod, httpJobMessage.RequestUri); + + // 添加请求报文体,默认只支持发送 application/json 类型 + if (httpJobMessage.HttpMethod != HttpMethod.Get + && httpJobMessage.HttpMethod != HttpMethod.Head + && !string.IsNullOrWhiteSpace(httpJobMessage.Body)) + { + var stringContent = new StringContent(httpJobMessage.Body, Encoding.UTF8); + stringContent.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + httpRequestMessage.Content = stringContent; + } + + // 更多自定义参数======================== + // Your Code .... + + // 发送请求并确保成功 + var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage, stoppingToken); + + // 解析返回值 + var bodyString = await httpResponseMessage.Content.ReadAsStringAsync(stoppingToken); + + // 输出日志 + _logger.LogInformation($"Received HTTP response body with a length of <{bodyString.Length}> output as follows - {(int)httpResponseMessage.StatusCode}{Environment.NewLine}{bodyString}"); + + // 设置本次执行结果 + context.Result = Penetrates.Serialize(new + { + httpResponseMessage.StatusCode, + Body = bodyString + }); + } +} +``` + +3. 注册自定义 `Http` 作业 + +```cs showLineNumbers {4-7} +services.AddSchedule(options => +{ + // 创建 HTTP 作业消息 + var httpJobMessage = new YourHttpJobMessage(); + var jobBuilder = JobBuilder.Create() + // 添加作业附加信息 + .AddProperty(nameof(MyHttpJob), Schedular.Serialize(httpJobMessage)); + // 添加作业 + options.AddJob(jobBuilder, Triggers.PeriodSeconds(5)); +}); +``` + +### 26.1.4.11 设置本次执行结果 + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.7.7 +` 版本使用。 + +::: + +有时候我们希望能够记录本次作业触发器触发返回结果,可通过 `context.Result` 进行设置。 + +也可以通过该值来判断作业是否成功执行,如设置了 `Result` 值但实际发现 `trigger.Result` 为 `null`,那么也就是本次执行未成功。 + +```cs showLineNumbers{13} +public class MyJob : IJob +{ + private readonly ILogger _logger; + public MyJob(ILogger logger) + { + _logger = logger; + } + + public Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + _logger.LogInformation($"{context}"); + + context.Result = "设置本次执行的值"; + return Task.CompletedTask; + } +} +``` + +:::note `Result` 和 `Properties` + +除了通过 `context.Result` 设置作业本次执行结果以外,还可以通过 `jobDetail.AddOrUpdateProperty(key, value)` 的方式设置。区别在于前者会将值同步到 `Trigger` 的 `Result` 中,后者会将值同步在 `JobDetail` 的 `Properties` 中。 + +::: + +## 26.1.5 作业触发器 `Trigger` 及构建器 + +### 26.1.5.1 关于作业触发器 + +框架提供了 `Trigger` 类型来描述作业具体的触发时间,`Trigger` 类型提供以下**只读属性**: + +| 属性名 | 属性类型 | 默认值 | 说明 | +| ------------------- | --------------- | ------- | -------------------------------------------------------------------------- | +| `TriggerId` | `string` | | 作业触发器 `Id` | +| `JobId` | `string` | | 作业 `Id` | +| `TriggerType` | `string` | | 作业触发器类型,存储的是类型的 `FullName` | +| `AssemblyName` | `string` | | 作业触发器类型所在程序集,存储的是程序集 `Name` | +| `Args` | `string` | | 作业触发器初始化参数,运行时将反序列化为 `object[]` 类型并作为构造函数参数 | +| `Description` | `string` | | 描述信息 | +| `Status` | `TriggerStatus` | `Ready` | 作业触发器状态 | +| `StartTime` | `DateTime?` | | 起始时间 | +| `EndTime` | `DateTime?` | | 结束时间 | +| `LastRunTime` | `DateTime?` | | 最近运行时间 | +| `NextRunTime` | `DateTime?` | | 下一次运行时间 | +| `NumberOfRuns` | `long` | `0` | 触发次数 | +| `MaxNumberOfRuns` | `long` | `0` | 最大触发次数,`0`:不限制,`n`:N 次 | +| `NumberOfErrors` | `long` | `0` | 出错次数 | +| `MaxNumberOfErrors` | `long` | `0` | 最大出错次数,`0`:不限制,`n`:N 次 | +| `NumRetries` | `int` | `0` | 重试次数 | +| `RetryTimeout` | `int` | `1000` | 重试间隔时间,毫秒单位 | +| `StartNow` | `bool` | `true` | 是否立即启动 | +| `RunOnStart` | `bool` | `false` | 是否启动时执行一次 | +| `ResetOnlyOnce` | `bool` | `true` | 是否在启动时重置最大触发次数等于一次的作业 | +| `Result` | `string` | | 本次执行返回结果,`Furion 4.8.7.7+` | +| `ElapsedTime` | `long` | `0` | 本次执行耗时,单位 `ms`,`Furion 4.8.7.7+` | +| `UpdatedTime` | `DateTime?` | | 作业触发器更新时间 | + +### 26.1.5.2 作业触发器状态 + +作业触发器状态指示了当前作业触发器的状态,使用 `TriggerStatus` 枚举类型(`uint`),该类型包含以下枚举成员。 + +| 枚举名 | 枚举值 | 说明 | +| -------------- | ------ | ------------------------------------------------------------ | +| `Backlog` | `0` | 积压,起始时间大于当前时间 | +| `Ready` | `1` | 就绪 | +| `Running` | `2` | 正在运行 | +| `Pause` | `3` | 暂停 | +| `Blocked` | `4` | 阻塞,本该执行但是没有执行 | +| `ErrorToReady` | `5` | 由失败进入就绪,运行错误当并未超出最大错误数,进入下一轮就绪 | +| `Archived` | `6` | 归档,结束时间小于当前时间 | +| `Panic` | `7` | 崩溃,错误次数超出了最大错误数 | +| `Overrun` | `8` | 超限,运行次数超出了最大限制 | +| `Unoccupied` | `9` | 无触发时间,下一次执行时间为 `null` | +| `NotStart` | `10` | 初始化时未启动 | +| `Unknown` | `11` | 未知作业触发器,作业触发器运行时类型为 `null` | +| `Unhandled` | `12` | 未知作业处理程序,作业处理程序类型运行时类型为 `null` | + +### 26.1.5.3 关于作业触发器构建器 + +作业触发器 `Trigger` 是作业调度模块提供运行时的**只读类型**,那么我们该如何创建或变更 `Trigger` 对象呢? + +`TriggerBuilder` 是作业调度模块提供可用来生成运行时 `Trigger` 的类型,这样做的好处可避免外部直接修改运行时 `Trigger` 数据,还能实现任何修改动作监听,也能避免多线程抢占情况。 + +作业调度模块提供了多种方式用来创建 `TriggerBuilder` 对象。 + +1. **通过 `Create` 静态方法创建** + +```cs showLineNumbers {2,5,8,11,14,17,20} +// 根据作业触发器 Id 创建 +var triggerBuilder = TriggerBuilder.Create("trigger1"); + +// 根据 Trigger 派生类类型创建 +var triggerBuilder = TriggerBuilder.Create(); + +// 根据 Trigger 派生类类型 + 构造函数参数创建 +var triggerBuilder = TriggerBuilder.Create("* * * * *", CronStringFormat.Default); + +// 根据程序集名称和类型完全限定名(FullName)创建 +var triggerBuilder = TriggerBuilder.Create("Furion", "Furion.Schedule.PeriodTrigger"); + +// 根据程序集名称和类型完全限定名(FullName) + 构造函数参数创建 +var triggerBuilder = TriggerBuilder.Create("Furion", "Furion.Schedule.PeriodTrigger", 1000); + +// 根据 Type 类型创建 +var triggerBuilder = TriggerBuilder.Create(typeof(PeriodTrigger)); + +// 根据 Type 类型 + 构造函数参数创建 +var triggerBuilder = TriggerBuilder.Create(typeof(CronTrigger), "* * * * *", CronStringFormat.Default); +``` + +2. **通过 `Trigger` 类型创建** + +这种方式常用于在运行时更新作业触发器。 + +```cs showLineNumbers +var triggerBuilder = TriggerBuilder.From(trigger); + +//也可以通过以下方式 +var triggerBuilder = trigger.GetBuilder(); +``` + +3. **通过 `JSON` 字符串创建** + +该方式非常灵活,可从配置文件,`JSON` 字符串,或其他能够返回 `JSON` 字符串的地方创建。 + +```cs showLineNumbers {2-26} +var triggerBuilder = Triggers.From(@" +{ + ""triggerId"": ""job1_trigger1"", + ""jobId"": ""job1"", + ""triggerType"": ""Furion.Schedule.CronTrigger"", + ""assemblyName"": ""Furion"", + ""args"": ""[\""* * * * *\"",0]"", + ""description"": null, + ""status"": 1, + ""startTime"": null, + ""endTime"": null, + ""lastRunTime"": ""2022-12-04 16:13:00.000"", + ""nextRunTime"": null, + ""numberOfRuns"": 1, + ""maxNumberOfRuns"": 0, + ""numberOfErrors"": 0, + ""maxNumberOfErrors"": 0, + ""numRetries"": 0, + ""retryTimeout"": 1000, + ""startNow"": true, + ""runOnStart"": false, + ""resetOnlyOnce"": true, + ""result"": null, + ""elapsedTime"": 100, + ""updatedTime"": ""2022-12-04 16:13:00.045"" +}"); +``` + +如果使用的是 `.NET7`,可使用 `"""` 避免转义,如: + +```cs showLineNumbers {2-26} +var triggerBuilder = Triggers.From(""" +{ + "triggerId": "job1_trigger1", + "jobId": "job1", + "triggerType": "Furion.Schedule.CronTrigger", + "assemblyName": "Furion", + "args": "[\"* * * * *\",0]", + "description": null, + "status": 8, + "startTime": null, + "endTime": null, + "lastRunTime": "2022-12-04 16:13:00.000", + "nextRunTime": null, + "numberOfRuns": 1, + "maxNumberOfRuns": 0, + "numberOfErrors": 0, + "maxNumberOfErrors": 0, + "numRetries": 0, + "retryTimeout": 1000, + "startNow": true, + "runOnStart": false, + "resetOnlyOnce": true, + "result": null, + "elapsedTime": 100, + "updatedTime": "2022-12-04 16:13:00.045" +} +"""); +``` + +:::important 关于属性名匹配规则 + +支持 `CamelCase(驼峰命名法)`,`Pascal(帕斯卡命名法)` 命名方式。 + +**不支持 `UnderScoreCase(下划线命名法)`** ,如 `"include_annotations": true` + +::: + +4. **还可以通过 `Clone` 静态方法从一个 `TriggerBuilder` 创建** + +```cs showLineNumbers +var triggerBuilder = TriggerBuilder.Clone(fromTriggerBuilder); +``` + +:::important 克隆说明 + +克隆操作只会克隆 `AssemblyName`,`TriggerType`,`Args`,`Description`,`StartTime`,`EndTime`,`MaxNumberOfRuns`,`MaxNumberOfErrors`,`NumRetries`,`RetryTimeout`,`StartNow`,`RunOnStart`,`ResetOnlyOnce`。 + +**不会克隆 `TriggerId`,`JobId`,`Status`,`LastRunTime`,`NextRunTime`,`NumberOfRuns`,`NumberOfErrors`,`Result`,`ElapsedTime`,PersistentConnection`UpdatedTime`。** + +::: + +5. **还可以通过 `LoadFrom` 实例方法填充当前的 `TriggerBuilder`** + +比如可以传递匿名类型,类类型,字典 `Dictionary` 类型: + +```cs showLineNumbers {2,9,16,23} +// 会覆盖所有相同的值 +triggerBuilder.LoadFrom(new +{ + Description = "我是描述", + StartTime = DateTime.Now +}); + +// 支持多个填充,还可以配置跳过 null 值覆盖 +triggerBuilder.LoadFrom(new +{ + Description = "我是另外一个描述", + StartTime = default(object), +}, ignoreNullValue: true); + +// 支持忽略特定属性名映射 +triggerBuilder.LoadFrom(new +{ + Description = "我是另外一个描述", + TriggerId = "trigger1" +}, ignorePropertyNames: new[]{ "description" }); + +// 支持字典类型 +triggerBuilder.LoadFrom(new Dictionary +{ + {"Description", "这是新的描述" }, + {"updatedTime", DateTime.Now } +}); +``` + +:::important 关于属性名匹配规则 + +支持 `CamelCase(驼峰命名法)`,`Pascal(帕斯卡命名法)` 和 `UnderScoreCase(下划线命名法)` 命名方式。 + +::: + +### 26.1.5.4 内置作业触发器构建器 + +为了方便快速实现作业触发器,作业调度模块内置了 `Period(间隔)` 和 `Cron(表达式)` 作业触发器,可通过 `TriggerBuilder` 类型或 `Triggers` 静态类创建。 + +- **`TriggerBuilder` 方式** + +```c showLineNumbers {2,5} +// 创建毫秒周期(间隔)作业触发器构建器 +var triggerBuilder = TriggerBuilder.Period(5000); + +// 创建 Cron 表达式作业触发器构建器 +var triggerBuilder = TriggerBuilder.Cron("* * * * *", CronStringFormat.Default); +``` + +- **`Triggers` 方式,❤️ 推荐** + +`Triggers` 静态类具备 `TriggerBuilder` 所有的静态方法同时还添加了不少更加便捷的静态方法。 + +```cs showLineNumbers {1,11,31} +// 间隔 Period 方式 +// 创建毫秒周期(间隔)作业触发器构建器 +var triggerBuilder = Triggers.Period(5000); +// 创建秒周期(间隔)作业触发器构建器 +var triggerBuilder = Triggers.PeriodSeconds(5); +// 创建分钟周期(间隔)作业触发器构建器 +var triggerBuilder = Triggers.PeriodMinutes(5); +// 创建小时周期(间隔)作业触发器构建器 +var triggerBuilder = Triggers.PeriodHours(5); + +// Cron 表达式方式 +// 创建 Cron 表达式作业触发器构建器 +var triggerBuilder = Triggers.Cron("* * * * *", CronStringFormat.Default); +// 创建每秒开始作业触发器构建器 +var triggerBuilder = Triggers.Secondly(); +// 创建每分钟开始作业触发器构建器 +var triggerBuilder = Triggers.Minutely(); +// 创建每小时开始作业触发器构建器 +var triggerBuilder = Triggers.Hourly(); +// 创建每天(午夜)开始作业触发器构建器 +var triggerBuilder = Triggers.Daily(); +// 创建每月1号(午夜)开始作业触发器构建器 +var triggerBuilder = Triggers.Monthly(); +// 创建每周日(午夜)开始作业触发器构建器 +var triggerBuilder = Triggers.Weekly(); +// 创建每年1月1号(午夜)开始作业触发器构建器 +var triggerBuilder = Triggers.Yearly(); +// 创建每周一至周五(午夜)开始作业触发器构建器 +var triggerBuilder = Triggers.Workday(); + +// Cron 表达式 Macro At 方式 +// 每第 3 秒 +var triggerBuilder = Triggers.SecondlyAt(3); +// 每第 3,5,6 秒 +var triggerBuilder = Triggers.SecondlyAt(3, 5, 6); + +// 每分钟第 3 秒 +var triggerBuilder = Triggers.MinutelyAt(3); +// 每分钟第 3,5,6 秒 +var triggerBuilder = Triggers.MinutelyAt(3, 5, 6); + +// 每小时第 3 分钟 +var triggerBuilder = Triggers.HourlyAt(3); +// 每小时第 3,5,6 分钟 +var triggerBuilder = Triggers.HourlyAt(3, 5, 6); + +// 每天第 3 小时正(点) +var triggerBuilder = Triggers.DailyAt(3); +// 每天第 3,5,6 小时正(点) +var triggerBuilder = Triggers.DailyAt(3, 5, 6); + +// 每月第 3 天零点正 +var triggerBuilder = Triggers.MonthlyAt(3); +// 每月第 3,5,6 天零点正 +var triggerBuilder = Triggers.MonthlyAt(3, 5, 6); + +// 每周星期 3 零点正 +var triggerBuilder = Triggers.WeeklyAt(3); +var triggerBuilder = Triggers.WeeklyAt("WED"); // SUN(星期天),MON,TUE,WED,THU,FRI,SAT +// 每周星期 3,5,6 零点正 +var triggerBuilder = Triggers.WeeklyAt(3, 5, 6); +var triggerBuilder = Triggers.WeeklyAt("WED", "FRI", "SAT"); +// 还支持混合 +var triggerBuilder = Triggers.WeeklyAt(3, "FRI", 6); + +// 每年第 3 月 1 日零点正 +var triggerBuilder = Triggers.YearlyAt(3); +var triggerBuilder = Triggers.YearlyAt("MAR"); // JAN(一月),FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC +// 每年第 3,5,6 月 1 日零点正 +var triggerBuilder = Triggers.YearlyAt(3); +var triggerBuilder = Triggers.YearlyAt(3, 5, 6); +var triggerBuilder = Triggers.YearlyAt("MAR", "MAY", "JUN"); +// 还支持混合 +var triggerBuilder = Triggers.YearlyAt(3, "MAY", 6); +``` + +### 26.1.5.5 自定义作业触发器 + +除了使用框架提供的 `PeriodTrigger` 和 `CronTrigger` 以外,还可以自定义作业触发器,只需要继承 `Trigger` 并重写 `GetNextOccurrence` 方法即可,如实现一个间隔两秒的作业触发器。 + +```cs showLineNumbers {1,3,5} +public class CustomTrigger : Trigger +{ + public override DateTime GetNextOccurrence(DateTime startAt) + { + return startAt.AddSeconds(2); + } +} +``` + +之后可通过 `TriggerBuilder.Create` 或 `Triggers.Create` 创建即可: + +```cs showLineNumbers {3} +services.AddSchedule(options => +{ + options.AddJob(Triggers.Create()); +}); +``` + +查看作业执行结果: + +```bash showLineNumbers {12,14,16} +info: 2022-12-04 17:19:25.0980531 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is running. +info: 2022-12-04 17:19:25.1027083 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is preloading... +info: 2022-12-04 17:19:25.2702054 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended to the schedule. +info: 2022-12-04 17:19:25.2723418 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + The scheduler of successfully appended to the schedule. +warn: 2022-12-04 17:19:25.2999295 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + Schedule hosted service preload completed, and a total of <1> schedulers are appended. +info: 2022-12-04 17:19:27.2849015 +08:00 星期日 L MyJob[0] #8 + [C] 1ts 2022-12-04 17:19:27.234 -> 2022-12-04 17:19:29.232 +info: 2022-12-04 17:19:29.2604639 +08:00 星期日 L MyJob[0] #4 + [C] 2ts 2022-12-04 17:19:29.232 -> 2022-12-04 17:19:31.225 +info: 2022-12-04 17:19:31.2422514 +08:00 星期日 L MyJob[0] #10 + [C] 3ts 2022-12-04 17:19:31.225 -> 2022-12-04 17:19:33.207 +``` + +**另外,自定义作业触发器还支持配置构造函数参数** + +:::important 参数特别说明 + +如果自定义作业触发器包含参数,那么**必须满足以下两个条件**: + +- **参数必须通过唯一的构造函数传入,有且最多只能拥有一个构造函数** +- **参数的类型只能是 `int`,`string`,`bool`,`null` 或由它们组成的数组类型** + +::: + +```cs showLineNumbers {1,3,5,8,12} +public class CustomTrigger : Trigger +{ + public CustomTrigger(int seconds) // 可支持多个参数 + { + Seconds = seconds; + } + + private int Seconds { get; set; } + + public override DateTime GetNextOccurrence(DateTime startAt) + { + return startAt.AddSeconds(Seconds); + } +} +``` + +之后可通过 `TriggerBuilder.Create` 或 `Triggers.Create` 创建并传入参数。 + +```cs showLineNumbers {3} +services.AddSchedule(options => +{ + options.AddJob(Triggers.Create(3)); +}); +``` + +查看作业执行结果: + +```bash showLineNumbers {12,14,16} +info: 2022-12-04 17:23:09.3029251 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is running. +info: 2022-12-04 17:23:09.3205593 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is preloading... +info: 2022-12-04 17:23:09.7081119 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended to the schedule. +info: 2022-12-04 17:23:09.7506504 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + The scheduler of successfully appended to the schedule. +warn: 2022-12-04 17:23:09.9380816 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + Schedule hosted service preload completed, and a total of <1> schedulers are appended. +info: 2022-12-04 17:23:12.6291716 +08:00 星期日 L MyJob[0] #6 + [C] 1ts 2022-12-04 17:23:12.590 -> 2022-12-04 17:23:15.582 +info: 2022-12-04 17:23:15.6141563 +08:00 星期日 L MyJob[0] #9 + [C] 2ts 2022-12-04 17:23:15.582 -> 2022-12-04 17:23:18.572 +info: 2022-12-04 17:23:18.5857464 +08:00 星期日 L MyJob[0] #8 + [C] 3ts 2022-12-04 17:23:18.572 -> 2022-12-04 17:23:21.551 +``` + +自定义作业触发器除了可重写 `GetNextOccurrence` 方法之后,还提供了 `ShouldRun` 和 `ToString` 方法可重写,如: + +```cs showLineNumbers {15,21} +public class CustomTrigger : Trigger +{ + public CustomTrigger(int seconds) + { + Seconds = seconds; + } + + private int Seconds { get; set; } + + public override DateTime GetNextOccurrence(DateTime startAt) + { + return startAt.AddSeconds(Seconds); + } + + public override bool ShouldRun(JobDetail jobDetail, DateTime startAt) + { + // 在这里进一步控制,如果返回 false,则作业触发器跳过执行 + return base.ShouldRun(jobDetail, startAt); + } + + public override string ToString() + { + return $"<{TriggerId}> 自定义递增 {Seconds}s 触发器"; + } +} +``` + +推荐重写 `GetNextRunTime` 和 `ToString` 方法即可,如: + +```cs showLineNumbers {11} +public class MyJob : IJob +{ + private readonly ILogger _logger; + public MyJob(ILogger logger) + { + _logger = logger; + } + + public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + _logger.LogInformation($"{context}"); + await Task.CompletedTask; + } +} +``` + +查看作业执行结果: + +```bash showLineNumbers {12,14,16} +info: 2022-12-04 17:26:43.9120082 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is running. +info: 2022-12-04 17:26:43.9166481 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is preloading... +info: 2022-12-04 17:26:44.1786114 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended to the schedule. +info: 2022-12-04 17:26:44.1816154 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + The scheduler of successfully appended to the schedule. +warn: 2022-12-04 17:26:44.2077386 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + Schedule hosted service preload completed, and a total of <1> schedulers are appended. +info: 2022-12-04 17:26:47.1904549 +08:00 星期日 L MyJob[0] #8 + [C] 自定义递增 3s 触发器 2022-12-04 17:26:47.139 -> 2022-12-04 17:26:50.145 +info: 2022-12-04 17:26:50.1652618 +08:00 星期日 L MyJob[0] #6 + [C] 自定义递增 3s 触发器 2022-12-04 17:26:50.145 -> 2022-12-04 17:26:53.129 +info: 2022-12-04 17:26:53.1426614 +08:00 星期日 L MyJob[0] #8 + [C] 自定义递增 3s 触发器 2022-12-04 17:26:53.129 -> 2022-12-04 17:26:56.106 +``` + +### 26.1.5.6 作业触发器特性及自定义 + +如果 `JobBuilder` 配置了 `IncludeAnnotations` 参数且为 `true`,那么将会自动解析 `IJob` 的实现类型的所有继承 `TriggerAttribute` 的特性,目前作业调度模块内置了以下作业触发器特性: + +- `[Period(5000)]`:毫秒周期(间隔)作业触发器特性 +- `[PeriodSeconds(5)]`:秒周期(间隔)作业触发器特性 +- `[PeriodMinutes(5)]`:分钟周期(间隔)作业触发器特性 +- `[PeriodHours(5)]`:小时周期(间隔)作业触发器特性 +- `[Cron("* * * * *", CronStringFormat.Default)]`:Cron 表达式作业触发器特性 +- `[Secondly]`:每秒开始作业触发器特性 +- `[Minutely]`:每分钟开始作业触发器特性 +- `[Hourly]`:每小时开始作业触发器特性 +- `[Daily]`:每天(午夜)开始作业触发器特性 +- `[Monthly]`:每月 1 号(午夜)开始作业触发器特性 +- `[Weekly]`:每周日(午夜)开始作业触发器特性 +- `[Yearly]`:每年 1 月 1 号(午夜)开始作业触发器特性 +- `[Workday]`:每周一至周五(午夜)开始触发器特性 +- `[SecondlyAt]`:特定秒开始作业触发器特性 +- `[MinutelyAt]`:每分钟特定秒开始作业触发器特性 +- `[HourlyAt]`:每小时特定分钟开始作业触发器特性 +- `[DailyAt]`:每天特定小时开始作业触发器特性 +- `[MonthlyAt]`:每月特定天(午夜)开始作业触发器特性 +- `[WeeklyAt]`:每周特定星期几(午夜)开始作业触发器特性 +- `[YearlyAt]`:每年特定月 1 号(午夜)开始作业触发器特性 + +使用如下: + +```cs showLineNumbers {3,6,10,13} +services.AddSchedule(options => +{ + options.AddJob(JobBuilder.Create().SetIncludeAnnotations(true)); + + // 也支持自定义配置 + 特性扫描 + options.AddJob(JobBuilder.Create().SetIncludeAnnotations(true) + , Triggers.PeriodSeconds(5)); + + // 或者通过类型扫描 + options.AddJob(typeof(MyJobj).ScanToBuilder()); + + // 还可以批量扫描 Furion 4.8.2.4+ + options.AddJob(App.EffectiveTypes.ScanToBuilders()); +}); +``` + +```cs showLineNumbers {1-3} +[Minutely] +[PeriodSeconds(5)] +[Cron("*/3 * * * * *", CronStringFormat.WithSeconds)] +public class MyJob : IJob +{ + private readonly ILogger _logger; + public MyJob(ILogger logger) + { + _logger = logger; + } + + public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + _logger.LogInformation($"{context}"); + await Task.CompletedTask; + } +} +``` + +查看作业执行结果: + +```bash showLineNumbers {6,8,10,12,16,18,20,30} +info: 2022-12-04 17:35:47.0211372 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is running. +info: 2022-12-04 17:35:47.0267027 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is preloading... +info: 2022-12-04 17:35:47.2906591 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended to the schedule. +info: 2022-12-04 17:35:47.2921849 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended to the schedule. +info: 2022-12-04 17:35:47.2961669 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended to the schedule. +info: 2022-12-04 17:35:47.2979859 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + The scheduler of successfully appended to the schedule. +warn: 2022-12-04 17:35:47.3194555 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + Schedule hosted service preload completed, and a total of <1> schedulers are appended. +info: 2022-12-04 17:35:48.0588231 +08:00 星期日 L MyJob[0] #8 + [C] */3 * * * * * 1ts 2022-12-04 17:35:48.000 -> 2022-12-04 17:35:51.000 +info: 2022-12-04 17:35:51.0240459 +08:00 星期日 L MyJob[0] #9 + [C] */3 * * * * * 2ts 2022-12-04 17:35:51.000 -> 2022-12-04 17:35:54.000 +info: 2022-12-04 17:35:52.2643935 +08:00 星期日 L MyJob[0] #12 + [C] 5s 1ts 2022-12-04 17:35:52.246 -> 2022-12-04 17:35:57.227 +info: 2022-12-04 17:35:54.0175524 +08:00 星期日 L MyJob[0] #6 + [C] */3 * * * * * 3ts 2022-12-04 17:35:54.000 -> 2022-12-04 17:35:57.000 +info: 2022-12-04 17:35:57.0270544 +08:00 星期日 L MyJob[0] #9 + [C] */3 * * * * * 4ts 2022-12-04 17:35:57.000 -> 2022-12-04 17:36:00.000 +info: 2022-12-04 17:35:57.2433514 +08:00 星期日 L MyJob[0] #12 + [C] 5s 2ts 2022-12-04 17:35:57.227 -> 2022-12-04 17:36:02.208 +info: 2022-12-04 17:36:00.0151605 +08:00 星期日 L MyJob[0] #14 + [C] */3 * * * * * 5ts 2022-12-04 17:36:00.000 -> 2022-12-04 17:36:03.000 +info: 2022-12-04 17:36:00.0315972 +08:00 星期日 L MyJob[0] #8 + [C] * * * * * 1ts 2022-12-04 17:36:00.000 -> 2022-12-04 17:37:00.000 +info: 2022-12-04 17:36:02.2203934 +08:00 星期日 L MyJob[0] #12 + [C] 5s 3ts 2022-12-04 17:36:02.208 -> 2022-12-04 17:36:07.184 +``` + +--- + +**除了使用内置特性,我们还可以自定义作业触发器特性**,如: + +```cs showLineNumbers {1,2,5} +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] +public class CustomAttribute : TriggerAttribute +{ + public CustomAttribute(int seconds) + : base(typeof(CustomTrigger), seconds) + { + } +} +``` + +:::tip 自定义作业触发器必备条件 + +- 必须继承 `TriggerAttribute` 特性类 +- 至少包含一个构造函数且通过基类构造函数配置 `:base(实际触发器类型, 构造函数参数)` +- 推荐添加 `[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]` 特性 + +::: + +使用如下: + +```cs showLineNumbers {1} +[Custom(3)] +public class MyJob : IJob +{ + private readonly ILogger _logger; + public MyJob(ILogger logger) + { + _logger = logger; + } + + public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + _logger.LogInformation($"{context}"); + await Task.CompletedTask; + } +} +``` + +查看作业执行结果: + +```bash showLineNumbers {12,14,16} +info: 2022-12-04 17:44:12.2702884 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is running. +info: 2022-12-04 17:44:12.2872399 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is preloading... +info: 2022-12-04 17:44:12.5730241 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended to the schedule. +info: 2022-12-04 17:44:12.5751444 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + The scheduler of successfully appended to the schedule. +warn: 2022-12-04 17:44:12.6174459 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + Schedule hosted service preload completed, and a total of <1> schedulers are appended. +info: 2022-12-04 17:44:15.5850848 +08:00 星期日 L MyJob[0] #6 + [C] 自定义递增 3s 触发器 2022-12-04 17:44:15.537 -> 2022-12-04 17:44:18.542 +info: 2022-12-04 17:44:18.5693881 +08:00 星期日 L MyJob[0] #8 + [C] 自定义递增 3s 触发器 2022-12-04 17:44:18.542 -> 2022-12-04 17:44:21.527 +info: 2022-12-04 17:44:21.5396428 +08:00 星期日 L MyJob[0] #6 + [C] 自定义递增 3s 触发器 2022-12-04 17:44:21.527 -> 2022-12-04 17:44:24.504 +``` + +--- + +**作业触发器特性还提供了多个属性配置**,如: + +- `TriggerId`:作业触发器 `Id`,`string` 类型 +- `Description`:描述信息,`string` 类型 +- `StartTime`:起始时间,`string` 类型 +- `EndTime`:结束时间,`string` 类型 +- `MaxNumberOfRuns`:最大触发次数,`long` 类型,`0`:不限制;`n`:N 次 +- `MaxNumberOfErrors`:最大出错次数,`long` 类型,`0`:不限制;`n`:N 次 +- `NumRetries`:重试次数,`int` 类型,默认值 `0` +- `RetryTimeout`:重试间隔时间,`int` 类型,默认值 `1000` +- `StartNow`:是否立即启动,`bool` 类型,默认值 `true` +- `RunOnStart`:是否启动时执行一次,`bool` 类型,默认值 `false` +- `ResetOnlyOnce`:是否在启动时重置最大触发次数等于一次的作业,`bool` 类型,默认值 `true` + +使用如下: + +```cs showLineNumbers {1} +[PeriodSeconds(5, TriggerId = "trigger1", Description = "这是一段描述")] +public class MyJob : IJob +{ + // ... +} +``` + +### 26.1.5.7 设置作业触发器构建器 + +`TriggerBuilder` 提供了和 `Trigger` 完全匹配的 `Set[属性名]` 方法来配置作业触发器各个属性,如: + +```cs showLineNumbers {3,29} + services.AddSchedule(options => + { + var triggerBuilder = Triggers.Period(5000) + .SetTriggerId("trigger1") // 作业触发器 Id + .SetTriggerType("Furion", "Furion.Schedule.PeriodTrigger") // 作业触发器类型,支持多个重载 + .SetTriggerType() // 作业触发器类型,支持多个重载 + .SetTriggerType(typeof(PeriodTrigger)) // 作业触发器类型,支持多个重载 + .SetArgs("[5000]") // 作业触发器参数 object[] 序列化字符串类型,支持多个重载 + .SetArgs(5000) // 作业触发器参数,支持多个重载 + .SetDescription("作业触发器描述") // 作业触发器描述 + .SetStatus(TriggerStatus.Ready) // 作业触发器状态 + .SetStartTime(DateTime.Now) // 作业触发器起始时间 + .SetEndTime(DateTime.Now.AddMonths(1)) // 作业触发器结束时间 + .SetLastRunTime(DateTime.Now.AddSeconds(-5)) // 作业触发器最近运行时间 + .SetNextRunTime(DateTime.Now.AddSeconds(5)) // 作业触发器下一次运行时间 + .SetNumberOfRuns(1) // 作业触发器触发次数 + .SetMaxNumberOfRuns(100) // 作业触发器最大触发器次数 + .SetNumberOfErrors(1) // 作业触发器出错次数 + .SetMaxNumberOfErrors(100) // 作业触发器最大出错次数 + .SetNumRetries(3) // 作业触发器出错重试次数 + .SetRetryTimeout(1000) // 作业触发器重试间隔时间 + .SetStartNow(true) // 作业触发器是否立即启动 + .SetRunOnStart(false) // 作业触发器是否启动时执行一次 + .SetResetOnlyOnce(true) // 作业触发器是否在启动时重置最大触发次数等于一次的作业 + .SetResult("本次返回结果") // 作业触发器本次执行返回结果,Furion 4.8.7.7+ + .SetElapsedTime(100) // 作业触发器本次执行耗时,Furion 4.8.7.7+ + ; + + options.AddJob(triggerBuilder); + }); +``` + +### 26.1.5.8 作业触发器持久化方法 + +作业触发器构建器 `TriggerBuilder` 提供了三个标记作业持久化行为的方法: + +- `Appended()`:标记作业触发器构建器是新增的,届时生成的 `SQL` 是 `INSERT` 语句 +- `Updated()`:标记作业触发器构建器已被更新,届时生成的 `SQL` 是 `Updated` 语句,如果标记为此操作,那么当前作业调度器初始化时将新增至内存中 +- `Removed()`:标记作业触发器构建器已被删除,届时生成的 `SQL` 是 `Deleted` 语句,如果标记为此操作,那么当前作业调度器初始化时将不会添加至作业计划中 + +```cs showLineNumbers {4-6} + services.AddSchedule(options => +{ + options.AddJob( + Triggers.PeriodSeconds(5).SetTriggerId("trigger1").Appended() + , Triggers.PeriodSeconds(5).SetTriggerId("trigger2").Updated() + , Triggers.PeriodSeconds(5).SetTriggerId("trigger3").Removed()); +}); +``` + +查看作业调度器初始化日志: + +```bash showLineNumbers {6,8,10,16,18} +info: 2022-12-04 18:29:22.3997873 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is running. +info: 2022-12-04 18:29:22.4045304 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is preloading... +info: 2022-12-04 18:29:22.5473237 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully removed to the schedule. +info: 2022-12-04 18:29:22.5504289 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended to the schedule. +info: 2022-12-04 18:29:22.5521396 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended and updated to the schedule. +info: 2022-12-04 18:29:22.5535657 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + The scheduler of successfully appended to the schedule. +warn: 2022-12-04 18:29:22.5896298 +08:00 星期日 L System.Logging.ScheduleService[0] #1 + Schedule hosted service preload completed, and a total of <1> schedulers are appended. +info: 2022-12-04 18:29:27.5981907 +08:00 星期日 L MyJob[0] #14 + [C] 5s 1ts 2022-12-04 18:29:27.507 -> 2022-12-04 18:29:32.500 +info: 2022-12-04 18:29:27.6002420 +08:00 星期日 L MyJob[0] #15 + [C] 5s 1ts 2022-12-04 18:29:27.507 -> 2022-12-04 18:29:32.500 +info: 2022-12-04 18:29:32.5850223 +08:00 星期日 L MyJob[0] #12 + [C] 5s 2ts 2022-12-04 18:29:32.500 -> 2022-12-04 18:29:37.548 +info: 2022-12-04 18:29:32.6034646 +08:00 星期日 L MyJob[0] #8 + [C] 5s 2ts 2022-12-04 18:29:32.500 -> 2022-12-04 18:29:37.548 +``` + +### 26.1.5.9 多种格式字符串输出 + +`Trigger` 和 `TriggerBuilder` 都提供了多种将自身转换成特定格式的字符串。 + +1. **转换成 `JSON` 字符串** + +```cs showLineNumbers +var json = trigger.ConvertToJSON(); +``` + +字符串打印如下: + +```json showLineNumbers +{ + "triggerId": "job1_trigger1", + "jobId": "job1", + "triggerType": "Furion.Schedule.PeriodTrigger", + "assemblyName": "Furion", + "args": "[5000]", + "description": null, + "status": 2, + "startTime": null, + "endTime": null, + "lastRunTime": "2022-12-04 17:52:34.768", + "nextRunTime": "2022-12-04 17:52:39.769", + "numberOfRuns": 1, + "maxNumberOfRuns": 0, + "numberOfErrors": 0, + "maxNumberOfErrors": 0, + "numRetries": 0, + "retryTimeout": 1000, + "startNow": true, + "runOnStart": false, + "resetOnlyOnce": true, + "result": null, + "elapsedTime": 100, + "updatedTime": "2022-12-04 17:52:34.803" +} +``` + +2. **转换成 `SQL` 字符串** + +```cs showLineNumbers {2,6,9,13,16,20} +// 输出新增 SQL,使用 CamelCase 属性命名 +var insertSql = trigger.ConvertToSQL("tbName" + , PersistenceBehavior.Appended + , NamingConventions.CamelCase); +// 更便捷拓展 +var insertSql = trigger.ConvertToInsertSQL("tbName", NamingConventions.CamelCase); + +// 输出删除 SQL,使用 Pascal 属性命名 +var deleteSql = trigger.ConvertToSQL("tbName" + , PersistenceBehavior.Removed + , NamingConventions.Pascal); +// 更便捷拓展 +var deleteSql = trigger.ConvertToDeleteSQL("tbName", NamingConventions.Pascal); + +// 输出更新 SQL,使用 UnderScoreCase 属性命名 +var updateSql = trigger.ConvertToSQL("tbName" + , PersistenceBehavior.Updated + , NamingConventions.UnderScoreCase); +// 更便捷拓展 +var updateSql = trigger.ConvertToUpdateSQL("tbName", NamingConventions.UnderScoreCase); +``` + +字符串打印如下: + +```sql showLineNumbers {2,53,56} +-- 新增操作 +INSERT INTO tbName( + triggerId, + jobId, + triggerType, + assemblyName, + args, + description, + status, + startTime, + endTime, + lastRunTime, + nextRunTime, + numberOfRuns, + maxNumberOfRuns, + numberOfErrors, + maxNumberOfErrors, + numRetries, + retryTimeout, + startNow, + runOnStart, + resetOnlyOnce, + result, + elapsedTime, + updatedTime +) +VALUES( + 'job1_trigger1', + 'job1', + 'Furion.Schedule.PeriodTrigger', + 'Furion', + '[5000]', + NULL, + 2, + NULL, + NULL, + '2022-12-04 17:54:42.693', + '2022-12-04 17:54:47.721', + 1, + 0, + 0, + 0, + 0, + 1000, + 1, + 0, + 1, + NULL, + 100, + '2022-12-04 17:54:42.754' +); +-- 删除操作 +DELETE FROM tbName +WHERE TriggerId = 'job1_trigger1' AND JobId = 'job1'; +-- 更新操作 +UPDATE tbName +SET + trigger_id = 'job1_trigger1', + job_id = 'job1', + trigger_type = 'Furion.Schedule.PeriodTrigger', + assembly_name = 'Furion', + args = '[5000]', + description = NULL, + status = 2, + start_time = NULL, + end_time = NULL, + last_run_time = '2022-12-04 17:54:42.693', + next_run_time = '2022-12-04 17:54:47.721', + number_of_runs = 1, + max_number_of_runs = 0, + number_of_errors = 0, + max_number_of_errors = 0, + num_retries = 0, + retry_timeout = 1000, + start_now = 1, + run_on_start = 0, + reset_only_once = 1, + result = NULL, + elapsedTime = 100, + updated_time = '2022-12-04 17:54:42.754' +WHERE trigger_id = 'job1_trigger1' AND job_id = 'job1'; +``` + +3. **转换成 `Monitor` 字符串** + +```cs showLineNumbers +var monitor = trigger.ConvertToMonitor(); +``` + +字符串打印如下: + +```bash showLineNumbers +┏━━━━━━━━━━━ Trigger ━━━━━━━━━━━ +┣ Furion.Schedule.PeriodTrigger +┣ +┣ triggerId: job1_trigger1 +┣ jobId: job1 +┣ triggerType: Furion.Schedule.PeriodTrigger +┣ assemblyName: Furion +┣ args: [5000] +┣ description: +┣ status: Running +┣ startTime: +┣ endTime: +┣ lastRunTime: 2022-12-04 17:56:55.384 +┣ nextRunTime: 2022-12-04 17:57:00.379 +┣ numberOfRuns: 1 +┣ maxNumberOfRuns: 0 +┣ numberOfErrors: 0 +┣ maxNumberOfErrors: 0 +┣ numRetries: 0 +┣ retryTimeout: 1000 +┣ startNow: True +┣ runOnStart: False +┣ resetOnlyOnce: True +┣ result: +┣ elapsedTime: 100 +┣ updatedTime: 2022-12-04 17:56:55.413 +┗━━━━━━━━━━━ Trigger ━━━━━━━━━━━ +``` + +4. **简要字符串输出** + +```cs showLineNumbers +var str = trigger.ToString(); +``` + +字符串打印如下: + +```bash showLineNumbers + 5s 这是一段描述 1ts +``` + +### 26.1.5.10 自定义 `SQL` 输出配置 + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.2 +` 版本使用。 + +::: + +```cs showLineNumbers {1,3,8,13,18} +services.AddSchedule(options => +{ + options.Trigger.ConvertToSQL = (tableName, columnNames, trigger, behavior, naming) => + { + // 生成新增 SQL + if (behavior == PersistenceBehavior.Appended) + { + return trigger.ConvertToInsertSQL(tableName, naming); + } + // 生成更新 SQL + else if (behavior == PersistenceBehavior.Updated) + { + return trigger.ConvertToUpdateSQL(tableName, naming); + } + // 生成删除 SQL + else if (behavior == PersistenceBehavior.Removed) + { + return trigger.ConvertToDeleteSQL(tableName, naming); + } + + return string.Empty; + }; +}); +``` + +- `ConvertToSQL` 委托参数说明 + - `tableName`:数据库表名称,`string` 类型 + - `columnNames`:数据库列名:`string[]` 类型,只能通过 `索引` 获取 + - `trigger`:作业信息 `Trigger` 对象 + - `behavior`:持久化 `PersistenceBehavior` 类型,用于标记新增,更新还是删除操作 + - `naming`:命名法 `NamingConventions` 类型,包含 `CamelCase(驼峰命名法)`,`Pascal(帕斯卡命名法)` 和 `UnderScoreCase(下划线命名法)` + +:::caution 注意事项 + +如果在该自定义 `SQL` 输出方法中调用 `trigger.ConvertToSQL(..)` 会导致死循环。 + +::: + +### 26.1.5.11 查看最近运行记录 + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.4.3 +` 版本使用。 + +::: + +在 `Furion 4.8.4.3+` 版本新增了 `GetTimelines()` 方法,可获取内存中作业触发器最近运行的 `5` 条记录,如: + +```cs showLineNumbers +var timelines = trigger.GetTimelines(); // => [{numberOfRuns: 2, lastRunTime: "2023-01-03 14:00:08"}, {numberOfRuns: 1, lastRunTime: "2023-01-03 14:00:03"}, ...] +``` + +`timelines` 返回值为 `IEnumerable` 类型,其中 `TriggerTimeline` 类型提供以下属性: + +- `TriggerTimeline` + - `NumberOfRuns`:当前运行次数,`long` 类型 + - `LastRunTime`:最近运行时间,`DateTime?` 类型 + - `NextRunTime`:下一次运行时间,`DateTime?` 类型 + - `Status`:作业触发器状态,`TriggerStatus` 枚举类型 + - `Result`:本次执行结果,`string` 类型 + - `ElapsedTime`:本次执行耗时,`long` 类型,单位 `ms` + +### 26.1.5.12 更改作业触发器触发时间 + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.7.31 +` 版本使用。 + +::: + +如果需要更改作业触发器触发时间通常需要程序员自行组合 `.SetTriggerType()` 和 `.SetArgs(args)`,但在实际开发中代码非常不直观,所以提供了一些列的 `.AlterTo` 方法,如: + +```cs showLineNumbers {1,11,31,76} +// 间隔 Period 方式 +// 设置毫秒周期(间隔)作业触发器 +triggerBuilder.AlterToPeriod(5000); +// 设置秒周期(间隔)作业触发器 +triggerBuilder.AlterToPeriodSeconds(5); +// 设置分钟周期(间隔)作业触发器 +triggerBuilder.AlterToPeriodMinutes(5); +// 设置小时周期(间隔)作业触发器 +triggerBuilder.AlterToPeriodHours(5); + +// Cron 表达式方式 +// 设置 Cron 表达式作业触发器 +triggerBuilder.AlterToCron("* * * * *", CronStringFormat.Default); +// 设置每秒开始作业触发器 +triggerBuilder.AlterToSecondly(); +// 设置每分钟开始作业触发器 +triggerBuilder.AlterToMinutely(); +// 设置每小时开始作业触发器 +triggerBuilder.AlterToHourly(); +// 设置每天(午夜)开始作业触发器 +triggerBuilder.AlterToDaily(); +// 设置每月1号(午夜)开始作业触发器 +triggerBuilder.AlterToMonthly(); +// 设置每周日(午夜)开始作业触发器 +triggerBuilder.AlterToWeekly(); +// 设置每年1月1号(午夜)开始作业触发器 +triggerBuilder.AlterToYearly(); +// 设置每周一至周五(午夜)开始作业触发器 +triggerBuilder.AlterToWorkday(); + +// Cron 表达式 Macro At 方式 +// 每第 3 秒 +triggerBuilder.AlterToSecondlyAt(3); +// 每第 3,5,6 秒 +triggerBuilder.AlterToSecondlyAt(3, 5, 6); + +// 每分钟第 3 秒 +triggerBuilder.AlterToMinutelyAt(3); +// 每分钟第 3,5,6 秒 +triggerBuilder.AlterToMinutelyAt(3, 5, 6); + +// 每小时第 3 分钟 +triggerBuilder.AlterToHourlyAt(3); +// 每小时第 3,5,6 分钟 +triggerBuilder.AlterToHourlyAt(3, 5, 6); + +// 每天第 3 小时正(点) +triggerBuilder.AlterToDailyAt(3); +// 每天第 3,5,6 小时正(点) +triggerBuilder.AlterToDailyAt(3, 5, 6); + +// 每月第 3 天零点正 +triggerBuilder.AlterToMonthlyAt(3); +// 每月第 3,5,6 天零点正 +triggerBuilder.AlterToMonthlyAt(3, 5, 6); + +// 每周星期 3 零点正 +triggerBuilder.AlterToWeeklyAt(3); +triggerBuilder.AlterToWeeklyAt("WED"); // SUN(星期天),MON,TUE,WED,THU,FRI,SAT +// 每周星期 3,5,6 零点正 +triggerBuilder.AlterToWeeklyAt(3, 5, 6); +triggerBuilder.AlterToWeeklyAt("WED", "FRI", "SAT"); +// 还支持混合 +triggerBuilder.AlterToWeeklyAt(3, "FRI", 6); + +// 每年第 3 月 1 日零点正 +triggerBuilder.AlterToYearlyAt(3); +triggerBuilder.AlterToYearlyAt("MAR"); // JAN(一月),FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC +// 每年第 3,5,6 月 1 日零点正 +triggerBuilder.AlterToYearlyAt(3); +triggerBuilder.AlterToYearlyAt(3, 5, 6); +triggerBuilder.AlterToYearlyAt("MAR", "MAY", "JUN"); +// 还支持混合 +triggerBuilder.AlterToYearlyAt(3, "MAY", 6); + +// 设置自定义作业触发器 +// 泛型方式 +trigger.AlterTo(); +trigger.AlterTo(30); +// 程序集方式 +trigger.AlterTo("YourAssembly", "YourAssembly.CustomTrigger"); +trigger.AlterTo("YourAssembly", "YourAssembly.CustomTrigger", 30); +//类型方式 +trigger.AlterTo(typeof(CustomTrigger)); +trigger.AlterTo(typeof(CustomTrigger), 30); +``` + +## 26.1.6 作业计划 `Scheduler` 及构建器 + +### 26.1.6.1 关于作业计划 + +所谓的作业计划(`Scheduler`)是将作业信息(`JobDetail`),作业触发器(`Trigger`)和作业处理程序(`IJob`)关联起来,并添加到作业调度器中等待调度执行。 + +作业计划(`Scheduler`)类型对外是不公开的,但提供了对应的 `IScheduler` 接口进行操作。 + +### 26.1.6.2 关于作业计划构建器 + +作业计划 `Scheduler` 是框架提供运行时的**内部只读类型**,那么我们该如何创建或变更 `Scheduler` 对象呢? + +`SchedulerBuilder` 是框架提供可用来生成运行时 `Scheduler` 的类型,这样做的好处可避免外部直接修改运行时 `Scheduler` 数据,还能实现任何修改动作监听,也能避免多线程抢占情况。 + +作业调度模块提供了多种方式用来创建 `SchedulerBuilder` 对象。 + +1. **通过 `Create` 静态方法创建** + +```cs showLineNumbers {1,4,10,16,22} +// 通过作业 Id 创建 +var schedulerBuilder = SchedulerBuilder.Create("job1"); + +// 通过泛型方式 +var schedulerBuilder = SchedulerBuilder.Create(Triggers.PeriodSeconds(5), Triggers.Minutely()); +var schedulerBuilder = SchedulerBuilder.Create(true, Triggers.PeriodSeconds(5), Triggers.Minutely()); +var schedulerBuilder = SchedulerBuilder.Create("job1", Triggers.PeriodSeconds(5), Triggers.Minutely()); +var schedulerBuilder = SchedulerBuilder.Create("job1", true, Triggers.PeriodSeconds(5), Triggers.Minutely()); + +// 通过类型方式 +var schedulerBuilder = SchedulerBuilder.Create(typeof(MyJob), Triggers.PeriodSeconds(5), Triggers.Minutely()); +var schedulerBuilder = SchedulerBuilder.Create(typeof(MyJob), true, Triggers.PeriodSeconds(5), Triggers.Minutely()); +var schedulerBuilder = SchedulerBuilder.Create(typeof(MyJob), "job1", Triggers.PeriodSeconds(5), Triggers.Minutely()); +var schedulerBuilder = SchedulerBuilder.Create(typeof(MyJob), "job1", true, Triggers.PeriodSeconds(5), Triggers.Minutely()); + +// 通过委托方式 +var schedulerBuilder = SchedulerBuilder.Create((context, stoppingToken) => {}, Triggers.PeriodSeconds(5), Triggers.Minutely()); +var schedulerBuilder = SchedulerBuilder.Create((context, stoppingToken) => {}, true, Triggers.PeriodSeconds(5), Triggers.Minutely()); +var schedulerBuilder = SchedulerBuilder.Create((context, stoppingToken) => {}, "job1", Triggers.PeriodSeconds(5), Triggers.Minutely()); +var schedulerBuilder = SchedulerBuilder.Create((context, stoppingToken) => {}, "job1", true, Triggers.PeriodSeconds(5), Triggers.Minutely()); + +// 通过 JobBuilder 和 0 或 N 个 TriggerBuilder 创建 +var schedulerBuilder = SchedulerBuilder.Create( + JobBuilder.Create() + , Triggers.PeriodSeconds(5), Triggers.Minutely()); +``` + +2. **通过 `IScheduler` 接口创建** + +这种方式常用于在运行时更新作业信息。 + +```cs showLineNumbers +var schedulerBuilder = SchedulerBuilder.From(scheduler); + +//也可以通过以下方式 +var schedulerBuilder = scheduler.GetBuilder(); +``` + +3. **通过 `JSON` 字符串创建** + +该方式非常灵活,可从配置文件,`JSON` 字符串,或其他能够返回 `JSON` 字符串的地方创建。 + +```cs showLineNumbers {1,3,14} +var schedulerBuilder = SchedulerBuilder.From(@" +{ + ""jobDetail"": { + ""jobId"": ""job1"", + ""groupName"": null, + ""jobType"": ""MyJob"", + ""assemblyName"": ""ConsoleApp32"", + ""description"": null, + ""concurrent"": true, + ""includeAnnotations"": false, + ""properties"": ""{}"", + ""updatedTime"": ""2022-12-04 11:51:00.483"" + }, + ""triggers"": [{ + ""triggerId"": ""job1_trigger1"", + ""jobId"": ""job1"", + ""triggerType"": ""Furion.Schedule.PeriodTrigger"", + ""assemblyName"": ""Furion"", + ""args"": ""[5000]"", + ""description"": null, + ""status"": 2, + ""startTime"": null, + ""endTime"": null, + ""lastRunTime"": ""2022-12-04 17:52:34.768"", + ""nextRunTime"": ""2022-12-04 17:52:39.769"", + ""numberOfRuns"": 1, + ""maxNumberOfRuns"": 0, + ""numberOfErrors"": 0, + ""maxNumberOfErrors"": 0, + ""numRetries"": 0, + ""retryTimeout"": 1000, + ""startNow"": true, + ""runOnStart"": false, + ""resetOnlyOnce"": true, + ""result"": null, + ""elapsedTime"": 100, + ""updatedTime"": ""2022-12-04 17:52:34.803"" + }] +} +"); +``` + +如果使用的是 `.NET7`,可使用 `"""` 避免转义,如: + +```cs showLineNumbers {1,3,14} +var schedulerBuilder = SchedulerBuilder.From(""" +{ + "jobDetail": { + "jobId": "job1", + "groupName": null, + "jobType": "MyJob", + "assemblyName": "ConsoleApp32", + "description": null, + "concurrent": true, + "includeAnnotations": false, + "properties": "{}", + "updatedTime": "2022-12-04 11:51:00.483" + }, + "triggers": [{ + "triggerId": "job1_trigger1", + "jobId": "job1", + "triggerType": "Furion.Schedule.PeriodTrigger", + "assemblyName": "Furion", + "args": "[5000]", + "description": null, + "status": 2, + "startTime": null, + "endTime": null, + "lastRunTime": "2022-12-04 17:52:34.768", + "nextRunTime": "2022-12-04 17:52:39.769", + "numberOfRuns": 1, + "maxNumberOfRuns": 0, + "numberOfErrors": 0, + "maxNumberOfErrors": 0, + "numRetries": 0, + "retryTimeout": 1000, + "startNow": true, + "runOnStart": false, + "resetOnlyOnce": true, + "result": null, + "elapsedTime": 10, + "updatedTime": "2022-12-04 17:52:34.803" + }] +} +"""); +``` + +:::important 关于属性名匹配规则 + +支持 `CamelCase(驼峰命名法)`,`Pascal(帕斯卡命名法)` 命名方式。 + +**不支持 `UnderScoreCase(下划线命名法)`** ,如 `"include_annotations": true` + +::: + +4. **还可以通过 `Clone` 静态方法从一个 `SchedulerBuilder` 创建** + +```cs showLineNumbers +var schedulerBuilder = SchedulerBuilder.Clone(fromSchedulerBuilder); +``` + +:::important 克隆说明 + +克隆操作将克隆 `JobBuilder` 和 `TriggerBuilders`,同时持久化行为会被标记为 `Appended`。 + +::: + +### 26.1.6.3 设置作业计划构建器 + +`SchedulerBuilder` 提供了多个方法操作 `JobBuilder` 和 `TriggerBuilder`,如: + +```cs showLineNumbers +// 获取作业信息构建器 +var jobBuilder = schedulerBuilder.GetJobBuilder(); + +// 获取所有作业触发器构建器 +var triggerBuilders = schedulerBuilder.GetTriggerBuilders(); + +// 获取单个作业触发器构建器 +var triggerBuilder = schedulerBuilder.GetTriggerBuilder("job1_trigger1"); +var triggerBuilder = schedulerBuilder.GetTriggerBuilder("not_found_trigger_id"); // => null + +// 更新作业信息构建器 +schedulerBuilder.UpdateJobBuilder(jobBuilder); +// 如果通过 .GetJobBuilder() 方式获取,那么可直接更新,无需调用 .UpdateJobBuilder(jobBuilder); +schedulerBuilder.UpdateJobBuilder(newJobBuilder, replace: true); + +// 添加作业触发器构建器 +schedulerBuilder.AddTriggerBuilder(triggerBuilder1, triggerBuilder2, ...); + +// 更新作业触发器构建器 +schedulerBuilder.UpdateTriggerBuilder(triggerBuilder1, triggerBuilder2, ...); +// 还可以选择覆盖更新还是不覆盖 +schedulerBuilder.UpdateTriggerBuilder(new[] { triggerBuilder1, triggerBuilder2, ... }, replace: true); + +// 删除作业触发器构建器,注意不是真的删除,而是标记为 Removed 删除状态 +schedulerBuilder.RemoveTriggerBuilder("trigger1", "trigger2", ...); + +// 清除所有作业触发器构建器,注意不是真的删除,而是标记为 Removed 删除状态 +schedulerBuilder.ClearTriggerBuilders(); + +// 输出为 JSON 格式 +var json = schedulerBuilder.ConvertToJSON(); +var json = schedulerBuilder.ConvertToJSON(NamingConventions.CamelCase); + +// 将作业计划构建器转换成可枚举的 Dictionary +foreach(var (jobBuilder, triggerBuilder) in schedulerBuilder.GetEnumerable()) +{ + // .... +} +``` + +### 26.1.6.4 作业计划构建器持久化方法 + +作业计划构建器 `SchedulerBuilder` 提供了三个标记作业持久化行为的方法: + +- `Appended()`:标记作业计划构建器是新增的,届时生成的 `SQL` 是 `INSERT` 语句 +- `Updated()`:标记作业计划构建器已被更新,届时生成的 `SQL` 是 `Updated` 语句,如果标记为此操作,那么当前作业调度器初始化时将新增至内存中 +- `Removed()`:标记作业计划构建器已被删除,届时生成的 `SQL` 是 `Deleted` 语句,如果标记为此操作,那么当前作业调度器初始化时将不会添加至调度器中 + +```cs showLineNumbers {3-5} +services.AddSchedule(options => +{ + options.AddJob(SchedulerBuilder.Create("job1").Appended() + , SchedulerBuilder.Create("job2").Updated() + , SchedulerBuilder.Create("job3").Removed()); +}); +``` + +查看作业调度器初始化日志: + +```bash showLineNumbers {6,8,10,12} +info: 2022-12-05 12:14:42.8481157 +08:00 星期一 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is running. +info: 2022-12-05 12:14:42.8597028 +08:00 星期一 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is preloading... +info: 2022-12-05 12:14:42.9360896 +08:00 星期一 L System.Logging.ScheduleService[0] #1 + The scheduler of successfully appended to the schedule. +info: 2022-12-05 12:14:42.9471072 +08:00 星期一 L System.Logging.ScheduleService[0] #1 + The scheduler of successfully appended and updated to the schedule. +info: 2022-12-05 12:14:42.9562673 +08:00 星期一 L System.Logging.ScheduleService[0] #1 + The scheduler of successfully removed to the schedule. +warn: 2022-12-05 12:14:42.9748930 +08:00 星期一 L System.Logging.ScheduleService[0] #1 + Schedule hosted service preload completed, and a total of <2> schedulers are appended. +``` + +### 26.1.6.5 多种格式字符串输出 + +`Scheduler/IScheduler` 和 `SchedulerBuilder` 都提供了多种将自身转换成特定格式的字符串。 + +1. **转换成 `JSON` 字符串** + +```cs showLineNumbers +var json = schedulerBuilder.ConvertToJSON(); +``` + +字符串打印如下: + +```json showLineNumbers {2,13} +{ + "jobDetail": { + "jobId": "job1", + "groupName": null, + "jobType": "MyJob", + "assemblyName": "ConsoleApp32", + "description": null, + "concurrent": true, + "includeAnnotations": false, + "properties": "{}", + "updatedTime": "2022-12-04 11:51:00.483" + }, + "triggers": [ + { + "triggerId": "job1_trigger1", + "jobId": "job1", + "triggerType": "Furion.Schedule.PeriodTrigger", + "assemblyName": "Furion", + "args": "[5000]", + "description": null, + "status": 2, + "startTime": null, + "endTime": null, + "lastRunTime": "2022-12-04 17:52:34.768", + "nextRunTime": "2022-12-04 17:52:39.769", + "numberOfRuns": 1, + "maxNumberOfRuns": 0, + "numberOfErrors": 0, + "maxNumberOfErrors": 0, + "numRetries": 0, + "retryTimeout": 1000, + "startNow": true, + "runOnStart": false, + "resetOnlyOnce": true, + "result": null, + "elapsedTime": 100, + "updatedTime": "2022-12-04 17:52:34.803" + } + ] +} +``` + +## 26.1.7 作业调度器 `ScheduleOptionsBuilder` 配置选项 + +### 26.1.7.1 关于 `ScheduleOptionsBuilder` + +`ScheduleOptionsBuilder` 配置选项主要是用来初始化作业调度器及相关服务配置的。只作为 `services.AddSchedule` 服务注册的配置参数,如: + +```cs showLineNumbers {2,8-9} +// 通过委托的方式配置 +services.AddSchedule(options => +{ + // options 类型为 ScheduleOptionsBuilder +}); + +// 自行创建对象实例方式配置 +var scheduleOptionsBuilder = new ScheduleOptionsBuilder(); +services.AddSchedule(scheduleOptionsBuilder); +``` + +### 26.1.7.2 `ScheduleOptionsBuilder` 内置属性和方法 + +- **内置属性配置** + +```cs showLineNumbers {4,7,10,13,16,21,24,29} +services.AddSchedule(options => +{ + // 是否使用 UTC 时间,该配置主要用来作为作业调度器检查时间格式的依据 + options.UseUtcTimestamp = false; + + // 是否输出作业调度器日志 + options.LogEnabled = true; + + // 配置集群 Id,默认值为启动程序集的名称 + options.ClusterId = "cluster1"; + + // 配置输出 SQL 的数据库类型,Furion 4.8.2.3+ + options.BuildSqlType = SqlTypes.SqlServer; + + // 配置作业信息 JobDetail 相关配置,如配置自定义 SQL 输出 + options.JobDetail.ConvertToSQL((tableName, columnNames, jobDetail, behavior, naming) => + { + }); + + // 启用作业执行日志输出,Furion 4.8.3.7+ 版本支持 + options.JobDetail.LogEnabled = true; // 默认 false + + // 配置作业触发器 Trigger 相关配置,如配置自定义 SQL 输出 + options.Trigger.ConvertToSQL((tableName, columnNames, trigger, behavior, naming) => + { + }); + + // 定义未捕获的异常,通常是 Task 异常 + options.UnobservedTaskExceptionHandler = (obj, args) => + { + }; +}); +``` + +- **内置方法配置** + +```cs showLineNumbers {4-18,21-28,31,34,37,40} +services.AddSchedule(options => +{ + // 添加作业 + options.AddJob(schedulerBuilder); + options.AddJob(schedulerBuilder, schedulerBuilder1, ...); // Furion 4.8.2.4+ + options.AddJob(jobBuilder, triggerBuilder, ...); + options.AddJob(triggerBuilder, ...); + options.AddJob("作业 Id", triggerBuilder, ...); + options.AddJob("作业 Id", concurrent: true, triggerBuilder, ...); + options.AddJob(concurrent: true, triggerBuilder, ...); + options.AddJob(typeof(MyJob), triggerBuilder, ...); + options.AddJob(typeof(MyJob), "作业 Id", triggerBuilder, ...); + options.AddJob(typeof(MyJob), "作业 Id", concurrent: true, triggerBuilder, ...); + options.AddJob(typeof(MyJob), concurrent: true, triggerBuilder, ...); + options.AddJob((context, stoppingToken) => {}, triggerBuilder, ...); + options.AddJob((context, stoppingToken) => {}, "作业 Id", triggerBuilder, ...); + options.AddJob((context, stoppingToken) => {}, "作业 Id", concurrent: true, triggerBuilder, ...); + options.AddJob((context, stoppingToken) => {}, concurrent: true, triggerBuilder, ...); + + // 添加 HTTP Job,Furion 4.8.7.7+ + options.AddHttpJob(request => {}, triggerBuilder, ...); + options.AddHttpJob(request => {}, "作业 Id", triggerBuilder, ...); + options.AddHttpJob(request => {}, "作业 Id", concurrent: true, triggerBuilder, ...); + options.AddHttpJob(request => {}, concurrent: true, triggerBuilder, ...); + options.AddHttpJob(request => {}, triggerBuilder, ...); + options.AddHttpJob(request => {}, "作业 Id", triggerBuilder, ...); + options.AddHttpJob(request => {}, "作业 Id", concurrent: true, triggerBuilder, ...); + options.AddHttpJob(request => {}, concurrent: true, triggerBuilder, ...); + + // 添加作业执行监视器 + options.AddMonitor(); + + // 添加作业执行器 + options.AddExecutor(); + + // 添加作业持久化器 + options.AddPersistence(); + + // 注册作业集群服务 + options.AddClusterServer(); +}); +``` + +## 26.1.8 作业监视器 `IJobMonitor` + +调度作业服务提供了 `IJobMonitor` 监视器接口,实现该接口可以监视所有作业处理程序执行事件,包括 `执行之前、执行之后,执行异常`。 + +通过作业监视器可以实现作业完整生命周期控制,还能实现作业执行异常发送短信或邮件通知管理员或项目维护者。 + +如添加 `YourJobMonitor`: + +```cs showLineNumbers {1,9,15} +public class YourJobMonitor : IJobMonitor +{ + private readonly ILogger _logger; + public YourJobMonitor(ILogger logger) + { + _logger = logger; + } + + public Task OnExecutingAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + _logger.LogInformation("执行之前:{context}", context); + return Task.CompletedTask; + } + + public Task OnExecutedAsync(JobExecutedContext context, CancellationToken stoppingToken) + { + _logger.LogInformation("执行之后:{context}", context); + + if (context.Exception != null) + { + _logger.LogError(context.Exception, "执行出错啦:{context}", context); + } + + return Task.CompletedTask; + } +} +``` + +最后,在注册 `Schedule` 服务中注册 `YourJobMonitor`: + +```cs showLineNumbers {4} +services.AddSchedule(options => +{ + // 添加作业执行监视器 + options.AddMonitor(); +}); +``` + +执行结果如下: + +```bash showLineNumbers {12,16,26,28} +info: 2022-12-05 14:09:47.2337395 +08:00 星期一 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is running. +info: 2022-12-05 14:09:47.2401561 +08:00 星期一 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is preloading... +info: 2022-12-05 14:09:47.2780446 +08:00 星期一 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended to the schedule. +info: 2022-12-05 14:09:47.2810119 +08:00 星期一 L System.Logging.ScheduleService[0] #1 + The scheduler of successfully appended to the schedule. +warn: 2022-12-05 14:09:47.2941716 +08:00 星期一 L System.Logging.ScheduleService[0] #1 + Schedule hosted service preload completed, and a total of <1> schedulers are appended. +info: 2022-12-05 14:09:52.3190129 +08:00 星期一 L ConsoleApp32.YourJobMonitor[0] #4 + 执行之前: [C] 5s 1ts 2022-12-05 14:09:52.241 -> 2022-12-05 14:09:57.260 +info: 2022-12-05 14:09:52.3240208 +08:00 星期一 L MyJob[0] #4 + [C] 5s 1ts 2022-12-05 14:09:52.241 -> 2022-12-05 14:09:57.260 +fail: 2022-12-05 14:09:52.5253398 +08:00 星期一 L System.Logging.ScheduleService[0] #4 + Error occurred executing [C] 5s 1ts 2022-12-05 14:09:52.241 -> 2022-12-05 14:09:57.260. + ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + System.Exception: 模拟出错 + at MyJob.ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) in D:\Workplaces\Study\CSharp\ConsoleApp32\ConsoleApp32\Program.cs:line 28 + at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass23_3.<b__3>d.MoveNext() in D:\Workplaces\OpenSources\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 220 + --- End of stack trace from previous location --- + at Furion.FriendlyException.Retry.InvokeAsync(Func`1 action, Int32 numRetries, Int32 retryTimeout, Boolean finalThrow, Type[] exceptionTypes, Func`2 fallbackPolicy, Action`2 retryAction) in D:\Workplaces\OpenSources\Furion\framework\Furion\FriendlyException\Retry.cs:line 87 + at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass23_2.<b__2>d.MoveNext() in D:\Workplaces\OpenSources\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 218 + ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +info: 2022-12-05 14:09:52.5288429 +08:00 星期一 L ConsoleApp32.YourJobMonitor[0] #4 + 执行之后: [C] 5s 1ts 2022-12-05 14:09:52.241 -> 2022-12-05 14:09:57.260 +fail: 2022-12-05 14:09:52.5318526 +08:00 星期一 L ConsoleApp32.YourJobMonitor[0] #4 + 执行出错啦: [C] 5s 1ts 2022-12-05 14:09:52.241 -> 2022-12-05 14:09:57.260 + ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + System.InvalidOperationException: Error occurred executing [C] 5s 1ts 2022-12-05 14:09:52.241 -> 2022-12-05 14:09:57.260. + ---> System.Exception: 模拟出错 + at MyJob.ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) in D:\Workplaces\Study\CSharp\ConsoleApp32\ConsoleApp32\Program.cs:line 28 + at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass23_3.<b__3>d.MoveNext() in D:\Workplaces\OpenSources\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 220 + --- End of stack trace from previous location --- + at Furion.FriendlyException.Retry.InvokeAsync(Func`1 action, Int32 numRetries, Int32 retryTimeout, Boolean finalThrow, Type[] exceptionTypes, Func`2 fallbackPolicy, Action`2 retryAction) in D:\Workplaces\OpenSources\Furion\framework\Furion\FriendlyException\Retry.cs:line 87 + at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass23_2.<b__2>d.MoveNext() in D:\Workplaces\OpenSources\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 218 + --- End of inner exception stack trace --- + ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +``` + +**还可以设置执行失败重试,如:** + +```cs showLineNumbers {3} +services.AddSchedule(options => +{ + options.AddJob(Triggers.PeriodSeconds(5).SetNumRetries(3)); // 重试 3 次 + options.AddMonitor(); +}); +``` + +执行结果如下: + +```bash showLineNumbers {12,16,20,24,28,39,41} +info: 2022-12-05 14:25:15.9316915 +08:00 星期一 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is running. +info: 2022-12-05 14:25:15.9391765 +08:00 星期一 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is preloading... +info: 2022-12-05 14:25:15.9737767 +08:00 星期一 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended to the schedule. +info: 2022-12-05 14:25:15.9754882 +08:00 星期一 L System.Logging.ScheduleService[0] #1 + The scheduler of successfully appended to the schedule. +warn: 2022-12-05 14:25:15.9892059 +08:00 星期一 L System.Logging.ScheduleService[0] #1 + Schedule hosted service preload completed, and a total of <1> schedulers are appended. +info: 2022-12-05 14:25:21.0056685 +08:00 星期一 L ConsoleApp32.YourJobMonitor[0] #4 + 执行之前: [C] 5s 1ts 2022-12-05 14:25:20.937 -> 2022-12-05 14:25:25.949 +info: 2022-12-05 14:25:21.0140485 +08:00 星期一 L MyJob[0] #4 + [C] 5s 1ts 2022-12-05 14:25:20.937 -> 2022-12-05 14:25:25.949 +warn: 2022-12-05 14:25:21.0754973 +08:00 星期一 L System.Logging.ScheduleService[0] #4 + Retrying 1/3 times for [C] 5s 1ts 2022-12-05 14:25:20.937 -> 2022-12-05 14:25:25.949 +info: 2022-12-05 14:25:22.0935914 +08:00 星期一 L MyJob[0] #4 + [C] 5s 1ts 2022-12-05 14:25:20.937 -> 2022-12-05 14:25:25.949 +warn: 2022-12-05 14:25:22.1574937 +08:00 星期一 L System.Logging.ScheduleService[0] #4 + Retrying 2/3 times for [C] 5s 1ts 2022-12-05 14:25:20.937 -> 2022-12-05 14:25:25.949 +info: 2022-12-05 14:25:23.1666732 +08:00 星期一 L MyJob[0] #4 + [C] 5s 1ts 2022-12-05 14:25:20.937 -> 2022-12-05 14:25:25.949 +warn: 2022-12-05 14:25:23.2213212 +08:00 星期一 L System.Logging.ScheduleService[0] #4 + Retrying 3/3 times for [C] 5s 1ts 2022-12-05 14:25:20.937 -> 2022-12-05 14:25:25.949 +info: 2022-12-05 14:25:24.2337356 +08:00 星期一 L MyJob[0] #4 + [C] 5s 1ts 2022-12-05 14:25:20.937 -> 2022-12-05 14:25:25.949 +fail: 2022-12-05 14:25:24.3832385 +08:00 星期一 L System.Logging.ScheduleService[0] #4 + Error occurred executing [C] 5s 1ts 2022-12-05 14:25:20.937 -> 2022-12-05 14:25:25.949. + ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + System.Exception: 模拟出错 + at MyJob.ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) in D:\Workplaces\Study\CSharp\ConsoleApp32\ConsoleApp32\Program.cs:line 28 + at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass23_3.<b__3>d.MoveNext() in D:\Workplaces\OpenSources\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 220 + --- End of stack trace from previous location --- + at Furion.FriendlyException.Retry.InvokeAsync(Func`1 action, Int32 numRetries, Int32 retryTimeout, Boolean finalThrow, Type[] exceptionTypes, Func`2 fallbackPolicy, Action`2 retryAction) in D:\Workplaces\OpenSources\Furion\framework\Furion\FriendlyException\Retry.cs:line 99 + at Furion.FriendlyException.Retry.InvokeAsync(Func`1 action, Int32 numRetries, Int32 retryTimeout, Boolean finalThrow, Type[] exceptionTypes, Func`2 fallbackPolicy, Action`2 retryAction) in D:\Workplaces\OpenSources\Furion\framework\Furion\FriendlyException\Retry.cs:line 110 + at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass23_2.<b__2>d.MoveNext() in D:\Workplaces\OpenSources\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 218 + ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +info: 2022-12-05 14:25:24.3857991 +08:00 星期一 L ConsoleApp32.YourJobMonitor[0] #4 + 执行之后: [C] 5s 1ts 2022-12-05 14:25:20.937 -> 2022-12-05 14:25:25.949 +fail: 2022-12-05 14:25:24.3888126 +08:00 星期一 L ConsoleApp32.YourJobMonitor[0] #4 + 执行出错啦: [C] 5s 1ts 2022-12-05 14:25:20.937 -> 2022-12-05 14:25:25.949 + ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + System.InvalidOperationException: Error occurred executing [C] 5s 1ts 2022-12-05 14:25:20.937 -> 2022-12-05 14:25:25.949. + ---> System.Exception: 模拟出错 + at MyJob.ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) in D:\Workplaces\Study\CSharp\ConsoleApp32\ConsoleApp32\Program.cs:line 28 + at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass23_3.<b__3>d.MoveNext() in D:\Workplaces\OpenSources\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 220 + --- End of stack trace from previous location --- + at Furion.FriendlyException.Retry.InvokeAsync(Func`1 action, Int32 numRetries, Int32 retryTimeout, Boolean finalThrow, Type[] exceptionTypes, Func`2 fallbackPolicy, Action`2 retryAction) in D:\Workplaces\OpenSources\Furion\framework\Furion\FriendlyException\Retry.cs:line 99 + at Furion.FriendlyException.Retry.InvokeAsync(Func`1 action, Int32 numRetries, Int32 retryTimeout, Boolean finalThrow, Type[] exceptionTypes, Func`2 fallbackPolicy, Action`2 retryAction) in D:\Workplaces\OpenSources\Furion\framework\Furion\FriendlyException\Retry.cs:line 110 + at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass23_2.<b__2>d.MoveNext() in D:\Workplaces\OpenSources\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 218 + --- End of inner exception stack trace --- + ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +``` + +### 26.1.8.1 关于参数 `JobExecutionContext` + +`IJobMonitor` 提供的 `OnExecutingAsync` 和 `OnExecutedAsync` 接口方法都包含一个 `context` 参数,前者是 `JobExecutingContext`,后者是 `JobExecutedContext`,它们都有一个共同的基类 `JobExecutionContext`。 + +`JobExecutionContext` 提供了以下公共属性和公共方法: + +- **`JobExecutionContext` 属性列表** + - `JobId`:作业 `Id` + - `TriggerId`:当前触发器 `Id` + - `JobDetail`:作业信息 + - `Trigger`:作业触发器 + - `OccurrenceTime`:**作业计划触发时间,最准确的记录时间** + - `RunId`:本次作业执行唯一 `Id`,`Furion 4.8.5.1+` 提供 + - `Result`:设置/读取本次作业执行结果,`Furion 4.8.7.7+` 提供 + - `ServiceProvider`:服务提供器,`Furion 4.8.7.10+` 提供 +- **`JobExecutionContext` 方法列表** + - `.ConvertToJSON(naming)`:将作业计划转换成 `JSON` 字符串 + - `.ToString()`:将作业执行信息输出为简要字符串 + +

+ +- **`JobExecutingContext`** 在基类基础上拓展了 `ExecutingTime` 属性: + - `ExecutingTime`:执行前时间 +- **`JobExecutedContext`** 则在基类基础上拓展了 `ExecutedTime` 和 `Exception` 属性: + - `ExecutedTime`:执行后时间 + - `Exception`:执行异常 + +## 26.1.9 作业执行器 `IJobExecutor` + +调度作业服务提供了 `IJobExecutor` 执行器接口,可以让开发者自定义作业处理函数执行策略,如 `超时控制,失败重试等等`。 + +### 26.1.9.1 实现重试策略 + +如添加 `YourJobExecutor`: + +```cs showLineNumbers {1,12,14,17-20} +public class YourJobExecutor : IJobExecutor +{ + private readonly ILogger _logger; + public YourJobExecutor(ILogger logger) + { + _logger = logger; + } + + public async Task ExecuteAsync(JobExecutingContext context, IJob jobHandler, CancellationToken stoppingToken) + { + // 实现失败重试策略,如失败重试 3 次 + await Retry.InvokeAsync(async () => + { + await jobHandler.ExecuteAsync(context, stoppingToken); + }, 3, 1000 + // 每次重试输出日志 + , retryAction: (total, times) => + { + _logger.LogWarning("Retrying {current}/{times} times for {context}", times, total, context); + }); + } +} +``` + +接着模拟 `MyJob` 执行出错: + +```cs showLineNumbers {1,13} +public class MyJob : IJob +{ + private readonly ILogger _logger; + public MyJob(ILogger logger) + { + _logger = logger; + } + + public Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + _logger.LogInformation($"{context}"); + + throw new Exception("模拟出错"); + return Task.CompletedTask; + } +} +``` + +最后,在注册 `Schedule` 服务中注册 `YourJobExecutor`: + +```cs showLineNumbers {4} +services.AddSchedule(options => +{ + // 添加作业执行器 + options.AddExecutor(); +}); +``` + +执行结果如下: + +```bash showLineNumbers {14,18,22} +info: 2022-12-05 14:36:41.2085688 +08:00 星期一 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is running. +info: 2022-12-05 14:36:41.2162510 +08:00 星期一 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is preloading... +info: 2022-12-05 14:36:41.2885816 +08:00 星期一 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended to the schedule. +info: 2022-12-05 14:36:41.2912130 +08:00 星期一 L System.Logging.ScheduleService[0] #1 + The scheduler of successfully appended to the schedule. +warn: 2022-12-05 14:36:41.3102057 +08:00 星期一 L System.Logging.ScheduleService[0] #1 + Schedule hosted service preload completed, and a total of <1> schedulers are appended. +info: 2022-12-05 14:36:46.3329097 +08:00 星期一 L MyJob[0] #13 + [C] 5s 1ts 2022-12-05 14:36:46.249 -> 2022-12-05 14:36:51.274 +warn: 2022-12-05 14:36:46.3910063 +08:00 星期一 L ConsoleApp32.YourJobExecutor[0] #13 + Retrying 1/3 times for [C] 5s 1ts 2022-12-05 14:36:46.249 -> 2022-12-05 14:36:51.274 +info: 2022-12-05 14:36:47.4014898 +08:00 星期一 L MyJob[0] #13 + [C] 5s 1ts 2022-12-05 14:36:46.249 -> 2022-12-05 14:36:51.274 +warn: 2022-12-05 14:36:47.4471172 +08:00 星期一 L ConsoleApp32.YourJobExecutor[0] #13 + Retrying 2/3 times for [C] 5s 1ts 2022-12-05 14:36:46.249 -> 2022-12-05 14:36:51.274 +info: 2022-12-05 14:36:48.4539737 +08:00 星期一 L MyJob[0] #13 + [C] 5s 1ts 2022-12-05 14:36:46.249 -> 2022-12-05 14:36:51.274 +warn: 2022-12-05 14:36:48.4880918 +08:00 星期一 L ConsoleApp32.YourJobExecutor[0] #13 + Retrying 3/3 times for [C] 5s 1ts 2022-12-05 14:36:46.249 -> 2022-12-05 14:36:51.274 +info: 2022-12-05 14:36:49.4984333 +08:00 星期一 L MyJob[0] #13 + [C] 5s 1ts 2022-12-05 14:36:46.249 -> 2022-12-05 14:36:51.274 +fail: 2022-12-05 14:36:49.6714485 +08:00 星期一 L System.Logging.ScheduleService[0] #13 + Error occurred executing [C] 5s 1ts 2022-12-05 14:36:46.249 -> 2022-12-05 14:36:51.274. + ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + System.Exception: 模拟出错 + at MyJob.ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) in D:\Workplaces\Study\CSharp\ConsoleApp32\ConsoleApp32\Program.cs:line 31 + at ConsoleApp32.YourJobExecutor.<>c__DisplayClass2_0.<b__0>d.MoveNext() in D:\Workplaces\Study\CSharp\ConsoleApp32\ConsoleApp32\YourJobExecutor.cs:line 20 + --- End of stack trace from previous location --- + at Furion.FriendlyException.Retry.InvokeAsync(Func`1 action, Int32 numRetries, Int32 retryTimeout, Boolean finalThrow, Type[] exceptionTypes, Func`2 fallbackPolicy, Action`2 retryAction) in D:\Workplaces\OpenSources\Furion\framework\Furion\FriendlyException\Retry.cs:line 99 + at Furion.FriendlyException.Retry.InvokeAsync(Func`1 action, Int32 numRetries, Int32 retryTimeout, Boolean finalThrow, Type[] exceptionTypes, Func`2 fallbackPolicy, Action`2 retryAction) in D:\Workplaces\OpenSources\Furion\framework\Furion\FriendlyException\Retry.cs:line 110 + at ConsoleApp32.YourJobExecutor.ExecuteAsync(JobExecutingContext context, IJob jobHandler, CancellationToken stoppingToken) in D:\Workplaces\Study\CSharp\ConsoleApp32\ConsoleApp32\YourJobExecutor.cs:line 18 + at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass23_2.<b__2>d.MoveNext() in D:\Workplaces\OpenSources\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 232 + ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +``` + +### 26.1.9.2 实现超时控制 + +如添加 `YourJobExecutor`: + +```cs showLineNumbers {12} +public class YourJobExecutor : IJobExecutor +{ + private readonly ILogger _logger; + public YourJobExecutor(ILogger logger) + { + _logger = logger; + } + + public async Task ExecuteAsync(JobExecutingContext context, IJob jobHandler, CancellationToken stoppingToken) + { + await jobHandler.ExecuteAsync(context, stoppingToken) + .WaitAsync(TimeSpan.FromMilliseconds(3000)); // 设置 3 秒超时 + } +} +``` + +接着模拟 `MyJob` 执行超时: + +```cs showLineNumbers {12} +public class MyJob : IJob +{ + private readonly ILogger _logger; + public MyJob(ILogger logger) + { + _logger = logger; + } + + public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + _logger.LogInformation($"{context}"); + await Task.Delay(6000); // 模拟耗时 6 秒 + } +} +``` + +执行结果如下: + +```bash showLineNumbers {16} +info: 2022-12-20 13:57:01.7251541 +08:00 星期二 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is running. +info: 2022-12-20 13:57:01.7336016 +08:00 星期二 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is preloading... +info: 2022-12-20 13:57:02.2846096 +08:00 星期二 L System.Logging.ScheduleService[0] #1 + The trigger for scheduler of successfully appended to the schedule. +info: 2022-12-20 13:57:02.3448819 +08:00 星期二 L System.Logging.ScheduleService[0] #1 + The scheduler of successfully appended to the schedule. +warn: 2022-12-20 13:57:02.3800053 +08:00 星期二 L System.Logging.ScheduleService[0] #1 + Schedule hosted service preload completed, and a total of <1> schedulers are appended. +info: 2022-12-20 13:57:07.3261111 +08:00 星期二 L MyJob[0] #14 + [C] 5s 1ts 2022-12-20 13:57:07.240 -> 2022-12-20 13:57:12.260 +fail: 2022-12-20 13:57:10.5743871 +08:00 星期二 L System.Logging.ScheduleService[0] #14 + Error occurred executing [C] 5s 1ts 2022-12-20 13:57:07.240 -> 2022-12-20 13:57:12.260. + ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + System.TimeoutException: The operation has timed out. + at YourJobExecutor.ExecuteAsync(JobExecutingContext context, IJob jobHandler, CancellationToken stoppingToken) in D:\Workplaces\Study\CSharp\ConsoleApp32\ConsoleApp32\Program.cs:line 41 + at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass23_2.<b__2>d.MoveNext() in D:\Workplaces\OpenSources\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 234 + ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +``` + +:::tip 关于 `WaitAsync` 说明 + +`WaitAsync` 是 `.NET6+` 新增的 `Task` 拓展方法,如需在 `.NET5` 中支持,可添加以下拓展: + +```cs showLineNumbers +public static async Task WaitAsync(this Task task, TimeSpan timeout) +{ + using var timeoutCancellationTokenSource = new CancellationTokenSource(); + var delayTask = Task.Delay(timeout, timeoutCancellationTokenSource.Token); + + if(await Task.WhenAny(task, delayTask) == task) + { + timeoutCancellationTokenSource.Cancel(); + await task; + } + else + { + throw new TimeoutException("The operation has timed out.") + } +} +``` + +::: + +### 26.1.9.3 更多控制 + +作业执行器功能远不止于此,通过自定义作业执行器还可以实现分片作业,关联子作业,故障转移,集群等控制。 + +## 26.1.10 作业计划工厂 `ISchedulerFactory` + +作业计划工厂提供了程序运行时操作作业调度器,作业计划等诸多方法。 + +`ISchedulerFactory` 被注册为 `单例` 服务,允许在任何可依赖注入的服务获取,如: + +```cs showLineNumbers {4,8,11,16} +public class YourService: IYourService +{ + private readonly ISchedulerFactory _schedulerFactory; + public YourService(ISchedulerFactory schedulerFactory) + { + _schedulerFactory = schedulerFactory; + + // 也可以通过 App.GetService() 获取 + } + + public void SomeMethod([FromServices]ISchedulerFactory schedulerFactory) + { + } + + // .NET7+ 或 Furion 4.8.0+ + public void SomeMethod(ISchedulerFactory schedulerFactory) + { + } +} +``` + +### 26.1.10.1 查找所有作业 + +```cs showLineNumbers {1,5,9,13} +// 查找所有作业,包括 JobType == null 的非有效作业 +var jobs = _schedulerFactory.GetJobs(); +var jobsOfModels = _schedulerFactory.GetJobsOfModels(); + +// 查找特定分组的作业,包括 JobType == null 的非有效作业 +var jobs = _schedulerFactory.GetJobs("group1"); +var jobsOfModels = _schedulerFactory.GetJobsOfModels("group1"); + +// 查找所有作业,仅 JobType != null 有效作业 +var jobs = _schedulerFactory.GetJobs(active: true); +var jobsOfModels = _schedulerFactory.GetJobsOfModels(active: true); + +// 查找特定分组的作业,仅 JobType != null 有效作业 +var jobs = _schedulerFactory.GetJobs("group1", true); +var jobsOfModels = _schedulerFactory.GetJobsOfModels("group1", true); +``` + +### 26.1.10.2 查找下一批触发的作业 + +```cs showLineNumbers {1,5} +// 查找下一批触发的作业 +var nextRunJobs = _schedulerFactory.GetNextRunJobs(DateTime.Now); +var nextRunJobsOfModels = _schedulerFactory.GetNextRunJobsOfModels(DateTime.Now); + +// 查找特定分组下一批触发的作业 +var nextRunJobs = _schedulerFactory.GetNextRunJobs(DateTime.Now, "group1"); +var nextRunJobsOfModels = _schedulerFactory.GetNextRunJobsOfModels(DateTime.Now, "group1"); +``` + +### 26.1.10.3 获取单个作业 + +```cs showLineNumbers {1,6} +// 返回 ScheduleResult 类型 +var scheduleResult = _schedulerFactory.TryGetJob("job1", out var scheduler); // 如果存在返回 => ScheduleResult.Succeed +var scheduleResult = _schedulerFactory.TryGetJob("not_found", out var scheduler); // => ScheduleResult.NotFound +var scheduleResult = _schedulerFactory.TryGetJob("", out var scheduler); // => ScheduleResult.NotIdentify + +// 返回 IScheduler 类型 +var scheduler = _schedulerFactory.GetJob("job1"); // 如果存在返回 IScheduler +var scheduler = _schedulerFactory.GetJob("not_found"); // => null +var scheduler = _schedulerFactory.GetJob(""); // => null +``` + +### 26.1.10.4 保存作业 + +保存作业是框架提供强大且简单的方法,支持 `新增`,`编辑`,`删除` 作业,也就是三大操作都可以直接通过此方法直接操作。 + +```cs showLineNumbers +// 返回 ScheduleResult 类型 +var scheduleResult = _schedulerFactory.TrySaveJob(schedulerBuilder, out var scheduler); + +// 无返回值,支持多个 +_schedulerFactory.SaveJob(schedulerBuilder1, schedulerBuilder2, ...) +``` + +:::tip 关于保存作业的背后行为 + +默认情况下,保存作业需要传递 `SchedulerBuilder` 对象,这个对象可通过 `GetJob(jobId)` 获取,如: + +```cs showLineNumbers +var schedulerBuilder = _schedulerFactory.GetJob("jobId")?.GetBuilder(); +``` + +此时它的内部 `Behavior` 属性被标记为 `PersistenceBehavior.Updated`,也就是更新状态,那么对于这个构建器的任何操作都会标记为 `更新` 操作。 + +如果通过 `.Appended()` 或 `.Removed()` 方法标记之后,那么它的操作行为就发生变化了。 + +- **如果被标记为 `.Appended()`**,那么它将进行 `新增` 操作。如: + +```cs showLineNumbers +schedulerBuilder.Appended(); +``` + +- **如果被标记为 `.Removed()`**,那么它将进行 `删除` 操作。如: + +```cs showLineNumbers +schedulerBuilder.Removed(); +``` + +比如以下的代码实则是 `新增` 或 `删除` 或 `更新` 操作: + +```cs showLineNumbers {1,4,8} +// 实际做新增操作 +var scheduleResult = _schedulerFactory.TrySaveJob(SchedulerBuilder.Create(), out var scheduler); // Create 方法默认标记为 Appended + +// 实际做删除操作 +var schedulerBuilder = _schedulerFactory.GetJob("jobId")?.GetBuilder(); +var scheduleResult = _schedulerFactory.TrySaveJob(schedulerBuilder?.Removed(), out var scheduler); // 标记为 Removed + +// 实际做更新操作 +var scheduleResult = _schedulerFactory.TrySaveJob(SchedulerBuilder.Create().Updated(), out var scheduler); // Create 方法默认标记为 Appended,但调用 Updated() 方法 +``` + +另外,作业触发器 `Trigger` 也具备相同的行为。 + +::: + +### 26.1.10.5 添加作业 + +框架提供了非常多的重载方法添加作业,如: + +```cs showLineNumbers {1,5,9,22,35,48,62} +// SchedulerBuilder 方式 +var scheduleResult = _schedulerFactory.TryAddJob(schedulerBuilder, out var scheduler); +_schedulerFactory.AddJob(schedulerBuilder1, schedulerBuilder2, ...); + +// JobBuilder + TriggerBuilders 方式 +var scheduleResult = _schedulerFactory.TryAddJob(jobBuilder, new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler); +_schedulerFactory.TryAddJob(jobBuilder, triggerBuilder1, triggerBuilder2, ...); + +// 泛型方式 +var scheduleResult = _schedulerFactory.TryAddJob(new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler); +_schedulerFactory.AddJob(triggerBuilder1, triggerBuilder2, ...); +// 支持配置作业 Id +var scheduleResult = _schedulerFactory.TryAddJob("job1", new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler); +_schedulerFactory.AddJob("job1", triggerBuilder1, triggerBuilder2, ...); +// 支持配置作业 Id + 串行/并行 +var scheduleResult = _schedulerFactory.TryAddJob("job1", true, new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler); +_schedulerFactory.AddJob("job1", true, triggerBuilder1, triggerBuilder2, ...); +// 支持配置 串行/ 并行 +var scheduleResult = _schedulerFactory.TryAddJob(true, new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler); +_schedulerFactory.AddJob(true, triggerBuilder1, triggerBuilder2, ...); + +// 类型方式 +var scheduleResult = _schedulerFactory.TryAddJob(typeof(MyJob), new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler); +_schedulerFactory.AddJob(typeof(MyJob), triggerBuilder1, triggerBuilder2, ...); +// 支持配置作业 Id +var scheduleResult = _schedulerFactory.TryAddJob(typeof(MyJob), "job1", new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler); +_schedulerFactory.AddJob(typeof(MyJob), "job1", triggerBuilder1, triggerBuilder2, ...); +// 支持配置作业 Id + 串行/并行 +var scheduleResult = _schedulerFactory.TryAddJob(typeof(MyJob), "job1", true, new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler); +_schedulerFactory.AddJob(typeof(MyJob), "job1", true, triggerBuilder1, triggerBuilder2, ...); +// 支持配置 串行/ 并行 +var scheduleResult = _schedulerFactory.TryAddJob(typeof(MyJob), true, new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler); +_schedulerFactory.AddJob(typeof(MyJob), true, triggerBuilder1, triggerBuilder2, ...); + +// 动态作业委托方式 +var scheduleResult = _schedulerFactory.TryAddJob((context, stoppingToken) => { }, new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler); +_schedulerFactory.AddJob((context, stoppingToken) => { }, triggerBuilder1, triggerBuilder2, ...); +// 支持配置作业 Id +var scheduleResult = _schedulerFactory.TryAddJob((context, stoppingToken) => { }, "job1", new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler); +_schedulerFactory.AddJob((context, stoppingToken) => { }, "job1", triggerBuilder1, triggerBuilder2, ...); +// 支持配置作业 Id + 串行/并行 +var scheduleResult = _schedulerFactory.TryAddJob((context, stoppingToken) => { }, "job1", true, new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler); +_schedulerFactory.AddJob((context, stoppingToken) => { }, "job1", true, triggerBuilder1, triggerBuilder2, ...); +// 支持配置 串行/ 并行 +var scheduleResult = _schedulerFactory.TryAddJob((context, stoppingToken) => { }, true, new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler); +_schedulerFactory.AddJob((context, stoppingToken) => { }, true, triggerBuilder1, triggerBuilder2, ...); + +// HTTP 作业,Furion 4.8.7.7+ +// 泛型方式 +var scheduleResult = _schedulerFactory.TryAddHttpJob(request => {}, new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler); +_schedulerFactory.AddHttpJob(request => {}, triggerBuilder1, triggerBuilder2, ...); +// 支持配置作业 Id +var scheduleResult = _schedulerFactory.TryAddHttpJob(request => {}, "job1", new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler); +_schedulerFactory.AddHttpJob(request => {}, "job1", triggerBuilder1, triggerBuilder2, ...); +// 支持配置作业 Id + 串行/并行 +var scheduleResult = _schedulerFactory.TryAddHttpJob(request => {}, "job1", true, new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler); +_schedulerFactory.AddHttpJob(request => {}, "job1", true, triggerBuilder1, triggerBuilder2, ...); +// 支持配置 串行/ 并行 +var scheduleResult = _schedulerFactory.TryAddHttpJob(request => {}, true, new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler); +_schedulerFactory.AddHttpJob(true, triggerBuilder1, triggerBuilder2, ...); + +// 默认方式 +var scheduleResult = _schedulerFactory.TryAddHttpJob(request => {}, new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler); +_schedulerFactory.AddHttpJob(request => {}, triggerBuilder1, triggerBuilder2, ...); +// 支持配置作业 Id +var scheduleResult = _schedulerFactory.TryAddHttpJob(request => {}, "job1", new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler); +_schedulerFactory.AddHttpJob(request => {}, "job1", triggerBuilder1, triggerBuilder2, ...); +// 支持配置作业 Id + 串行/并行 +var scheduleResult = _schedulerFactory.TryAddHttpJob(request => {}, "job1", true, new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler); +_schedulerFactory.AddHttpJob(request => {}, "job1", true, triggerBuilder1, triggerBuilder2, ...); +// 支持配置 串行/ 并行 +var scheduleResult = _schedulerFactory.TryAddHttpJob(request => {}, true, new[] { triggerBuilder1, triggerBuilder2, ...}, out var scheduler); +_schedulerFactory.AddHttpJob(request => {}, true, triggerBuilder1, triggerBuilder2, ...); +``` + +### 26.1.10.6 更新作业 + +```cs showLineNumbers +// 返回 ScheduleResult 方式 +var scheduleResult = _schedulerFactory.TryUpdateJob(schedulerBuilder, out var scheduler); + +// 无返回值方式 +_schedulerFactory.UpdateJob(schedulerBuilder1, schedulerBuilder2, ...); +``` + +### 26.1.10.7 删除作业 + +```cs showLineNumbers +// 返回 ScheduleResult 方式 +var scheduleResult = _schedulerFactory.TryRemoveJob("job1", out var scheduler); +var scheduleResult = _schedulerFactory.TryRemoveJob(scheduler); + +// 无返回值方式 +_schedulerFactory.RemoveJob("job1", "job2", ...); +_schedulerFactory.RemoveJob(scheduler1, scheduler2, ...); +``` + +### 26.1.10.8 检查作业是否存在 + +```cs showLineNumbers {1,4} +var isExist = _schedulerFactory.ContainsJob("job1"); + +// 还可以通过 group 查找 +var isExist = _schedulerFactory.ContainsJob("job1", "group1"); +``` + +### 26.1.10.9 启动所有作业 + +```cs showLineNumbers {1,4} +_schedulerFactory.StartAll(); + +// 还可以通过 group 启动 +_schedulerFactory.StartAll("group1"); +``` + +### 26.1.10.10 暂停所有作业 + +```cs showLineNumbers {1,4} + _schedulerFactory.PauseAll(); + +// 还可以通过 group 操作 + _schedulerFactory.PauseAll("group1"); +``` + +### 26.1.10.11 删除所有作业 + +```cs showLineNumbers {1,4} + _schedulerFactory.RemoveAll(); + +// 还可以通过 group 操作 + _schedulerFactory.RemoveAll("group1"); +``` + +### 26.1.10.12 强制触发所有作业持久化操作 + +```cs showLineNumbers {1,4} + _schedulerFactory.PersistAll(); + +// 还可以通过 group 操作 + _schedulerFactory.PersistAll("group1"); +``` + +### 26.1.10.13 校对所有作业 + +```cs showLineNumbers {1,4} + _schedulerFactory.CollateAll(); + +// 还可以通过 group 操作 + _schedulerFactory.CollateAll("group1"); +``` + +### 26.1.10.14 强制唤醒作业调度器 + +正常情况下,作业调度器会自动管理 `CPU` 休眠和唤醒,但一些特殊情况下需要强制唤醒作业调度器(比如调度器假死了,被回收了...),可通过以下方式: + +```cs showLineNumbers +_schedulerFactory.CancelSleep(); +``` + +### 26.1.10.15 立即执行作业 + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.7.11 +` 版本使用。 + +::: + +```cs showLineNumbers +// 带返回值 +var scheduleResult = _schedulerFactory.TryRunJob("job1"); +// 不带返回值 +_schedulerFactory.RunJob("job1"); +``` + +:::important 注意事项 + +如果作业本身处于 `(Pause)暂停`、`(NotStart)初始化时未启动`、`(Unoccupied)无触发时间` 状态,那么点击 **立即执行** 后将自动转至 `就绪` 状态。 + +::: + +## 26.1.11 作业计划 `IScheduler` + +作业计划 `Scheduler` 的默认实现接口是 `IScheduler`,该接口主要用来操作当前(单个)作业。 + +### 26.1.11.1 获取 `SchedulerModel` 实例 + +获取 `SchedulerModel` 之后可直接访问 `JobDetail` 和 `Trigger` 对象。 + +```cs showLineNumbers +var schedulerModel = scheduler.GetModel(); +``` + +### 26.1.11.2 获取 `SchedulerBuilder` + +```cs showLineNumbers +var schedulerBuilder = scheduler.GetBuilder(); +``` + +### 26.1.11.3 获取 `JobBuilder` + +```cs showLineNumbers +var jobBuilder = scheduler.GetJobBuilder(); +``` + +### 26.1.11.4 获取 `TriggerBuilder` 集合 + +```cs showLineNumbers +var triggerBuilders = scheduler.GetTriggerBuilders(); +``` + +### 26.1.11.5 获取单个 `TriggerBuilder` + +```cs showLineNumbers +var triggerBuilder = scheduler.GetTriggerBuilder("trigger1"); +``` + +### 26.1.11.6 获取作业信息 + +```cs showLineNumbers +var jobDetail = scheduler.GetJobDetail(); +``` + +### 26.1.11.7 获取作业触发器集合 + +```cs showLineNumbers +var triggers = scheduler.GetTriggers(); +``` + +### 26.1.11.8 获取单个作业触发器 + +```cs showLineNumbers {1,6} +// 返回 ScheduleResult 方式 +var scheduleResult = scheduler.TryGetTrigger("trigger1", out var trigger); // 如果存在返回 ScheduleResult.Succeed +var scheduleResult = scheduler.TryGetTrigger("not_found", out var trigger); // => ScheduleResult.NotFound +var scheduleResult = scheduler.TryGetTrigger("", out var trigger); // => ScheduleResult.NotIdentify + +// 返回 Trigger 方式 +var trigger = scheduler.GetTrigger("trigger1"); // 如果存在返回 Trigger +var trigger = scheduler.GetTrigger("not_found"); // => null +var trigger = scheduler.GetTrigger(""); // => null +``` + +### 26.1.11.9 保存作业触发器 + +保存作业触发器是框架提供强大且简单的方法,支持 `新增`,`编辑`,`删除` 作业触发器,也就是三大操作都可以直接通过此方法直接操作。 + +```cs showLineNumbers +// 返回 ScheduleResult 类型 +var scheduleResult = scheduler.TrySaveTrigger(triggerBuilder, out var trigger); + +// 无返回值,支持多个 +scheduler.SaveTrigger(triggerBuilder1, triggerBuilder2, ...) +``` + +:::tip 关于保存作业触发器的背后行为 + +默认情况下,保存作业触发器需要传递 `TriggerBuilder` 对象,这个对象可通过 `GetTriggerBuilder(triggerId)` 获取,如: + +```cs showLineNumbers +var triggerBuilder = scheduler.GetTriggerBuilder("trigger1"); +``` + +此时它的内部 `Behavior` 属性被标记为 `PersistenceBehavior.Updated`,也就是更新状态,那么对于这个构建器的任何操作都会标记为 `更新` 操作。 + +如果通过 `.Appended()` 或 `.Removed()` 方法标记之后,那么它的操作行为就发生变化了。 + +- **如果被标记为 `.Appended()`**,那么它将进行 `新增` 操作。如: + +```cs showLineNumbers +triggerBuilder.Appended(); +``` + +- **如果被标记为 `.Removed()`**,那么它将进行 `删除` 操作。如: + +```cs showLineNumbers +triggerBuilder.Removed(); +``` + +比如以下的代码实则是 `新增` 或 `删除` 或 `更新` 操作: + +```cs showLineNumbers {1,4,8} +// 实际做新增操作 +var scheduleResult = scheduler.TrySaveTrigger(Triggers.PeriodSeconds(5), out var trigger); // Create 方法默认标记为 Appended + +// 实际做删除操作 +var triggerBuilder = scheduler.GetTriggerBuilder("trigger1"); +var scheduleResult = scheduler.TrySaveTrigger(triggerBuilder?.Removed(), out var trigger); // 标记为 Removed + +// 实际做更新操作 +var scheduleResult = scheduler.TrySaveTrigger(Trigggers.PeriodSeconds(5).Updated(), out var trigger); // Create 方法默认标记为 Appended,但调用 Updated() 方法 +``` + +::: + +### 26.1.11.10 更新作业信息 + +```cs showLineNumbers {2,5,9,16} +// 返回 ScheduleResult 类型 +var scheduleResult = scheduler.TryUpdateDetail(jobBuilder, out var jobDetail); + +// 无返回值 +scheduler.UpdateDetail(jobBuilder); + +// Furion 4.8.6+ 支持 +// 返回 ScheduleResult 类型 +var scheduleResult = Scheduler.TryUpdateDetail(jobBuilder => +{ + jobBuilder.SetDescription("~~~"); +}, out var jobDetail); + +// Furion 4.8.6+ 支持 +// 无返回值 +scheduler.UpdateDetail(jobBuilder => +{ + jobBuilder.SetDescription("~~~"); +}); +``` + +### 26.1.11.11 添加作业触发器 + +```cs showLineNumbers +// 返回 ScheduleResult 类型 +var scheduleResult = scheduler.TryAddTrigger(triggerBuilder, out var trigger); + +// 无返回值,支持多个 +scheduler.AddTrigger(triggerBuilder1, triggerBuilder2, ...); +``` + +### 26.1.11.12 更新作业触发器 + +```cs showLineNumbers {2,5,9,16} +// 返回 ScheduleResult 类型 +var scheduleResult = scheduler.TryUpdateTrigger(triggerBuilder, out var trigger); + +// 无返回值,支持多个 +scheduler.UpdateTrigger(triggerBuilder1, triggerBuilder2, ...); + +// Furion 4.8.6+ 支持 +// 返回 ScheduleResult 类型 +var scheduleResult = scheduler.TryUpdateTrigger("triggerId", triggerBuilder => +{ + triggerBuilder.SetDescription("~~"); +}, out var trigger); + +// Furion 4.8.6+ 支持 +// 无返回值 +scheduler.UpdateTrigger("triggerId", triggerBuilder => +{ + triggerBuilder.SetDescription("~~"); +}); +``` + +### 26.1.11.13 删除作业触发器 + +```cs showLineNumbers +// 返回 ScheduleResult 类型 +var scheduleResult = scheduler.TryRemoveTrigger("trigger1", out var trigger); + +// 无返回值,支持多个 +scheduler.RemoveTrigger("trigger1", "trigger2", ...); +``` + +### 26.1.11.14 删除当前作业 + +```cs showLineNumbers +// 返回 ScheduleResult 类型 +var scheduleResult = scheduler.TryRemove(); + +// 无返回值 +scheduler.Remove(); +``` + +### 26.1.11.15 判断作业触发器是否存在 + +```cs showLineNumbers +bool isExist = scheduler.ContainsTrigger("trigger1"); +``` + +### 26.1.11.16 启动作业触发器 + +```cs showLineNumbers +bool succeed = scheduler.StartTrigger("trigger1"); +``` + +### 26.1.11.17 暂停作业触发器 + +```cs showLineNumbers +bool succeed = scheduler.PauseTrigger("trigger1"); +``` + +### 26.1.11.18 强制触发作业持久化操作 + +```cs showLineNumbers +scheduler.Persist(); +``` + +### 26.1.11.19 启动当前作业 + +```cs showLineNumbers +scheduler.Start(); +``` + +### 26.1.11.20 暂停当前作业 + +```cs showLineNumbers +scheduler.Pause(); +``` + +### 26.1.11.21 校对当前作业 + +```cs showLineNumbers +scheduler.Collate(); +``` + +### 26.1.11.22 强制刷新当前作业 + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.3.3 +` 版本使用。 + +::: + +通常情况下我们通过 `_schedulerFactory.GetJob("jobId")` 获取到作业之后,然后对这个作业进行操作,**但操作之后这个对象并不能同步更改,需要反复调用 `GetJob` 方法。**。 + +所以在 `Furion 4.8.3.3+` 版本之后,**`IScheduler` 任何操作都将自动调用 `Reload()` 方法刷新变量**。 + +```cs showLineNumbers +// 也可以自己手动强制刷新(通常不需要调用下面代码~~) +scheduler.Reload(); +``` + +### 26.1.11.23 转换成 `JSON` 格式 + +```cs showLineNumbers +var json = scheduler.ConvertToJSON(); +var json = scheduler.ConvertToJSON(NamingConventions.CamelCase); +``` + +### 26.1.11.24 转换成可枚举字典 + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.4 +` 版本使用。 + +::: + +通常我们在开发应用时,需要将作业计划信息进行拆解,比如一个作业计划包含两个作业触发器,那么可以通过 `scheduler.GetEnumerable()` 方法生成可枚举字典对象,字典中的项数量等于作业触发器数量。 + +```cs showLineNumbers {1} +foreach (var (jobDetail, trigger) in scheduler.GetEnumerable()) +{ + // .... +} +``` + +### 26.1.11.25 立即执行作业 + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.7.11 +` 版本使用。 + +::: + +```cs showLineNumbers +scheduler.Run(); +``` + +:::important 注意事项 + +如果作业本身处于 `(Pause)暂停`、`(NotStart)初始化时未启动`、`(Unoccupied)无触发时间` 状态,那么点击 **立即执行** 后将自动转至 `就绪` 状态。 + +::: + +## 26.1.12 作业持久化器 `IJobPersistence` + +### 26.1.12.1 关于作业持久化器 + +作业持久化器指的是可以通过存储介质如数据库中加载作业信息到内存中,又可以将内存中作业调度器的作业信息实时同步回存储介质中。 + +### 26.1.12.2 实现作业持久化器 + +调度作业服务提供了非常简单的 `IJobPersistence` 接口,只需实现该接口即可实现持久化,如实现数据库持久化: + +```cs showLineNumbers {1,3,9,18,24} +public class DbJobPersistence : IJobPersistence +{ + public IEnumerable Preload() + { + // 作业调度服务启动时运行时初始化,可通过数据库加载,或者其他方式 + return Array.Empty(); + } + + public SchedulerBuilder OnLoading(SchedulerBuilder builder) + { + // 如果是更新操作,则 return builder.Updated(); 将生成 UPDATE 语句 + // 如果是新增操作,则 return builder.Appended(); 将生成 INSERT 语句 + // 如果是删除操作,则 return builder.Removed(); 将生成 DELETE 语句 + // 如果无需标记操作,返回 builder 默认值即可 + return builder; + } + + public void OnChanged(PersistenceContext context) + { + var sql = context.ConvertToSQL("job_detail"); + // 这里执行 sql 即可 💖 + } + + public void OnTriggerChanged(PersistenceTriggerContext context) + { + var sql = context.ConvertToSQL("job_trigger"); + // 这里执行 sql 即可 💖 + } +} +``` + +之后在 `Startup.cs` 中注册: + +```cs showLineNumbers {3} +services.AddSchedule(options => +{ + options.AddPersistence(); +}); +``` + +可能有些开发者看到这里一脸不解,持久化不应该这么简单啊!其实就是这么简单.... + +### 26.1.12.3 `IJobPersistence` 详细说明 + +`IJobPersistence` 接口提供了以下四个方法: + +- **`Preload`:作业调度服务启动时调用,可在这里动态创建作业计划构建器并返回。** + +```cs showLineNumbers {1,3,5,9,13,16} +public IEnumerable Preload() +{ + // 可以这里查询数据库返回 + + // 这里可以扫描程序集动态创建返回 + return App.EffectiveTypes.Where(t => t.IsJobType()) + .Select(t => SchedulerBuilder.Create(JobBuilder.Create(t), t.ScanTriggers())); + + // 如果类型贴有 [JobDetail] 特性,还可以一键扫描返回 + return App.EffectiveTypes.Where(t => t.IsJobType()) + .Select(t => t.ScanToBuilder()); + + // 还可以更简单~~ + return App.EffectiveTypes.ScanToBuilders(); + + // 也可以手动返回 + return new[] + { + SchedulerBuilder.Create(JobBuilder.Create(), Triggers.Minutely()) + } +} +``` + +**查看 `EFCore` 和 `SqlSugar` 使用示例:** + + + + +```cs showLineNumbers {4,6,23-60,66} +/// +/// 作业持久化(数据库) +/// +public class DbJobPersistence : IJobPersistence, IDisposable +{ + private readonly IServiceScope _serviceScope; + private readonly IRepository _jobRepository; + private readonly IRepository _triggerRepository; + + public DbJobPersistence(IServiceScopeFactory scopeFactory) + { + _serviceScope = scopeFactory.CreateScope(); + var services = _serviceScope.ServiceProvider; + + _jobRepository = services.GetService>(); + _triggerRepository = services.GetService>(); + } + + /// + /// 作业调度服务启动时 + /// + /// + public IEnumerable Preload() + { + // 获取所有定义的作业 + var allJobs = App.EffectiveTypes.ScanToBuilders(); + + // 若数据库不存在任何作业,则直接返回 + if (!_jobRepository.Any(u => true)) return allJobs; + + // 遍历所有定义的作业 + foreach (var schedulerBuilder in allJobs) + { + // 获取作业信息构建器 + var jobBuilder = schedulerBuilder.GetJobBuilder(); + + // 加载数据库数据 + var dbDetail = _jobRepository.FirstOfDefault(u => u.JobId == jobBuilder.JobId); + if (dbDetail == null) continue; + + // 同步数据库数据 + jobBuilder.LoadFrom(dbDetail); + + // 遍历所有作业触发器 + foreach (var (_, triggerBuilder) in schedulerBuilder.GetEnumerable()) + { + // 加载数据库数据 + var dbTrigger = _triggerRepository.FirstOrDefault(u => u.JobId == jobBuilder.JobId && u.TriggerId == triggerBuilder.TriggerId); + if (dbTrigger == null) continue; + + triggerBuilder.LoadFrom(dbTrigger) + .Updated(); // 标记更新 + } + + // 标记更新 + schedulerBuilder.Updated(); + } + + return allJobs; + } + + // ... + + public void Dispose() + { + _serviceScope?.Dispose(); + } +} +``` + + + + +```cs showLineNumbers {4,6,17-62} +/// +/// 作业持久化(数据库) +/// +public class DbJobPersistence : IJobPersistence +{ + private readonly IServiceScopeFactory _scopeFactory; + + public DbJobPersistence(IServiceScopeFactory scopeFactory) + { + _scopeFactory = scopeFactory; + } + + /// + /// 作业调度服务启动时 + /// + /// + public IEnumerable Preload() + { + // 创建服务作用域 + using var serviceScope = scopeFactory.CreateScope(); + var services = serviceScope.ServiceProvider; + + // 解析仓储 + var jobRepository = services.GetService>(); + var triggerRepository = services.GetService>(); + + // 获取所有定义的作业 + var allJobs = App.EffectiveTypes.ScanToBuilders(); + + // 若数据库不存在任何作业,则直接返回 + if (!jobRepository.IsAny(u => true)) return allJobs; + + // 遍历所有定义的作业 + foreach (var schedulerBuilder in allJobs) + { + // 获取作业信息构建器 + var jobBuilder = schedulerBuilder.GetJobBuilder(); + + // 加载数据库数据 + var dbDetail = jobRepository.GetFirst(u => u.JobId == jobBuilder.JobId); + if (dbDetail == null) continue; + + // 同步数据库数据 + jobBuilder.LoadFrom(dbDetail); + + // 遍历所有作业触发器 + foreach (var (_, triggerBuilder) in schedulerBuilder.GetEnumerable()) + { + // 加载数据库数据 + var dbTrigger = triggerRepository.GetFirst(u => u.JobId == jobBuilder.JobId && u.TriggerId == triggerBuilder.TriggerId); + if (dbTrigger == null) continue; + + triggerBuilder.LoadFrom(dbTrigger) + .Updated(); // 标记更新 + } + + // 标记更新 + schedulerBuilder.Updated(); + } + + return allJobs; + } + + // ... +} +``` + + + + +- **`OnLoading`:作业计划初始化通知,通常在这里进一步修改初始化作业计划构建器。** + +在作业调度器服务启动时会遍历程序中所有作业计划构建器,然后逐条调用该方法,开发者可以在这里进一步修改作业计划构建器数据,之后**选择性返回 `SchedulerBuilder` 的持久化行为**。 + +`SchedulerBuilder` 提供 `Updated()`,`Appended()` 和 `Removed()` 来作为持久化行为标记。此标记将决定最终生成的 `SQL` 是 `UPDATE` 还是 `INSERT` 还是 `UPDATE`。 + +```cs showLineNumbers {4,8,12-13,16} +public SchedulerBuilder OnLoading(SchedulerBuilder builder) +{ + // 比如这里修改作业信息描述 + builder.GetJobBuilder() + .SetDescription("这是描述~~"); + + // 还可以修改触发器 + builder.GetTriggerBuilder("trigger1") + .SetDescription("这是触发器描述~~"); + + // 还可以通过数据库查询返回填充 😎 + builder.GetJobBuilder() + .LoadFrom(dbJobDetail); // dbJobDetail 表示根据 jobId 查询数据库返回的对象 + + // 还可以获取枚举对象逐条更新 + foreach(var (jobBuilder, triggerBuilder) in builder.GetEnumerable()) + { + jobBuilder.SetDescription("...."); + triggerBuilder.Updated(); // 标记该触发器已被更新,并生成 UPDATE 语句 + triggerBuilder.Removed(); // 标记该触发器已被删除,并生成 DELETE 语句 + } + + // 标记从其他地方更新,比如数据库 + return builder; +} +``` + +如果存储介质(如数据库)已经删除该作业,开发者可以标记为 `Removed()`,这样该作业会从内存中移除。 + +```cs showLineNumbers {3,6} +public SchedulerBuilder OnLoading(SchedulerBuilder builder) +{ + // 比如这里根据 jobId 查询数据库已经确认数据不存在了 + + // 标记从其他地方移除 + return builder.Removed(); +} +``` + +如果存储介质(如数据库)新增了新作业但内存中不存在,开发者可以标记为 `Append()`,这样该作业会添加到内存中,**但原有的 `builder` 就会被丢弃**。 + +```cs showLineNumbers {4,6,8,11} +public SchedulerBuilder OnLoading(SchedulerBuilder builder) +{ + // 比如在这里动态创建作业计划构建器 + var newBuilder = SchedulerBuilder.Create(Triggers.Minutely()); + // 还可以克隆一个 + var newBuilder = SchedulerBuilder.Clone(builder); + // 还可以读取配置文件/JSON + var newBuilder = SchedulerBuilder.From(json); + + // 返回新的作业计划构建器并标记为新增 + return newBuilder.Appended(); +} +``` + +- **`OnChanged`:作业计划 `Scheduler` 的 `JobDetail` 变化时调用。** + +只要作业计划有任何变化都将触发该方法,该方法有一个 `PersistenceContext` 类型的参数 `context`,`PersistenceContext` 包含以下成员: + +- **`PersistenceContext` 属性列表** + - `JobId`:作业 `Id`,`string` 类型 + - `JobDetail`:作业信息,`JobDetail` 类型 + - `Behavior`:持久化行为,`PersistenceBehavior` 枚举类型,包含 `Appended`,`Updated` 和 `Removed` 三个枚举成员 +- **`PersistenceContext` 方法列表** + - `ConvertToSQL`:将 `PersistenceContext` 转换成 `SQL` 字符串,`Behavior` 属性值不同,生成的 `SQL` 不同 + - `ConvertToJSON`:将 `PersistenceContext` 转换成 `JSON` 字符串 + - `ConvertToMonitor`:将 `PersistenceContext` 转换成 `Monitor` 字符串 + - `ToString`:将 `PersistenceContext` 转换成 `简要` 字符串 + - `GetNaming`:提供将特定字符串输出不同的命名规则字符串 + +```cs showLineNumbers {4,6,8,10} +public void OnChanged(PersistenceContext context) +{ + // 输出 CamelCase(驼峰命名法)SQL 语句,默认值 + var sql = context.ConvertToSQL("job_detail"); + // 输出 Pascal(帕斯卡命名法) SQL 语句 + var sql = context.ConvertToSQL("job_detail", NamingConventions.Pascal); + // 输出 UnderScoreCase(下划线命名法) SQL 语句 + var sql = context.ConvertToSQL("job_detail", NamingConventions.UnderScoreCase); + + // 你要做的只是执行 SQL 了!!! 😎 +} +``` + +- **`OnTriggerChanged`:作业计划 `Scheduler` 的触发器 `Trigger` 变化时调用。** + +只要作业计划**触发器**有任何变化都将触发该方法,该方法有一个 `PersistenceTriggerContext` 类型的参数 `context`,`PersistenceTriggerContext` 继承自 `PersistenceContext`: + +- **`PersistenceTriggerContext` 属性列表** + - `JobId`:作业 `Id`,`string` 类型 + - `JobDetail`:作业信息,`JobDetail` 类型 + - `TriggerId`:作业触发器 `Id`,`string` 类型 + - `Trigger`:作业触发器,`Trigger` 类型 + - `Behavior`:持久化行为,`PersistenceBehavior` 枚举类型,包含 `Appended`,`Updated` 和 `Removed` 三个枚举成员 +- **`PersistenceTriggerContext` 方法列表** + - `ConvertToSQL`:将 `PersistenceTriggerContext` 转换成 `SQL` 字符串,`Behavior` 属性值不同,生成的 `SQL` 不同 + - `ConvertToJSON`:将 `PersistenceTriggerContext` 转换成 `JSON` 字符串,只包含 `Trigger` + - `ConvertAllToJSON`:将 `PersistenceTriggerContext` 转换成 `JSON` 字符串,包含 `JobDetail` 和 `Trigger` + - `ConvertToMonitor`:将 `PersistenceTriggerContext` 转换成 `Monitor` 字符串 + - `ToString`:将 `PersistenceTriggerContext` 转换成 `简要` 字符串 + - `GetNaming`:提供将特定字符串输出不同的命名规则字符串 + +```cs showLineNumbers {4,6,8,10} +public void OnTriggerChanged(PersistenceTriggerContext context) +{ + // 输出 CamelCase(驼峰命名法)SQL 语句,默认值 + var sql = context.ConvertToSQL("job_trigger"); + // 输出 Pascal(帕斯卡命名法) SQL 语句 + var sql = context.ConvertToSQL("job_trigger", NamingConventions.Pascal); + // 输出 UnderScoreCase(下划线命名法) SQL 语句 + var sql = context.ConvertToSQL("job_trigger", NamingConventions.UnderScoreCase); + + // 你要做的只是执行 SQL 了!!! 😎 +} +``` + +:::tip 小知识 + +默认情况下,生成的 `SQL` 属于标准 `SQL` 语句,但未必适合所有数据库类型,所以我们可以指定 `BuildSqlType` 来生成特定数据库的语句,如: + +```cs showLineNumbers {4} +services.AddSchedule(options => +{ + // 配置输出 SQL 的数据库类型,Furion 4.8.2.3+ + options.BuildSqlType = SqlTypes.SqlServer; +}); +``` + +::: + +### 26.1.12.4 各类数据库创建作业持久化表语句 + + + + +**可自行调整列命名规则。** + +```sql showLineNumbers {1,14} +CREATE TABLE "JobDetails" ( + "Id" INTEGER NOT NULL CONSTRAINT "PK_JobDetails" PRIMARY KEY AUTOINCREMENT, + "JobId" TEXT NOT NULL, + "GroupName" TEXT NULL, + "JobType" TEXT NULL, + "AssemblyName" TEXT NULL, + "Description" TEXT NULL, + "Concurrent" INTEGER NOT NULL, + "IncludeAnnotations" INTEGER NOT NULL, + "Properties" TEXT NULL, + "UpdatedTime" TEXT NULL +); + +CREATE TABLE "JobTriggers" ( + "Id" INTEGER NOT NULL CONSTRAINT "PK_JobTriggers" PRIMARY KEY AUTOINCREMENT, + "TriggerId" TEXT NOT NULL, + "JobId" TEXT NOT NULL, + "TriggerType" TEXT NULL, + "AssemblyName" TEXT NULL, + "Args" TEXT NULL, + "Description" TEXT NULL, + "Status" INTEGER NOT NULL, + "StartTime" TEXT NULL, + "EndTime" TEXT NULL, + "LastRunTime" TEXT NULL, + "NextRunTime" TEXT NULL, + "NumberOfRuns" INTEGER NOT NULL, + "MaxNumberOfRuns" INTEGER NOT NULL, + "NumberOfErrors" INTEGER NOT NULL, + "MaxNumberOfErrors" INTEGER NOT NULL, + "NumRetries" INTEGER NOT NULL, + "RetryTimeout" INTEGER NOT NULL, + "StartNow" INTEGER NOT NULL, + "RunOnStart" INTEGER NOT NULL, + "ResetOnlyOnce" INTEGER NOT NULL, + "Result" TEXT NULL, + "ElapsedTime" INTEGER NOT NULL, + "UpdatedTime" TEXT NULL +); +``` + + + + +**可自行调整列命名规则。** + +```sql showLineNumbers {1,16} +CREATE TABLE [JobDetails] ( + [Id] int NOT NULL IDENTITY, + [JobId] nvarchar(max) NOT NULL, + [GroupName] nvarchar(max) NULL, + [JobType] nvarchar(max) NULL, + [AssemblyName] nvarchar(max) NULL, + [Description] nvarchar(max) NULL, + [Concurrent] bit NOT NULL, + [IncludeAnnotations] bit NOT NULL, + [Properties] nvarchar(max) NULL, + [UpdatedTime] datetime2 NULL, + CONSTRAINT [PK_JobDetails] PRIMARY KEY ([Id]) +); +GO + +CREATE TABLE [JobTriggers] ( + [Id] int NOT NULL IDENTITY, + [TriggerId] nvarchar(max) NOT NULL, + [JobId] nvarchar(max) NOT NULL, + [TriggerType] nvarchar(max) NULL, + [AssemblyName] nvarchar(max) NULL, + [Args] nvarchar(max) NULL, + [Description] nvarchar(max) NULL, + [Status] bigint NOT NULL, + [StartTime] datetime2 NULL, + [EndTime] datetime2 NULL, + [LastRunTime] datetime2 NULL, + [NextRunTime] datetime2 NULL, + [NumberOfRuns] bigint NOT NULL, + [MaxNumberOfRuns] bigint NOT NULL, + [NumberOfErrors] bigint NOT NULL, + [MaxNumberOfErrors] bigint NOT NULL, + [NumRetries] int NOT NULL, + [RetryTimeout] int NOT NULL, + [StartNow] bit NOT NULL, + [RunOnStart] bit NOT NULL, + [ResetOnlyOnce] bit NOT NULL, + [Result] nvarchar(max) NULL, + [ElapsedTime] bigint NOT NULL, + [UpdatedTime] datetime2 NULL, + CONSTRAINT [PK_JobTriggers] PRIMARY KEY ([Id]) +); +``` + + + + +**可自行调整列命名规则。** + +```sql showLineNumbers {3,17} +ALTER DATABASE CHARACTER SET utf8mb4; + +CREATE TABLE `JobDetails` ( + `Id` int NOT NULL AUTO_INCREMENT, + `JobId` longtext CHARACTER SET utf8mb4 NOT NULL, + `GroupName` longtext CHARACTER SET utf8mb4 NULL, + `JobType` longtext CHARACTER SET utf8mb4 NULL, + `AssemblyName` longtext CHARACTER SET utf8mb4 NULL, + `Description` longtext CHARACTER SET utf8mb4 NULL, + `Concurrent` tinyint(1) NOT NULL, + `IncludeAnnotations` tinyint(1) NOT NULL, + `Properties` longtext CHARACTER SET utf8mb4 NULL, + `UpdatedTime` datetime(6) NULL, + CONSTRAINT `PK_JobDetails` PRIMARY KEY (`Id`) +) CHARACTER SET=utf8mb4; + +CREATE TABLE `JobTriggers` ( + `Id` int NOT NULL AUTO_INCREMENT, + `TriggerId` longtext CHARACTER SET utf8mb4 NOT NULL, + `JobId` longtext CHARACTER SET utf8mb4 NOT NULL, + `TriggerType` longtext CHARACTER SET utf8mb4 NULL, + `AssemblyName` longtext CHARACTER SET utf8mb4 NULL, + `Args` longtext CHARACTER SET utf8mb4 NULL, + `Description` longtext CHARACTER SET utf8mb4 NULL, + `Status` int unsigned NOT NULL, + `StartTime` datetime(6) NULL, + `EndTime` datetime(6) NULL, + `LastRunTime` datetime(6) NULL, + `NextRunTime` datetime(6) NULL, + `NumberOfRuns` bigint NOT NULL, + `MaxNumberOfRuns` bigint NOT NULL, + `NumberOfErrors` bigint NOT NULL, + `MaxNumberOfErrors` bigint NOT NULL, + `NumRetries` int NOT NULL, + `RetryTimeout` int NOT NULL, + `StartNow` tinyint(1) NOT NULL, + `RunOnStart` tinyint(1) NOT NULL, + `ResetOnlyOnce` tinyint(1) NOT NULL, + `Result` longtext CHARACTER SET utf8mb4 NULL, + `ElapsedTime` bigint NOT NULL, + `UpdatedTime` datetime(6) NULL, + CONSTRAINT `PK_JobTriggers` PRIMARY KEY (`Id`) +) CHARACTER SET=utf8mb4; +``` + + + + +**可自行调整列命名规则。** + +```sql showLineNumbers {1,15} +CREATE TABLE "JobDetails" ( + "Id" integer GENERATED BY DEFAULT AS IDENTITY, + "JobId" text NOT NULL, + "GroupName" text NULL, + "JobType" text NULL, + "AssemblyName" text NULL, + "Description" text NULL, + "Concurrent" boolean NOT NULL, + "IncludeAnnotations" boolean NOT NULL, + "Properties" text NULL, + "UpdatedTime" timestamp with time zone NULL, + CONSTRAINT "PK_JobDetails" PRIMARY KEY ("Id") +); + +CREATE TABLE "JobTriggers" ( + "Id" integer GENERATED BY DEFAULT AS IDENTITY, + "TriggerId" text NOT NULL, + "JobId" text NOT NULL, + "TriggerType" text NULL, + "AssemblyName" text NULL, + "Args" text NULL, + "Description" text NULL, + "Status" bigint NOT NULL, + "StartTime" timestamp with time zone NULL, + "EndTime" timestamp with time zone NULL, + "LastRunTime" timestamp with time zone NULL, + "NextRunTime" timestamp with time zone NULL, + "NumberOfRuns" bigint NOT NULL, + "MaxNumberOfRuns" bigint NOT NULL, + "NumberOfErrors" bigint NOT NULL, + "MaxNumberOfErrors" bigint NOT NULL, + "NumRetries" integer NOT NULL, + "RetryTimeout" integer NOT NULL, + "StartNow" boolean NOT NULL, + "RunOnStart" boolean NOT NULL, + "ResetOnlyOnce" boolean NOT NULL, + "Result" text NULL, + "ElapsedTime" bigint NOT NULL, + "UpdatedTime" timestamp with time zone NULL, + CONSTRAINT "PK_JobTriggers" PRIMARY KEY ("Id") +); +``` + + + + +**可自行调整列命名规则。** + +```sql showLineNumbers {1,15} +CREATE TABLE "JobDetails" ( + "Id" NUMBER(10) GENERATED BY DEFAULT ON NULL AS IDENTITY NOT NULL, + "JobId" NVARCHAR2(2000) NOT NULL, + "GroupName" NVARCHAR2(2000), + "JobType" NVARCHAR2(2000), + "AssemblyName" NVARCHAR2(2000), + "Description" NVARCHAR2(2000), + "Concurrent" NUMBER(1) NOT NULL, + "IncludeAnnotations" NUMBER(1) NOT NULL, + "Properties" NVARCHAR2(2000), + "UpdatedTime" TIMESTAMP(7), + CONSTRAINT "PK_JobDetails" PRIMARY KEY ("Id") +); + +CREATE TABLE "JobTriggers" ( + "Id" NUMBER(10) GENERATED BY DEFAULT ON NULL AS IDENTITY NOT NULL, + "TriggerId" NVARCHAR2(2000) NOT NULL, + "JobId" NVARCHAR2(2000) NOT NULL, + "TriggerType" NVARCHAR2(2000), + "AssemblyName" NVARCHAR2(2000), + "Args" NVARCHAR2(2000), + "Description" NVARCHAR2(2000), + "Status" NUMBER(10) NOT NULL, + "StartTime" TIMESTAMP(7), + "EndTime" TIMESTAMP(7), + "LastRunTime" TIMESTAMP(7), + "NextRunTime" TIMESTAMP(7), + "NumberOfRuns" NUMBER(19) NOT NULL, + "MaxNumberOfRuns" NUMBER(19) NOT NULL, + "NumberOfErrors" NUMBER(19) NOT NULL, + "MaxNumberOfErrors" NUMBER(19) NOT NULL, + "NumRetries" NUMBER(10) NOT NULL, + "RetryTimeout" NUMBER(10) NOT NULL, + "StartNow" NUMBER(1) NOT NULL, + "RunOnStart" NUMBER(1) NOT NULL, + "ResetOnlyOnce" NUMBER(1) NOT NULL, + "Result" NVARCHAR2(2000), + "ElapsedTime" NUMBER(19) NOT NULL, + "UpdatedTime" TIMESTAMP(7), + CONSTRAINT "PK_JobTriggers" PRIMARY KEY ("Id") +); +``` + + + + +**可自行调整列命名规则。** + +```sql showLineNumbers {1,15} +CREATE TABLE "JobDetails" ( + "Id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL, + "JobId" BLOB SUB_TYPE TEXT NOT NULL, + "GroupName" BLOB SUB_TYPE TEXT, + "JobType" BLOB SUB_TYPE TEXT, + "AssemblyName" BLOB SUB_TYPE TEXT, + "Description" BLOB SUB_TYPE TEXT, + "Concurrent" BOOLEAN NOT NULL, + "IncludeAnnotations" BOOLEAN NOT NULL, + "Properties" BLOB SUB_TYPE TEXT, + "UpdatedTime" TIMESTAMP, + CONSTRAINT "PK_JobDetails" PRIMARY KEY ("Id") +); + +CREATE TABLE "JobTriggers" ( + "Id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL, + "TriggerId" BLOB SUB_TYPE TEXT NOT NULL, + "JobId" BLOB SUB_TYPE TEXT NOT NULL, + "TriggerType" BLOB SUB_TYPE TEXT, + "AssemblyName" BLOB SUB_TYPE TEXT, + "Args" BLOB SUB_TYPE TEXT, + "Description" BLOB SUB_TYPE TEXT, + "Status" BIGINT NOT NULL, + "StartTime" TIMESTAMP, + "EndTime" TIMESTAMP, + "LastRunTime" TIMESTAMP, + "NextRunTime" TIMESTAMP, + "NumberOfRuns" BIGINT NOT NULL, + "MaxNumberOfRuns" BIGINT NOT NULL, + "NumberOfErrors" BIGINT NOT NULL, + "MaxNumberOfErrors" BIGINT NOT NULL, + "NumRetries" INTEGER NOT NULL, + "RetryTimeout" INTEGER NOT NULL, + "StartNow" BOOLEAN NOT NULL, + "RunOnStart" BOOLEAN NOT NULL, + "ResetOnlyOnce" BOOLEAN NOT NULL, + "Result" BLOB SUB_TYPE TEXT, + "ElapsedTime" BIGINT NOT NULL, + "UpdatedTime" TIMESTAMP, + CONSTRAINT "PK_JobTriggers" PRIMARY KEY ("Id") +); +``` + + + + +**可自行调整列命名规则。** + +```cs showLineNumbers {1,56} +public class JobDetail +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + + /// + /// 作业 Id + /// + public string JobId { get; set; } + + /// + /// 作业组名称 + /// + public string? GroupName { get; set; } + + /// + /// 作业处理程序类型 + /// + /// 存储的是类型的 FullName + public string? JobType { get; set; } + + /// + /// 作业处理程序类型所在程序集 + /// + /// 存储的是程序集 Name + public string? AssemblyName { get; set; } + + /// + /// 描述信息 + /// + public string? Description { get; set; } + + /// + /// 是否采用并行执行 + /// + /// 如果设置为 false,那么使用串行执行 + public bool Concurrent { get; set; } = true; + + /// + /// 是否扫描 IJob 实现类 [Trigger] 特性触发器 + /// + public bool IncludeAnnotations { get; set; } = false; + + /// + /// 作业信息额外数据 + /// + public string? Properties { get; set; } = "{}"; + + /// + /// 作业更新时间 + /// + public DateTime? UpdatedTime { get; set; } +} + +public class JobTrigger +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + + /// + /// 作业触发器 Id + /// + public string TriggerId { get; set; } + + /// + /// 作业 Id + /// + public string JobId { get; set; } + + /// + /// 作业触发器类型 + /// + /// 存储的是类型的 FullName + public string? TriggerType { get; set; } + + /// + /// 作业触发器类型所在程序集 + /// + /// 存储的是程序集 Name + public string? AssemblyName { get; set; } + + /// + /// 作业触发器参数 + /// + /// 运行时将反序列化为 object[] 类型并作为构造函数参数 + public string? Args { get; set; } + + /// + /// 描述信息 + /// + public string? Description { get; set; } + + /// + /// 作业触发器状态 + /// + public TriggerStatus Status { get; set; } = TriggerStatus.Ready; + + /// + /// 起始时间 + /// + public DateTime? StartTime { get; set; } + + /// + /// 结束时间 + /// + public DateTime? EndTime { get; set; } + + /// + /// 最近运行时间 + /// + public DateTime? LastRunTime { get; set; } + + /// + /// 下一次运行时间 + /// + public DateTime? NextRunTime { get; set; } + + /// + /// 触发次数 + /// + public long NumberOfRuns { get; set; } + + /// + /// 最大触发次数 + /// + /// + /// 0:不限制 + /// n:N 次 + /// + public long MaxNumberOfRuns { get; set; } + + /// + /// 出错次数 + /// + public long NumberOfErrors { get; set; } + + /// + /// 最大出错次数 + /// + /// + /// 0:不限制 + /// n:N 次 + /// + public long MaxNumberOfErrors { get; set; } + + /// + /// 重试次数 + /// + public int NumRetries { get; set; } = 0; + + /// + /// 重试间隔时间 + /// + /// 默认1000毫秒 + public int RetryTimeout { get; set; } = 1000; + + /// + /// 是否立即启动 + /// + public bool StartNow { get; set; } = true; + + /// + /// 是否启动时执行一次 + /// + public bool RunOnStart { get; set; } = false; + + /// + /// 是否在启动时重置最大触发次数等于一次的作业 + /// + /// 解决因持久化数据已完成一次触发但启动时不再执行的问题 + public bool ResetOnlyOnce { get; set; } = true; + + /// + /// 本次执行结果 + /// + public string? Result { get; set; } + + /// + /// 本次执行耗时 + /// + public long ElapsedTime { get; set; } + + /// + /// 作业触发器更新时间 + /// + public DateTime? UpdatedTime { get; set; } +} +``` + + + + +**可自行调整列命名规则。** + +```cs showLineNumbers {2,66} +[SugarTable("JobDetail", "作业信息表")] +public class JobDetail +{ + /// + /// Id + /// + [SugarColumn(ColumnDescription = "Id", IsPrimaryKey = true, IsIdentity = true)] + public virtual long Id { get; set; } + + /// + /// 作业 Id + /// + [SugarColumn(ColumnDescription = "作业Id")] + public virtual string JobId { get; set; } + + /// + /// 组名称 + /// + [SugarColumn(ColumnDescription = "组名称")] + public string? GroupName { get; set; } + + /// + /// 作业类型 FullName + /// + [SugarColumn(ColumnDescription = "作业类型")] + public string? JobType { get; set; } + + /// + /// 程序集 Name + /// + [SugarColumn(ColumnDescription = "程序集")] + public string? AssemblyName { get; set; } + + /// + /// 描述信息 + /// + [SugarColumn(ColumnDescription = "描述信息")] + public string? Description { get; set; } + + /// + /// 是否并行执行 + /// + [SugarColumn(ColumnDescription = "是否并行执行")] + public bool Concurrent { get; set; } = true; + + /// + /// 是否扫描特性触发器 + /// + [SugarColumn(ColumnDescription = "是否扫描特性触发器")] + public bool IncludeAnnotations { get; set; } = false; + + /// + /// 额外数据 + /// + [SugarColumn(ColumnDescription = "额外数据", ColumnDataType = "longtext,text,clob")] + public string? Properties { get; set; } = "{}"; + + /// + /// 更新时间 + /// + [SugarColumn(ColumnDescription = "更新时间")] + public DateTime? UpdatedTime { get; set; } +} + +[SugarTable("JobTrigger", "作业触发器表")] +public class JobTrigger +{ + /// + /// Id + /// + [SugarColumn(ColumnDescription = "Id", IsPrimaryKey = true, IsIdentity = true)] + public virtual long Id { get; set; } + + /// + /// 触发器 Id + /// + [SugarColumn(ColumnDescription = "触发器Id")] + public virtual string TriggerId { get; set; } + + /// + /// 作业 Id + /// + [SugarColumn(ColumnDescription = "作业Id")] + public virtual string JobId { get; set; } + + /// + /// 触发器类型 FullName + /// + [SugarColumn(ColumnDescription = "触发器类型")] + public string? TriggerType { get; set; } + + /// + /// 程序集 Name + /// + [SugarColumn(ColumnDescription = "程序集")] + public string? AssemblyName { get; set; } + + /// + /// 参数 + /// + [SugarColumn(ColumnDescription = "参数")] + public string? Args { get; set; } + + /// + /// 描述信息 + /// + [SugarColumn(ColumnDescription = "描述信息")] + public string? Description { get; set; } + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态")] + public TriggerStatus Status { get; set; } = TriggerStatus.Ready; + + /// + /// 起始时间 + /// + [SugarColumn(ColumnDescription = "起始时间")] + public DateTime? StartTime { get; set; } + + /// + /// 结束时间 + /// + [SugarColumn(ColumnDescription = "结束时间")] + public DateTime? EndTime { get; set; } + + /// + /// 最近运行时间 + /// + [SugarColumn(ColumnDescription = "最近运行时间")] + public DateTime? LastRunTime { get; set; } + + /// + /// 下一次运行时间 + /// + [SugarColumn(ColumnDescription = "下一次运行时间")] + public DateTime? NextRunTime { get; set; } + + /// + /// 触发次数 + /// + [SugarColumn(ColumnDescription = "触发次数")] + public long NumberOfRuns { get; set; } + + /// + /// 最大触发次数(0:不限制,n:N次) + /// + [SugarColumn(ColumnDescription = "最大触发次数")] + public long MaxNumberOfRuns { get; set; } + + /// + /// 出错次数 + /// + [SugarColumn(ColumnDescription = "出错次数")] + public long NumberOfErrors { get; set; } + + /// + /// 最大出错次数(0:不限制,n:N次) + /// + [SugarColumn(ColumnDescription = "最大出错次数")] + public long MaxNumberOfErrors { get; set; } + + /// + /// 重试次数 + /// + [SugarColumn(ColumnDescription = "重试次数")] + public int NumRetries { get; set; } + + /// + /// 重试间隔时间(ms) + /// + [SugarColumn(ColumnDescription = "重试间隔时间(ms)")] + public int RetryTimeout { get; set; } = 1000; + + /// + /// 是否立即启动 + /// + [SugarColumn(ColumnDescription = "是否立即启动")] + public bool StartNow { get; set; } = true; + + /// + /// 是否启动时执行一次 + /// + [SugarColumn(ColumnDescription = "是否启动时执行一次")] + public bool RunOnStart { get; set; } = false; + + /// + /// 是否在启动时重置最大触发次数等于一次的作业 + /// + /// 解决因持久化数据已完成一次触发但启动时不再执行的问题 + [SugarColumn(ColumnDescription = "是否在启动时重置最大触发次数等于一次的作业")] + public bool ResetOnlyOnce { get; set; } = true; + + /// + /// 本次执行结果 + /// + [SugarColumn(ColumnDescription = "本次执行结果")] + public string? Result { get; set; } + + /// + /// 本次执行耗时 + /// + [SugarColumn(ColumnDescription = "本次执行耗时")] + public long ElapsedTime { get; set; } + + /// + /// 更新时间 + /// + [SugarColumn(ColumnDescription = "更新时间")] + public DateTime? UpdatedTime { get; set; } +} +``` + + + + +## 26.1.13 作业集群控制 + +框架提供简单的集群功能,但并不能达到负载均衡的效果,而仅仅提供了故障转移的功能,当一个服务的作业调度器宕机时,另一个服务的作业调度器会启动。 + +### 26.1.13.1 实现集群故障转移 + +1. **创建 `JobClusterServer` 类并实现 `IJobClusterServer`** + +```cs showLineNumbers {1,9,21-37,46,55,65} +public class JobClusterServer : IJobClusterServer +{ + /// + /// 当前作业调度器启动通知 + /// + /// 作业集群服务上下文 + public void Start(JobClusterContext context) + { + // 在作业集群表中,如果 clusterId 不存在,则新增一条(否则更新一条),并设置 status 为 ClusterStatus.Waiting + } + + /// + /// 等待被唤醒 + /// + /// 作业集群服务上下文 + /// + public async Task WaitingForAsync(JobClusterContext context) + { + var clusterId = context.ClusterId; + + while (true) + { + try + { + // 在这里查询数据库,根据以下两种情况处理 + // 1) 如果作业集群表已有 status 为 ClusterStatus.Working 则继续循环 + // 2) 如果作业集群表中还没有其他服务或只有自己,则插入一条集群服务或调用 await WorkNowAsync(clusterId); 之后 return; + // 3) 如果作业集群表中没有 status 为 ClusterStatus.Working 的,调用 await WorkNowAsync(clusterId); 之后 return; + + await WorkNowAsync(clusterId); + return; + } + catch { } + + // 控制集群心跳频率 + await Task.Delay(3000); + } + } + + /// + /// 当前作业调度器停止通知 + /// + /// 作业集群服务上下文 + public void Stop(JobClusterContext context) + { + // 在作业集群表中,更新 clusterId 的 status 为 ClusterStatus.Crashed + } + + /// + /// 当前作业调度器宕机 + /// + /// 作业集群服务上下文 + public void Crash(JobClusterContext context) + { + // 在作业集群表中,更新 clusterId 的 status 为 ClusterStatus.Crashed + } + + /// + /// 指示集群可以工作 + /// + /// 集群 Id + /// + private Task WorkNowAsync(string clusterId) + { + // 在作业集群表中,更新 clusterId 的 status 为 ClusterStatus.Working + + // 模拟数据库更新操作(耗时) + await Task.Delay(3000); + } +} +``` + +2. **注册集群服务** + +```cs showLineNumbers {3-4} +services.AddSchedule(options => +{ + options.ClusterId = "cluster1"; + options.AddClusterServer(); +}); +``` + +3. **作业集群输出日志** + +```bash showLineNumbers {4,6,8,16} +info: 2022-12-05 18:26:11.4045753 +08:00 星期一 L System.Logging.ScheduleService[0] #1 + Schedule hosted service is running. +info: 2022-12-05 18:26:11.4126431 +08:00 星期一 L System.Logging.ScheduleService[0] #1 + The job cluster of service has been enabled, and waiting for instructions. +warn: 2022-12-05 18:26:14.4333100 +08:00 星期一 L System.Logging.ScheduleService[0] #6 + The job cluster of service worked now, and the current schedule hosted service will be preloading. +info: 2022-12-05 18:26:14.4411758 +08:00 星期一 L System.Logging.ScheduleService[0] #6 + Schedule hosted service is preloading... +info: 2022-12-05 18:26:14.4684974 +08:00 星期一 L System.Logging.ScheduleService[0] #6 + The trigger for scheduler of successfully appended to the schedule. +info: 2022-12-05 18:26:14.4701128 +08:00 星期一 L System.Logging.ScheduleService[0] #6 + The scheduler of successfully appended to the schedule. +warn: 2022-12-05 18:26:14.4765709 +08:00 星期一 L System.Logging.ScheduleService[0] #6 + Schedule hosted service preload completed, and a total of <1> schedulers are appended. +info: 2022-12-05 18:26:19.5089541 +08:00 星期一 L MyJob[0] #16 + [C] 5s 1ts 2022-12-05 18:26:19.434 -> 2022-12-05 18:26:24.441 +``` + +### 26.1.13.2 作业集群数据库表设计 + +只需包含 `Id`,`ClusterId`,`Description`,`Status`,`UpdatedTime` 字段即可,其中 `Status` 是 `ClusterStatus` 枚举类型。 + +- **`ClusterStatus`** 包含以下枚举成员 + - `Crashed`:宕机 + - `Working`:正常工作 + - `Waiting`:等待被唤醒,默认值 + +### 26.1.13.3 如何实现负载均衡 + +框架只提供了简单的故障转移的集群功能,如需实现负载均衡,可通过 `TCP/IP` 套接字实现。 + +## 26.1.14 `ScheduleServe` 静态类 + +该功能 **建议** 仅限不能通过 `services.AddXXX` 方式使用,比如控制台,`Winfrom/WPF` 等。 + +```cs showLineNumbers {1,3} +IDisposable dispose = ScheduleServe.Run(options => +{ + options.AddJob(Triggers.Secondly()); +}); +``` + +这种方式有一个隐藏的巨大隐藏 “骚操作”:**可以在任何地方创建作业调度服务,多次调用可以创建多个作业调度器。** + +:::tip 推荐使用 `Serve.Run()` 或 `Serve.RunGeneric()` 方式替代 + +`Furion` 框架提供了 `Serve.Run()` 方式支持跨平台使用,还能支持注册更多服务,如: + +```cs showLineNumbers {1,3,5} +Serve.Run(services => +{ + services.AddSchedule(options => + { + options.Add(Triggers.Secondly()); + }); +}) +``` + +如无需 `Web` 功能,可通过 `Serve.RunGeneric` 替代 `Serve.Run`。 + +::: + +## 26.1.15 如何部署 + +如果在项目中使用了定时任务且部署到 `IIS` 中,那么需要设置 `IIS` 禁止回收,[点击查看 `IIS` 回收问题解决方案](/docs/deploy-iis#3415-iis-%E5%9B%9E%E6%94%B6%E9%97%AE%E9%A2%98%E5%92%8C%E9%85%8D%E7%BD%AE) + +:::warning 部署建议 + +建议定时任务采用 `Worker Service` 独立部署方式,不应依托 `Web` 项目进程中。[查看【 Worker Service】章节](/docs/process-service.mdx) + +::: + +### 26.1.15.1 `Worker Service` 代码集成例子 + +**1. 安装 `Furion` 或 `Sundial` 包** + +```bash showLineNumbers +# 完整的开发框架 +dotnet add package Furion; + +# 只需要定时任务服务功能 +dotnet add package Sundial +``` + +**2. 注册 `Schedule` 服务** + +```cs showLineNumbers {3-4,19-22} +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Furion.Schedule; +// using Sundial; + +namespace FurionWorkers; + +public class Program +{ + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureServices((hostContext, services) => + { + services.AddSchedule(options => + { + options.AddJob(Triggers.PeriodSeconds(5)); + }); + }); +} +``` + +:::tip 小知识 + +如果使用 `Serve` 模式,那么代码将非常精简,无需上面第二个步骤的代码~,如: + +```cs showLineNumbers {1,3-6} +Serve.RunGeneric(services => +{ + services.AddSchedule(options => + { + options.AddJob(Triggers.PeriodSeconds(5)); + }); +}) +``` + +::: + +## 26.1.16 `Dashboard` 看板功能 + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.4 +` 版本使用。 + +::: + +在 `Furion 4.8.4+` 版本内置了一个嵌入的定时任务看板 `UI`,只需要在 `Startup.cs` 中启用即可,如: + +:::tip 在 `Sundial` 中使用 + +如果使用的是 `Sundial` 独立开源项目,只需要安装 `Sundial.Dashboard` 包即可,无需安装 `Sundial`,前者已添加了后者的引用。 + +::: + +```cs showLineNumbers {2,5} +app.UseStaticFiles(); +app.UseScheduleUI(); + +// 还可以配置生产环境关闭 +app.UseScheduleUI(options => +{ + options.RequestPath = "/custom-job"; // Furion 4.8.5.6+ 版本支持,必须以 / 开头且不以 / 结尾 + options.DisableOnProduction = true; + options.SyncRate = 300; // 控制看板刷新频率,默认 300ms,Furion 4.8.7.43+ 支持,`Furion 4.8.8.29+` 已移除 +}); +``` + +:::caution 中间件说明 + +`app.UseScheduleUI()` 必须在 `app.UseStaticFiles()` 之后注册。 + +::: + +### 26.1.16.1 在 `Worker Service` 中注册 + +默认情况下,`Worker Service` 不提供 `Web` 功能,那么自然而然不能提供 `Web` 看板功能,如果想使其支持,可通过以下步骤: + +1. 添加日志配置(`appsettings.json` 和 `appsettings.Development.json`) + +```json showLineNumbers {6-7} +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information", + "Microsoft.AspNetCore": "Warning", + "System.Net.Http.HttpClient": "Warning" + } + } +} +``` + +2. 注册中间件服务 + +```cs showLineNumbers {8,11-15} +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using WorkerService1; + +IHost host = Host.CreateDefaultBuilder(args) + .ConfigureServices(services => + { + services.AddSchedule(); + services.AddHostedService(); + }) + .ConfigureWebHostDefaults(builder => builder.Configure(app => + { + app.UseStaticFiles(); + app.UseScheduleUI(); + })) + .Build(); + +host.Run(); +``` + +### 26.1.16.2 看板配置选项 + +`app.UseScheduleUI` 提供了可选的 `ScheduleUIOptions` 配置选项,提供以下配置: + +- `RequestPath`:配置看板入口地址,`string` 类型,默认 `/schedule`,需以 `/` 开头,结尾不包含 `/` +- `DisableOnProduction`:是否在生产环境关闭,`bool` 类型,默认 `false` +- ~~`SyncRate`:控制看板刷新频率,`int` 类型,默认 `300`,单位 `毫秒`,`Furion 4.8.7.43+` 支持,**`Furion 4.8.8.29+` 已移除**~~ +- `VisualPath`:二级虚拟目录路径,`string` 类型,需以 `/` 开头,结尾不包含 `/` ,`Furion 4.8.8.20+` 支持 + +接着打开浏览器并访问 `/schedule` 地址即可: + + + +前端源码地址:[https://gitee.com/dotnetchina/Furion/tree/v4/clients/schedule-dashboard](https://gitee.com/dotnetchina/Furion/tree/v4/clients/schedule-dashboard) + +## 26.1.17 常见问题 + +### 26.1.17.1 作业处理程序中获取当前时间存在偏差 + +通常我们会在 `IJob` 实现类型中获取当前时间,但是这个时间可能存在着极小的误差,如: + +```cs showLineNumbers {5,8} +public class MyJob : IJob +{ + public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + var nowTime = DateTime.Now; // 此时的时间未必是真实触发时间,因为还包含创建线程,初始化等等时间 + + // 正确的做法是 + var nowTime = context.OccurrenceTime; + } +} +``` + +### 26.1.17.2 作业触发器参数序列化/反序列化 + +框架提供了 `Schedular` 静态类方法:`Serialize/Deserialize` 可对作业触发器参数 `object[]` 类型进行序列化和反序列化操作,通常在开发定时任务管理后台时非常有用。如: + +```cs showLineNumbers {1,5} +// 序列化,方便组合 `UI` 不同输入框作业触发器参数 +var args = new object[] { "* * * * * *", CronStringFormat.WithSeconds }; +var stringArgs = Schedular.Serialize(args); + +// 反序列化,方便拆开作业触发器参数在 `UI` 不同列展示 +var stringArgs = "[\"* * * * *\",0]"; +var args = Schedular.Deserialize(stringArgs); +``` + +### 26.1.17.3 作业信息额外数据序列化/反序列化 + +框架提供了 `Schedular` 静态类方法:`Serialize/Deserialize` 可对作业信息额外是数据 `Dictionary` 类型进行序列化和反序列化操作,通常在开发定时任务管理后台时非常有用。如: + +```cs showLineNumbers {1,5} +// 序列化,方便组合 `UI` 不同输入框作业信息额外数据 +var jobData = new Dictionary { { "name", "Furion" } }; +var stringJobData = Schedular.Serialize(jobData); + +// 反序列化,方便拆开作业作业信息额外数据在 `UI` 不同列展示 +var stringJobData = "{\"name\":\"Furion\"}"; +var args = Schedular.Deserialize>(stringJobData); +``` + +### 26.1.17.4 作业处理程序延迟处理 + +在作业处理程序中如需使用到延迟线程操作,推荐使用 `Task.Delay` 而不是 `Thread.Sleep`,原因是后者是同步延迟会阻塞线程,而且不能取消。 + +### 26.1.17.5 定时任务部署说明 + +部署定时任务请确保服务器(操作系统)不会进入休眠且长时间运行。 + +如果在项目中使用了定时任务且部署到 `IIS` 中,那么需要设置 `IIS` 禁止回收,避免事件总线服务进入休眠,[点击查看 `IIS` 回收问题解决方案](./deploy-iis#3415-iis-%E5%9B%9E%E6%94%B6%E9%97%AE%E9%A2%98%E5%92%8C%E9%85%8D%E7%BD%AE)。 + +## 26.1.18 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/json-serialization.mdx b/handbook/docs/json-serialization.mdx new file mode 100644 index 0000000000000000000000000000000000000000..7edfa7b9e75b612bc4859f1985aa8a5d045bc78a --- /dev/null +++ b/handbook/docs/json-serialization.mdx @@ -0,0 +1,854 @@ +--- +id: json-serialization +title: 23. JSON 序列化 +sidebar_label: 23. JSON 序列化 +--- + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **新特性** + + -  新增 时间戳模型绑定器将时间戳转换为 `DateTime/DateTimeOffset` 类型 4.9.1.5 ⏱️2023.11.20 [df3053c](https://gitee.com/dotnetchina/Furion/commit/df3053cf081d5e4d8eb63d567ed95c45267e0969) + -  新增 `Newtonsoft.Json` 自动将时间戳转换为 `DateTime/DateTimeOffset` 类型 4.9.1.3 ⏱️2023.11.17 [78a589d](https://gitee.com/dotnetchina/Furion/commit/78a589d99eb5985b576e4c96acd6e4890391d6ff) + -  新增 `System.Text.Json` 自动将时间戳转换为 `DateTime/DateTimeOffset` 类型 4.9.1.2 ⏱️2023.11.17 [abd5196](https://gitee.com/dotnetchina/Furion/commit/abd5196f5c5160a5df96dad80c7c5aa51b96d5b9) + -  新增 `System.Text.Json` 和 `Newtonsoft.Json` 对粘土对象 `Clay` 支持 4.8.8.1 ⏱️2023.04.18 [#I6WKRZ](https://gitee.com/dotnetchina/Furion/issues/I6WKRZ) + +- **突破性变化** + + -  调整 **`IJsonSerializerProvider` 序列化接口,添加 `Deserialize` 反序列化方法** 4.8.8.15 ⏱️2023.05.15 [!815](https://gitee.com/dotnetchina/Furion/pulls/815) 感谢 [@YaChengMu](https://gitee.com/YaChengMu) + +
+ 查看变化 +
+ +添加 `25-32行` 接口方法: + +```cs showLineNumbers {25-32} +namespace Furion.JsonSerialization; + +/// +/// Json 序列化提供器 +/// +public interface IJsonSerializerProvider +{ + /// + /// 序列化对象 + /// + /// + /// + /// + string Serialize(object value, object jsonSerializerOptions = default); + + /// + /// 反序列化字符串 + /// + /// + /// + /// + /// + T Deserialize(string json, object jsonSerializerOptions = default); + + /// + /// 反序列化字符串 + /// + /// + /// + /// + /// + object Deserialize(string json, Type returnType, object jsonSerializerOptions = default); + + /// + /// 返回读取全局配置的 JSON 选项 + /// + /// + object GetSerializerOptions(); +} +``` + +如果使用 `Newtonsoft.Json` 则只需添加以下实现即可: + +```cs showLineNumbers {8-11} +/// +/// 反序列化字符串 +/// +/// +/// +/// +/// +public object Deserialize(string json, Type returnType, object jsonSerializerOptions = null) +{ + return JsonConvert.DeserializeObject(json, returnType, (jsonSerializerOptions ?? GetSerializerOptions()) as JsonSerializerSettings); +} +``` + +
+
+ +
+
+
+ +:::important 版本说明 + +以下内容仅限 `Furion 1.16.0 +` 版本使用。 + +::: + +## 23.1 什么是 `JSON` + +> JSON (JavaScript Object Notation, JS 对象标记) 是一种轻量级的数据交换格式。它基于 ECMAScript (w3c 制定的 js 规范)的一个子集,采用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言。 易于人阅读和编写,同时也易于机器解析和生成,并有效地提升网络传输效率。 + +简单来说,JSON,是一种数据格式,在与后端的数据交互中有较为广泛的应用。 + +## 23.2 关于序列化库 + +目前在 C# 语言中有两个主流的 `JSON` 序列化操作库: + +- `System.Text.Json`:`.NET Core` 内置 `JSON` 序列化库,也是 `Furion` 框架默认实现 +- `Newtonsoft.Json`:目前使用人数最多的 `JSON` 序列化库,需要安装 `Microsoft.AspNetCore.Mvc.NewtonsoftJson` 拓展包 + +由于目前 `System.Text.Json` 相比 `Newtonsoft.Json` 功能和稳定性有许多不足之处,比如循环引用问题在 `System.Text.Json` 无解。但在 `.NET 6` 之后得到解决。 + +`Furion` 框架为了解决多种序列化工具配置和用法上的差异问题,抽象出了 `IJsonSerializerProvider` 接口。 + +## 23.3 `IJsonSerializerProvider` 接口 + +`Furion` 框架提供了 `IJsonSerializerProvider` 接口规范,同时**要求实现该接口的实体都必须采用单例模式**,该接口定义代码如下: + +```cs showLineNumbers +namespace Furion.JsonSerialization; + +/// +/// Json 序列化提供器 +/// +public interface IJsonSerializerProvider +{ + /// + /// 序列化对象 + /// + /// + /// + /// + string Serialize(object value, object jsonSerializerOptions = default); + + /// + /// 反序列化字符串 + /// + /// + /// + /// + /// + T Deserialize(string json, object jsonSerializerOptions = default); + + /// + /// 反序列化字符串 + /// + /// + /// + /// + /// + object Deserialize(string json, Type returnType, object jsonSerializerOptions = default); + + /// + /// 返回读取全局配置的 JSON 选项 + /// + /// + object GetSerializerOptions(); +} +``` + +:::important 默认实现 + +`SystemTextJsonSerializerProvider` 类是 `IJsonSerializerProvider` 接口的默认实现,在应用启动时已默认注册。 + +::: + +## 23.4 如何使用 + +### 23.4.1 获取序列化对象 + +`Furion` 框架提供了两种方式获取 `IJsonSerializerProvider` 实例: + +- 构造函数注入 `IJsonSerializerProvider` +- 静态类 `JSON.GetJsonSerializer()` 方式,**查看 [JSON 静态类](./global/json.mdx)** + +如: + +```cs showLineNumbers {10,13} +using Furion.DynamicApiController; +using Furion.JsonSerialization; + +namespace Furion.Application +{ + public class JsonDemo : IDynamicApiController + { + private readonly IJsonSerializerProvider _jsonSerializer; + private readonly IJsonSerializerProvider _jsonSerializer2; + public JsonDemo(IJsonSerializerProvider jsonSerializer) + { + _jsonSerializer = jsonSerializer; + _jsonSerializer2 = JSON.GetJsonSerializer(); + } + } +} +``` + +### 23.4.2 序列化对象 + +```cs showLineNumbers +public string GetText() +{ + return _jsonSerializer.Serialize(new + { + Id = 1, + Name = "Furion" + }); +} +``` + +### 23.4.3 反序列化字符串 + +```cs showLineNumbers +public object GetObject() +{ + var json = "{\"Id\":1,\"Name\":\"Furion\"}"; + var obj = _jsonSerializer.Deserialize(json); + return obj; +} +``` + +:::important 特别注意 + +`System.Text.Json` 默认反序列化大小写敏感,也就是不完全匹配的属性名称不会自动赋值。这时候我们可以全局配置或单独配置。 + +- 全局配置 + +```cs showLineNumbers +services.AddControllersWithViews() + .AddJsonOptions(options => { + options.JsonSerializerOptions.PropertyNameCaseInsensitive = true; + }); +``` + +- 单独配置 + +```cs showLineNumbers +var obj = _jsonSerializer.Deserialize(json, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); +``` + +::: + +### 23.4.4 序列化更多配置 + +`Furion` 框架不推荐一个框架中有多种序列化实现类,也就是说使用 `System.Text.Json` 就不要使用 `Newtonsoft.Json`,反之亦然。 + +如需配置更多选项,只需创建 `JsonSerializerOptions` 配置对象即可,如: + +```cs showLineNumbers {5} +var json = _jsonSerializer.Serialize(new + { + Id = 1, + Name = "Furion" + }, new JsonSerializerOptions { + WriteIndented = true + }); +``` + +## 23.5 高级用法 + +### 23.5.1 自定义序列化提供器 + +正如上文所说,`Furion` 默认的 `IJsonSerializerProvider` 实现方式是 `System.Text.Json` 库,如需替换为 `Newtonsoft.Json`,只需以下步骤即可: + +:::tip 无需安装 + +在 `Furion 4.6.5+` 版本已经内置了 `Microsoft.AspNetCore.Mvc.NewtonsoftJson` 拓展包,也就是直接在 `Startup.cs` 中注册即可。 + +::: + +1. 安装 `Microsoft.AspNetCore.Mvc.NewtonsoftJson` 拓展,并在 `Startup.cs` 中注册 + +```cs showLineNumbers {2} +services.AddControllersWithViews() + .AddNewtonsoftJson(); +``` + +2. 实现 `IJsonSerializerProvider` 提供器 + +```cs showLineNumbers {9} +using Furion.JsonSerialization; +using Newtonsoft.Json; + +namespace Furion.Core; + +/// +/// Newtonsoft.Json 实现 +/// +public class NewtonsoftJsonSerializerProvider : IJsonSerializerProvider, ISingleton +{ + /// + /// 序列化对象 + /// + /// + /// + /// + public string Serialize(object value, object jsonSerializerOptions = null) + { + return JsonConvert.SerializeObject(value, (jsonSerializerOptions ?? GetSerializerOptions()) as JsonSerializerSettings); + } + + /// + /// 反序列化字符串 + /// + /// + /// + /// + /// + public T Deserialize(string json, object jsonSerializerOptions = null) + { + return JsonConvert.DeserializeObject(json, (jsonSerializerOptions ?? GetSerializerOptions()) as JsonSerializerSettings); + } + + /// + /// 反序列化字符串 + /// + /// + /// + /// + /// + public object Deserialize(string json, Type returnType, object jsonSerializerOptions = null) + { + return JsonConvert.DeserializeObject(json, returnType, (jsonSerializerOptions ?? GetSerializerOptions()) as JsonSerializerSettings); + } + + /// + /// 返回读取全局配置的 JSON 选项 + /// + /// + public object GetSerializerOptions() + { + return App.GetOptions()?.SerializerSettings; + } +} +``` + +### 23.5.2 序列化属性名大写(属性原样输出) + +- `System.Text.Json` 方式 + +```cs showLineNumbers +services.AddControllersWithViews() + .AddJsonOptions(options => { + options.JsonSerializerOptions.PropertyNamingPolicy = null; + // options.JsonSerializerOptions.DictionaryKeyPolicy = null; // 配置 Dictionary 类型序列化输出 + }); +``` + +- `Newtonsoft.Json` 方式 + +```cs showLineNumbers +services.AddControllersWithViews() + .AddNewtonsoftJson(options => + { + options.SerializerSettings.ContractResolver = new DefaultContractResolver(); + }); +``` + +:::important 特别注意 + +采用 `Newtonsoft.Json` 方式接口返回值能够正常输出,但是 `Swagger` 界面中的 `Example Values` 依然显示小写字母开头的属性,这时只需要再添加 `System.Text.Json` 配置即可,如: + +```cs showLineNumbers +.AddJsonOptions(options => { + options.JsonSerializerOptions.PropertyNamingPolicy = null; + }); +``` + +主要原因是 `Swagger` 拓展包底层依赖了 `System.Text.Json`。 + +::: + +### 23.5.3 时间格式化(时间戳转时间) + +- `System.Text.Json` 方式 + +需引用 `System.Text.Json` 命名空间。 + +```cs showLineNumbers {2,5} +services.AddControllersWithViews() + .AddJsonOptions(options => + { + // 在 4.6.5 之前的版本使用 .AddDateFormatString + options.JsonSerializerOptions.Converters.AddDateTimeTypeConverters("yyyy-MM-dd HH:mm:ss"); + }); +``` + +:::note 小提示 + +如果使用使用了 `DateTimeOffset` 类型,那么可以设置 `.AddDateTimeTypeConverters("yyyy-MM-dd HH:mm:ss", true)` 第二个参数为 `true`,自动转换成本地时间。 + +如果使用了 `Mysql` 数据库,且使用了 `Pomelo.EntityFrameworkCore.MySql` 包,那么会出现时区问题,比如少 8 小时,可以尝试配置第二个参数为 `true`。 + +::: + + +- `Newtonsoft.Json` 方式 + +需引用 `Newtonsoft.Json` 命名空间。 + +```cs showLineNumbers {2,4,6-7} +services.AddControllersWithViews() + .AddNewtonsoftJson(options => + { + options.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss"; + + // 在 Furion 4.9.1.3+ 支持 + // options.SerializerSettings.Converters.AddDateTimeTypeConverters(); + }); +``` + +:::tip 时间戳转时间类型 + +在 `Furion 4.9.1.3+` 版本中,该操作还支持自动将 `13/10` 位时间戳数值或字符串转换成 `DateTime/DateTime?/DateTimeOffset/DateTimeOffset?` 类型。 + +如将 `1699459200000` 时间戳转换为 `DateTime` 类型。 + +针对 `GET/HEAD` 请求可通过贴 `[ModelBinder(BinderType = typeof(TimestampToDateTimeModelBinder))]` 特性处理。 + +::: + +### 23.5.4 忽略循环引用 + +- `System.Text.Json` 方式 + +```cs showLineNumbers +services.AddControllersWithViews() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles; + }); +``` + +:::important 特别说明 + +在 `.NET 5` 中,`System.Text.Json` 并不支持处理循环引用问题,以上的解决方案仅限用于 `.NET 6 Preview 2+`。😂 + +::: + +需引用 `System.Text.Json` 命名空间。 + +- `Newtonsoft.Json` 方式 + +```cs showLineNumbers +services.AddControllersWithViews() + .AddNewtonsoftJson(options => + { + options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; + }); +``` + +### 23.5.5 包含成员字段序列化 + +- `System.Text.Json` 方式 + +```cs showLineNumbers +services.AddControllersWithViews() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.IncludeFields = true; + }); +``` + +需引用 `System.Text.Json` 命名空间。 + +- `Newtonsoft.Json` 方式 + +无需配置。 + +### 23.5.6 允许尾随逗号 + +- `System.Text.Json` 方式 + +```cs showLineNumbers +services.AddControllersWithViews() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.AllowTrailingCommas = true; + }); +``` + +需引用 `System.Text.Json` 命名空间。 + +- `Newtonsoft.Json` 方式 + +无需配置。 + +### 23.5.7 允许注释 + +- `System.Text.Json` 方式 + +```cs showLineNumbers +services.AddControllersWithViews() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.ReadCommentHandling = JsonCommentHandling.Skip; + }); +``` + +需引用 `System.Text.Json` 命名空间。 + +- `Newtonsoft.Json` 方式 + +无需配置。 + +### 23.5.8 处理(中文)乱码问题 + +默认情况下,若序列化对象包含中文或特殊字符会被转义,如: + +```json showLineNumbers {5} +{ + "statusCode": 500, + "data": null, + "succeeded": false, + "errors": "\u8BF7\u6C42\u53C2\u6570ips\u4E0D\u5141\u8BB8\u4E3A\u7A7A\uFF01", + "extras": null, + "timestamp": 1698390015 +} +``` + +这时只需要通过下面配置即可: + +- `System.Text.Json` 方式 + +```cs showLineNumbers +services.AddControllersWithViews() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping; + }); +``` + +需引用 `System.Text.Json` 命名空间。 + +- `Newtonsoft.Json` 方式 + +无需配置。 + +### 23.5.9 不区分大小写 + +- `System.Text.Json` 方式 + +```cs showLineNumbers +services.AddControllersWithViews() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.PropertyNameCaseInsensitive = true; + }); +``` + +需引用 `System.Text.Json` 命名空间。 + +- `Newtonsoft.Json` 方式 + +:::tip 更多序列化配置 + +这里只列举常用见的序列化配置,如需查看更多配置,可查阅 [System.Text.Json 文档](https://docs.microsoft.com/zh-cn/dotnet/standard/serialization/system-text-json-configure-options?pivots=dotnet-5-0) + +::: + +### 23.5.10 忽略特定属性序列化 + +有时候我们不希望对象中某个对象被序列化出来或者不想在 `Swagger` 中显示,这时候只需要在属性贴该特性即可: + +```cs showLineNumbers +[Newtonsoft.Json.JsonIgnore] // 针对 Newtonsoft +[System.Text.Json.Serialization.JsonIgnore] // 针对 System.Text.Json +public string PropertyName {get; set;} +``` + +### 23.5.11 动态对象属性名大写问题 + +有时候使用了动态对象后发现属性名出现了大写情况(首字母),这个时候可以尝试使用以下方法解决: + +```cs showLineNumbers +.AddNewtonsoftJson(options => +{ + options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); +}) +``` + +### 23.5.12 忽略所有 `null` 属性 + +- `System.Text.Json` 方式 + +```cs showLineNumbers {4} +services.AddControllersWithViews() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + }); +``` + +- `Newtonsoft.Json` 方式 + +```cs showLineNumbers {3} +.AddNewtonsoftJson(options => +{ + options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; +}) +``` + +### 23.5.13 忽略所有默认值属性 + +- `System.Text.Json` 方式 + +```cs showLineNumbers {4} +services.AddControllersWithViews() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault; + }); +``` + +- `Newtonsoft.Json` 方式 + +```cs showLineNumbers {3} +.AddNewtonsoftJson(options => +{ + options.SerializerSettings.DefaultValueHandling = DefaultValueHandling.Ignore; +}) +``` + +### 23.5.14 控制属性序列化顺序 + +```cs showLineNumbers +[Newtonsoft.Json.JsonProperty(Order = 0)] // 针对 Newtonsoft +[System.Text.Json.Serialization.JsonPropertyOrder(0)] // 针对 System.Text.Json +public string PropertyName {get; set;} +``` + +### 23.5.15 重命名序列化名称(属性名称) + +```cs showLineNumbers +[Newtonsoft.Json.JsonProperty("newName")] // 针对 Newtonsoft +[System.Text.Json.Serialization.JsonPropertyName("newName")] // 针对 System.Text.Json +public string PropertyName {get; set;} +``` + +### 23.5.16 `JSON` 字符串缩进 + +- `System.Text.Json` 方式 + +```cs showLineNumbers {4} +services.AddControllersWithViews() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.WriteIndented = true; + }); +``` + +- `Newtonsoft.Json` 方式 + +```cs showLineNumbers {3} +.AddNewtonsoftJson(options => +{ + options.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.Indented; +}) +``` + +### 23.5.17 `long` 类型序列化时转 `string` + +有时候我们需要将 ` long` 类型序列化时转为 `string` 类型,防止 `JavaScript` 出现精度溢出问题,这个时候可以尝试使用以下方法解决: + +- `System.Text.Json` 方式 + +```cs showLineNumbers {2,4} +services.AddControllersWithViews() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.Converters.AddLongTypeConverters(); + }); +``` + +- `Newtonsoft.Json` 方式 + +```cs showLineNumbers {3} +.AddNewtonsoftJson(options => +{ + options.SerializerSettings.Converters.AddLongTypeConverters(); +}) +``` + +:::note 关于 `Dictionary<,>` 类型包含 `long` 处理 + +默认情况下,`System.Text.Json` 不支持 `Dictionary<,>` 类型的序列化设置 `Converter` 操作,这个时候可以换成 `Newtonsoft.Json` 处理,如: + +```cs showLineNumbers {3} +.AddNewtonsoftJson(options => +{ + options.SerializerSettings.Converters.AddLongTypeConverters(); +}) +``` + +同时创建 `NewtonsoftJsonSerializerProvider.cs` 文件写入即可: + +```cs showLineNumbers +namespace YourProject.Core; + +public class NewtonsoftJsonSerializerProvider : IJsonSerializerProvider, ISingleton +{ + public string Serialize(object value, object jsonSerializerOptions = null) + { + return JsonConvert.SerializeObject(value, (jsonSerializerOptions ?? GetSerializerOptions()) as JsonSerializerSettings); + } + + public T Deserialize(string json, object jsonSerializerOptions = null) + { + return JsonConvert.DeserializeObject(json, (jsonSerializerOptions ?? GetSerializerOptions()) as JsonSerializerSettings); + } + + public object GetSerializerOptions() + { + return App.GetOptions()?.SerializerSettings; + } + + public object Deserialize(string json, Type returnType, object jsonSerializerOptions = null) + { + return JsonConvert.DeserializeObject(json, returnType, (jsonSerializerOptions ?? GetSerializerOptions()) as JsonSerializerSettings); + } +} +``` + +::: + +### 23.5.18 `DateOnly` 和 `TimeOnly` 类型序列化支持 + +在 `.NET6+` 添加了 `DateOnly` 和 `TimeOnly` 类型,`Furion 4.7.9+` 提供了支持。 + +- `System.Text.Json` 方式 + +```cs showLineNumbers {2,4-5} +services.AddControllersWithViews() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.Converters.AddDateOnlyConverters(); // DateOnly + options.JsonSerializerOptions.Converters.AddTimeOnlyConverters(); // TimeOnly + }); +``` + +- `Newtonsoft.Json` 方式 + +```cs showLineNumbers {3-4} +.AddNewtonsoftJson(options => +{ + options.SerializerSettings.Converters.AddDateOnlyConverters(); // DateOnly + options.SerializerSettings.Converters.AddTimeOnlyConverters(); // TimeOnly +}) +``` + +### 23.5.19 粘土对象 `Clay` 类型序列化支持 + +默认情况下,`Clay` 为动态类型对象,不支持直接通过 `System.Text.Json` 和 `Newtonsoft.Json` 进行序列化和反序列化,这时只需添加以下配置即可。 + +- `System.Text.Json` 方式 + +```cs showLineNumbers {2,4} +services.AddControllersWithViews() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.Converters.AddClayConverters(); + }); +``` + +- `Newtonsoft.Json` 方式 + +```cs showLineNumbers {3} +.AddNewtonsoftJson(options => +{ + options.SerializerSettings.Converters.AddClayConverters(); +}) +``` + +### 23.5.20 `DateTimeOffset` 反序列化异常 + +以下处理只针对 `Newtonsoft.Json`,数据为 `0001-01-01 00:00:00` 的情形下反序列化为 `DateTimeOffset` 类型报错: + +```txt showLineNumbers +Could not convert string to DateTimeOffset: 0001-01-01 00:00:00 +``` + +相关问题讨论:[https://stackoverflow.com/questions/50628374/json-net-deserializing-datetimeoffset-value-fails-for-datetimeoffset-minvalue-wi/50631270#50631270](https://stackoverflow.com/questions/50628374/json-net-deserializing-datetimeoffset-value-fails-for-datetimeoffset-minvalue-wi/50631270#50631270) + +- `System.Text.Json` 方式 + +无需配置 + +- `Newtonsoft.Json` 方式 + +```cs showLineNumbers {3-5} +.AddNewtonsoftJson(options => +{ + options.SerializerSettings.MetadataPropertyHandling = MetadataPropertyHandling.Ignore; + options.SerializerSettings.DateParseHandling = DateParseHandling.None; + options.SerializerSettings.Converters.Add(new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal }); +}); +``` + +### 23.5.21 继承/派生类序列化问题 + +在 `.NET7` 之前,`System.Text.Json` 默认不支持序列化基类属性,但也提供了解决方法: + +```cs showLineNumbers {1,4} +// 第一种,使用 泛型 +var json = JsonSerializer.Serialize(value); + +// 第二种,使用 Type 参数 +var json = JsonSerializer.Serialize(value, value.GetType()); +``` + +查看详细文档:[https://learn.microsoft.com/zh-cn/dotnet/standard/serialization/system-text-json/polymorphism?pivots=dotnet-6-0](https://learn.microsoft.com/zh-cn/dotnet/standard/serialization/system-text-json/polymorphism?pivots=dotnet-6-0) + +### 23.5.22 枚举和字符串互转问题 + +默认情况下,只能将枚举转换为数字或将数字转换为枚举对象,我们也可以通过局部或全局配置实现字符串互转,如: + +- 局部 + +```cs showLineNumbers {1} +[JsonConverter(typeof(JsonStringEnumConverter))] +public Gender Gender { get; set; } +``` + +- 全局 + +```cs showLineNumbers {2-4} +services.AddControllersWithViews() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + }); +``` + +## 23.6 `DataTable`、`DataSet`、`Tuple` 、`JArray`,`JObject`,`JToken` 等序列化问题 + +由于 `Furion` 默认采用 `System.Text.Json` 进行序列化,但是不支持复杂类型,如 `DataTable`、`DataSet`、`Tuple` 、`JArray`,`JObject`、`JToken` 等类型,所以需要更换成 `NewtonsoftJson` 即可,见 [JSON 序列化 - 23.5.1 自定义序列化提供器](./json-serialization#2351-自定义序列化提供器) + +## 23.7 `System.Text.Json` 和 `Newtonsoft.Json` 完整差异化对比 + +[https://docs.microsoft.com/zh-cn/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to?pivots=dotnet-5-0](https://docs.microsoft.com/zh-cn/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to?pivots=dotnet-5-0) + +## 23.8 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/jsonschema.mdx b/handbook/docs/jsonschema.mdx new file mode 100644 index 0000000000000000000000000000000000000000..faa70b5e5389063d6b2190c23769e4c621399b4e --- /dev/null +++ b/handbook/docs/jsonschema.mdx @@ -0,0 +1,110 @@ +--- +id: jsonschema +title: 2.13 JSON Schema 使用 +sidebar_label: 2.13 JSON Schema 使用 +description: 配置文件也要智能提示 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +## 2.13.1 关于 `JSON Schema` + +`JSON Schema` 是用于验证 `JSON` 数据结构的强大工具,`Schema` 可以理解为模式或者规则。 + +**有了 `JSON Schema` 再也不怕配置写错的情况了!** + +## 2.13.2 学习 `JSON Schema` 编写 + +作为一个框架提供 `JSON Schema` 是非常有必要的,可以让开发者在添加配置的时候能够有智能提示和校验功能,如果想学习 `JSON Schema` 编写可以查看以下文档: + +- [https://json-schema.apifox.cn/](https://json-schema.apifox.cn/) +- [https://zhuanlan.zhihu.com/p/355175938](https://zhuanlan.zhihu.com/p/355175938) + +这里也提供一个非常便捷的创建 `JSON Schema` 的在线网站,可以根据 `json` 文件内容自动生成 `JSON Schema`,之后进行小量修改即可: + +[https://hellosean1025.github.io/json-schema-visual-editor/](https://hellosean1025.github.io/json-schema-visual-editor/) + +## 2.13.3 框架提供 + +`Furion` 框架提供了完整的 `Furion` 和 `ASP.NET Core` 的 `JSON Schema` 文件,通过该文件可以在编写配置文件时提供完整的智能提示和校验。 + +**[查看 `JSON Schema` 源码地址](https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json)** + +## 2.13.4 如何使用 + +使用方式非常简单,只需要在 `.json` 文件的头部添加 `"$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json",` 即可,如: + +```json showLineNumbers {2} +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "Microsoft.EntityFrameworkCore": "Information", + "Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information" + } + }, + "AllowedHosts": "*" +} +``` + + + + + + + + + + + +## 2.13.5 `JSON Schema` 失效解决 + +如果添加了 `"$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json",` 依然无法进行智能提示校验,可尝试关闭 `.json` 文件再重新打开。 + +如果上面步骤依然无效,那么还可以通过下面方式: + +### 2.13.5.1 `Visual Studio` + + + + + +### 2.13.5.2 `Visual Studio Code` + +重启 `Visual Studio Code` 即可。 + +## 2.13.6 如何更新 `JSON Schema` + +默认情况下,`JSON Schema` 在第一次获取之后会自动缓存起来,可能会导致 `.json` 文件提示错误,这个时候只需要删除缓存即可。 + +### 2.13.6.1 `Visual Studio` + +打开电脑的 `运行` 并输入 `%AppData%`,之后进入 `C:\Users\你的电脑用户名\AppData\Local\Microsoft\VisualStudio` 下 + + + + + + + +**之后删除 `http/https` 开头的文件即可。** + +### 2.13.6.2 `Visual Studio Code` + +同上,运行进入 `%AppData%`,之后进入下列路径:`C:\Users\你的电脑用户名\AppData\Roaming\Code\User\globalStorage\vscode.json-language-features\json-schema-cache` + + + +**之后删除这些文件即可。** + +## 2.13.7 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/local-language.mdx b/handbook/docs/local-language.mdx new file mode 100644 index 0000000000000000000000000000000000000000..080b5ef46b293ba87a07bc6bb8e3a947afaac2ba --- /dev/null +++ b/handbook/docs/local-language.mdx @@ -0,0 +1,786 @@ +--- +id: local-language +title: 21. 全球化和本地化 +sidebar_label: 21. 全球化和本地化(多语言) +--- + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **新特性** + + -  新增 多语言支持 `L.GetDefaultCulture()` 获取本地配置默认语言 4.8.8.49 ⏱️2023.10.25 [!858](https://gitee.com/dotnetchina/Furion/pulls/858) + -  新增 多语言支持 `L.GetString(name, culture)` 获取指定区域翻译 4.8.8.41 ⏱️2023.08.04 [044b0ed](https://gitee.com/dotnetchina/Furion/commit/044b0edfbd622c7c69d685267aafa9f5855a9167) + -  新增 多语言支持 `DateTime` 时间格式化配置节点 `DateTimeFormatCulture` 4.8.7.31 ⏱️2023.03.31 [#I6RUOU](https://gitee.com/dotnetchina/Furion/issues/I6RUOU) + -  新增 **多语言支持 `.json` 文件配置方式(推荐)** 4.8.6 ⏱️2023.02.08 [#I6DL71](https://gitee.com/dotnetchina/Furion/issues/I6DL71) [#I5DXKP](https://gitee.com/dotnetchina/Furion/issues/I5DXKP) + -  新增 `L.SetCurrentUICulture(culture)` 和 `L.GetCurrentUICulture()` 静态方法,可在运行时动态修改当前线程区域性 4.8.3.10 ⏱️2022.12.23 [#I66JWA](https://gitee.com/dotnetchina/Furion/issues/I66JWA) + -  新增 `L.SetCulture(culture, immediately)` 方法重载,可配置运行时修改多语言立即有效 4.8.3.10 ⏱️2022.12.23 [#I66JWA](https://gitee.com/dotnetchina/Furion/issues/I66JWA) + +- **其他更改** + + -  调整 多语言中间件 `app.UseAppLocalization()` 添加 `Action` 委托参数 4.8.7.30 ⏱️2023.03.31 [#I6RUOU](https://gitee.com/dotnetchina/Furion/issues/I6RUOU) + +- **文档** + + -  新增 **多语言 `.json` 配置方式文档** + +
+
+
+ +import useBaseUrl from "@docusaurus/useBaseUrl"; + +## 21.1 全球化和本地化 + +**全球化** 是设计支持不同区域性的应用程序的过程。 全球化添加了对一组有关特定地理区域的已定义语言脚本的输入、显示和输出支持。 + +**本地化** 是将已经针对可本地化性进行处理的全球化应用调整为特定的区域性/区域设置的过程。 + +通俗来说,就是使应用或系统支持多语言切换。`Furion` 框架提供了完整支持多语言处理的服务。 + +## 21.2 注册服务 + +在使用多语言服务之前,必须先注册服务,如: + +```cs showLineNumbers {4,14-15} +public void ConfigureServices(IServiceCollection services) +{ + services.AddControllersWithViews() + .AddAppLocalization(); // 注册多语言 +} + +public void Configure(IApplicationBuilder app, IWebHostEnvironment env) +{ + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + // 配置多语言,必须在 路由注册之前 + app.UseAppLocalization(); + + app.UseStaticFiles(); + app.UseRouting(); + + // 其他中间件 +} +``` + +:::important 特别注意 + +`app.UseAppLocalization();` 必须在 `app.UseRouting();` 之前注册。 + +::: + +## 21.3 如何使用 + +### 21.3.1 配置 `LocalizationSettings` + +添加 `LocalizationSettings` 配置选项: + +```json showLineNumbers {2,3} +{ + "LocalizationSettings": { + "SupportedCultures": ["zh-CN", "en-US"], // 配置支持的语言列表 + "DefaultCulture": "zh-CN" // 配置默认语言,如果不配置,取 SupportedCultures 的第一项 + } +} +``` + +### 21.3.2 创建 `Resources` 文件夹 + +接下来在 `Web启动项目层` 添加 `Resources` 文件夹,如图:(**可配置放置任意层,见配置 `AssemblyName`**) + + + +## 21.4 `L` 静态类 + +`Furion` 框架主要通过 `L` 静态类完成多语言转换,该静态类有以下属性和方法: + +- **`L.Text[文本]`**:转换文本多语言 +- **`L.Html[HTML代码, 格式化]`**:转换 `Html` 多语言 +- `L.TextOf()[文本]`:转换文本多语言,使用该方式那么资源文件需使用完整类型限定名,Furion 4.8.6+ 支持 +- `L.HtmlOf()[HTML代码, 格式化]`:转换 `Html` 多语言,使用该方式那么资源文件需使用完整类型限定名,Furion 4.8.6+ 支持 +- `L.SetCulture(区域码)`:设置当前语言区域,默认在下一次请求有效,第二个参数设置 `true` 立即有效 +- `L.GetSelectCulture()`:获取当前的语言区域 +- `L.GetCultures()`:获取系统支持的多语言列表 +- `L.SetCurrentUICulture(区域码)`:运行时设置当前线程语言区域,立即有效,`Furion 4.8.3.10+` 版本有效 +- `L.GetCurrentUICulture()`: 获取当前线程 UI 区域性,`Furion 4.8.3.10+` 版本有效 +- `L.GetString(u => u.属性)`: 根据表达式获取翻译,`Furion 4.8.3.10+` 版本有效 +- `L.GetString(name, culture)`: 获取指定的区域翻译,`Furion 4.8.8.41+` 版本有效 +- `L.GetDefaultCulture()`: 获取本地配置默认语言,`Furion 4.8.8.49+` 版本有效 + +:::tip 小提示 + +通过 `L.SetCulture` 实际上是往客户端写入区域 `Cookie` 数据,也就是只对下一次请求有效,如果希望运行时立即有效,可通过 `L.SetCurrentUICulture` 设置或一起设置即可。 + +也可以直接通过设置 `L.SetCulture(区域码, true)` 立即生效(包含当前请求(线程)和下一次请求)。 + +::: + +## 21.5 使用例子 + +通过上面的配置步骤之后,我们就可以通过 `L` 静态类在代码任何位置使用了,如: + +### 21.5.1 在类中使用 + +```cs showLineNumbers +// 文本多语言 +var apiInterface = L.Text["API 接口"]; +var sourceCode = L.Text["源码地址"]; +var other = L.Text["其他{0}", "的"]; + +// HTML 标记多语言 +var name = L.Html["Hello {0}", name]; +``` + +### 21.5.2 在视图中使用 + +```html showLineNumbers {1,6,9} +@using Furion.Localization + +
+

让 .NET 开发更简单,更通用,更流行。

+

+ @L.Text["API 接口"]      @L.Text["源码地址"] +

+
+``` + +### 21.5.3 在验证特性中使用 + +```cs showLineNumbers +[Required(ErrorMessage = "必填消息")] +``` + +**所有验证特性已经自动支持多语言配置了,无需通过 `L.Text[]` 调用。** + +### 21.5.4 在异常消息中使用 + +```cs showLineNumbers {8} +using Furion.FriendlyException; + +namespace Furion.Application +{ + [ErrorCodeType] + public enum ErrorCodes + { + [ErrorCodeItemMetadata("用户名不能为空")] + z1000 + } +} +``` + +**所有异常消息特性已经自动支持多语言配置了,无需通过 `L.Text[]` 调用。** + +```cs showLineNumbers +throw Oops.Oh(ErrorCodes.z1000); // 自动应用多语言 +``` + +### 21.5.5 `SharedResource` 模式 + +:::important 版本说明 + +以下内容仅限 `Furion 4.3.7 +` 版本使用。 + +::: + +正常情况下,我们都是通过 `L.Text["Hello"]` 方式输出 `Hello` 在不同的区域语言的翻译,但是会导致 `Hello` 硬编码字符串散落到处都是。 + +所以 `Furion` 提供了 `SharedResource` 模式,只需要创建一个 `SharedResource.cs` 类(可以任何名字)并添加对应的属性即可,如: + +```cs showLineNumbers {3,5,7} +namespace Furion.Core; + +public class SharedResource +{ + public string Hello { get; set; } + + public string Name { get; set; } +} +``` + +使用如下: + +```cs showLineNumbers +var hello = L.GetString(u => u.Hello); // 比 L.Text["Hello"]; 容易维护 +var name = L.GetString(u => u.Name); // 比 L.Text["Name"]; 容易维护 +``` + +当然也有更简单的方式,就是使用 `nameof`,如: + +```cs showLineNumbers +var hello = L.Text[nameof(SharedResource.Hello)]; // 比 L.Text["Hello"]; 容易维护 +var name = L.Text[nameof(SharedResource.Name)]; // 比 L.Text["Name"]; 容易维护 +``` + +## 21.6 创建语言翻译文件 + +在 `Furion` 框架中,如果没找到对应的语言翻译文件,则自动显示字符串文本,如: + +```cs showLineNumbers +L.Text["没找到"]; // => 如果设置为英文,但是没有文件,则直接输出 “没找到” +``` + +### 21.6.1 在 `Resources` 文件夹中创建语言文件 + +接下来,我们只需要在刚刚的 `Resources` 文件夹中添加 `.resx` 资源文件即可,资源文件命名规则:`Lang.区域码.resx`,如:`Lang.en-US.resx`。 + + + + + +

+ +接下来,只需要把对应语言版本的键值对填写即可。 + +:::tip 自定义资源文件名或存放程序集 + +默认情况下,资源文件名必须以 `Lang` 开头,且默认放在启动层,如果需要自定义,添加配置文件即可 + +```json showLineNumbers +{ + "LocalizationSettings": { + "LanguageFilePrefix": "MyLang" + // "AssemblyName": "你的其他层程序集名称" + } +} +``` + +之后,就可以:`MyLang.区域码.resx`。 + +::: + +## 21.7 切换语言 + +`Furion` 提供了三种语言切换方式进行切换语言: + +- `URL 参数` 方式: `?culture=en-US`,**此方式优先级最高**,格式为:`culture=区域码` +- `Cookies` 方式:调用 `L.SetCulture(区域码)` 方式切换 +- `客户端浏览器语言自动匹配`:如果前面两种方式都没有设置,**支持自动根据客户端浏览器语言进行匹配。** + +### 21.7.1 `URL 参数` 方式 + + + +### 21.7.2 `Cookies` 方式 + +此方式只需要提供一个 `api` 或设置代码即可: + +```cs showLineNumbers +L.SetCulture("en-US"); // en-US 也可以通过前端传递过来,这样就可以不用 `culture` 参数了,可以自定义参数。 +``` + +这样就可以直接根据客户端存储的 `cookies` 自动切换了。 + +### 21.7.3 `客户端浏览器语言自动切换` + +推荐此方式,可以自动根据浏览器的语言自动配置: + + + +### 21.7.4 请求报文头 `Accept-Language` 方式 + +如果使用第三方工具请求,也可以添加请求报文头方式,如: + +```txt showLineNumbers +Accept-Language: en-US +``` + +### 21.7.5 请求报文头 `Cookie` 方式 + +如果使用第三方工具请求,也可以添加请求报文头方式,如: + +```txt showLineNumbers +Cookie: c%3Den-US%7Cuic%3Den-USen-US +``` + +它的明文格式为:`c=en-US|uic=en-US`。 + +:::caution 注意 + +这种方式不能使用 `明文`,否则无效。另外 `c` 和 `uic` 的值通常一致。 + +::: + +## 21.8 依赖注入方式使用 + +`Furion` 框架也兼容 `.NET Core` 自带的依赖注入方式,如: + +```cs showLineNumbers {3,5,7,12} + public class TestController : Controller + { + private readonly IStringLocalizer _localizer; + + public TestController(IStringLocalizerFactory factory) + { + _localizer = factory.Create(); + } + + public IActionResult About() + { + ViewData["Message"] = _localizer["Your application description page."]; + } + } +``` + +## 21.9 `LocalizationSettings` 配置 + +- `LocalizationSettings` 多语言配置根节点 + - `ResourcesPath`:资源目录,`string` 类型,默认 `Resources` + - `SupportedCultures`:支持的语言区域码类别,`string[]` 类型 + - `DefaultCulture`:默认语言区域码,如果为空,则取 `SupportedCultures` 第一项 + - `LanguageFilePrefix`:配置资源文件前缀,`string` 类型,默认 `Lang` + - `AssemblyName`:配置资源文件存放程序集名,`string` 类型,默认 `启动层` 名称 + +## 21.10 关于中文不能切换问题 + +若通过 `dotnet build` 命令行编译后发布的代码(比如 `jenkins` 以及其他 `devops` 工具),无法生成 `zh-CN` 资源文件,会出现中文无法显示的问题,这时候只需要将 `Lang.zh-CN.resx` 修改为:`Lang.zh-Hans.resx` 或 `Lang.zh-Hant.resx` 或 `Lang.zh.resx` 即可。 + +相关文档说明 [https://docs.microsoft.com/zh-cn/dotnet/api/system.globalization.cultureinfo?view=net-6.0](https://docs.microsoft.com/zh-cn/dotnet/api/system.globalization.cultureinfo?view=net-6.0) + +## 21.11 基于 `JSON` 文件多语言 + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.6 +` 版本使用。 + +::: + +在默认情况下,微软推荐使用 `.resx` 资源文件格式作为多语言配置文件格式,**但此方式无法在运行时进行修改**。所以社区更多使用的是 `.json` 文件格式作为多语言配置,可在运行时动态修改。 + +--- + +**`Furion` 框架默认不提供 `json` 多语言配置功能,但非常容易集成第三方多语言库**,在这里推荐:[https://github.com/hishamco/My.Extensions.Localization.Json](https://github.com/hishamco/My.Extensions.Localization.Json): + +**1. 安装 `My.Extensions.Localization.Json` 拓展,通常安装在 `XXX.Web.Core` 层** + +```bash showLineNumbers +dotnet add package My.Extensions.Localization.Json +``` + +**2. 在 `Startup.cs` 中注册服务** + +```cs showLineNumbers {4-8,19} +public void ConfigureServices(IServiceCollection services) +{ + services.AddControllersWithViews() + .AddAppLocalization(settings => + { + // 集成第三方 json 配置 + services.AddJsonLocalization(options => options.ResourcesPath = settings.ResourcesPath); + }); // 注册多语言 +} + +public void Configure(IApplicationBuilder app, IWebHostEnvironment env) +{ + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + // 配置多语言,必须在 路由注册之前 + app.UseAppLocalization(); + + app.UseStaticFiles(); + app.UseRouting(); + + // 其他中间件 +} +``` + +:::important 特别注意 + +`app.UseAppLocalization();` 必须在 `app.UseRouting();` 之前注册。 + +::: + +**3. 在 `Resources` 文件夹中添加 `.json` 文件即可,资源文件命名规则:`Lang.区域码.json`,如:`Lang.en-US.json`。** + + + + + +:::tip 自定义资源文件名或存放程序集 + +默认情况下,资源文件名必须以 `Lang` 开头,且默认放在启动层,如果需要自定义,添加配置文件即可 + +```json showLineNumbers +{ + "LocalizationSettings": { + "LanguageFilePrefix": "MyLang" + // "AssemblyName": "你的其他层程序集名称" + } +} +``` + +之后,就可以:`MyLang.区域码.json`。 + +::: + +**4. 在代码中使用** + +```cs showLineNumbers +L.Text["Furion"]; // => 如果设置为英文,但是没有文件,则直接输出 “Furion” +``` + +:::tip 小知识 + +**集成第三方多语言库除了注册服务和文件后缀名有区别,其他用法一模一样(包括全局配置,用法等)**,也就是任何时候替换成第三方都可以,业务使用代码无需更改。 + +::: + +## 21.12 多语言其他实现 + +在 `asp.net core 本地化` 文档中微软推荐了三个拓展开源项目:[https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/localization-extensibility?view=aspnetcore-6.0#localization-resources](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/localization-extensibility?view=aspnetcore-6.0#localization-resources) + +选择自己合适的即可。 + + + +## 21.13 在 `WinForm/WPF` 中使用 + +框架提供了完整的多语言在 `WinForm/WPF` 中使用。 + +### 21.13.1 `WinForm` 中使用 + +- 注册 `services.AddAppLocalization()` 服务 + +```cs showLineNumbers {10-13} +using Microsoft.Extensions.DependencyInjection; + +namespace WinFormsApp1; + +internal static class Program +{ + [STAThread] + static void Main() + { + Serve.RunNative(services => + { + services.AddAppLocalization(); + }); + ApplicationConfiguration.Initialize(); + Application.Run(new Form1()); + } +} +``` + +- 添加 `appsettings.json` 配置文件,并设置属性为 `如果较新则复制` + +```json showLineNumbers {2} +{ + "LocalizationSettings": { + "SupportedCultures": ["zh-CN", "en-US"], // 配置支持的语言列表 + "DefaultCulture": "zh-CN" // 配置默认语言,如果不配置,取 SupportedCultures 的第一项 + } +} +``` + +- 添加 `Resources` 文件夹并添加 `Lang.区域码.resx` 资源文件 + +参考 [21.6.1 在 Resources 文件夹中创建语言文件](./local-language.mdx#2161-在-resources-文件夹中创建语言文件) + +- 在代码中使用 + +```cs showLineNumbers {2,4} +// 设置当前线程 UI 区域性 +L.SetCurrentUICulture("en-US"); +// 通过 L.Text[Key] 获取 +label1.Text = L.Text["API 接口"]; +``` + +### 21.13.2 在 `WPF` 中使用 + +- 注册 `services.AddAppLocalization()` 服务 + +```cs showLineNumbers {7-10} +namespace WpfApp1; + +public partial class App : Application +{ + public App() + { + Serve.RunNative(services => + { + services.AddAppLocalization(); + }); + } +} +``` + +- 添加 `appsettings.json` 配置文件,并设置属性为 `如果较新则复制` + +```json showLineNumbers {2} +{ + "LocalizationSettings": { + "SupportedCultures": ["zh-CN", "en-US"], // 配置支持的语言列表 + "DefaultCulture": "zh-CN" // 配置默认语言,如果不配置,取 SupportedCultures 的第一项 + } +} +``` + +- 添加 `Resources` 文件夹并添加 `Lang.区域码.resx` 资源文件 + +参考 [21.6.1 在 Resources 文件夹中创建语言文件](./local-language.mdx#2161-在-resources-文件夹中创建语言文件) + +- 在代码中使用 + +```cs showLineNumbers {2,4} +// 设置当前线程 UI 区域性 +L.SetCurrentUICulture("en-US"); +// 通过 L.Text[Key] 获取 +label1.Name = L.Text["API 接口"]; +``` + +## 21.14 关于 `DateTime.Now` 问题 + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.7.31 +` 版本使用。 + +::: + +启用了多语言功能之后很有可能会影响 `DateTime.Now` 获取的时间所在的时区,比如设置多语言区域设置为 `en-US`,那么获取的就是国外的时间。所以,**尽可能的使用 `DateTime.UtcNow` 或者 `DateTimeOffset.UtNow` 记录时间**。 + +当然也可以添加配置,固定时间区域为特定时区,比如北京时间。 + +```json showLineNumbers {2,3} +{ + "LocalizationSettings": { + "DateTimeFormatCulture": "zh-CN" + } +} +``` + +## 21.15 关于 `Blazor` 项目问题 + +如果使用的是 `.razor` 组件,那么只支持请求头中携带 `Accept-Language` 和 `cookies` 方式,有关配置可查阅相关 `Issue`:[https://gitee.com/dotnetchina/Furion/issues/I7ERGO](https://gitee.com/dotnetchina/Furion/issues/I7ERGO) + +## 21.16 区域码列表 + +- af 公用荷兰语 +- af-ZA 公用荷兰语 - 南非 +- sq 阿尔巴尼亚 +- sq-AL 阿尔巴尼亚 -阿尔巴尼亚 +- ar 阿拉伯语 +- ar-DZ 阿拉伯语 -阿尔及利亚 +- ar-BH 阿拉伯语 -巴林 +- ar-EG 阿拉伯语 -埃及 +- ar-IQ 阿拉伯语 -伊拉克 +- ar-JO 阿拉伯语 -约旦 +- ar-KW 阿拉伯语 -科威特 +- ar-LB 阿拉伯语 -黎巴嫩 +- ar-LY 阿拉伯语 -利比亚 +- ar-MA 阿拉伯语 -摩洛哥 +- ar-OM 阿拉伯语 -阿曼 +- ar-QA 阿拉伯语 -卡塔尔 +- ar-SA 阿拉伯语 - 沙特阿拉伯 +- ar-SY 阿拉伯语 -叙利亚共和国 +- ar-TN 阿拉伯语 -北非的共和国 +- ar-AE 阿拉伯语 - 阿拉伯联合酋长国 +- ar-YE 阿拉伯语 -也门 +- hy 亚美尼亚 +- hy-AM 亚美尼亚的 -亚美尼亚 +- az Azeri +- az-AZ-Cyrl Azeri-(西里尔字母的) 阿塞拜疆 +- az-AZ-Latn Azeri(拉丁文)- 阿塞拜疆 +- eu 巴斯克 +- eu-ES 巴斯克 -巴斯克 +- be Belarusian +- be-BY Belarusian-白俄罗斯 +- bg 保加利亚 +- bg-BG 保加利亚 -保加利亚 +- ca 嘉泰罗尼亚 +- ca-ES 嘉泰罗尼亚 -嘉泰罗尼亚 +- zh-HK 华 - 香港的 SAR +- zh-MO 华 - 澳门的 SAR +- zh-CN 华 -中国 +- zh-CHS 华 (单一化) +- zh-SG 华 -新加坡 +- zh-TW 华 -台湾 +- zh-CHT 华 (传统的) +- hr 克罗埃西亚 +- hr-HR 克罗埃西亚 -克罗埃西亚 +- cs 捷克 +- cs-CZ 捷克 - 捷克 +- da 丹麦文 +- da-DK 丹麦文 -丹麦 +- div Dhivehi +- div-MV Dhivehi-马尔代夫 +- nl 荷兰 +- nl-BE 荷兰 -比利时 +- nl-NL 荷兰 - 荷兰 +- en 英国 +- en-AU 英国 -澳洲 +- en-BZ 英国 -伯利兹 +- en-CA 英国 -加拿大 +- en-CB 英国 -加勒比海 +- en-IE 英国 -爱尔兰 +- en-JM 英国 -牙买加 +- en-NZ 英国 - 新西兰 +- en-PH 英国 -菲律宾共和国 +- en-ZA 英国 - 南非 +- en-TT 英国 - 千里达托贝哥共和国 +- en-GB 英国 - 英国 +- en-US 英国 - 美国 +- en-ZW 英国 -津巴布韦 +- et 爱沙尼亚 +- et-EE 爱沙尼亚的 -爱沙尼亚 +- fo Faroese +- fo-FO Faroese- 法罗群岛 +- fa 波斯语 +- fa-IR 波斯语 -伊朗王国 +- fi 芬兰语 +- fi-FI 芬兰语 -芬兰 +- fr 法国 +- fr-BE 法国 -比利时 +- fr-CA 法国 -加拿大 +- fr-FR 法国 -法国 +- fr-LU 法国 -卢森堡 +- fr-MC 法国 -摩纳哥 +- fr-CH 法国 -瑞士 +- gl 加利西亚 +- gl-ES 加利西亚 -加利西亚 +- ka 格鲁吉亚州 +- ka-GE 格鲁吉亚州 -格鲁吉亚州 +- de 德国 +- de-AT 德国 -奥地利 +- de-DE 德国 -德国 +- de-LI 德国 -列支敦士登 +- de-LU 德国 -卢森堡 +- de-CH 德国 -瑞士 +- el 希腊 +- el-GR 希腊 -希腊 +- gu Gujarati +- gu-IN Gujarati-印度 +- he 希伯来 +- he-IL 希伯来 -以色列 +- hi 北印度语 +- hi-IN 北印度的 -印度 +- hu 匈牙利 +- hu-HU 匈牙利的 -匈牙利 +- is 冰岛语 +- is-IS 冰岛的 -冰岛 +- id 印尼 +- id-ID 印尼 -印尼 +- it 意大利 +- it-IT 意大利 -意大利 +- it-CH 意大利 -瑞士 +- ja 日本 +- ja-JP 日本 -日本 +- kn 卡纳达语 +- kn-IN 卡纳达语 -印度 +- kk Kazakh +- kk-KZ Kazakh-哈萨克 +- kok Konkani +- kok-IN Konkani-印度 +- ko 韩国 +- ko-KR 韩国 -韩国 +- ky Kyrgyz +- ky-KZ Kyrgyz-哈萨克 +- lv 拉脱维亚 +- lv-LV 拉脱维亚的 -拉脱维亚 +- lt 立陶宛 +- lt-LT 立陶宛 -立陶宛 +- mk 马其顿 +- mk-MK 马其顿 -FYROM +- ms 马来 +- ms-BN 马来 -汶莱 +- ms-MY 马来 -马来西亚 +- mr 马拉地语 +- mr-IN 马拉地语 -印度 +- mn 蒙古 +- mn-MN 蒙古 -蒙古 +- no 挪威 +- nb-NO 挪威 (Bokm?l) - 挪威 +- nn-NO 挪威 (Nynorsk)- 挪威 +- pl 波兰 +- pl-PL 波兰 -波兰 +- pt 葡萄牙 +- pt-BR 葡萄牙 -巴西 +- pt-PT 葡萄牙 -葡萄牙 +- pa Punjab 语 +- pa-IN Punjab 语 -印度 +- ro 罗马尼亚语 +- ro-RO 罗马尼亚语 -罗马尼亚 +- ru 俄国 +- ru-RU 俄国 -俄国 +- sa 梵文 +- sa-IN 梵文 -印度 +- sr-SP-Cyrl 塞尔维亚 -(西里尔字母的) 塞尔维亚共和国 +- sr-SP-Latn 塞尔维亚 (拉丁文)- 塞尔维亚共和国 +- sk 斯洛伐克 +- sk-SK 斯洛伐克 -斯洛伐克 +- sl 斯洛文尼亚 +- sl-SI 斯洛文尼亚 -斯洛文尼亚 +- es 西班牙 +- es-AR 西班牙 -阿根廷 +- es-BO 西班牙 -玻利维亚 +- es-CL 西班牙 -智利 +- es-CO 西班牙 -哥伦比亚 +- es-CR 西班牙 - 哥斯达黎加 +- es-DO 西班牙 - 多米尼加共和国 +- es-EC 西班牙 -厄瓜多尔 +- es-SV 西班牙 - 萨尔瓦多 +- es-GT 西班牙 -危地马拉 +- es-HN 西班牙 -洪都拉斯 +- es-MX 西班牙 -墨西哥 +- es-NI 西班牙 -尼加拉瓜 +- es-PA 西班牙 -巴拿马 +- es-PY 西班牙 -巴拉圭 +- es-PE 西班牙 -秘鲁 +- es-PR 西班牙 - 波多黎各 +- es-ES 西班牙 -西班牙 +- es-UY 西班牙 -乌拉圭 +- es-VE 西班牙 -委内瑞拉 +- sw Swahili +- sw-KE Swahili-肯尼亚 +- sv 瑞典 +- sv-FI 瑞典 -芬兰 +- sv-SE 瑞典 -瑞典 +- syr Syriac +- syr-SY Syriac-叙利亚共和国 +- ta 坦米尔 +- ta-IN 坦米尔 -印度 +- tt Tatar +- tt-RU Tatar-俄国 +- te Telugu +- te-IN Telugu-印度 +- th 泰国 +- th-TH 泰国 -泰国 +- tr 土耳其语 +- tr-TR 土耳其语 -土耳其 +- uk 乌克兰 +- uk-UA 乌克兰 -乌克兰 +- ur Urdu +- ur-PK Urdu-巴基斯坦 +- uz Uzbek +- uz-UZ-Cyrl Uzbek-(西里尔字母的) 乌兹别克斯坦 +- uz-UZ-Latn Uzbek(拉丁文)- 乌兹别克斯坦 +- vi 越南 +- vi-VN 越南 -越南 + +## 21.17 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: + +--- + +:::note 了解更多 + +想了解更多 `多语言` 知识可查阅 [ASP.NET Core - 全局化和本地化](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/localization?view=aspnetcore-5.0) 章节。 + +::: diff --git a/handbook/docs/logging.mdx b/handbook/docs/logging.mdx new file mode 100644 index 0000000000000000000000000000000000000000..33811fd509eb8b2532a07b690ff2ed2abd1e48e6 --- /dev/null +++ b/handbook/docs/logging.mdx @@ -0,0 +1,2043 @@ +--- +id: logging +title: 18. 日志记录 +sidebar_label: 18. 日志记录 +--- + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **新特性** + + -  新增 控制台日志 `AddConsoleFormatter` 服务支持 `WriteFilter` 属性过滤 4.8.8.52 ⏱️2023.11.07 [516acb4](https://gitee.com/dotnetchina/Furion/commit/516acb455e9eae477cfce1052442fc30c9c4dfb9) + -  新增 监听日志 `LoggingMonitor` 支持打印输出 `requestHeaders` 请求头信息 4.8.8.50 ⏱️2023.10.27 [#I8BHM3](https://gitee.com/dotnetchina/Furion/issues/I8BHM3) + -  新增 监听日志 `LoggingMonitor` 支持配置日志输出级别 4.8.8.41 ⏱️2023.08.25 [#I7SRTP](https://gitee.com/dotnetchina/Furion/issues/I7SRTP) + -  新增 **监听日志 `LoggingMonitor` 支持 `Razor Pages`** 4.8.8.16 ⏱️2023.05.15 [#I7332C](https://gitee.com/dotnetchina/Furion/issues/I7332C) + -  新增 **日志配置 `WithStackFrame`,可控制是否输出产生日志的程序集,类型和具体方法** 4.8.7.16 ⏱️2023.03.19 [5ad6ae2](https://gitee.com/dotnetchina/Furion/commit/5ad6ae241d1798ad788e42569a15d68686db4fa1) + +
+ 查看变化 +
+ +启用 `WithStackFrame` 日志配置后,可输出程序集,类型,方法签名信息。 + +```cs showLineNumbers {4,10,16} +// 控制台日志 +services.AddConsoleFormatter(options => +{ + options.WithStackFrame = true; +}); + +// 文件日志 +services.AddFileLogging(options => +{ + options.WithStackFrame = true; +}); + +// 数据库日志 +services.AddDatabaseLogging(options => +{ + options.WithStackFrame = true; +}); +``` + +日志输出如下: + +```bash showLineNumbers {2,5,8,11,14,17,20} +info: 2023-03-17 18:25:06.7988349 +08:00 星期五 L System.Logging.EventBusService[0] #1 + [Furion.dll] async Task Furion.EventBus.EventBusHostedService.ExecuteAsync(CancellationToken stoppingToken) + EventBus hosted service is running. +info: 2023-03-17 18:25:08.1393952 +08:00 星期五 L Microsoft.Hosting.Lifetime[14] #1 + [System.Private.CoreLib.dll] void System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start(ref TStateMachine stateMachine) + Now listening on: https://localhost:5001 +info: 2023-03-17 18:25:08.1620391 +08:00 星期五 L Microsoft.Hosting.Lifetime[14] #1 + [System.Private.CoreLib.dll] void System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start(ref TStateMachine stateMachine) + Now listening on: http://localhost:5000 +info: 2023-03-17 18:25:08.1972456 +08:00 星期五 L Microsoft.Hosting.Lifetime[0] #1 + [Microsoft.Extensions.Hosting.dll] void Microsoft.Extensions.Hosting.Internal.ConsoleLifetime.OnApplicationStarted() + Application started. Press Ctrl+C to shut down. +info: 2023-03-17 18:25:08.2456579 +08:00 星期五 L Microsoft.Hosting.Lifetime[0] #1 + [Microsoft.Extensions.Hosting.dll] void Microsoft.Extensions.Hosting.Internal.ConsoleLifetime.OnApplicationStarted() + Hosting environment: Development +info: 2023-03-17 18:25:08.2746134 +08:00 星期五 L Microsoft.Hosting.Lifetime[0] #1 + [System.Private.CoreLib.dll] void System.Threading.CancellationTokenSource.ExecuteCallbackHandlers(bool throwOnFirstException) + Content root path: D:\Workplaces\OpenSources\Furion\samples\Furion.Web.Entry +info: 2023-03-17 18:25:18.1917784 +08:00 星期五 L Furion.Application.TestLoggerServices[0] #16 + [Furion.Application.dll] void Furion.Application.TestLoggerServices.测试日志() + 我是一个日志 20 +``` + +这样就清楚地知道日志是哪个程序集、哪个类型、哪个方法输出的了。 + +
+
+ +- -  新增 审计日志 `LoggingMonitor` 支持对参数贴 `[SuppressMonitor]` 特性跳过记录 4.8.7.3 ⏱️2023.03.01 [#I6IVGW](https://gitee.com/dotnetchina/Furion/issues/I6IVGW) +- -  新增 审计日志 `LoggingMonitor` 监听 `TraceId`、`ThreadId`、`Accept-Language` 4.8.7.1 ⏱️2023.02.27 [df35201](https://gitee.com/dotnetchina/Furion/commit/df35201622f4d908ab423baff27caef856a23527) +- -  新增 审计日志 `LoggingMonitor` 支持配置序列化属性命名规则 4.8.6.12 ⏱️2023.02.21 [#I6GPUP](https://gitee.com/dotnetchina/Furion/issues/I6GPUP) +- -  新增 审计日志 `LoggingMonitor` 支持 `[DisplayName]` 特性解析和 `Title` 属性记录 4.8.5.10 ⏱️2023.02.07 [#I6DHMF](https://gitee.com/dotnetchina/Furion/issues/I6DHMF) +- -  新增 审计日志 `LoggingMonitor` 记录 `HTTP` 响应状态码 4.8.5.2 ⏱️2023.01.30 [abb4cbd](https://gitee.com/dotnetchina/Furion/commit/abb4cbdab9f45f53cad8468352d1c14ac8c54b42) + +- **突破性变化** + + -  调整 **监听日志 `WriteFilter` 和 `ConfigureLogger` 的 `ActionExecutingContext` 和 `ActionExecutedContext` 类型为 `FilterContext`** 4.8.8.16 ⏱️2023.05.15 [#I7332C](https://gitee.com/dotnetchina/Furion/issues/I7332C) + +- **问题修复** + + -  修复 审计日志不支持 `dynamic/JsonElement` 序列化问题 4.8.8.45 ⏱️2023.09.29 [#I84SD5](https://gitee.com/dotnetchina/Furion/issues/I84SD5) + -  修复 审计日志解析 `DateTime` 类型参数不是本地时间问题 4.8.8.33 ⏱️2023.06.29 [#I7GW32](https://gitee.com/dotnetchina/Furion/issues/I7GW32) + -  修复 `LoggingMonitor` 打印泛型类型如果存在多个泛型参数问题 4.8.8.8 ⏱️2023.05.04 [8d9cb74](https://gitee.com/dotnetchina/Furion/commit/8d9cb7457c736a91bc428ce61da553df40107960) + -  修复 日志输出 `JSON` 格式漏掉了 `UseUtcTimestamp` 和 `TraceId` 键值 4.8.7.21 ⏱️2023.03.27 [5c90e65](https://gitee.com/dotnetchina/Furion/commit/5c90e652b20dc36450ea0322fe6d22cd2a39d5e6) + -  修复 日志消息没有处理 `\n` 换行符对齐问题 4.8.7.6 ⏱️2023.03.10 [759bcc5](https://gitee.com/dotnetchina/Furion/commit/759bcc5dca09017f37a5be6ad2beefad33214fae) + -  修复 审计日志 `LoggingMonitor` 对特定参数贴有 `[FromServices]` 特性依旧记录问题 4.8.7.3 ⏱️2023.03.01 [17b134e](https://gitee.com/dotnetchina/Furion/commit/17b134efd82baa31bff6a0e763f93839c767c364) + -  修复 在数据库日志的 `IDatabaseLoggingWriter` 实现类中依赖注入 `ILogger<>` 导致死循环 4.8.5.4 ⏱️2023.02.01 [#I6C6QU](https://gitee.com/dotnetchina/Furion/issues/I6C6QU) + -  修复 数据库日志提供程序在应用程序终止时出现空异常问题 4.8.5 ⏱️2023.01.28 [#I6AZ8Y](https://gitee.com/dotnetchina/Furion/issues/I6AZ8Y) + -  修复 数据库日志注册在一些特殊情况下丢失日志上下文问题 4.8.4.6 ⏱️2023.01.04 [#I68PDF](https://gitee.com/dotnetchina/Furion/issues/I68PDF) + -  修复 在类中贴 `[SuppressMonitor]` 特性但 `LoggingMonitor` 依然输出问题 4.8.4 ⏱️2022.12.30 [#I6882I](https://gitee.com/dotnetchina/Furion/issues/I6882I) + -  修复 `LoggingMonitor` 序列化 `IQueryable<>` 或 `OData` 返回值类型出现死循环问题 4.8.3.4 ⏱️2022.12.10 [7e8c9d0](https://gitee.com/dotnetchina/Furion/commit/7e8c9d0e3910c4d0cfa18a7a15cbe8415d34dd66) + -  修复 **通过 `Ctrl + C` 终止应用程序后获取 `TraceId` 出现对象已释放异常** 4.8.1.12 ⏱️2022.12.07 [55c3e49](https://gitee.com/dotnetchina/Furion/commit/55c3e4942b1f89e0548d4cf90453937ba4ea512c) + -  修复 日志模块因 `v4.8.0+` 版本导致写入数据库日志空异常问题 4.8.2.1 ⏱️2022.11.28 [8d9d72b](https://gitee.com/dotnetchina/Furion/commit/8d9d72b5bfcb8dfc730462c9313266ec0661d561) + +- **其他更改** + + -  调整 审计日志日志 `LoggingMonitor` 返回值泛型字符串显示格式 4.8.7.1 ⏱️2023.02.27 [df35201](https://gitee.com/dotnetchina/Furion/commit/df35201622f4d908ab423baff27caef856a23527) + -  调整 `LoggingMonitor` 解析授权逻辑,如果接口未授权则不打印授权信息 4.8.2.1 ⏱️2022.11.28 [#I63D2E](https://gitee.com/dotnetchina/Furion/issues/I63D2E) + +
+
+
+ +import useBaseUrl from "@docusaurus/useBaseUrl"; + +## 18.1 关于日志 + +通常日志指的是**系统日志**和**程序日志**。 + +**系统日志** 是记录系统中硬件、软件和系统问题的信息,同时还可以监视系统中发生的事件。用户可以通过它来检查错误发生的原因,或者寻找受到攻击时攻击者留下的痕迹。系统日志包括系统日志、应用程序日志和安全日志。 + +**程序日志** 是程序运行中产生的日志,通常由框架运行时或开发者提供的日志。包括请求日志,异常日志、审计日志、行为日志等。 + +## 18.2 日志作用 + +在项目开发中,都不可避免的使用到日志。没有日志虽然不会影响项目的正确运行,但是没有日志的项目可以说是不完整的。日志在调试,错误或者异常定位,数据分析中的作用是不言而喻的。 + +- 调试 + +在项目调试时,查看栈信息可以方便地知道当前程序的运行状态,输出的日志便于记录程序在之前的运行结果。 + +- 错误定位 + +不要以为项目能正确跑起来就可以高枕无忧,项目在运行一段时候后,可能由于数据问题,网络问题,内存问题等出现异常。这时日志可以帮助开发或者运维人员快速定位错误位置,提出解决方案。 + +- 数据分析 + +大数据的兴起,使得大量的日志分析成为可能,ELK 也让日志分析门槛降低了很多。日志中蕴含了大量的用户数据,包括点击行为,兴趣偏好等,用户画像对于公司下一步的战略方向有一定指引作用。 + +## 18.3 日志级别 + +日志级别可以有效的对日志信息进行归类,方便准确的查看特定日志内容。通常日志类别有以下级别: + +| 级别 | 值 | 方法 | 描述 | +| :-----------------: | --- | :------------: | ---------------------------------------------------------------------------------------------------------- | +| Trace(跟踪) | 0 | LogTrace | 包含最详细的消息。 这些消息可能包含敏感的应用数据。 这些消息默认情况下处于禁用状态,并且不应在生产中启用。 | +| Debug(调试) | 1 | LogDebug | 用于调试和开发。 由于量大,请在生产中小心使用。 | +| Information(信息) | 2 | LogInformation | 跟踪应用的常规流。 可能具有长期值。 | +| Warning(警告) | 3 | LogWarning | 对于异常事件或意外事件。 通常包括不会导致应用失败的错误或情况。 | +| Error(错误) | 4 | LogError | 表示无法处理的错误和异常。 这些消息表示当前操作或请求失败,而不是整个应用失败。 | +| Critical(严重) | 5 | LogCritical | 需要立即关注的失败。 例如数据丢失、磁盘空间不足。 | + +## 18.4 如何使用 + +在 `.NET 5` 框架中,微软已经为我们内置了 `日志组件`,正常情况下,无需我们引用第三方包进行日志记录。`.NET 5` 框架为我们提供了两种日志对象创建方式。 + +### 18.4.1 `ILogger` 泛型方式 + +使用非常简单,可以通过 `ILogger` 对象进行注入,如: + +```cs showLineNumbers {5} +public class PrivacyModel : PageModel +{ + private readonly ILogger _logger; + + public PrivacyModel(ILogger logger) + { + _logger = logger; + } + + public void OnGet() + { + _logger.LogInformation("GET Pages.PrivacyModel called."); + } +} +``` + +:::tip 小知识 + +通过泛型 `ILogger` 方式写入日志,那么默认将 `T` 类型完整类型名称作为 `日志类别`。 + +::: + +### 18.4.2 `ILoggerFactory` 工厂方式 + +使用工厂方式,需手动传入 `日志类别`,如: + +```cs showLineNumbers {5,7} +public class ContactModel : PageModel +{ + private readonly ILogger _logger; + + public ContactModel(ILoggerFactory logger) + { + _logger = logger.CreateLogger("MyCategory"); + } + + public void OnGet() + { + _logger.LogInformation("GET Pages.ContactModel called."); + } +} +``` + +### 18.4.3 `Log` 静态类方式 + +:::important 版本说明 + +以下内容仅限 `Furion 4.2.1 +` 版本使用。 + +::: + +```cs showLineNumbers {2,5,10-15} +// 创建日志对象 +var logger = Log.CreateLogger("日志名称"); + +// 创建日志工厂 +using var loggerFactory = Log.CreateLoggerFactory(builder => { + // .... +}); + +// 日志记录 +Log.Information("Information"); +Log.Warning("Warning"); +Log.Error("Error"); +Log.Debug("Debug"); +Log.Trace("Trace"); +Log.Critical("Critical"); +``` + +### 18.4.4 `懒人模式` 😁 + +在 `Furion` 框架中,提供了更懒的方式写入日志,也就是通过字符串拓展的方式写入,如: + +```cs showLineNumbers +"简单日志".LogInformation(); + +"百小僧 新增了一条记录".LogInformation(); + +"程序出现异常啦".LogError(); + +"这是自定义类别日志".SetCategory().LogInformation(); +``` + +通过字符串拓展方式可以在任何时候方便记录日志,专门为懒人提供的。 + +## 18.5 输出到控制台 + +在 `ASP.NET Core` 应用程序中,主机启动时默认注册了 `ConsoleLoggerProvider` 提供器,也就是控制台日志输出提供器,所以无需任何注册服务即可在控制台输出。 + +```bash showLineNumbers +info: Furion.EventBus.EventBusHostedService[0] + EventBus Hosted Service is running. +info: Microsoft.Hosting.Lifetime[14] + Now listening on: https://localhost:5001 +info: Microsoft.Hosting.Lifetime[14] + Now listening on: http://localhost:5000 +info: Microsoft.Hosting.Lifetime[0] + Application started. Press Ctrl+C to shut down. +info: Microsoft.Hosting.Lifetime[0] + Hosting environment: Development +info: Microsoft.Hosting.Lifetime[0] + Content root path: C:\Workplaces\Furion\samples\Furion.Web.Entry\ +``` + +### 18.5.1 日志过滤/筛选 + +通过日志筛选器可以对日志进行归类写入。 + +```cs showLineNumbers {2,4,11,13} +// 例子一:根据日志级别输出 +services.AddConsoleFormatter(options => +{ + options.WriteFilter = (logMsg) => // Furion 4.8.8.52+ 版本支持 + { + return logMsg.LogLevel == LogLevel.Information; + }; +}); + +// 例子二,根据任何规则,比如特定的类名 +services.AddConsoleFormatter(options => // Furion 4.8.8.52+ 版本支持 +{ + options.WriteFilter = (logMsg) => + { + return logMsg.LogName == "System.Logging.LoggingMonitor"; + }; +}); +``` + +### 18.5.2 日志标准化(美化)模板 + +:::important 版本说明 + +以下内容仅限 `Furion 4.5.0 +` 版本使用。 + +::: + +在 `ASP.NET Core` 默认控制台日志相对简洁,并未包含常见的日志时间、线程 `Id` 等,而且自定义模板也相对复杂,所以 `Furion 4.5.0+` 版本提供了简化配置,如: + +1. `Startup.cs` 方式 + +```cs showLineNumbers +services.AddConsoleFormatter(); +``` + +2. `.NET5` 方式 + +```cs showLineNumbers {2,4} +Host.CreateDefaultBuilder(args) + .ConfigureLogging(logging => + { + logging.AddConsoleFormatter(); + }); +``` + +3. `.NET6` 方式 + +```cs showLineNumbers {3} +var builder = WebApplication.CreateBuilder(args); + +builder.Logging.AddConsoleFormatter(); +``` + +4. `Serve.Run()` 方式 + +```cs showLineNumbers {1,3,7} +Serve.Run(RunOptions.Default.AddWebComponent()); + +public class WebComponent : IWebComponent +{ + public void Load(WebApplicationBuilder builder, ComponentContext componentContext) + { + builder.Logging.AddConsoleFormatter(); + } +} +``` + +输出结果: + +```bash showLineNumbers +info: 2023-03-23 11:51:02.3757469 +08:00 星期四 L Microsoft.Hosting.Lifetime[14] #1 + Now listening on: https://localhost:7025 +info: 2023-03-23 11:51:02.4993301 +08:00 星期四 L Microsoft.Hosting.Lifetime[14] #1 + Now listening on: http://localhost:5217 +info: 2023-03-23 11:51:02.5058785 +08:00 星期四 L Microsoft.Hosting.Lifetime[0] #1 + Application started. Press Ctrl+C to shut down. +info: 2023-03-23 11:51:02.5100496 +08:00 星期四 L Microsoft.Hosting.Lifetime[0] #1 + Hosting environment: Development +info: 2023-03-23 11:51:02.5127095 +08:00 星期四 L Microsoft.Hosting.Lifetime[0] #1 + Content root path: C:\Users\snrcsoft\source\repos\WebApplication1\WebApplication1 +``` + +### 18.5.3 自定义日志模板 + +```cs showLineNumbers {1,3,13,15} +services.AddConsoleFormatter(options => +{ + options.MessageFormat = (logMsg) => + { + var stringBuilder = new StringBuilder(); + stringBuilder.Append(DateTime.Now.ToString("o")); + // 其他的。。。自己组装 + return stringBuilder.ToString(); + }; +}); + +// 输出为 JSON 格式,Furion 4.5.2+ +services.AddConsoleFormatter(options => +{ + options.MessageFormat = LoggerFormatter.Json; + // Furion 4.8.0+ 新增 JSON 美化输出 + options.MessageFormat = LoggerFormatter.JsonIndented; +}); +``` + +### 18.5.4 自定义日志输出时间格式 + +:::important 版本说明 + +以下内容仅限 `Furion 4.5.1 +` 版本使用。 + +::: + +```cs showLineNumbers {1,3} +services.AddConsoleFormatter(options => +{ + options.DateFormat = "yyyy-MM-dd HH:mm:ss.fffffff zzz dddd"; +}); +``` + +```bash showLineNumbers {1} +info: 2022-09-28 02:02:20(+08:00) 星期三 System.Logging.EventBusService[0] #1 + EventBus Hosted Service is running. +info: 2022-09-28 02:02:22(+08:00) 星期三 Microsoft.Hosting.Lifetime[14] #1 + Now listening on: https://localhost:5001 +info: 2022-09-28 02:02:22(+08:00) 星期三 Microsoft.Hosting.Lifetime[14] #1 + Now listening on: http://localhost:5000 +info: 2022-09-28 02:02:22(+08:00) 星期三 Microsoft.Hosting.Lifetime[0] #1 + Application started. Press Ctrl+C to shut down. +info: 2022-09-28 02:02:22(+08:00) 星期三 Microsoft.Hosting.Lifetime[0] #1 + Hosting environment: Development +info: 2022-09-28 02:02:22(+08:00) 星期三 Microsoft.Hosting.Lifetime[0] #1 + Content root path: D:\Workplaces\OpenSources\Furion\samples\Furion.Web.Entry\ +``` + +### 18.5.5 自定义日志输出程序 + +:::important 版本说明 + +以下内容仅限 `Furion 4.5.2 +` 版本使用。 + +::: + +`ASP.NET Core` 和 `Furion` 框架都提供了标准化日志输出,如果对颜色,格式有要求,可使用下列代码进行自定义。 + +```cs showLineNumbers {1,3,5} +services.AddConsoleFormatter(options => +{ + options.WriteHandler = (logMsg, scopeProvider, writer, fmtMsg, opt) => + { + writer.WriteLine(fmtMsg); + }; +}); +``` + +### 18.5.6 输出日志 `TraceId/HttpContextId` + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.1.3 +` 版本使用。 + +::: + +在生产环境中,日志的输出是非常频繁的,但是很难从日志文件中判断哪些日志是属于同一个请求输出的,这时启用 `WithTraceId` 配置即可。 + +```cs showLineNumbers {3} +services.AddConsoleFormatter(options => +{ + options.WithTraceId = true; +}); +``` + +输出日志如下: + +```bash showLineNumbers {15,17} +info: 2022-11-24 14:34:55.1717549 +08:00 星期四 L System.Logging.EventBusService[0] #1 + EventBus Hosted Service is running. +info: 2022-11-24 14:34:55.2504015 +08:00 星期四 L System.Logging.ScheduleService[0] #1 + Schedule Hosted Service is running. +info: 2022-11-24 14:34:56.4280796 +08:00 星期四 L Microsoft.Hosting.Lifetime[14] #1 + Now listening on: https://localhost:5001 +info: 2022-11-24 14:34:56.4331170 +08:00 星期四 L Microsoft.Hosting.Lifetime[14] #1 + Now listening on: http://localhost:5000 +info: 2022-11-24 14:34:56.4384567 +08:00 星期四 L Microsoft.Hosting.Lifetime[0] #1 + Application started. Press Ctrl+C to shut down. +info: 2022-11-24 14:34:56.4408766 +08:00 星期四 L Microsoft.Hosting.Lifetime[0] #1 + Hosting environment: Development +info: 2022-11-24 14:34:56.4427659 +08:00 星期四 L Microsoft.Hosting.Lifetime[0] #1 + Content root path: D:\Workplaces\OpenSources\Furion\samples\Furion.Web.Entry +info: 2022-11-24 14:35:06.8507338 +08:00 Thursday L Furion.Application.TestLoggerServices[0] #17 '00-48df9ac5c8280de2f301faa44a23a10c-b75678d9f3883b0b-00' + 我是一个日志 20 +info: 2022-11-24 14:35:16.0373384 +08:00 星期四 L Furion.Application.TestLoggerServices[0] #17 '00-ff4fb15d6ff41a0411784e66400f0dfd-962bc25eff788b25-00' + 我是一个日志 20 +``` + +### 18.5.7 输出日志 `程序集/类型/方法` + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.7.16 +` 版本使用。 + +::: + +在生产环境中,有时候我们希望知道这个日志是在哪个程序集,哪个类中哪个方法输出,这时启用 `WithStackFrame` 配置即可。 + +```cs showLineNumbers {3} +services.AddConsoleFormatter(options => +{ + options.WithStackFrame = true; +}); +``` + +输出日志如下: + +```bash showLineNumbers {2,5,8,11,14,17,20} +info: 2023-03-17 18:25:06.7988349 +08:00 星期五 L System.Logging.EventBusService[0] #1 + [Furion.dll] async Task Furion.EventBus.EventBusHostedService.ExecuteAsync(CancellationToken stoppingToken) + EventBus hosted service is running. +info: 2023-03-17 18:25:08.1393952 +08:00 星期五 L Microsoft.Hosting.Lifetime[14] #1 + [System.Private.CoreLib.dll] void System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start(ref TStateMachine stateMachine) + Now listening on: https://localhost:5001 +info: 2023-03-17 18:25:08.1620391 +08:00 星期五 L Microsoft.Hosting.Lifetime[14] #1 + [System.Private.CoreLib.dll] void System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start(ref TStateMachine stateMachine) + Now listening on: http://localhost:5000 +info: 2023-03-17 18:25:08.1972456 +08:00 星期五 L Microsoft.Hosting.Lifetime[0] #1 + [Microsoft.Extensions.Hosting.dll] void Microsoft.Extensions.Hosting.Internal.ConsoleLifetime.OnApplicationStarted() + Application started. Press Ctrl+C to shut down. +info: 2023-03-17 18:25:08.2456579 +08:00 星期五 L Microsoft.Hosting.Lifetime[0] #1 + [Microsoft.Extensions.Hosting.dll] void Microsoft.Extensions.Hosting.Internal.ConsoleLifetime.OnApplicationStarted() + Hosting environment: Development +info: 2023-03-17 18:25:08.2746134 +08:00 星期五 L Microsoft.Hosting.Lifetime[0] #1 + [System.Private.CoreLib.dll] void System.Threading.CancellationTokenSource.ExecuteCallbackHandlers(bool throwOnFirstException) + Content root path: D:\Workplaces\OpenSources\Furion\samples\Furion.Web.Entry +info: 2023-03-17 18:25:18.1917784 +08:00 星期五 L Furion.Application.TestLoggerServices[0] #16 + [Furion.Application.dll] void Furion.Application.TestLoggerServices.测试日志() + 我是一个日志 20 +``` + +## 18.6 输出到文件 + +### 18.6.1 基础使用 + +```cs showLineNumbers {2,5,8} +// 例子一:启动层根目录输出 +services.AddFileLogging("application.log"); + +// 例子二:支持路径 +services.AddFileLogging("logs/application.log"); + +// 例子三:支持日志追加还是覆盖,设置 true 为追加,false 为覆盖 +services.AddFileLogging("application.log", true); +``` + +### 18.6.2 从配置文件读取配置 + +:::important 特别注意 + +**只有不在 `.AddFile` 第一个参数配置文件名才会自动加载配置,也就是文件名应该配置在配置文件中。** + +::: + +文件日志配置说明: + +```json showLineNumbers {2,7-13,16-22} +{ + "Logging": { + "LogLevel": { + "Default": "Information" + // .... appsettings 默认配置 + }, + "File": { + "FileName": "application.log", // 日志文件完整路径或文件名,推荐 .log 作为拓展名 + "Append": true, // 追加到已存在日志文件或覆盖它们 + "MinimumLevel": "Information", // 最低日志记录级别 + "FileSizeLimitBytes": 0, // 控制每一个日志文件最大存储大小,单位是 B,也就是 1024 才等于 1KB,默认无限制,如果指定了该值,那么日志文件大小超出了该配置就会创建新的日志文件,新创建的日志文件命名规则:文件名+[递增序号].log + "MaxRollingFiles": 0 // 控制最大创建的日志文件数量,默认无限制,配合 FileSizeLimitBytes 使用,如果指定了该值,那么超出该值将从最初日志文件中从头写入覆盖 + } + }, + // 自定义配置节点 + "MyLogger": { + "FileName": "application.log", + "Append": true, + "MinimumLevel": "Information", + "FileSizeLimitBytes": 0, + "MaxRollingFiles": 0 + } +} +``` + +```cs showLineNumbers {2,5,13,16} +// 例子一:默认读取 Logging:File 节点 +services.AddFileLogging(); + +// 例子二:默认读取 Logging:File 节点,支持更多配置 +services.AddFileLogging(options => +{ + options.MinimumLevel = LogLevel.Warning; + + // 其他配置... +}); + +// 例子三:自定义配置节点 +services.AddFileLogging(() => "MyLogger"); + +// 例子四:自定义配置节点,支持更多配置 +services.AddFileLogging(() => "MyLogger", options => +{ + options.MinimumLevel = LogLevel.Warning; + + // 其他配置... +}); +``` + +### 18.6.3 自定义日志文件名规则 + +```cs showLineNumbers {2,5,14,23} +// 例子一:支持系统环境变量,如%SystemDrive%,%SystemRoot% +services.AddFileLogging("application%SystemDrive%-%SystemRoot%.log"); + +// 例子二:每天创建一个日志文件 +services.AddFileLogging("application-{0:yyyy}-{0:MM}-{0:dd}.log", options => +{ + options.FileNameRule = fileName => + { + return string.Format(fileName, DateTime.UtcNow); + }; +}); + +// 例子三,任何自己喜欢的命名规则 +services.AddFileLogging("application-{0:yyyy}-{0:MM}-{0:dd}.log", options => +{ + options.FileNameRule = fileName => + { + // your rule... + }; +}); + +// 例子四,批量设置多个 +Array.ForEach(new[] { LogLevel.Information, LogLevel.Warning, LogLevel.Error }, logLevel => +{ + services.AddFileLogging("application-{1}-{0:yyyy}-{0:MM}-{0:dd}.log", options => + { + options.FileNameRule = fileName => string.Format(fileName, DateTime.UtcNow, logLevel.ToString()); + options.WriteFilter = logMsg => logMsg.LogLevel == logLevel; + }); +}); +``` + +### 18.6.4 日志过滤器/筛选器 + +通过日志筛选器可以对日志进行归类写入 + +```cs showLineNumbers {2,10,19} +// 例子一:根据日志级别输出 +services.AddFileLogging("infomation.log", options => +{ + options.WriteFilter = (logMsg) => + { + return logMsg.LogLevel == LogLevel.Information; + }; +}); + +services.AddFileLogging("error.log", options => +{ + options.WriteFilter = (logMsg) => + { + return logMsg.LogLevel == LogLevel.Error; + }; +}); + +// 例子二,根据任何规则,比如特定的类名 +services.AddFileLogging("someclass.log", options => +{ + options.WriteFilter = (logMsg) => + { + return logMsg.LogName.Contains("SomeClassName"); + }; +}); +``` + +### 18.6.5 自定义日志模板 + +默认情况下,`Furion` 提供了标准的日志输出模板,如: + +```bash showLineNumbers +2022-07-23T20:16:29.3459053+08:00 [INF] [Furion.EventBus.EventBusHostedService] [0] EventBus Hosted Service is running. +2022-07-23T20:16:29.5827366+08:00 [INF] [Microsoft.Hosting.Lifetime] [0] Application started. Press Ctrl+C to shut down. +2022-07-23T20:16:29.5828798+08:00 [INF] [Microsoft.Hosting.Lifetime] [0] Hosting environment: Development +2022-07-23T20:16:29.5829377+08:00 [INF] [Microsoft.Hosting.Lifetime] [0] Content root path: C:\Workplaces\Furion\samples\Furion.Web.Entry\ +``` + +如需自定义: + +```cs showLineNumbers {2,16,34,47,49} +// 例子一,自定义日志模板(常用) +services.AddFileLogging("mytemplate.log", options => +{ + options.MessageFormat = (logMsg) => + { + var stringBuilder = new StringBuilder(); + + stringBuilder.Append(DateTime.Now.ToString("o")); + // 其他的。。。自己组装 + + return stringBuilder.ToString(); + }; +}); + +// 例子二,需要输出 json 格式,比如对接阿里云日志,kibana第三方日志使用这个 +services.AddFileLogging("mytemplate.log", options => +{ + options.MessageFormat = (logMsg) => + { + // 高性能写入 + return logMsg.WriteArray(writer => + { + writer.WriteStringValue(DateTime.Now.ToString("o")); + writer.WriteStringValue(logMsg.LogLevel.ToString()); + writer.WriteStringValue(logMsg.LogName); + writer.WriteNumberValue(logMsg.EventId.Id); + writer.WriteStringValue(logMsg.Message); + writer.WriteStringValue(logMsg.Exception?.ToString()); + }); + }; +}); + +// 例子二,需要输出 json (自定义)格式,比如对接阿里云日志,kibana第三方日志使用这个 +services.AddFileLogging("mytemplate.log", options => +{ + options.MessageFormat = (logMsg) => + { + // 高性能写入 + return logMsg.Write(writer => + { + // write 对象为 Utf8JsonWriter,可通过流写入,性能极高 + }); + }; +}); + +// 输出为 JSON 格式,Furion 4.5.2+ +services.AddFileLogging("mytemplate.log", options => +{ + options.MessageFormat = LoggerFormatter.Json; + // Furion 4.8.0+ 新增 JSON 美化输出 + options.MessageFormat = LoggerFormatter.JsonIndented; +}); +``` + +### 18.6.6 日志写入失败处理 + +有时候可能因为日志文件被打开或者其他应用程序占用了,那么就会导致日志写入失败,这时候可以进行其他相关处理: + +```cs showLineNumbers {2,11,15} +// 例子一:其他处理 +services.AddFileLogging("template-obj.log", options => +{ + options.HandleWriteError = (writeError) => + { + // ~~ + }; +}); + +// 例子二,启用备用日志文件功能,也就是如果文件被占用了,可以创建新的备用日志继续写入,推荐!!! +services.AddFileLogging("template-obj.log", options => +{ + options.HandleWriteError = (writeError) => + { + writeError.UseRollbackFileName(Path.GetFileNameWithoutExtension(writeError.CurrentFileName) + "-oops" + Path.GetExtension(writeError.CurrentFileName)); + }; +}); +``` + +### 18.6.7 自定义日志输出时间格式 + +:::important 版本说明 + +以下内容仅限 `Furion 4.5.1 +` 版本使用。 + +::: + +```cs showLineNumbers {1,3} +services.AddFileLogging("application.log", options => +{ + options.DateFormat = "yyyy-MM-dd HH:mm:ss.fffffff zzz dddd"; +}); +``` + +```bash showLineNumbers {1} +info: 2022-09-28 02:02:20(+08:00) 星期三 System.Logging.EventBusService[0] #1 + EventBus Hosted Service is running. +info: 2022-09-28 02:02:22(+08:00) 星期三 Microsoft.Hosting.Lifetime[14] #1 + Now listening on: https://localhost:5001 +info: 2022-09-28 02:02:22(+08:00) 星期三 Microsoft.Hosting.Lifetime[14] #1 + Now listening on: http://localhost:5000 +info: 2022-09-28 02:02:22(+08:00) 星期三 Microsoft.Hosting.Lifetime[0] #1 + Application started. Press Ctrl+C to shut down. +info: 2022-09-28 02:02:22(+08:00) 星期三 Microsoft.Hosting.Lifetime[0] #1 + Hosting environment: Development +info: 2022-09-28 02:02:22(+08:00) 星期三 Microsoft.Hosting.Lifetime[0] #1 + Content root path: D:\Workplaces\OpenSources\Furion\samples\Furion.Web.Entry\ +``` + +### 18.6.8 输出日志 `TraceId/HttpContextId` + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.1.3 +` 版本使用。 + +::: + +在生产环境中,日志的输出是非常频繁的,但是很难从日志文件中判断哪些日志是属于同一个请求输出的,这时启用 `WithTraceId` 配置即可。 + +```cs showLineNumbers {3} +services.AddFileLogging(options => +{ + options.WithTraceId = true; +}); +``` + +输出日志如下: + +```bash showLineNumbers {15,17} +info: 2022-11-24 14:34:55.1717549 +08:00 星期四 L System.Logging.EventBusService[0] #1 + EventBus Hosted Service is running. +info: 2022-11-24 14:34:55.2504015 +08:00 星期四 L System.Logging.ScheduleService[0] #1 + Schedule Hosted Service is running. +info: 2022-11-24 14:34:56.4280796 +08:00 星期四 L Microsoft.Hosting.Lifetime[14] #1 + Now listening on: https://localhost:5001 +info: 2022-11-24 14:34:56.4331170 +08:00 星期四 L Microsoft.Hosting.Lifetime[14] #1 + Now listening on: http://localhost:5000 +info: 2022-11-24 14:34:56.4384567 +08:00 星期四 L Microsoft.Hosting.Lifetime[0] #1 + Application started. Press Ctrl+C to shut down. +info: 2022-11-24 14:34:56.4408766 +08:00 星期四 L Microsoft.Hosting.Lifetime[0] #1 + Hosting environment: Development +info: 2022-11-24 14:34:56.4427659 +08:00 星期四 L Microsoft.Hosting.Lifetime[0] #1 + Content root path: D:\Workplaces\OpenSources\Furion\samples\Furion.Web.Entry +info: 2022-11-24 14:35:06.8507338 +08:00 Thursday L Furion.Application.TestLoggerServices[0] #17 '00-48df9ac5c8280de2f301faa44a23a10c-b75678d9f3883b0b-00' + 我是一个日志 20 +info: 2022-11-24 14:35:16.0373384 +08:00 星期四 L Furion.Application.TestLoggerServices[0] #17 '00-ff4fb15d6ff41a0411784e66400f0dfd-962bc25eff788b25-00' + 我是一个日志 20 +``` + +### 18.6.9 输出日志 `程序集/类型/方法` + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.7.16 +` 版本使用。 + +::: + +在生产环境中,有时候我们希望知道这个日志是在哪个程序集,哪个类中哪个方法输出,这时启用 `WithStackFrame` 配置即可。 + +```cs showLineNumbers {3} +services.AddFileLogging(options => +{ + options.WithStackFrame = true; +}); +``` + +输出日志如下: + +```bash showLineNumbers {2,5,8,11,14,17,20} +info: 2023-03-17 18:25:06.7988349 +08:00 星期五 L System.Logging.EventBusService[0] #1 + [Furion.dll] async Task Furion.EventBus.EventBusHostedService.ExecuteAsync(CancellationToken stoppingToken) + EventBus hosted service is running. +info: 2023-03-17 18:25:08.1393952 +08:00 星期五 L Microsoft.Hosting.Lifetime[14] #1 + [System.Private.CoreLib.dll] void System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start(ref TStateMachine stateMachine) + Now listening on: https://localhost:5001 +info: 2023-03-17 18:25:08.1620391 +08:00 星期五 L Microsoft.Hosting.Lifetime[14] #1 + [System.Private.CoreLib.dll] void System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start(ref TStateMachine stateMachine) + Now listening on: http://localhost:5000 +info: 2023-03-17 18:25:08.1972456 +08:00 星期五 L Microsoft.Hosting.Lifetime[0] #1 + [Microsoft.Extensions.Hosting.dll] void Microsoft.Extensions.Hosting.Internal.ConsoleLifetime.OnApplicationStarted() + Application started. Press Ctrl+C to shut down. +info: 2023-03-17 18:25:08.2456579 +08:00 星期五 L Microsoft.Hosting.Lifetime[0] #1 + [Microsoft.Extensions.Hosting.dll] void Microsoft.Extensions.Hosting.Internal.ConsoleLifetime.OnApplicationStarted() + Hosting environment: Development +info: 2023-03-17 18:25:08.2746134 +08:00 星期五 L Microsoft.Hosting.Lifetime[0] #1 + [System.Private.CoreLib.dll] void System.Threading.CancellationTokenSource.ExecuteCallbackHandlers(bool throwOnFirstException) + Content root path: D:\Workplaces\OpenSources\Furion\samples\Furion.Web.Entry +info: 2023-03-17 18:25:18.1917784 +08:00 星期五 L Furion.Application.TestLoggerServices[0] #16 + [Furion.Application.dll] void Furion.Application.TestLoggerServices.测试日志() + 我是一个日志 20 +``` + +## 18.7 输出到数据库/其他存储介质 + +将日志输出到数据库中也是非常常见的需求,`Furion` 把该功能做到了非常简单,支持任何存储介质。 + +在写入数据库/其他存储介质之前需创建数据库日志写入器并实现 `IDatabaseLoggingWriter` 接口,**支持多个**,如: + +```cs showLineNumbers {1,5,8,12} +using Furion.Logging; + +namespace YourProject.Core; + +public class DatabaseLoggingWriter : IDatabaseLoggingWriter +{ + // 支持构造函数注入任何实例,会自动释放任何服务,比如注入 IRepository,或者 SqlSugarClient + public DatabaseLoggingWriter() + { + } + + public void Write(LogMessage logMsg, bool flush) + { + // 这里写你任何插入数据库的操作,无需 try catch + } +} +``` + +:::caution 注意事项 + +在 `Furion 4.8.5.4` 版本之前,如果在 `DatabaseLoggingWriter` 中注入 `ILogger<>` 对象将会导致死循环,原因是 `IDatabaseLoggingWriter` 本身就是日志输出的最终介质,如果这里还输出日志,将导致递归初始化日志实例。 + +但在 `Furion 4.8.5.4+` 版本之后,如果注入了 `ILogger<>` 实例,那么将强制性将实例化对象为 `EmptyLogger` 对象,但调用其输出日志方法将不会有任何效果,如:`logger.LogInformation("...")`; + +::: + +你没看错,就这么简单!! + +### 18.7.1 基础使用 + +```cs showLineNumbers {2,5} +// 例子一,默认配置 +services.AddDatabaseLogging(options => {}); + +// 例子二:自定义配置 +services.AddDatabaseLogging(options => +{ + options.MinimumLevel = LogLevel.Warning; + + // 其他配置... +}); +``` + +### 18.7.2 从配置文件中读取 + +:::important 特别注意 + +**只有 `.AddDatabase` 第一个参数为空才会自动加载配置。** + +::: + +数据库日志配置说明: + +```json showLineNumbers {2,7-9,12-14} +{ + "Logging": { + "LogLevel": { + "Default": "Information" + // .... appsettings 默认配置 + }, + "Database": { + "MinimumLevel": "Information" // 最低日志记录级别 + } + }, + // 自定义配置节点 + "MyLogger": { + "MinimumLevel": "Information" + } +} +``` + +```cs showLineNumbers {2,5,5,18,25} +// 例子一:默认读取 Logging:Database 节点 +services.AddDatabaseLogging(); + +// 例子二:默认读取 Logging:Database 节点,支持更多配置 +services.AddDatabaseLogging(default(string), options => +{ + options.MinimumLevel = LogLevel.Warning; + + // 其他配置... +}); + +// 例子三:自定义配置节点 +services.AddDatabaseLogging("MyLogger"); +// 或 +services.AddDatabaseLogging(() => "MyLogger"); + +// 例子四:自定义配置节点,支持更多配置 +services.AddDatabaseLogging("MyLogger", options => +{ + options.MinimumLevel = LogLevel.Warning; + + // 其他配置... +}); +// 或 +services.AddDatabaseLogging(() => "MyLogger", options => +{ + options.MinimumLevel = LogLevel.Warning; + + // 其他配置... +}); +``` + +### 18.7.3 日志过滤器/筛选器 + +通过日志筛选器可以对日志进行归类写入 + +```cs showLineNumbers {2,10,19} +// 例子一:根据日志级别输出,可以分别定义 IDatabaseLoggingWriter,也可以用同一个底层进行判断 +services.AddDatabaseLogging(options => +{ + options.WriteFilter = (logMsg) => + { + return logMsg.LogLevel == LogLevel.Information; + }; +}); +// 可以分别定义 IDatabaseLoggingWriter,也可以用同一个底层进行判断 +services.AddDatabaseLogging(options => +{ + options.WriteFilter = (logMsg) => + { + return logMsg.LogLevel == LogLevel.Error; + }; +}); + +// 例子二,根据任何规则,比如特定的类名 +services.AddDatabaseLogging(options => +{ + options.WriteFilter = (logMsg) => + { + return logMsg.LogName.Contains("SomeClassName"); + }; +}); +``` + +### 18.7.4 自定义日志模板 + +```cs showLineNumbers {1,3,15,17} +services.AddDatabaseLogging(options => +{ + options.MessageFormat = (logMsg) => + { + var stringBuilder = new StringBuilder(); + + stringBuilder.Append(DateTime.Now.ToString("o")); + // 其他的。。。自己组装 + + return stringBuilder.ToString(); + }; +}); + +// 输出为 JSON 格式,Furion 4.5.2+ +services.AddDatabaseLogging(options => +{ + options.MessageFormat = LoggerFormatter.Json; + // Furion 4.8.0+ 新增 JSON 美化输出 + options.MessageFormat = LoggerFormatter.JsonIndented; +}); +``` + +### 18.7.5 日志写入失败处理 + +有时候可能因为数据库连接异常或其他原因连接池满,那么就会导致日志写入失败,这时候可以进行其他相关处理: + +```cs showLineNumbers {2} +// 例子一:其他处理 +services.AddDatabaseLogging(options => +{ + options.HandleWriteError = (writeError) => + { + // ~~ + }; +}); +``` + +### 18.7.6 输出日志 `TraceId/HttpContextId` + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.1.3 +` 版本使用。 + +::: + +在生产环境中,日志的输出是非常频繁的,但是很难从日志文件中判断哪些日志是属于同一个请求输出的,这时启用 `WithTraceId` 配置即可。 + +```cs showLineNumbers {3} +services.AddDatabaseLogging(options => +{ + options.WithTraceId = true; +}); +``` + +输出日志如下: + +```bash showLineNumbers {15,17} +info: 2022-11-24 14:34:55.1717549 +08:00 星期四 L System.Logging.EventBusService[0] #1 + EventBus Hosted Service is running. +info: 2022-11-24 14:34:55.2504015 +08:00 星期四 L System.Logging.ScheduleService[0] #1 + Schedule Hosted Service is running. +info: 2022-11-24 14:34:56.4280796 +08:00 星期四 L Microsoft.Hosting.Lifetime[14] #1 + Now listening on: https://localhost:5001 +info: 2022-11-24 14:34:56.4331170 +08:00 星期四 L Microsoft.Hosting.Lifetime[14] #1 + Now listening on: http://localhost:5000 +info: 2022-11-24 14:34:56.4384567 +08:00 星期四 L Microsoft.Hosting.Lifetime[0] #1 + Application started. Press Ctrl+C to shut down. +info: 2022-11-24 14:34:56.4408766 +08:00 星期四 L Microsoft.Hosting.Lifetime[0] #1 + Hosting environment: Development +info: 2022-11-24 14:34:56.4427659 +08:00 星期四 L Microsoft.Hosting.Lifetime[0] #1 + Content root path: D:\Workplaces\OpenSources\Furion\samples\Furion.Web.Entry +info: 2022-11-24 14:35:06.8507338 +08:00 Thursday L Furion.Application.TestLoggerServices[0] #17 '00-48df9ac5c8280de2f301faa44a23a10c-b75678d9f3883b0b-00' + 我是一个日志 20 +info: 2022-11-24 14:35:16.0373384 +08:00 星期四 L Furion.Application.TestLoggerServices[0] #17 '00-ff4fb15d6ff41a0411784e66400f0dfd-962bc25eff788b25-00' + 我是一个日志 20 +``` + +### 18.7.7 输出日志 `程序集/类型/方法` + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.7.16 +` 版本使用。 + +::: + +在生产环境中,有时候我们希望知道这个日志是在哪个程序集,哪个类中哪个方法输出,这时启用 `WithStackFrame` 配置即可。 + +```cs showLineNumbers {3} +services.AddDatabaseLogging(options => +{ + options.WithStackFrame = true; +}); +``` + +输出日志如下: + +```bash showLineNumbers {2,5,8,11,14,17,20} +info: 2023-03-17 18:25:06.7988349 +08:00 星期五 L System.Logging.EventBusService[0] #1 + [Furion.dll] async Task Furion.EventBus.EventBusHostedService.ExecuteAsync(CancellationToken stoppingToken) + EventBus hosted service is running. +info: 2023-03-17 18:25:08.1393952 +08:00 星期五 L Microsoft.Hosting.Lifetime[14] #1 + [System.Private.CoreLib.dll] void System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start(ref TStateMachine stateMachine) + Now listening on: https://localhost:5001 +info: 2023-03-17 18:25:08.1620391 +08:00 星期五 L Microsoft.Hosting.Lifetime[14] #1 + [System.Private.CoreLib.dll] void System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start(ref TStateMachine stateMachine) + Now listening on: http://localhost:5000 +info: 2023-03-17 18:25:08.1972456 +08:00 星期五 L Microsoft.Hosting.Lifetime[0] #1 + [Microsoft.Extensions.Hosting.dll] void Microsoft.Extensions.Hosting.Internal.ConsoleLifetime.OnApplicationStarted() + Application started. Press Ctrl+C to shut down. +info: 2023-03-17 18:25:08.2456579 +08:00 星期五 L Microsoft.Hosting.Lifetime[0] #1 + [Microsoft.Extensions.Hosting.dll] void Microsoft.Extensions.Hosting.Internal.ConsoleLifetime.OnApplicationStarted() + Hosting environment: Development +info: 2023-03-17 18:25:08.2746134 +08:00 星期五 L Microsoft.Hosting.Lifetime[0] #1 + [System.Private.CoreLib.dll] void System.Threading.CancellationTokenSource.ExecuteCallbackHandlers(bool throwOnFirstException) + Content root path: D:\Workplaces\OpenSources\Furion\samples\Furion.Web.Entry +info: 2023-03-17 18:25:18.1917784 +08:00 星期五 L Furion.Application.TestLoggerServices[0] #16 + [Furion.Application.dll] void Furion.Application.TestLoggerServices.测试日志() + 我是一个日志 20 +``` + +### 18.7.8 关于高频写入存储介质 + +在一些高频写入存储介质(如数据库)日志的场景,为避免频繁解析服务(创建和销毁链接)和创建作用域导致内存及 `CPU` 飙高,可使用 **类全局作用域** 和所有服务都采取单例的方式: + +```cs showLineNumbers {1,3,8-9,14,20-23} +public class DatabaseLoggingWriter : IDatabaseLoggingWriter, IDisposable +{ + private readonly IServiceScope _serviceScope; + private readonly IRepository _logRepository; // 可替换成任何数据库操作库 + + public DatabaseLoggingWriter(IServiceScopeFactory scopeFactory) + { + _serviceScope = scopeFactory.CreateScope(); + _logRepository = _serviceScope.ServiceProvider.GetRequiredService>(); + } + + public void Write(LogMessage logMsg, bool flush) + { + _logRepository.Insert(.....); + } + + /// + /// 释放服务作用域 + /// + public void Dispose() + { + _serviceScope.Dispose(); + } +} +``` + +### 18.7.9 关于数据库日志循环输出日志 + +微软提供的 `EFCore` 或者第三方 `ORM` 本身操作数据库时自带日志输出,这就会导致 `IDatabaseLoggingWriter` 的 `Write` 死循环,解决这个问题有以下方法: + +1. 创建新的数据库操作实例并关闭日志 +2. 更新到 `Furion 4.7.0+` 版本 **(推荐)** +3. 自行根据业务逻辑过滤 + +**如不存在该问题可关闭框架自带死循环检测功能(对性能有提升作用)**: + +```cs showLineNumbers {3} +services.AddDatabaseLogging(options => +{ + options.IgnoreReferenceLoop = false; +}); +``` + +## 18.8 添加日志提供器方式 + +框架提供了多种添加日志提供器方式,可在应用启动或运行时。 + +### 18.8.1 `LoggerFactory` 方式 + +`Furion` 也提供了运行时动态添加日志提供器并写入: + +```cs showLineNumbers {1,3-4,8-12,23-26} +public class SomeController : IDynamicApiController, IDisposable +{ + private readonly ILoggerFactory _loggerFactory; + private readonly ILogger _logger; + + public SomeController() + { + _loggerFactory = LoggerFactory.Create(builder => + { + builder.AddFile("application.log"); + }); + _logger = _loggerFactory.CreateLogger("MyCategory"); + + // 可以多个重复上面代码 + } + + [HttpGet] + public void SomeAction() + { + _logger.LogInformation("some log..."); + } + + public void Dispose() + { + _loggerFactory.Dispose(); + } +} +``` + +:::warning 特别注意 + +通过运行时的方式需要自行释放 `ILoggerFactory` 对象,也就是需要实现 `IDisposable` 接口,然后进行 `_loggerFactory` 释放。 + +可查看相关 `Issue`:[https://gitee.com/dotnetchina/Furion/issues/I7KJ5H](https://gitee.com/dotnetchina/Furion/issues/I7KJ5H) + +::: + +### 18.8.2 `ILoggingBuilder` 方式 + +`Furion` 也提供了原生 `services.AddLogging(builder => {})` 方式配置,如 + +```cs showLineNumbers {1,3,5} +services.AddLogging(builder => +{ + builder.AddFile("applicaion.log"); + + builder.AddDatabase(); + + //.... +}); +``` + +## 18.9 记录请求日志 + +在 `ASP.NET 6` 中,框架默认提供了 `app.UseHttpLogging()` 记录 `HTTP` 请求日志功能,详细了解可查看官方文档 [ASP.NET Core - HTTP 日志记录](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/http-logging/?view=aspnetcore-6.0) + +当然也可以自定义中间件的方式写,只需要注入 `ILogger<>` 接口即可。 + +## 18.10 `Debug` 和 `Trace` 默认不输出问题 + +默认情况下,微软在 `appsettings.json` 和 `appsettings.Development.json` 中配置了 `Default` 日志级别,如需自定义: + +```json showLineNumbers {4} +{ + "Logging": { + "LogLevel": { + "Default": "Information" + } + } +} +``` + +这时候只需要修改 `Default` 为 `Debug` 或 `Trace` 即可,**注意不同环境加载不同的配置文件。开发环境应修改 `appsettings.Development.json` 下的配置。** + +## 18.11 `[LoggingMonitor]` 监听日志 + +在 `Furion 3.9.1` 版本新增了 `[LoggingMonitor]` 特性,支持在控制器或操作中贴该特性,可以实现强大的请求日志监听,方便测试,如: + +### 18.11.1 特性配置 + +```cs showLineNumbers {1,7} +using Furion.Logging; + +namespace Furion.Application; + +public class TestLoggerServices : IDynamicApiController +{ + [LoggingMonitor] + public PersonDto GetPerson(int id) + { + return new PersonDto + { + Id = id + }; + } +} +``` + +- `[LoggingMonitor]` 支持以下配置: + - `Title`:配置标题,`string` 类型,默认 `Logging Monitor` + - `WithReturnValue`:是否包含返回值打印,`bool` 类型,默认 `true`,`Furion 4.3.9+ 有效` + - `ReturnValueThreshold`:配置返回值字符串阈值,`int` 类型,默认 `0` 全量输出,`Furion 4.3.9+ 有效` + - `JsonBehavior`:配置 `LoggingMonitor` `Json` 输出行为,默认 `None`,`Furion 4.5.2+` 有效 + - `JsonIndented`:配置 `LoggingMonitor` `Json` 格式化行为,`bool` 类型,默认 `false`,`Furion 4.8.0+` 有效 + +输出日志为: + +```bash showLineNumbers +┏━━━━━━━━━━━ Logging Monitor ━━━━━━━━━━━ +┣ Furion.Application.TestLoggerServices.GetPerson (Furion.Application) +┣ +┣ 控制器名称: TestLoggerServices +┣ 操作名称: GetPerson +┣ 路由信息: [area]: ; [controller]: test-logger; [action]: person +┣ 请求方式: POST +┣ 请求地址: https://localhost:44316/api/test-logger/person/11 +┣ 来源地址: https://localhost:44316/api/index.html +┣ 浏览器标识: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36 Edg/103.0.1264.62 +┣ 客户端 IP 地址: 0.0.0.1 +┣ 服务端 IP 地址: 0.0.0.1 +┣ 服务端运行环境: Development +┣ 执行耗时: 31ms +┣ ━━━━━━━━━━━━━━━ 授权信息 ━━━━━━━━━━━━━━━ +┣ JWT Token: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEsIkFjY291bnQiOiJhZG1pbiIsImlhdCI6MTY1ODcxNjc5NywibmJmIjoxNjU4NzE2Nzk3LCJleHAiOjE2NTg3MTc5OTcsImlzcyI6ImRvdG5ldGNoaW5hIiwiYXVkIjoicG93ZXJieSBGdXJpb24ifQ.VYZkwwqCwlUy3aJjuL-og62I0rkxNQ96kSjEm3VgXtg +┣ +┣ UserId (integer): 1 +┣ Account (string): admin +┣ iat (integer): 1658716797 +┣ nbf (integer): 1658716797 +┣ exp (integer): 1658717997 +┣ iss (string): dotnetchina +┣ aud (string): powerby Furion +┣ ━━━━━━━━━━━━━━━ 参数列表 ━━━━━━━━━━━━━━━ +┣ Content-Type: +┣ +┣ id (Int32): 11 +┣ ━━━━━━━━━━━━━━━ 返回信息 ━━━━━━━━━━━━━━━ +┣ 类型: Furion.Application.Persons.PersonDto +┣ 返回值: {"Id":11,"Name":null,"Age":0,"Address":null,"PhoneNumber":null,"QQ":null,"CreatedTime":"0001-01-01T00:00:00+00:00","Childrens":null,"Posts":null} +┗━━━━━━━━━━━ Logging Monitor ━━━━━━━━━━━ +``` + +### 18.11.2 全局配置 + +如需全局启用 `LoggingMonitor` 功能,无需在每个控制器或者方法中贴,全局注册如下: + +```cs showLineNumbers +// 请使用 services.AddMonitorLogging(); 替代,并启用 GlobalEnabled: true +services.AddMvcFilter(); +``` + +:::tip `Furion 4.0.2` 新推荐配置 + +在 `Furion 4.0.2` 版本中新增了非常灵活方便的 `services.AddMonitorLogging()` 服务配置,可在配置中随意控制哪个类哪个方法启用或不启用。 + +- 注册服务 + +```cs showLineNumbers +services.AddMonitorLogging(); // 默认读取 Logging:Monitor 下配置,支持传入参数自定义 +``` + +- 添加配置 + +```json showLineNumbers {2,3,4-6} +{ + "Logging": { + "Monitor": { + "GlobalEnabled": false, // 是否启用全局拦截,默认 `false` + "IncludeOfMethods": [], // 是否指定拦截特定方法,当 GlobalEnabled: false 有效 + "ExcludeOfMethods": [], // 是否指定排除特定方法,当 GlobalEnabled: true 有效 + "LogLevel": "Information", // 配置监听日志输出级别,默认 Information + "BahLogLevel": "Information", // 配置 Oops.Oh 和 Oops.Bah 业务日志输出级别,默认 Information + "WithReturnValue": true, // 配置是否包含返回值,默认 `true`,Furion 4.3.9+ 有效 + "ReturnValueThreshold": 0, // 配置返回值字符串阈值,默认 0,全量输出,Furion 4.3.9+ 有效 + "JsonBehavior": "None", // 配置 LoggingMonitor Json 输出行为,默认 None,Furion 4.5.2+ 有效 + "JsonIndented": false, // 配置 LoggingMonitor Json 格式化行为,默认 false,Furion 4.8.2+ 有效 + "ContractResolver": "CamelCase", // 配置 LoggingMonitor 序列化属性命名规则,默认 CamelCase,Furion 4.8.6.12+ 有效 + "MethodsSettings": [ + // 配置被监视方法更多信息,Furion 4.3.9+ 有效 + { + "FullName": "Furion.Application.TestLoggerServices.MethodName", // 方法完全限定名 + "WithReturnValue": true, // 配置是否包含返回值,默认 `true`,Furion 4.3.9+ 有效 + "ReturnValueThreshold": 0, // 配置返回值字符串阈值,默认 0,全量输出,Furion 4.3.9+ 有效 + "JsonIndented": false, // 配置 LoggingMonitor Json 格式化行为,默认 false,Furion 4.8.2+ 有效 + "JsonBehavior": "None", // 配置 LoggingMonitor Json 输出行为,默认 None,Furion 4.5.2+ 有效 + "ContractResolver": "CamelCase" // 配置 LoggingMonitor 序列化属性命名规则,默认 CamelCase,Furion 4.8.6.12+ 有效 + } + ] + } + } +} +``` + +`IncludeOfMethods` 和 `ExcludeOfMethods` 方法签名格式为:`类完全限定名.方法名`,如:`Furion.Application.TestNamedServices.GetName`,`Furion.Application.TestNamedServices` 是类名,`GetName` 是方法名。 + +::: + +如果配置了全局请求监视日志,对个别不需要监视的接口方法只需要贴 `[SuppressMonitor]` 特性即可。 + +### 18.11.3 更多配置 + +:::important 版本说明 + +以下内容仅限 `Furion 4.3.9 +` 版本使用。 + +::: + +支持 `LoggingMonitor` 写入日志拦截,如添加额外数据: + +```cs showLineNumbers {1,3,6} +services.AddMonitorLogging(options => +{ + options.ConfigureLogger((logger, logContext, context) => + { + var httpContext = context.HttpContext; + logContext.Set("extra", "其他数据"); + }); +}); +``` + +除此之外,还支持配置 `json` 路径: + +```cs showLineNumbers +services.AddMonitorLogging(jsonKey: "YourKey:Monitor"); +``` + +### 18.11.4 `JSON` 格式 + +:::important 版本说明 + +以下内容仅限 `Furion 4.5.2 +` 版本使用。 + +::: + +1. 全局/局部启用 `Json` 输出配置 + +```cs showLineNumbers {4,9} +// 全局 +services.AddMonitorLogging(options => +{ + options.JsonBehavior = Furion.Logging.JsonBehavior.OnlyJson; + options.JsonIndented = true; // 是否美化 JSON,Furion 4.8.0+ 版本有效 +}); + +// 局部 +[LoggingMonitor(JsonBehavior = Furion.Logging.JsonBehavior.OnlyJson)] +// 是否美化 JSON,Furion 4.8.0+ 版本有效 +[LoggingMonitor(JsonBehavior = Furion.Logging.JsonBehavior.OnlyJson, JsonIndented = true)] +``` + +:::note 关于 `JsonBehavior` + +只有设置为 `JsonBehavior.OnlyJson` 时才不会输出**美观的**日志。 + +::: + +2. 写入存储介质 + +```cs showLineNumbers {14-18} +using Furion.Logging; + +namespace YourProject.Core; + +public class DatabaseLoggingWriter : IDatabaseLoggingWriter +{ + // 支持构造函数注入任何实例,会自动释放任何服务,比如注入 IRepository,或者 SqlSugarClient + public DatabaseLoggingWriter() + { + } + + public void Write(LogMessage logMsg, bool flush) + { + // 如果 JsonBehavior 配置为 OnlyJson 或者 All,那么 Context 就包含 loggingMonitor 的值 + // 如果 JsonBehavior 配置为 OnlyJson,那么可直接通过 logMsg.Message 获取结果就是 json 格式 + if (logMsg.LogName == "System.Logging.LoggingMonitor") + { + var jsonString = logMsg.Context.Get("loggingMonitor"); + } + + // 这里写你任何插入数据库的操作,无需 try catch + } +} +``` + +`Json` 输出格式如下: + +```json showLineNumbers +{ + "controllerName": "test-logger", + "controllerTypeName": "TestLoggerServices", + "actionName": "person", + "actionTypeName": "GetPerson", + "areaName": null, + "displayName": "Furion.Application.TestLoggerServices.GetPerson (Furion.Application)", + "localIPv4": "0.0.0.1", + "remoteIPv4": "0.0.0.1", + "httpMethod": "GET", + "requestUrl": "https://localhost:5001/api/test-logger/person/2", + "refererUrl": "https://localhost:5001/api/index.html?urls.primaryName=数据库操作演示", + "environment": "Development", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36 Edg/105.0.1343.53", + "requestHeaderAuthorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEsIkFjY291bnQiOiJhZG1pbiIsImlhdCI6MTY2NDQ1MDUwNSwibmJmIjoxNjY0NDUwNTA1LCJleHAiOjE2NjQ0NTE3MDUsImlzcyI6ImRvdG5ldGNoaW5hIiwiYXVkIjoicG93ZXJieSBGdXJpb24ifQ.-xocNcDQGoXClceoVU5QAHIkTcOZ7ZXo0hEbzghDfFI", + "timeOperationElapsedMilliseconds": 55, + "authorizationClaims": [ + { + "type": "UserId", + "valueType": "integer", + "value": "1" + }, + { + "type": "Account", + "valueType": "string", + "value": "admin" + }, + { + "type": "iat", + "valueType": "integer", + "value": "1664450505" + }, + { + "type": "nbf", + "valueType": "integer", + "value": "1664450505" + }, + { + "type": "exp", + "valueType": "integer", + "value": "1664451705" + }, + { + "type": "iss", + "valueType": "string", + "value": "dotnetchina" + }, + { + "type": "aud", + "valueType": "string", + "value": "powerby Furion" + } + ], + "parameters": [ + { + "name": "id", + "type": "System.Int32", + "value": 2 + } + ], + "returnInformation": { + "type": "Furion.UnifyResult.RESTfulResult`1[[System.Object, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]", + "actType": "Furion.Application.Persons.PersonDto", + "value": { + "StatusCode": 200, + "Data": { + "Id": 2, + "Name": null, + "Age": 0, + "Address": null, + "PhoneNumber": null, + "QQ": null, + "CreatedTime": "0001-01-01T00:00:00+00:00", + "Childrens": null, + "Posts": null + }, + "Succeeded": true, + "Errors": null, + "Extras": null, + "Timestamp": 1664450517341 + } + }, + "exception": { + "type": "System.DivideByZeroException", + "message": "Attempted to divide by zero.", + "stackTrace": " at Furion.Application.TestLoggerServices.测试日志监听8(Int32 id) in D:\\Workplaces\\OpenSources\\Furion\\samples\\Furion.Application\\TestLoggerServices.cs:line 78\r\n at lambda_method103(Closure , Object , Object[] )\r\n at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncObjectResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)\r\n at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.g__Logged|12_1(ControllerActionInvoker invoker)\r\n at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)" + }, + "validation": { + "errorCode": null, + "originErrorCode": null, + "message": "出错了啊。。。。" + } +} +``` + +### 18.11.5 全局过滤 `WriteFilter` + +:::important 版本说明 + +以下内容仅限 `Furion 4.5.9 +` 版本使用。 + +::: + +在 `Furion 4.5.9+` 版本新增了 `WriteFilter` 过滤功能,可根据自定义逻辑自定义过滤拦截: + +```cs showLineNumbers showLineNumbers {1,3,10} +services.AddMonitorLogging(options => +{ + options.WriteFilter = (context) => + { + // 获取控制器/操作描述器 + var controllerActionDescriptor = context.ActionDescriptor as ControllerActionDescriptor; + + // 你的逻辑....,需要拦截返回 false,否则 true + + return true; + }; +}); +``` + +### 18.11.6 输出 `JSON` 支持忽略属性名或属性类型 + +:::important 版本说明 + +以下内容仅限 `Furion 4.6.1 +` 版本使用。 + +::: + +有时接口的返回值包含不能被序列化的类型或者想忽略某些属性名不被序列化,这时候就需要用到这个。 + +- **局部配置** + +```cs showLineNumbers {3,4,10} +// 忽略名称和属性,支持单一配置或局部配置 +[LoggingMonitor(JsonBehavior = JsonBehavior.OnlyJson + , IgnorePropertyNames = new[] { "Bytes" } + , IgnorePropertyTypes = new[] { typeof(byte[]) })] +public object MethodName(int id) +{ + return new + { + Id = 10, + Bytes = File.ReadAllBytes("image.png") + }; +} +``` + +- **全局配置** + +```cs showLineNumbers {4-5} +// 忽略名称和属性,支持单一配置或局部配置 +services.AddMonitorLogging(options => +{ + options.IgnorePropertyNames = new[] { "Byte" }; + options.IgnorePropertyTypes = new[] { typeof(byte[]) }; +}); +``` + + + +### 18.11.7 将 `LoggingMonitor` 写入数据库 + +1. 注册 `.AddDatabaseLogging<>` 服务: + +```cs showLineNumbers {1,3,5} +services.AddDatabaseLogging(options => +{ + options.WriteFilter = (logMsg) => + { + return logMsg.LogName == "System.Logging.LoggingMonitor"; + }; +}); +``` + +2. 写入数据库 + +```cs showLineNumbers {1,5,8,12} +using Furion.Logging; + +namespace YourProject.Core; + +public class DatabaseLoggingWriter : IDatabaseLoggingWriter +{ + // 任何数据库 ORM 注入。。。 + public DatabaseLoggingWriter() + { + } + + public void Write(LogMessage logMsg, bool flush) + { + // 将 logMsg 的属性一一插入到数据库中~ + } +} +``` + +:::tip 单个 `DatabaseLoggingWriter` 情况 + +如果已经全局注册了: + +```cs showLineNumbers +services.AddDatabaseLogging(); // 注意这里没有过滤 logName +``` + +且不想多注册一个数据库日志服务,那么只需要在代码中过滤即可: + +```cs showLineNumbers {1,5,8,12,15} +using Furion.Logging; + +namespace YourProject.Core; + +public class DatabaseLoggingWriter : IDatabaseLoggingWriter +{ + // 任何数据库 ORM 注入。。。 + public DatabaseLoggingWriter() + { + } + + public void Write(LogMessage logMsg, bool flush) + { + // 将 logMsg 的属性一一插入到数据库中~ + if(logMsg.LogName == "System.Logging.LoggingMonitor") + { + // 写入审计表数据库 + } + else + { + // 写入其他表数据库 + } + } +} +``` + +::: + +### 18.11.8 支持 `[DisplayName]` 特性 + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.5.10 +` 版本使用。 + +::: + +通常我们会为每一个接口添加一个唯一名称,比如使用 `[DisplayName]` 特性,方便后续写入日志进行归类。 + +```cs showLineNumbers {4} +public class TestService: IDynamicApiController +{ + [LoggingMonitor] + [DisplayName("测试方法")] + public void TestMethod() + { + } +} +``` + +那么如果输出为 `JSON` 格式日志将会自动添加 `displayTitle` 键,值为 `测试方法`。 + +### 18.11.9 输出序列化键格式配置(属性大小写) + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.6.12 +` 版本使用。 + +::: + +该功能主要配置序列化时属性名输出规则,**注意此项并不配置 `JSON` 输出规则。** + +- **局部配置** + +```cs showLineNumbers {1} +[LoggingMonitor(ContractResolver = ContractResolverTypes.Default)] // CamelCase 或者 Default,默认是 CamelCase +public DataTable MethodName() +{ + var d = "select * from person".SqlQuery(); + return d; +} +``` + +- **全局配置** + +```cs showLineNumbers {3} +services.AddMonitorLogging(options => +{ + options.ContractResolver = ContractResolverTypes.Default; // CamelCase 或者 Default,默认是 CamelCase +}); +``` + +### 18.11.10 跳过特定参数记录 + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.7.3 +` 版本使用。 + +::: + +在 `Furion 4.8.7.3+` 版本新增 `[SuppressMonitor]` 特性支持标记参数(支持类型,方法)不被记录,如: + +```cs showLineNumbers {2} +[LoggingMonitor] +public string GetName([SuppressMonitor]SomeType type, int id) // type 参数将跳过记录 +{ + return nameof(Furion); +} +``` + +## 18.12 打印日志到 `Swagger` 中 + +在 `Furion` 框架中默认集成了 `MiniProfiler` 组件并与 `Swagger` 进行了结合,如需打印日志或调试代码,只需调用以下方法即可: + +```cs showLineNumbers +App.PrintToMiniProfiler("分类", "状态", "要打印的消息"); +``` + +## 18.13 静态 `Default()` 方式构建 + +```cs showLineNumbers +StringLoggingPart.Default().SetMessage("这是一个日志").LogInformation(); +``` + +## 18.14 规范日志模板 + +在 `Furion v3.5.3+` 新增了 `TP.Wrapper(...)` 规范模板,使用如下: + +```cs showLineNumbers {2} +// 生成模板字符串 +var template = TP.Wrapper("Furion 框架", "让 .NET 开发更简单,更通用,更流行。", + "##作者## 百小僧", + "##当前版本## v3.5.3", + "##文档地址## http://furion.baiqian.ltd", + "##Copyright## 百小僧, 百签科技(广东)有限公司"); + +Console.WriteLine(template); +``` + +日志打印模板如下: + +```bash showLineNumbers +┏━━━━━━━━━━━ Furion 框架 ━━━━━━━━━━━ +┣ 让 .NET 开发更简单,更通用,更流行。 +┣ +┣ 作者: 百小僧 +┣ 当前版本: v3.5.3 +┣ 文档地址: http://furion.baiqian.ltd +┣ Copyright: 百小僧, 百签科技(广东)有限公司 +┗━━━━━━━━━━━ Furion 框架 ━━━━━━━━━━━ +``` + +:::tip 关于属性生成 + +如果列表项以 `##属性名##` 开头,自动生成 `属性名:` 作为行首且自动等宽对齐。 + +`Furion 3.9.1` 之前版本使用 `[属性名]` 开头。 + +::: + +## 18.15 日志上下文 + +:::important 版本说明 + +以下内容仅限 `Furion 4.6.0 +` 版本使用。 + +::: + +### 18.15.1 添加上下文数据 + +有时候我们希望为日志提供额外数据,这时候可通过 `.ScopeContext()` 配置,如: + +```cs showLineNumbers {2,8,15,21,25,28} +// 写法一 +using (var scope = _logger.ScopeContext(ctx => ctx.Set("Name", "Furion").Set("UserId", 10))) +{ + _logger.LogInformation("我是一个日志 {id}", 20); +} + +// 写法二 +using var scope = _logger.ScopeContext(new Dictionary { + { "Name", "Furion" }, + { "UserId", 10 } +}); +_logger.LogInformation("我是一个日志 {id}", 20); + +// 写法三 +using var scope = _logger.ScopeContext(new LogContext { + // .... +}); +_logger.LogInformation("我是一个日志 {id}", 20); + +// 写法四 +var (logger, scoped) = Log.ScopeContext(new LogContext { + // ... +}); +logger.LogInformation("我是一个日志 {id}", 20); +scoped?.Dispose(); + +// 写法五 +"我是一个日志 {id}".ScopeContext(new LogContext { + // ... +}).LogInformation(); +``` + +### 18.15.2 读取上下文数据 + +在 `LogMessage` 对象中使用: + +```cs showLineNumbers {1,9,14,16} +var value = logMsg.Context.Get("Key"); + +// 比如在过滤中使用 +services.AddFileLogging("infomation.log", options => +{ + options.WriteFilter = (logMsg) => + { + // 还可以设置给运行时使用:logMsg.Context.Set(...); + return logMsg.Context.Get("Name") == "Furion"; + }; +}); + +// 在 IDatabaseLoggingWriter 中使用 +public void Write(LogMessage logMsg, bool flush) +{ + var name = logMsg.Context.Get("Name"); +} +``` + +### 18.15.3 日志上下文数据共享 + +```cs showLineNumbers{1,4,11,25,33} +public TestAppService: ITestAppService, IDisposable +{ + private readonly ILogger _logger; + private IDisposable _scopeProvider; + + public TestAppService(ILogger logger) + { + _logger = logger; + + // 添加全局用户信息上下文数据 + _scopeProvider = _logger.ScopeContext(ctx => ctx.Set("uid", "100").Set("uname", "百小僧")); + } + + public string GetName(int id) + { + // 共享全局上下文数据 + _logger.LogInformation("写入新的日志"); + + return "Furion"; + } + + public string GetTags(int id) + { + // 额外新增上下文数据 + using var scope = _logger.ScopeContext(ctx => ctx.Set("key", "value")); + _logger.LogInformation("设置额外的上上下文日志"); + + return "百小僧"; + } + + public void Dispose() + { + _scopeProvider.Dispose(); + } +} +``` + +## 18.16 关闭 `.NET Core` 底层日志 + +默认情况下,`.NET Core` 底层默认输出了很多日志,如需关闭只需在 `appsettings.json` 和 `appsettings.Development.json` 中添加 `"命名空间/日志类别":"最低日志级别"` 日志类别过滤即可,如: + +```json showLineNumbers {2,7-8} +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.EntityFrameworkCore": "Information", + "System.Net.Http.HttpClient": "Warning", + "命名空间/日志类别": "Warning" + } + } +} +``` + +小提醒:过滤 `.NET Core` 底层日志最低日志级别通常设置为 `Warning`。 + +## 18.17 全局日志过滤/筛选 + +在一些特定的情况下,我们希望能够对所有日志输出介质中的日志进行过滤,可以通过以下方式进行全局配置。 + +:::caution 特别注意 + +此方式会影响所有的日志介质输出,如控制台、文件、数据库等其他输出介质。 + +::: + +`.NET5` 版本: + +```cs showLineNumbers {1,2,4} +Host.CreateDefaultBuilder(args) + .ConfigureLogging(logging => + { + logging.AddFilter((provider, category, logLevel) => + { + return !new[] { "Microsoft.Hosting", "Microsoft.AspNetCore" }.Any(u => category.StartsWith(u)) + && logLevel >= LogLevel.Information; + }); + }) +``` + +`.NET6+` 版本: + +```cs showLineNumbers {1,3} +var builder = WebApplication.CreateBuilder(args); + +builder.Logging.AddFilter((provider, category, logLevel) => +{ + return !new[] { "Microsoft.Hosting", "Microsoft.AspNetCore" }.Any(u => category.StartsWith(u)) + && logLevel >= LogLevel.Information; +}); +``` + +或者 `Serve.Run` 方式 + +```cs showLineNumbers {1,3,7-10} +Serve.Run(RunOptions.Default.AddWebComponent()); + +public class WebComponent : IWebComponent +{ + public void Load(WebApplicationBuilder builder, ComponentContext componentContext) + { + builder.Logging.AddFilter((provider, category, logLevel) => + { + return !new[] { "Microsoft.Hosting", "Microsoft.AspNetCore" }.Any(u => category.StartsWith(u)) + && logLevel >= LogLevel.Information; + }); + } +} +``` + +:::warning 日志过滤无效情况 + +假如使用上述代码过滤无效(不能过滤默认的主机日志),那么请确认 `appsettings.json` 和 `appsettings.Development.json` 的 `Logging:Level` 是否如下: + +```json showLineNumbers {4-5} +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.EntityFrameworkCore": "Information" + } + } +} +``` + +如果配置了以下配置,**请删除**: + +```json +"Microsoft": "Warning", +"Microsoft.Hosting.Lifetime": "Information", +``` + +::: + +## 18.18 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: + +--- + +:::note 了解更多 + +想了解更多 `日志` 知识可查阅 [ASP.NET Core - 日志](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/logging/?view=aspnetcore-5.0) 章节。 + +::: diff --git a/handbook/docs/middleware.mdx b/handbook/docs/middleware.mdx new file mode 100644 index 0000000000000000000000000000000000000000..66e63a7a9a563b926a907a0e12bdef8eded1d43e --- /dev/null +++ b/handbook/docs/middleware.mdx @@ -0,0 +1,361 @@ +--- +id: middleware +title: 5.5 中间件 (Middleware) +sidebar_label: 5.5 中间件 (Middleware) +description: 监听或控制请求和响应就用它 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +## 5.5.1 关于中间件 + +中间件是一种装配到应用管道以处理请求和响应的软件。 每个组件: + +- 选择是否将请求传递到管道中的下一个组件。 +- 可在管道中的下一个组件前后执行工作。 +- 请求委托用于生成请求管道。 请求委托处理每个 `HTTP` 请求。 + +**一句话总结:中间件是比筛选器更底层,更上游的面向切面技术,其性能最高,可处理的应用范围远比过滤器广,如实现网关,`URL` 转发,限流等等。** + +:::tip 中间件更多内容 + +本章节暂不考虑将中间件展开讲,想了解更多知识可阅读官方文档 【[ASP.NET Core - 中间件](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/?view=aspnetcore-6.0)】 + +::: + +## 5.5.2 常见中间件 + +### 5.5.2.1 所有请求返回同一个结果 + +```cs showLineNumbers {1} +app.Run(async context => +{ + await context.Response.WriteAsync("Hello world!"); +}); +``` + +### 5.5.2.2 拦截所有请求(可多个) + +```cs showLineNumbers {1,7} +app.Use(async (context, next) => +{ + // 比如设置统一头 + context.Response.Headers["framework"] = "Furion"; + + // 执行下一个中间件 + await next.Invoke(); +}); + +// 多个 +app.Use(...); +``` + +### 5.5.2.3 特定路由中间件(可多个) + +```cs showLineNumbers {1,2,8} +app.Map("/hello", app => { + app.Run(async context => + { + await context.Response.WriteAsync("Map Test 1"); + }); +}); + +app.Map("/hello/say", app => { + // .... +}); +``` + +### 5.5.2.4 嵌套路由中间件(可多个) + +```cs showLineNumbers {1,2,5} +app.Map("/level1", level1App => { + level1App.Map("/level2a", level2AApp => { + // "/level1/level2a" processing + }); + level1App.Map("/level2b", level2BApp => { + // "/level1/level2b" processing + }); +}); +``` + +更多例子查看官方文档 [https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/?view=aspnetcore-6.0](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/?view=aspnetcore-6.0) + +## 5.5.3 自定义中间件 + +自定义中间件有多种方式,最简单的是通过 `app.Use` 方式,另外还支持独立类定义方式。 + +### 5.5.3.1 `app.Use` 方式 (不推荐) + +```cs showLineNumbers {1,12-13} title="Starup.cs" +app.Use(async (context, next) => +{ + var cultureQuery = context.Request.Query["culture"]; + if (!string.IsNullOrWhiteSpace(cultureQuery)) + { + var culture = new CultureInfo(cultureQuery); + + CultureInfo.CurrentCulture = culture; + CultureInfo.CurrentUICulture = culture; + } + + // 调用下一个中间件 + await next(context); +}); +``` + +### 5.5.3.2 `独立类` 方式(推荐) + +独立类的方式是目前最为推荐的方式,拓展性强,维护性高,如: + +- 定义中间件,建议以 `Middleware` 结尾: + +```cs showLineNumbers {5,7,9-12,14,25-26} +using System.Globalization; + +namespace Middleware.Example; + +public class RequestCultureMiddleware +{ + private readonly RequestDelegate _next; + + public RequestCultureMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContext context) + { + var cultureQuery = context.Request.Query["culture"]; + if (!string.IsNullOrWhiteSpace(cultureQuery)) + { + var culture = new CultureInfo(cultureQuery); + + CultureInfo.CurrentCulture = culture; + CultureInfo.CurrentUICulture = culture; + } + + // 调用下一个中间件 + await _next(context); + } +} +``` + +- 添加中间件拓展类 + +定义了中间件之后,需要创建这个中间件的拓展类,中间件拓展方法建议以 `Use` 开头,如: + +```cs showLineNumbers {1,3,5} +public static class RequestCultureMiddlewareExtensions +{ + public static IApplicationBuilder UseRequestCulture(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } +} +``` + +- 在 `Startup.cs` 中使用 + +```cs showLineNumbers {1,4} +public void Configure(IApplicationBuilder app, IWebHostEnvironment env) +{ + // ... 其他中间件 + app.UseRequestCulture(); + // ... 其他中间件 +} +``` + +### 5.5.3.3 配置更多参数 + +默认情况下,自定义独立类中间件构造函数只有一个 `RequestDelegate` 参数,除此之后,还可以注入服务接口/类,另外还支持传入任何其他类型。 + +- **服务类型参数** + +```cs showLineNumbers {8,11} +using System.Globalization; + +namespace Middleware.Example; + +public class RequestCultureMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public RequestCultureMiddleware(RequestDelegate next + , ILogger logger) + { + _next = next; + _logger = logger; + } + + public async Task InvokeAsync(HttpContext context) + { + // 其他代码 + + _logger.LogInformation("..."); + + // 调用下一个中间件 + await _next(context); + } +} +``` + +- **非服务类型参数** + +除此之外,还可以添加 `非服务参数` 参数,**但必须是声明在服务参数后。** + +```cs showLineNumbers {12,13} +using System.Globalization; + +namespace Middleware.Example; + +public class RequestCultureMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public RequestCultureMiddleware(RequestDelegate next + , ILogger logger + , int age + , string name) + { + _next = next; + _logger = logger; + } + + public async Task InvokeAsync(HttpContext context) + { + // 其他代码 + + _logger.LogInformation("..."); + + // 调用下一个中间件 + await _next(context); + } +} +``` + +之后还需要修改中间件拓展类: + +```cs showLineNumbers {3,5} +public static class RequestCultureMiddlewareExtensions +{ + public static IApplicationBuilder UseRequestCulture(this IApplicationBuilder builder, int age, string name) + { + return builder.UseMiddleware(new object[] {age, name }); + } +} +``` + +使用: + +```cs showLineNumbers +app.UseRequestCulture(30, "百小僧"); +``` + +## 5.5.4 中间件顺序 + +中间件是有执行顺序的,而且是**先注册的先执行,无法通过其他方式更改**,参考下图: + + + +## 5.5.5 依赖注入/解析服务 + +中间件有两种方式注入服务,一种是通过构造函数注入,一种是通过 `httpContext.RequestServices` 方式解析。 + +### 5.5.5.1 构造函数方式 + +```cs showLineNumbers {8,11-12} +using System.Globalization; + +namespace Middleware.Example; + +public class RequestCultureMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public RequestCultureMiddleware(RequestDelegate next + , ILogger logger + , IHostEnvironment hostEnvironment) + { + _next = next; + _logger = logger; + } + + public async Task InvokeAsync(HttpContext context) + { + // 其他代码 + + // 调用下一个中间件 + await _next(context); + } +} +``` + +### 5.5.5.2 `httpContext.RequestServices` 方式 + +`HttpContext` 提供了 `RequestServices` 属性方便解析服务。 + +```cs showLineNumbers {10-13,21-22} +using System.Globalization; + +namespace Middleware.Example; + +public class RequestCultureMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + // 构造函数注册 + public RequestCultureMiddleware(RequestDelegate next + , ILogger logger + , IHostEnvironment hostEnvironment) + { + _next = next; + _logger = logger; + } + + public async Task InvokeAsync(HttpContext context) + { + // 通过 context.RequestServices 解析 + var repository = context.RequestServices.GetService(); + + // 调用下一个中间件 + await _next(context); + } +} +``` + +## 5.5.6 常见问题 + +由于中间件是比较原始的切面方式,有时候我们需要获取**终点路由的特性**或者其他信息,则需要一点技巧: + +```cs showLineNumbers +// 获取终点路由特性 +var endpointFeature = context.Features.Get(); + +// 获取是否定义了特性 +var attribute = endpointFeature?.Endpoint?.Metadata?.GetMetadata() +``` + +:::important 注意事项 + +要想上面操作有效,也就是不为 `null`,需要满足以下条件,否则 `endpointFeature` 返回 `null`。 + +- 启用端点路由 `AddControllers()` 而不是 `AddMvc()` +- `UseRouting()` 和 `UseEndpoints()` 之间调用你的中间件 + +::: + +## 5.5.7 了解更多 + +想了解更多中间件知识可阅读官方文档 【[ASP.NET Core - 中间件](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/?view=aspnetcore-6.0)】 + +## 5.5.8 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/module-dev.mdx b/handbook/docs/module-dev.mdx new file mode 100644 index 0000000000000000000000000000000000000000..193ca2d5774fc658a1d847b45ab9b2bd567dc441 --- /dev/null +++ b/handbook/docs/module-dev.mdx @@ -0,0 +1,113 @@ +--- +id: module-dev +title: 28. 模块化开发 +sidebar_label: 28. 模块化开发 +--- + +:::important 特别注意 + +以下的模块化配置必须在 `appsettings.json` 下配置才有效,原因是启动的时候 `appsettings.json` 已经加载,自定义配置文件还未加载。 + +::: + +## 28.1 关于模块化开发 + +模块化是代码的组成的一种方式,模块化系统就像乐高玩具一样,一块一块零散积木堆积起一个精彩的世界。每种积木的形状各不相同,功能各不相同,积木与积木直接互相依赖,互相支撑。 + +### 28.1.1 模块化开发好处 + +模块化开发能够将不同的功能组装在一起,实现功能的累加,诸多功能组装在一起,最终形成项目。 + +## 28.2 模块分类 + +- `应用程序模块`:通常这类模块是完整的应用程序,可以独立运行,有自己的实体、服务、API 及 UI 组件等。 +- `框架级模块`:这类通常是解决某个业务功能进行开发的模块,比如上传文件、分布式缓存、数据验证等。 + +## 28.3 如何进行模块化开发 + +在 `Furion` 框架设计之初就考虑到模块化开发,所以从最初版本就具备此功能。启用 `Furion` 模块化支持非常简单。 + +### 28.3.1 启用模块化支持 + +```json showLineNumbers {2,3,4} +{ + "AppSettings": { + "EnabledReferenceAssemblyScan": true, // 启用模块化程序集扫描 + "SupportPackageNamePrefixs": ["Module1", "Module2",...] // 配置通过 NuGet 方式安装的模块化包,如果不是通过 NuGet 方式可不配置 + // "ExternalAssemblies": ["plugins/Module1.dll", "plugins/Module2.dll", ...] // 配置加载网站 plugins 目录下的模块程序集,可不配置 + } +} +``` + +### 28.3.2 各种添加模块配置方式 + +添加模块到现有的应用中有多种方式: + +1. **直接通过项目添加模块化引用或编辑 `.csproj` 添加模块** + +这种方式无需任何配置,`Furion` 框架可自动加载模块。 + +2. **通过添加 `.dll` 方式引用模块** + +需要配置 `EnabledReferenceAssemblyScan` 节点,如: + +```json showLineNumbers {3} +{ + "AppSettings": { + "EnabledReferenceAssemblyScan": true + } +} +``` + +3. **通过 `NuGet` 方式安装模块** + +如果通过 `NuGet` 方式安装模块,需要配置 `SupportPackageNamePrefixs` 节点,如: + +```json showLineNumbers {3} +{ + "AppSettings": { + "SupportPackageNamePrefixs": ["NuGet包名称", "NuGet包名称",...] + } +} +``` + +4. **通过动态加载 `.dll` 方式安装模块** + +这种方式通常是在网站创建一个文件夹放入模块化 `.dll` 文件,如:`plugins`,无需添加引用可自动加载。 + +```json showLineNumbers {3} +{ + "AppSettings": { + "ExternalAssemblies": ["plugins/Module1.dll", "plugins/Module2.dll", ...] + } +} +``` + +:::important 特别注意 + +如果非生产环境,那么 `plugins` 文件夹放在 `bin\Debug\net n.0\` 或 `bin\Release\net n.0` 目录下。 + +如果是生产环境,则直接放在根目录即可。 + +::: + +## 28.4 模块化开发注意事项 + +- 尽可能保证每个模块都有独立的路由地址格式:`/模块化名称/路由地址`,这样才能保证不会和现有的系统出现冲突。 +- 开发模块化是尽可能设计为完全独立的引用,**如果需要包含 UI 元素如视图、html/cs/javascript 应采用嵌入式方式**。 +- 模块化开发如果需要添加第三方服务,应配置在 `AppStartup` 的派生类中。 +- 模块化的所在程序集的注释文件 `.xml` 需放在和 `.dll` 同级目录 + +## 28.5 关于热插拔机制 + +在 `Furion v2.4.0 +` 版本之后,**框架移除了热加载和热卸载模块的功能**,原因是目前微软提供的 `AssemblyLoadContext + 文件夹监听` 处理 `.dll` 运行时热拔插机制还未成熟,此功能将在 `.NET8` 版本得到改善。 + +也就是,自 `v2.4.0+` 版本之后,`Furion` 框架只提供在启动时加载模块,不在运行时进行任何处理。 + +## 28.6 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/mongodb.mdx b/handbook/docs/mongodb.mdx new file mode 100644 index 0000000000000000000000000000000000000000..c35645e83b72062d178569978469272f1c414b14 --- /dev/null +++ b/handbook/docs/mongodb.mdx @@ -0,0 +1,118 @@ +--- +id: mongodb +title: 10.3 MongoDB 操作 +sidebar_label: 10.3 MongoDB 操作 +--- + +:::warning 温馨提醒 + +在 `Furion` 包中默认集成了 `EFCore`,**如果不使用 `EFCore`,可安装纯净版 `Furion.Pure` 代替 `Furion`**。 + +::: + +:::tip 查看最新拓展文档 + +[https://gitee.com/dotnetchina/Furion/pulls/423](https://gitee.com/dotnetchina/Furion/pulls/423) + +::: + +## 10.3.1 关于 MongoDB + +`MongoDB` 是一个基于分布式文件存储的数据库。由 `C++` 语言编写。旨在为 `WEB` 应用提供可扩展的高性能数据存储解决方案。 + +`MongoDB` 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。 + +## 10.3.2 如何集成 + +在 `Furion` 框架中,已经推出 `MongoDB` 拓展包 [Furion.Extras.DatabaseAccessor.MongoDB](https://www.nuget.org/packages/Furion.Extras.DatabaseAccessor.MongoDB)。 + +### 10.3.2.1 注册 `MongoDB` 服务 + +使用非常简单,只需要在 `Startup.cs` 中添加 `services.AddMongoDB(connectionString)` 即可。如: + +```cs showLineNumbers +// 方式一 +services.AddMongoDB("mongodb://localhost:27017"); + +// 方式二 +services.AddMongoDB(new MongoClientSettings {}); + +// 方式三 +services.AddMongoDB(new MongoUrl {}); +``` + +## 10.3.3 基本使用 + +在使用之前,我们可以通过构造函数注入 `IMongoDBRepository` 接口,如: + +- 非泛型版本 + +```cs showLineNumbers +private readonly IMongoDBRepository _mongoRepository; +public PersonService(IMongoDBRepository mongoRepository) +{ + _mongoRepository = mongoRepository; +} +``` + +### 10.3.3.1 常见例子 + +```cs showLineNumbers +var database = _mongoRepository.Context.GetDatabase("foo"); +var collection = database.GetCollection("bar"); + +await collection.InsertOneAsync(new BsonDocument("Name", "Jack")); + +var list = await collection.Find(new BsonDocument("Name", "Jack")) + .ToListAsync(); + +foreach(var document in list) +{ + Console.WriteLine(document["Name"]); +} +``` + +```cs showLineNumbers +public class Person +{ + public ObjectId Id { get; set; } + public string Name { get; set; } +} + +var database = _mongoRepository.Context.GetDatabase("foo"); +var collection = database.GetCollection("bar"); + +await collection.InsertOneAsync(new Person { Name = "Jack" }); + +var list = await collection.Find(x => x.Name == "Jack") + .ToListAsync(); + +foreach(var person in list) +{ + Console.WriteLine(person.Name); +} +``` + +### 10.3.3.2 获取 `MongoClient` 对象 + +`IMongoDBRepository` 只封装了 `MongoDB` 基础功能,如需获取更多操作可通过 `.Context` 属性获取 `MongoClient` 对象,如: + +```cs showLineNumbers +var client = _mongoRepository.Context; +``` + +## 10.3.4 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: + +--- + +:::note 了解更多 + +想了解更多 `MongoDB` 知识可查阅 [MongoDB 仓库](https://hub.fastgit.org/mongodb/mongo-csharp-driver)。 + +::: diff --git a/handbook/docs/net5-to-net6.mdx b/handbook/docs/net5-to-net6.mdx new file mode 100644 index 0000000000000000000000000000000000000000..a8790e64086b5c7742b0e4afe14659022861ea7b --- /dev/null +++ b/handbook/docs/net5-to-net6.mdx @@ -0,0 +1,75 @@ +--- +id: net5-to-net6 +title: 2.9 .NET5 升级 .NET6 +sidebar_label: 2.9 .NET5 升级 .NET6 +description: 了解如何从 .NET5 升级到 .NET6 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +## 2.9.1 升级注意事项 + +**目前使用 `Furion` 所有版本(0.x-3.x)的用户均可以快速无缝升级至 `Furion v4.x` 版本,只需要做少量更改即可。** + +### 2.9.1.1 安装 `.NET6 SDK` + +[https://dotnet.microsoft.com/download/dotnet/6.0](https://dotnet.microsoft.com/download/dotnet/6.0) + +### 2.9.1.2 编辑 `.csproj` 文件 + +编辑解决方案所有项目的 `.csproj` 文件,并替换 `net5.0` 为 `net6.0`,如: + + + +当然也可以使用 `Ctrl + F` 全局替换 + + + +### 2.9.1.3 升级 `NuGet` 包 + +将 `Furion` 所有包升级至 `v4.9.1.7` 版本,同时 `Microsoft` 所有包升级至 `v6.0.25` 版本,如: + + + +### 2.9.1.4 删除 `Startup.cs` 文件 + +删除 `Web 启动层` 的 `Startup.cs` 文件,如: + + + +### 2.9.1.5 编辑 `Web` 启动层 `.csproj` + +编辑 `Web` 启动层 `.csproj` 文件,并添加 `enable`,如: + + + +### 2.9.1.6 替换 `Program.cs` 内容为: + +```cs showLineNumbers +var builder = WebApplication.CreateBuilder(args).Inject(); +var app = builder.Build(); +app.Run(); +``` + +:::important v3.6.4+ 版本 + +如果使用 `Furion 3.6.4+` 版本,可直接使用 `Serve.Run(RunOptions.Default);` 替代上面即可。 + +::: + +:::tip 小提醒 + +如果使用了 `Serilog` 日志组件,可添加 `builder.UseSerilogDefault();` + +也可以使用 `Serve.Run(RunOptions.Default.ConfigureBuilder(builder => builder.UseSerilogDefault()));`。 + +如果使用了 `pgsql` 数据库,你还可能需要添加 `AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);` +=> [相关说明:在 .NET6 中 Npgsql 6.0 对时间戳的映射方式进行了一些重要的更改。](https://www.npgsql.org/doc/types/datetime.html) + +::: + + + +### 2.9.1.7 重新编译整个解决方案 + +升级完成!!! diff --git a/handbook/docs/net6-to-net7.mdx b/handbook/docs/net6-to-net7.mdx new file mode 100644 index 0000000000000000000000000000000000000000..c3127584323d1879991b1d356a5b90b4b43eb626 --- /dev/null +++ b/handbook/docs/net6-to-net7.mdx @@ -0,0 +1,42 @@ +--- +id: net6-to-net7 +title: 2.10 .NET6 升级 .NET7 +sidebar_label: 2.10 .NET6 升级 .NET7 +description: 了解如何从 .NET6 升级到 .NET7 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +:::caution .NET7 发布 + +**🚀🎉🔥 2022 年 11 月 08 日,微软发布了 .NET7 首个正式版。** + +::: + +## 2.10.1 升级注意事项 + +**目前使用 `Furion` 所有版本(0.x-3.x)的用户均可以快速无缝升级至 `Furion v4.x` 版本,只需要做少量更改即可。** + +### 2.10.1.1 安装 `.NET7 SDK` + +[https://dotnet.microsoft.com/download/dotnet/7.0](https://dotnet.microsoft.com/download/dotnet/7.0) + +### 2.10.1.2 编辑 `.csproj` 文件 + +编辑解决方案所有项目的 `.csproj` 文件,并替换 `net6.0` 为 `net7.0`,如: + + + +当然也可以使用 `Ctrl + F` 全局替换 + + + +### 2.10.1.3 升级 `NuGet` 包 + +将 `Furion` 所有包升级至 `v4.9.1.7` 版本,同时 `Microsoft` 所有包升级至 `v7.0.14` 版本,如: + + + +### 2.10.1.4 重新编译整个解决方案 + +升级完成!!! diff --git a/handbook/docs/net7-to-net8.mdx b/handbook/docs/net7-to-net8.mdx new file mode 100644 index 0000000000000000000000000000000000000000..811d43122e4f459ea584e4faa8913efb77fa64c3 --- /dev/null +++ b/handbook/docs/net7-to-net8.mdx @@ -0,0 +1,52 @@ +--- +id: net7-to-net8 +title: 2.11 .NET7 升级 .NET8 +sidebar_label: 2.11 .NET7 升级 .NET8 ✨ +description: 了解如何从 .NET7 升级到 .NET8 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +:::caution .NET8 发布 + +**🚀🎉🔥 2023 年 11 月 15 日,微软发布了 .NET8 首个正式版。** + +::: + +## 2.11.1 升级注意事项 + +**目前使用 `Furion` 所有版本(0.x-3.x)的用户均可以快速无缝升级至 `Furion v4.x` 版本,只需要做少量更改即可。** + +### 2.11.1.1 安装 `.NET8 SDK` + +[https://dotnet.microsoft.com/download/dotnet/8.0](https://dotnet.microsoft.com/download/dotnet/8.0) + +### 2.11.1.2 编辑 `.csproj` 文件 + +编辑解决方案所有项目的 `.csproj` 文件,并替换 `net7.0` 为 `net8.0`,如: + + + +当然也可以使用 `Ctrl + F` 全局替换 + + + +### 2.11.1.3 升级 `NuGet` 包 + +将 `Furion` 所有包升级至 `v4.9.1.7` 版本,同时 `Microsoft` 所有包升级至 `v8.0.0` 版本,如: + + + +### 2.11.1.4 重新编译整个解决方案 + +升级完成!!! + +### 2.11.1.5 升级失败解决方案 + +1. `Microsoft.EntityFrameworkCore.Tools` 版本冲突 + +若升级过程中出现 `Microsoft.EntityFrameworkCore.Tools` 版本冲突,**只需卸载后重新安装即可。** + +2. `Microsoft.EntityFrameworkCore` 提示 `Method not found: ` 错误 + +请确保使用的数据库驱动包有对应的 `8.0.0` 版本。 \ No newline at end of file diff --git a/handbook/docs/nuget-local.mdx b/handbook/docs/nuget-local.mdx new file mode 100644 index 0000000000000000000000000000000000000000..24856a2f6a8ce4e6f7d718834830b13016ae6897 --- /dev/null +++ b/handbook/docs/nuget-local.mdx @@ -0,0 +1,85 @@ +--- +id: nuget-local +title: 2.15 NuGet 本地调试包 +sidebar_label: 2.15 NuGet 本地调试包 +description: 如何使用 NuGet 本地调试包 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +## 2.15.1 关于本地测试包 + +正常情况下,我们都是通过 `NuGet` 在线平台(Visual Studio 内置的 `NuGet` 包管理器)安装拓展依赖,**但有时候我们不希望发布到 `NuGet` 平台或者只想在本地使用又或者先在本地测试通过再发布。** + +这样做的好处是: + +- 可以有效保护公司核心代码 +- 方便开源项目测试 + +## 2.15.2 如何配置 + +### 2.15.2.1 测试包命名规则 + +默认情况下,`Furion` 会根据 `Furion.版本号-beta.Issue编号` 规则生成 `.nupkg` 和 `.snupkg` 包,前者是不包含调试的无符号 `NuGet` 包,后者则是调试符号包(方便 `IDE` 调试用的),如: + +```showLineNumbers title="D:\Furion_NuGet_Test_Packages" +Furion.4.2.3-beta-I5MM3O.nupkg +Furion.4.2.3-beta-I5MM3O.snupkg +``` + +其中 `I5MM3O` 对应的正是 `Issue` 的编号:[https://gitee.com/dotnetchina/Furion/issues/I5MM3O](https://gitee.com/dotnetchina/Furion/issues/I5MM3O) + +### 2.15.2.2 配置本地包路径 + +有了这两个包之后,就可以在本地磁盘中创建文件夹并放进去,如 `D:\Furion_NuGet_Test_Packages`。 + + + +### 2.15.2.3 在 `Visual Studio` 中配置路径 + + + + + + + +:::tip 关于命令行操作 + +如果不使用可视化 `IDE` 配置,可通过下列命令行配置: + +```bash +dotnet restore "YourProject.Web.Entry/YourProject.Web.Entry.csproj" -s "D:\Furion_NuGet_Test_Packages"; +dotnet restore "YourProject.Web.Entry/YourProject.Web.Entry.csproj" -s "https://api.nuget.org/v3/index.json"; +``` + +::: + +### 2.15.2.4 选择测试版安装或更新 + + + + + + + +
+ +**测试通过后可以删除之前的配置即可**,如: + + + +## 2.15.3 `Visual Studio` 调试 `NuGet` 包 + +`Furion` 提供了 `.snupkg` 包,在安装 `Furion` 的时候自动下载到本地,如需启用源码调试,只需要启用以下配置即可: + + + +启用之后重新编译解决方案就可以通过 `F12` 跳转到源代码区并调试源代码了。 + +## 2.15.4 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/object-mapper.mdx b/handbook/docs/object-mapper.mdx new file mode 100644 index 0000000000000000000000000000000000000000..441d33fabf405379e6ee7932a1a44ecb7853e278 --- /dev/null +++ b/handbook/docs/object-mapper.mdx @@ -0,0 +1,141 @@ +--- +id: object-mapper +title: 13. 对象数据映射 +sidebar_label: 13. 对象数据映射 (Mapper) +--- + +:::important 使用 `Mapster` 以外的拓展说明 + +`Furion` 框架提供了 `Mapster` 的拓展 `Furion.Extras.ObjectMapper.Mapster`,如需使用第三方如 `AutoMapper` 则无需安装此拓展。 + +::: + +## 13.1 对象映射 + +简单来说,就是将一个对象的数据根据特定规则批量映射到另一个对象中,减少手工操作和降低人为出错率。如将 `DTO` 对象映射到 `Entity` 实体中,反之亦然。 + +## 13.2 先看例子 + +在过去,我们需要将一个对象的值转换到另一个对象中,我们需要这样做,如: + +```cs showLineNumbers {4-9} +var entity = repository.Find(1); + +var dto = new Dto(); +dto.Id = entity.Id; +dto.Name = entity.Name; +dto.Age = entity.Age; +dto.Address = entity.Address; +dto.FullName = entity.FirstName + entity.LastName; +dto.IdCard = entity.IdCard.Replace("1234", "****"); +``` + +上面的例子似乎没有任何问题,但是如果很多地方需要这样的赋值操作、或者相同的赋值操作在多个地方使用,又或者一个类中含有非常多的属性或自定义赋值操作。那么这样的操作效率极低,容易出错,且代码非常臃肿和冗余。 + +所以,实现自动映射赋值和支持特殊配置的需求就有了。目前 `C#` 平台有两个优秀的对象映射工具:`Mapster` 和 `AutoMapper`。**在 `Furion` 框架中,推荐使用 [Mapster](https://github.com/MapsterMapper/Mapster),[Mapster](https://github.com/MapsterMapper/Mapster) 是一款极易使用且超高性能的对象映射框架。** + +## 13.3 `Mapster` 使用 + +现在,我们可以通过 `Mapster` 提供的对象映射方法:`Adapt` 方法改造上面的例子: + +:::important 安装拓展包 + +在 `Furion.Core` 层安装 `Furion.Extras.ObjectMapper.Mapster` 拓展包,无需手动调用,`Furion` 会自动加载并调用。 + +::: + +### 13.3.1 快速入门 + +```cs showLineNumbers {2} +var entity = repository.Find(1); +var dto = entity.Adapt(); +``` + +仅仅一行代码就可以实现 `entity -> dto` 的转换,如果涉及到赋值的复制操作,如 `dto.FullName` 和 `dto.IdCard`,我们只需要自定义映射规则类即可。 + +### 13.3.2 自定义映射规则 + +```cs showLineNumbers {1,6,10-12} +using Mapster; +using System; + +namespace Furion.Application +{ + public class Mapper : IRegister + { + public void Register(TypeAdapterConfig config) + { + config.ForType() + .Map(dest => dest.FullName, src => src.FirstName + src.LastName) + .Map(dest => dest.IdCard, src => src.IdCard.Replace("1234", "****")); + } + } +} +``` + +:::tip 小知识 + +该映射文件 `Mapper.cs` 可以放在任何项目或文件夹中,`Furion` 会在程序启动的时候自动扫描并注入配置。 + +::: + +### 13.3.3 依赖注入方式 + +`Mapster` 除了提供 `Adapt` 拓展方法以外,同时还提供依赖注入的方式。 + +```cs showLineNumbers {1} +public Person(IMapper mapper) +{ + var dto = _mapper.Map(entity); +} +``` + +### 13.3.4 和 `EFCore` 配合 + +`Mapster` 还提供了 `ProjectToType` Linq 拓展方法减少我们手动 `Select` 操作,如: + +正常的操作: + +```cs showLineNumbers {2-7} +var destinations = context.Sources + .Select(p => new Destination { + Id = p.Id, + Name = p.Name, + Surname = p.Surname, + .... + }) + .ToList(); +``` + +使用 `Mapster` 之后: + +```cs showLineNumbers {2} + var destinations = context.Sources + .ProjectToType() + .ToList(); +``` + +## 13.4 全局默认配置 + +`Furion` 提供全局默认映射配置选项 `TypeAdapterConfig.GlobalSettings.Default`,可在 `Startup` 中配置即可,如: + +```cs showLineNumbers +TypeAdapterConfig.GlobalSettings.Default + .PreserveReference(true); +``` + +## 13.5 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: + +--- + +:::note 了解更多 + +想了解更多 `Mapster` 知识可查阅 [Mapster - Wiki](https://github.com/MapsterMapper/Mapster/wiki) 文档。 + +::: diff --git a/handbook/docs/options.mdx b/handbook/docs/options.mdx new file mode 100644 index 0000000000000000000000000000000000000000..9978655d1982af43ab9e1f188ddc4d7b4835364f --- /dev/null +++ b/handbook/docs/options.mdx @@ -0,0 +1,666 @@ +--- +id: options +title: 4.2 选项 +sidebar_label: 4.2 选项 +description: 配置也可以进行 OOP 操作 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +## 4.2.1 什么是选项 + +选项是 `ASP.NET Core` 推荐的动态读取配置的方式,这种方式将配置文件数据用一个**强类型**来托管,能够实现配置验证、默认值配置、实时读取等功能。 + +## 4.2.2 与配置的区别 + +选项实际上也是配置,但在后者的基础上添加了配置验证、默认值/后期配置设定及提供了多种接口读取配置信息,同时还支持供配置更改通知等强大灵活功能。 + +所以,除了一次性读取使用的配置以外,都应该选用 **选项** 替换 **配置**。 + +:::tip 知识导航 + +有关配置说明可查看《[4.1 配置](configuration.mdx)》 章节。 + +::: + +## 4.2.3 选项的使用 + +假设我们需要在系统运行时获取**系统名称、版本号及版权信息**,这些信息可能随时变化而且需要在多个地方使用。这时就需要将这些信息配置起来。具体步骤如下: + +### 4.2.3.1 配置 `appsettings.json` 信息 + +```json showLineNumbers {2-6} +{ + "AppInfo": { + "Name": "Furion", + "Version": "1.0.0", + "Company": "Baiqian" + } +} +``` + +### 4.2.3.2 创建 `AppInfoOptions` 强类型类 + +```cs showLineNumbers {1,5} +using Furion.ConfigurableOptions; + +namespace Furion.Application +{ + public class AppInfoOptions : IConfigurableOptions + { + public string Name { get; set; } + public string Version { get; set; } + public string Company { get; set; } + } +} +``` + +:::note 温馨提示 + +建议所有选项类都应该以 **`Options`** 命名结尾。 + +另外,`Furion` 框架提供了非常灵活的注册选项服务的方法,只需要继承 **`IConfigurableOptions`** 接口即可,该接口位于 **`Furion.ConfigurableOptions`** 命名空间下。 + +::: + +### 4.2.3.3 注册 `AppInfoOptions` 服务 + +**选项不同于配置,需在应用启动时注册** + +```cs showLineNumbers {12} title="Furion.Web.Core\FurWebCoreStartup.cs" +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; + +namespace Furion.Web.Core +{ + [AppStartup(800)] + public sealed class FurWebCoreStartup : AppStartup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddConfigurableOptions(); + } + } +} +``` + +### 4.2.3.4 读取 `AppInfoOptions` 信息 + +在 `Furion` 框架中,提供了多种读取方式: + +- 通过 `App.GetConfig(path)` 读取(**不推荐**) +- 通过依赖注入以下实例读取: + - `IOptions` + - `IOptionsSnapshot` + - `IOptionsMonitor` +- 通过 `App` 静态类提供的静态方法获取: + - `App.GetOptions()` + - `App.GetOptionsMonitor()` + - `App.GetOptionsSnapshot()` + +:::warning 特别注意 + +禁止在主机启动时通过 `App.GetOptions` 获取选项,如需获取配置选项理应通过 `App.GetConfig("配置节点", true)`。 + +::: + +(path)", + value: "App.GetConfig(path)", + }, + { label: "依赖注入方式", value: "依赖注入方式" }, + { label: "App.GetOptions()", value: "App.GetOptions()" } + ]} +> + + +```cs showLineNumbers {13-16} +using Furion.Application; +using Microsoft.AspNetCore.Mvc; + +namespace Furion.Web.Entry.Controllers +{ + [Route("api/[controller]")] + public class DefaultController : ControllerBase + { + [HttpGet] + public string Get() + { + // 不推荐采用此方式读取,该方式仅在 ConfigureServices 启动时使用 + var appInfo = App.GetConfig("AppInfo", true); + return $@"名称:{appInfo.Name}, + 版本:{appInfo.Version}, + 公司:{appInfo.Company}"; + } + } +} +``` + + + + +```cs showLineNumbers {3,15-17,27-29,31-33,35-37} +using Furion.Application; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; + +namespace Furion.Web.Entry.Controllers +{ + [Route("api/[controller]")] + public class DefaultController : ControllerBase + { + private readonly AppInfoOptions options1; + private readonly AppInfoOptions options2; + private readonly AppInfoOptions options3; + + public DefaultController( + IOptions options + , IOptionsSnapshot optionsSnapshot + , IOptionsMonitor optionsMonitor) + { + options1 = options.Value; + options2 = optionsSnapshot.Value; + options3 = optionsMonitor.CurrentValue; + } + + [HttpGet] + public string Get() + { + var info1 = $@"名称:{options1.Name}, + 版本:{options1.Version}, + 公司:{options1.Company}"; + + var info2 = $@"名称:{options2.Name}, + 版本:{options2.Version}, + 公司:{options2.Company}"; + + var info3 = $@"名称:{options3.Name}, + 版本:{options3.Version}, + 公司:{options3.Company}"; + + return $"{info1}-{info2}-{info3}"; + } + } +} +``` + + + + + +```cs showLineNumbers {12-15,17-20,22-25} +using Furion.Application; +using Microsoft.AspNetCore.Mvc; + +namespace Furion.Web.Entry.Controllers +{ + [Route("api/[controller]")] + public class DefaultController : ControllerBase + { + [HttpGet] + public string Get() + { + var options1 = App.GetOptions(); + var info1 = $@"名称:{options1.Name}, + 版本:{options1.Version}, + 公司:{options1.Company}"; + + var options2 = App.GetOptionsSnapshot(); + var info2 = $@"名称:{options2.Name}, + 版本:{options2.Version}, + 公司:{options2.Company}"; + + var options3 = App.GetOptionsMonitor(); + var info3 = $@"名称:{options3.Name}, + 版本:{options3.Version}, + 公司:{options3.Company}"; + + return $"{info1}-{info2}-{info3}"; + } + } +} +``` + + + + +### 4.2.3.5 如何选择读取方式 + +- 如果选项需要在多个地方使用,则无论任何时候都不推荐使用 `App.GetOptions()` +- 在可依赖注入类中,依赖注入 `IOptions[Snapshot|Monitor]` 读取 +- 在静态类/非依赖注入类中,选择 `App.GetOptions[Snapshot|Monitor]()` 读取 + +## 4.2.4 选项接口说明 + +`ASP.NET Core` 应用提供了多种读取选项的接口: + +- `IOptions`: + - 不支持: + - 在应用启动后读取配置数据 + - 命名选项 + - 注册为单一实例且可以注入到任何服务生存期 +- `IOptionsSnapshot`: + - 在每次请求时应重新计算选项的方案中有用 + - 注册为范围内,因此无法注入到单一实例服务 + - 支持命名选项 +- `IOptionsMonitor`: + - 用于检索选项并管理 TOptions 实例的选项通知。 + - 注册为单一实例且可以注入到任何服务生存期。 + - 支持: + - 更改通知 + - 命名选项 + - 可重载配置 + - 选择性选项失效 `(IOptionsMonitorCache)` + +:::warning 注意事项 + +在使用 `IConfigurableOptionsListener` 监听选项后,如要获取最新的配置信息,请使用 `App.GetOptionsMonitor()` 而不是 `App.GetOptions()`。 + +::: + +:::note 了解更多 + +想了解更多 `选项接口` 知识可查阅 [ASP.NET Core - 选项 - 选项接口](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/configuration/options?view=aspnetcore-5.0#options-interfaces) 小节。 + +::: + +## 4.2.5 选项自定义配置 + +我们知道,选项实际上需要和配置文件特定键值挂钩,那 `Furion` 是如何准确的找到配置文件中的键值的呢? + +### 4.2.5.1 选项查找键流程 + +- **没有贴 `[OptionsSettings]` 特性** + - 以 `Options` 结尾,则去除 `Options` 字符串 + - 否则返回 `类名称` +- **贴了 `[OptionsSettings]` 特性** + - 如果配置了 `Path` 属性,则返回 `Path` 的值 + - 否则返回 `类名称` + + + + +- 以 `Options` 结尾,则键名为:`AppInfo` + +```cs showLineNumbers {1} +public class AppInfoOptions : IConfigurableOptions +{ + public string Name { get; set; } + public string Version { get; set; } + public string Company { get; set; } +} +``` + +- 不以 `Options` 结尾,则键名为:`AppInfoSettings` + +```cs showLineNumbers {1} +public class AppInfoSettings : IConfigurableOptions +{ + public string Name { get; set; } + public string Version { get; set; } + public string Company { get; set; } +} +``` + + + + + +- 配置了 `Path` 属性,则键名为:`AppSettings:AppInfo` + +```cs showLineNumbers {1} +[OptionsSettings("AppSettings:AppInfo")] +public class AppInfoOptions : IConfigurableOptions +{ + public string Name { get; set; } + public string Version { get; set; } + public string Company { get; set; } +} +``` + +- 没有配置 `Path` 属性,,则键名为:`AppInfoSettings` + +```cs showLineNumbers {1} +[OptionsSettings] +public class AppInfoSettings : IConfigurableOptions +{ + public string Name { get; set; } + public string Version { get; set; } + public string Company { get; set; } +} +``` + + + + +## 4.2.6 `[OptionsSettings]` 说明 + +选项类可以通过 `[OptionsSettings]` 来配置查找路径值。 + +- `Path`:对应配置文件中的键,支持 **分层键** 字符串,参见:《[4.1 配置 - 4.1.3 路径符 查找节点](configuration/#413-路径符-查找节点)》 +- `PostConfigureAll`:选项后期配置,默认 `false`。[ASP.NET Core - 选项 - 选项后期配置](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/configuration/options?view=aspnetcore-3.1#options-post-configuration) + +## 4.2.7 选项验证 + +选项支持验证配置有效性,在 `Furion` 框架中,通过 `services.AddConfigurableOptions()` 注册选项默认启用了验证支持。 + +包括: + +- 特性方式 `DataAnnotations` +- 自定义复杂验证 `IValidateOptions` + + + + +```cs showLineNumbers {2,8,10,12} +using Furion.ConfigurableOptions; +using System.ComponentModel.DataAnnotations; + +namespace Furion.Application +{ + public class AppInfoOptions : IConfigurableOptions + { + [Required(ErrorMessage = "名称不能为空")] + public string Name { get; set; } + [Required, RegularExpression(@"^[0-9][0-9\.]+[0-9]$", ErrorMessage = "不是有效的版本号")] + public string Version { get; set; } + [Required, MaxLength(100)] + public string Company { get; set; } + } +} +``` + + + + +- 自定义验证类 `AppInfoValidation` 并继承 `IValidateOptions` 接口,同时实现 `Validate` 方法。 + +```cs showLineNumbers {1,6,8,12,15} +using Microsoft.Extensions.Options; +using System.Text.RegularExpressions; + +namespace Furion.Application +{ + public class AppInfoValidation : IValidateOptions + { + public ValidateOptionsResult Validate(string name, AppInfoOptions options) + { + if (!Regex.IsMatch(options.Version, @"^[0-9][0-9\.]+[0-9]$")) + { + return ValidateOptionsResult.Fail("不是有效的版本号"); + } + + return ValidateOptionsResult.Success; + } + } +} +``` + +- 选项类继承 `IConfigurableOptions` 接口,并实现该接口。 + +```cs showLineNumbers {6,16-18} +using Furion.ConfigurableOptions; +using System.ComponentModel.DataAnnotations; + +namespace Furion.Application +{ + public class AppInfoOptions : IConfigurableOptions + { + [Required(ErrorMessage = "名称不能为空")] + public string Name { get; set; } + [Required] + public string Version { get; set; } + [Required, MaxLength(100)] + public string Company { get; set; } + + // 选项后期配置 + public void PostConfigure(AppInfoOptions options, IConfiguration configuration) + { + } + } +} +``` + +- 完整代码如下: + +```cs showLineNumbers {4,9,19,25,27,31,34} +using Furion.ConfigurableOptions; +using Microsoft.Extensions.Options; +using System.ComponentModel.DataAnnotations; +using System.Text.RegularExpressions; + +namespace Furion.Application +{ + // 继承 IConfigurableOptions 接口 + public class AppInfoOptions : IConfigurableOptions + { + [Required(ErrorMessage = "名称不能为空")] + public string Name { get; set; } + [Required] + public string Version { get; set; } + [Required, MaxLength(100)] + public string Company { get; set; } + + // 选项后期配置 + public void PostConfigure(AppInfoOptions options) + { + } + } + + // 创建自定义验证类 + public class AppInfoValidation : IValidateOptions + { + public ValidateOptionsResult Validate(string name, AppInfoOptions options) + { + if (!Regex.IsMatch(options.Version, @"^[0-9][0-9\.]+[0-9]$")) + { + return ValidateOptionsResult.Fail("不是有效的版本号"); + } + + return ValidateOptionsResult.Success; + } + } +} +``` + + + + +:::note 特别说明 + +**`IConfigurableOptions`** 继承自 **`IConfigurableOptions`**,也就是自定义复杂验证默认具有 **`PostConfigure(TOptions options)`** 选项后期配置方法。关于《[4.2.8 选项后期配置](#428-选项后期配置)》将在下一小节说明。 + +::: + +## 4.2.8 选项后期配置 + +选项后期配置通俗一点来说,可以在运行时解析值或设定默认值/后期配置等。 + +在 `Furion` 框架中,配置选项后期配置很简单,只需要继承 `IConfigurableOptions` 接口并实现 `PostConfigure(TOptions options)` 方法。 + +```cs showLineNumbers {7,16-21} +using Furion.ConfigurableOptions; +using Microsoft.Extensions.Configuration; +using System.ComponentModel.DataAnnotations; + +namespace Furion.Application +{ + public class AppInfoOptions : IConfigurableOptions + { + [Required(ErrorMessage = "名称不能为空")] + public string Name { get; set; } + [Required] + public string Version { get; set; } + [Required, MaxLength(100)] + public string Company { get; set; } + + public void PostConfigure(AppInfoOptions options, IConfiguration configuration) + { + options.Name ??= "Furion"; + options.Version ??= "1.0.0"; + options.Company ??= "Baiqian"; + } + } +} +``` + +:::note 特别说明 + +**`IConfigurableOptions`** 继承自 **`IConfigurableOptions`**,也就是自定义复杂验证默认具有 **`PostConfigure(TOptions options, IConfiguration configuration)`** 选项后期配置方法。 + +::: + +## 4.2.9 选项更改通知(`热更新`) + +`Furion` 框架提供了非常简单且灵活的方式监听选项更改,也就是 **`appsettings.json` 或 自定义配置文件发生任何更改都会触发处理方法**。 + +使用非常简单,只需要继承 `IConfigurableOptionsListener` 接口并实现 `void OnListener(TOptions options, IConfiguration configuration)` 方法即可。 + +```cs showLineNumbers {5,11-15} +using Furion.ConfigurableOptions; + +namespace Furion.Application +{ + public class AppInfoOptions : IConfigurableOptionsListener + { + public string Name { get; set; } + public string Version { get; set; } + public string Company { get; set; } + + public void OnListener(AppInfoOptions options, IConfiguration configuration) + { + var name = options.Name; // 实时的最新值 + var version = options.Version; // 实时的最新值 + } + + public void PostConfigure(AppInfoOptions options, IConfiguration configuration) + { + } + } +} +``` + +:::note 特别说明 + +**`IConfigurableOptionsListener`** 继承自 **`IConfigurableOptions`**。 + +::: + +### 4.2.9.1 关于多次触发问题 + +在 `Furion` 底层使用的是 `ChangeToken.OnChange` 监听文件更改,但是此方式会导致 `OnListener` 触发两次,这并非是框架的 `bug`,而是 `.NET Core` 本身存在的问题,详见:[https://github.com/dotnet/aspnetcore/issues/2542](https://github.com/dotnet/aspnetcore/issues/2542) + +所以,`Furion` 框架也给出另一种解决方案可替代 `IConfigurableOptionsListener` 的方式,也就是通过局部注入 `IOptionsMonitor` 的方式,如: + +```cs showLineNumbers {1,3,7,9,14,19} +public class YourService : IYourService, IDisposable +{ + private readonly IDisposable _optionsReloadToken; + + private YourOptions _options; + + public YourService(IOptionsMonitor options) + { + (_optionsReloadToken, _options) = (options.OnChange(ReloadOptions), options.CurrentValue); + } + + private void ReloadOptions(YourOptions options) + { + _options = options; + } + + public void Dispose() + { + _optionsReloadToken?.Dispose(); + } +} +``` + +这种方式虽然啰嗦,但是可以很好和业务代码契合。 + +## 4.2.10 选项的优缺点 + +- 优点 + + - 强类型配置 + - 提供多种读取方式 + - 支持热加载 + - 支持设置默认值/后期配置 + - 支持在运行环境中动态配置 + - 支持验证配置有效性 + - 支持更改通知 + - 支持命名选项 + +- 缺点 + + - 需要定义对应类型 + - 需要在启动时注册 + +## 4.2.11 自定义属性 `Key` 映射 + +:::important 版本说明 + +以下内容仅限 `Furion v3.4.3+` 版本使用。 + +::: + +有时候我们在 `appsettings.json` 中配置的 `Key` 和选项定义的属性名不一样,这时候就需要用到 `[MapSettings]` 特性即可,如: + +```json showLineNumbers {4} +"AppInfo": { + "Name": "Furion", + "Version": "1.0.0", + "Company_Name": "Baiqian" +} +``` + +```cs showLineNumbers {6-7} +public class AppInfoOptions : IConfigurableOptions +{ + public string Name { get; set; } + public string Version { get; set; } + + [MapSettings("Company_Name")] + public string Company { get; set; } +} +``` + +:::tip 特别注意 + +`[MapSettings]` 配置的 `Key` 会自定应用选项的 `Key` 作为起始点,如实际上 `Company` 属性对应的 `Key` 为:`AppInfo:Company_Name`。 + +::: + +## 4.2.12 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: + +--- + +:::note 了解更多 + +想了解更多 `选项` 知识可查阅 [ASP.NET Core - 选项](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/configuration/options?view=aspnetcore-5.0) 章节。 + +::: diff --git a/handbook/docs/performance.mdx b/handbook/docs/performance.mdx new file mode 100644 index 0000000000000000000000000000000000000000..8cb7d74b6178a3ea4a465b39748df884f2e48f55 --- /dev/null +++ b/handbook/docs/performance.mdx @@ -0,0 +1,13 @@ +--- +id: performance +title: 36.2 负载压测 +sidebar_label: 36.2 负载压测 +--- + +:::tip 视频教程 + +【[负载压测视频教程](https://www.bilibili.com/video/BV1eo4y1Q7sJ/)】 + +::: + +文档紧急编写中,可以先看旧文档:https://monksoul.gitbook.io/hoa/ diff --git a/handbook/docs/pm2.mdx b/handbook/docs/pm2.mdx new file mode 100644 index 0000000000000000000000000000000000000000..aa8b941278a172d822bd71334b82bd963bc52a3f --- /dev/null +++ b/handbook/docs/pm2.mdx @@ -0,0 +1,265 @@ +--- +id: pm2 +title: 34.6 pm2 部署 +sidebar_label: 34.6 pm2 部署 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +## 34.6.1 关于 `pm2` + +`pm2` 是 `NodeJS` 平台高级生产流程管理器,也是一个守护进程管理器,它能够管理和保持应用程序 7\*24 小时在线。 + +`pm2` 优点: + +- 简单易用 +- 跨平台 +- 容器集成 +- 内置集群、负载均衡 +- 支持模块系统 +- 支持实时监测 +- 支持日志管理 +- 支持关键指标监控 + .... + +想了解更多 `pm2` 知识可查阅 [https://pm2.keymetrics.io/](https://pm2.keymetrics.io/)。 + +## 34.6.2 如何安装 + +1. **系统安装 `NodeJS` 环境 [https://nodejs.org/en/](https://nodejs.org/en/)** + +相信大部分人电脑都已经安装。 + +2. **通过 `npm` 或 `yarn` 全局安装 `pm2` 工具** + +npm: + +```bash showLineNumbers +npm install pm2@latest -g +``` + +yarn: + +```bash showLineNumbers +yarn global add pm2 +``` + +## 34.6.3 托管 `.NET5/6` 应用程序 + +### 34.6.3.1 非单文件/非独立发布模式 + +这种模式的特点就是需要服务器安装 `.NET5/6` 的环境 + +```bash showLineNumbers +pm2 start --name xxx dotnet -- xxx.dll +``` + +:::important 指定端口 + +如需指定端口,可使用下列命令: + +```bash showLineNumbers +pm2 start --name xxx dotnet -- PMS.Web.Entry.dll --urls=https://localhost:8089 +``` + +注意 `--` 后面可以写完整的 `dotnet` 命令。 + +::: + +:::tip 命令说明 + +`xxx.dll` 为项目发布后的启动层名称。 + +`--name` 配置应用程序在 `pm2` 中的唯一标识。 + +::: + +:::warning 终端说明 + +注意:通过 `--` 传递参数在 `powershell` 终端下无效,需要在 `cmd` 终端下才行。比如出现这样的错误: + + + +::: + +### 34.6.3.2 单文件/独立发布模式 + +这种模式的特点就是无需服务器安装 `.NET` 任何环境,可查阅 [单文件发布文档](./singlefile.mdx) + +```bash showLineNumbers +pm2 start --name xxx PMS.Web.Entry.exe +``` + +:::important 指定端口 + +如需指定端口,可使用下列命令: + +```bash showLineNumbers +pm2 start --name xxx PMS.Web.Entry.exe -- --urls=https://localhost:8089 +``` + +注意 `--` 后面可以写完整的 `dotnet` 命令。 + +::: + +:::tip 命令说明 + +`xxx.exe` 为项目发布后的启动层名称,如果名称包含 `空格`,则使用双引号包裹,如 `"x xx.exe"`。 + +`--name` 配置应用程序在 `pm2` 中的唯一标识。 + +::: + +:::warning 终端说明 + +注意:通过 `--` 传递参数在 `powershell` 终端下无效,需要在 `cmd` 终端下才行。 + + + +::: + +### 34.6.3.3 启用应用程序 + +执行上述命令后会显示启动成功日志,如: + +```bash showLineNumbers +PS C:\Users\bqrjsoft\Desktop\pms> pm2 start --name pms PMS.Web.Entry.exe +[PM2] Starting C:\Users\bqrjsoft\Desktop\pms\PMS.Web.Entry.exe in fork_mode (1 instance) +[PM2] Done. +┌─────┬────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐ +│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │ +├─────┼────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤ +│ 0 │ pms │ default │ N/A │ fork │ 41764 │ 0s │ 0 │ online │ 0% │ 85.0mb │ bqrjsoft │ disabled │ +└─────┴────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘ +PS C:\Users\bqrjsoft\Desktop\pms> +``` + + + +:::tip 关于端口 + +`ASP.NET Core` 程序默认端口都是 `5000`,如 `http://localhost:5000`。 + +::: + +### 34.6.3.4 `json` 配置/启动方式 + +`pm2` 部署也提供了非常简单的 `json` 配置方式,部署更简单,拓展性更强,不需要每次重复输入命令,如在项目启动项目下添加 `pm2.json`,并设置文件属性 `内容` 为 `始终复制`: + +```json showLineNumbers {3,15,16,19} title="PMS/Web.Entry/pm2.json" +{ + "apps": { + "name": "唯一标识", + "script": "dotnet", + "exec_mode": "fork", + "error_file": "logs/err.log", + "out_file": "logs/out.log", + "merge_logs": true, + "log_date_format": "YYYY-MM-DD HH:mm:ss", + "min_uptime": "60s", + "max_restarts": 30, + "autorestart": true, + "restart_delay": "60", + "args": [ + "PMS.Web.Entry.dll", + "--urls=http://*:5001" + ], + "env": { + "ASPNETCORE_ENVIRONMENT": "Production" + } + } +} +``` + +在发布后的文件目录下打开终端执行: + +```bash showLineNumbers +pm2 start pm2.json +``` + +## 34.6.4 `pm2` 常见操作 + +### 34.6.4.1 实时监听运行状态 + +```bash showLineNumbers +pm2 monit +``` + + + +### 34.6.4.2 显示运行日志 + +```bash showLineNumbers +pm2 logs +``` + + + +### 34.6.4.3 查看应用信息 + +```bash showLineNumbers +pm2 info pms +``` + +注意,`pms` 为您配置的 `--name` 名称。 + + + +### 34.6.4.4 随机启动 + +```bash showLineNumbers +pm2 startup +pm2 save +``` + +:::tip `Windows` 下随机启动 + +可查阅 [pm2-windows-startup](https://www.npmjs.com/package/pm2-windows-startup)。 + +```bash showLineNumbers +npm install pm2-windows-startup -g +pm2-startup install +pm2 save +``` + +::: + +### 34.6.4.5 集群模式(负载均衡) + +- 非单文件/非独立发布模式 + +```bash showLineNumbers +pm2 start "xxx.dll" -i max +``` + +- 单文件/独立发布模式 + +```bash showLineNumbers +pm2 start xxx.exe -i max +``` + +### 34.6.4.6 其他操作 + +```bash showLineNumbers +// 重启应用 +pm2 restart app_name + +// 重载应用 +pm2 reload app_name + +// 停止应用 +pm2 stop app_name + +// 删除应用 +pm2 delete app_name +``` + +更多 `pm2` 文档可查阅 [https://pm2.keymetrics.io/docs/usage/quick-start/](https://pm2.keymetrics.io/docs/usage/quick-start/) + +## 34.6.5 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/position.mdx b/handbook/docs/position.mdx new file mode 100644 index 0000000000000000000000000000000000000000..51873fd6ccc7cd8bbd47295d76da11bec5c87d24 --- /dev/null +++ b/handbook/docs/position.mdx @@ -0,0 +1,24 @@ +--- +id: position +title: 1.9 框架定位 +sidebar_label: 1.9 框架定位 +description: 用户迫切需要什么,那么我们就推什么 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +:::tip 微不足道的贡献 + +`.NET` 要在国内真正发展起来,必须得有一些追逐梦想的人在做着不计付出的事情,而我希望自己能贡献一份微薄之力。 + +::: + +很多人问过,我不知道该怎么回答,因为我自己几乎没有考虑过这个问题,还包括盈利的问题。 + +我没考虑的是 `Furion` 一定要是某种形式的框架,譬如企业级、微服务、单体等形式。**`Furion` 一贯的思路是,用户迫切需要什么,那么我们就推什么,尽管最终可能让它变成四不像,可说不定用户要的就是 “四不像”。** + +当然也要有所为有所不为,用户需要的,但感觉自己做不好的,还是别去碰了,精心完善现有功能才是正道。 + +好吧,其实多数时候,我们想得太多了,实际去做的太少了。就好像写这个内容时想的是个中长篇,可真正话到手上只有寥寥数语。 + +做事何尝不是如此,光有 `idea` 是不值钱的! diff --git a/handbook/docs/process-service.mdx b/handbook/docs/process-service.mdx new file mode 100644 index 0000000000000000000000000000000000000000..a1bc88ab60b4feb06eff74062278ede9cce94c24 --- /dev/null +++ b/handbook/docs/process-service.mdx @@ -0,0 +1,609 @@ +--- +id: process-service +title: 25. 辅助角色服务 +sidebar_label: 25. 辅助角色服务 (Worker Service) +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +## 25.1 关于辅助角色服务 + +`.NET Core 3.0` 新增了 `Worker Service` 的新项目模板,**可以编写长时间运行的后台服务,并且能轻松的部署成 `Windows服务` 或 `Linux 守护程序`**。 + +目前微软提供了两种方式创建长时间运行的后台服务: + +- **共宿主方式**:中小型项目推荐,无需单独部署 `Windows/Linux` 服务 +- **独立 `Worker Service` 方式**:需独立部署 `Windows/Linux` 服务 + +## 25.2 共宿主方式 + +共宿主方式指的是在现有的 `Web` 或其他应用程中创建类文件并派生自 `BackgroundService` 类即可。**这种方式的典型特点就是和应用共生存周期,应用启动时启动,应用结束停止运行。** + +:::tip 推荐等级 + +推荐中小型项目使用这种方式。 + +::: + +```cs showLineNumbers {8,11,12-17} +using Microsoft.Extensions.Hosting; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace YourPoject.Web.Core; + +public class Worker : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + Console.WriteLine(DateTime.Now); + + // 延迟 1 秒 + await Task.Delay(1000, stoppingToken); + } + } +} +``` + +之后在 `Startup.cs` 中注册即可: + +```cs showLineNumbers +services.AddHostedService(); +``` + +### 25.2.1 最佳实践 + +最好的实践方式是创建独立的类库项目:`YourProject.BackgroundServices`,之后添加 `YourPoject.Application` 和 `YourPoject.Core` 层引用,将所有的 `Worker` 放在该层,同时创建 `Startup.cs` 类进行 `Worker` 统一注册,如: + +```cs showLineNumbers {5,7,9-10} +using Microsoft.Extensions.DependencyInjection; + +namespace YourProject.BackgroundServices; + +public sealed class Startup : AppStartup +{ + public void ConfigureServices(IServiceCollection services) + { + services.AddHostedService(); + services.AddHostedService(); + } +} +``` + +## 25.3 独立 `Worker Service` 方式 + +**独立 `Worker Service` 方式的主要特点就是它是一个独立的项目**,和现有的项目没有直接关联关系,**需要分开独立部署**。 + +:::tip 推荐等级 + +推荐中大型项目使用这种方式,也就是独立部署成 `Windows Service` 或者 `Linux 守护进程`,具有独立生存周期,即使应用故障了也不会影响它的运行。 + +::: + +### 25.3.1 如何创建 `Worker Service` + +通过 `Visual Studio 2019` 提供的 `Worker Service` 可直接创建。如图: + + + +### 25.3.2 创建 `Worker` + +当我们创建好 `Worker Service` 项目时,已经自带了一个 `Worker` 类并继承自 `BackgroundService` 基类。 + +`Worker` 正是我们辅助角色的主要工作类,在这里我们编写我们所有的业务逻辑。通常 `Worker` 默认格式为: + +```cs showLineNumbers {9,18} +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace FurionWorkers +{ + public class Worker : BackgroundService + { + private readonly ILogger _logger; + + public Worker(ILogger logger) + { + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + _logger.LogInformation("Worker running at: {time}", DateTime.Now); + await Task.Delay(1000, stoppingToken); + } + } + } +} +``` + +**当我们创建了 `Worker` 类之后,需要在 `Program.cs` 中进行注册**,如: + +```cs showLineNumbers {17} +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace FurionWorkers +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureServices((hostContext, services) => + { + services.AddHostedService(); + }); + } +} +``` + +:::important 小知识 + +如果使用了 `Furion` 包后可实现自动注册,**请同时确保 `Worker` 声明为 `public` 级别。** + +::: + +### 25.3.3 多个 `Worker` + +`Worker Service` 是支持定义多个 `Worker` 进行协调工作的,每个 `Worker` 是完全独立的工作环境,但可共享同一主进程信息。 + +### 25.3.4 生命周期 + +`Worker Service` 为 `Worker` 提供了三个执行方法,分别代表三个生命周期: + +- `StartAsync`:负责启动 `Worker Service`,如果调用 `StartAsync` 方法的线程被一直阻塞了,那么 `Worker Service` 的启动就一直完成不了 +- `ExecuteAsync`:`Worker Service` 真正实现业务逻辑的地方,这里不能调用阻塞代码!!! +- `StopAsync`:负责结束 `Worker Service`,如果调用 `StopAsync` 方法的线程被一直阻塞了,那么 `Worker Service` 的结束就一直完成不了 + +```cs showLineNumbers {19,25,35} +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace FurionWorkers +{ + public class Worker : BackgroundService + { + private readonly ILogger _logger; + + public Worker(ILogger logger) + { + _logger = logger; + } + + // 启动 + public override Task StartAsync(CancellationToken cancellationToken) + { + return base.StartAsync(cancellationToken); + } + + // 执行逻辑 + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + _logger.LogInformation("Worker running at: {time}", DateTime.Now); + await Task.Delay(1000, stoppingToken); + } + } + + // 停止 + public override Task StopAsync(CancellationToken cancellationToken) + { + return base.StopAsync(cancellationToken); + } + } +} +``` + +### 25.3.5 集成 `Furion` + +`Worker Service` 集成 `Furion` 非常方便,只需要安装 `Furion` 的包即可,并在 `Program.cs` 中调用 `.Inject()` 方法,如: + +```cs showLineNumbers {15} +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace FurionWorkers +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .Inject() + .ConfigureServices((hostContext, services) => + { + // 以下代码可不用编写,Furion 已实现自动注册 Worker; + // services.AddHostedService(); + }); + } +} +``` + +默认情况下,`Inject()` 方法注册了 `日志、缓存、依赖注入、加载配置、自定义 Startup` 功能。 + +:::tip 小知识 + +集成 `Furion` 后会自动扫描 `Worker` 类并实现自动注册。 + +::: + +### 25.3.6 注册服务 + +`Worker Service` 注册服务和 `Web` 略有不同,`Web` 主要在 `Starup.cs` 类中注册,`Worker Service` 在 `Program.cs` 启动类的 `ConfigureServices` 方法中注册,如: + +```cs showLineNumbers {16-28} +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace FurionWorkers +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .Inject() + .ConfigureServices((hostContext, services) => + { + // 注册数据库服务 + services.AddDatabaseAccessor(options => + { + options.AddDb(); + }); + + // 注册远程请求 + services.AddRemoteRequest(); + + // 等等其他服务注册 + }); + } +} +``` + +## 25.4 实现简单定时任务 + +:::tip 小建议 + +**强烈建议使用 【[26.1 调度作业](/docs/job)】 章节内容实现强大的分布式定时任务。** + +::: + +`Furion` 框架为 `BackgroundService` 提供了定时任务的支持。 + +### 25.4.1 间隔执行方式 + +```cs showLineNumbers {7,18,20-29} +namespace WorkerService; + +public class Worker : BackgroundService +{ + private readonly ILogger _logger; + + private const int delay = 1000; + + public Worker(ILogger logger) + { + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + await Task.Delay(delay, stoppingToken); + + var taskFactory = new TaskFactory(System.Threading.Tasks.TaskScheduler.Current); + await taskFactory.StartNew(async () => + { + // 你的业务代码写到这里面 + + _logger.LogInformation("Worker running at: {time}", DateTime.Now); + + await Task.CompletedTask; + + }, stoppingToken); + } + } +} +``` + +### 25.4.2 `Cron` 表达式执行方式 + +:::tip 小知识 + +如需了解 `Cron` 表达式内容,可查阅 【[26.2 Cron 表达式](/docs/cron)】 章节内容。 + +::: + +```cs showLineNumbers {9,14,21,23-31} +using Furion.TimeCrontab; + +namespace WorkerService; + +public class Worker : BackgroundService +{ + private readonly ILogger _logger; + + private readonly Crontab _crontab; + + public Worker(ILogger logger) + { + _logger = logger; + _crontab = Crontab.Parse("* * * * * *", CronStringFormat.WithSeconds); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + await Task.Delay(_crontab.GetSleepTimeSpan(DateTime.Now), stoppingToken); + + var taskFactory = new TaskFactory(System.Threading.Tasks.TaskScheduler.Current); + await taskFactory.StartNew(async () => + { + // 你的业务代码写到这里面 + + _logger.LogInformation("Worker running at: {time}", DateTime.Now); + + await Task.CompletedTask; + }, stoppingToken); + } + } +} +``` + +:::caution `BackgroundService` 方式实现定时任务注意事项 + +通过这种方式只是简单的实现定时任务,但~~**不能对线程和时间进行精准控制,可能存在一些不执行或者重复执行等问题**~~。 + +**所以,强烈建议使用 【[26.1 调度作业](/docs/job)】 章节内容实现强大的分布式定时任务。** + +::: + +### 25.4.3 实现 `串行` 操作 + +默认情况下,定时任务都是采用 `并行` 的方式,也就是不会等待上一次任务完成,如果需要等待上一次任务完成,可以修改为 `串行` 方式: + +```cs showLineNumbers {11,23-24,30,37-38,40} +using Furion.TimeCrontab; + +namespace WorkerService; + +public class Worker : BackgroundService +{ + private readonly ILogger _logger; + + private readonly Crontab _crontab; + + private bool _isLock = false; + + public Worker(ILogger logger) + { + _logger = logger; + _crontab = Crontab.Parse("* * * * * *", CronStringFormat.WithSeconds); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + if (_isLock) goto next; + _isLock = true; + + var taskFactory = new TaskFactory(System.Threading.Tasks.TaskScheduler.Current); + var task = await taskFactory.StartNew(async () => + { + // 模拟耗时操作 + await Task.Delay(2000); + + _logger.LogInformation("Worker running at: {time}", DateTime.Now); + + await Task.CompletedTask; + }, stoppingToken); + + // 等待任务完成 + await task.ContinueWith(task => _isLock = false); + + next: + await Task.Delay(_crontab.GetSleepTimeSpan(DateTime.Now), stoppingToken); + } + } +} +``` + +## 25.5 依赖注入使用 + +`Worker Service` 只为 `Worker` 提供了**单例作用域**的服务注入,如果需要注入瞬时或作用域对象,需手动创建作用域,如: + +```cs showLineNumbers {7-8,18-19,24-25,28-32} +public class Worker : BackgroundService +{ + // 日志对象 + private readonly ILogger _logger; + + // 服务工厂 + private readonly IServiceScopeFactory _scopeFactory; + public Worker(ILogger logger + , IServiceScopeFactory scopeFactory) + { + _logger = logger; + _scopeFactory = scopeFactory; + } + + protected override Task ExecuteAsync(CancellationToken stoppingToken) + { + // 放在循环外可以避免高频下频繁创建作用域和解析服务 + using var scope = _scopeFactory.CreateScope(); + var services = scope.ServiceProvider; + + while (!stoppingToken.IsCancellationRequested) + { + // 放在循环内针对频率不是很高的操作 + // using var scope = _scopeFactory.CreateScope(); + // var services = scope.ServiceProvider; + + // 获取数据库上下文 + var dbContext = Db.GetDbContext(services); + // 获取仓储 + var respository = Db.GetRepository(services); + // 解析其他服务 + var otherService = services.GetService(); + } + + return Task.CompletedTask; + } +} +``` + +## 25.6 如何部署 + +### 25.6.1 共宿主方式 + +共宿主方式方式部署非常简单,只需要部署所在的 `Web` 或其他应用程序项目即可,会自动随着项目启动自动启动。 + +:::important 特别说明 + +如果部署到 `IIS` 中,可能存在 `Worker Service` 被回收的情况,毕竟是和网站同一个宿主。 + +::: + +### 25.6.2 独立 `Worker Service` 方式 + +`Worker Service` 支持部署到 `Windows Service` 中 或 `Linux 守护进程中` + +--- + +#### 部署到 `Windows Service` + +- **第一步**:安装 `Microsoft.Extensions.Hosting.WindowsServices` 拓展包 + +- **第二步**:在 `Program.cs` 中添加 `.UseWindowsService()` + +```cs showLineNumbers {15} +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace FurionWorkers +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseWindowsService() + .Inject() + .ConfigureServices((hostContext, services) => + { + // 以下代码可不用编写,Furion 已实现自动注册 Worker; + // services.AddHostedService(); + }); + } +} +``` + +- **第三步**:发布 `Worker Service`,可通过 `dotnet publish -c Release -o C:\FurionWorker` 命令发布或通过 `Visual Studio 2019` 发布。 + +独立发布不依赖 SDK 方式 `dotnet publish -c release -r win10-x64 --framework net6.0` + +- **第四步**:通过 `sc.exe` 工具来管理并创建 `Windows` 服务,通过 **管理员模式** 并打开控制台,输入: + +```cmd +sc.exe create FurionWorkerServices binPath= C:\FurionWorker\FurionWorker.exe +``` + +注意`=`后面要有一个空格 +创建成功后可通过 `sc.exe query FurionWorkerServices` 查看服务状态。 + +- **第五步** + +启动服务:`sc.exe start FurionWorkerServices`,启动之后就可以在 `Windows` 服务工具中查看了。 + +停止服务:`sc.exe stop NETCoreDemoWorkerService` + +删除服务:`sc.exe delete NETCoreDemoWorkerService` + +:::important 特别提醒 + +以上所有 `sc.exe` 命令必须在 **管理员** 模式下进行。 +sc.exe delete NETCoreDemoWorkerService, 执行删除时候, 把`Windows` 服务工具关闭, 否则, 电脑重启后才会显示删除; + +::: + +--- + +#### 部署到 `Linux 守护程序` + +- **第一步**:安装 `Microsoft.Extensions.Hosting.Systemd` 拓展包 + +- **第二步**:在 `Program.cs` 中添加 `.UseSystemd()` + +```cs showLineNumbers {15} +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace FurionWorkers +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseSystemd() + .Inject() + .ConfigureServices((hostContext, services) => + { + // 以下代码可不用编写,Furion 已实现自动注册 Worker; + // services.AddHostedService(); + }); + } +} +``` + +部署到 `Linux 守护进程` 就是这么简单。 + +## 25.7 关于 `Windows` 部署日志问题 + +默认情况下,使用 `Windows Services` 部署后,日志文件可能会在系统盘的 `System32` 下。 + + + +## 25.8 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/reference.mdx b/handbook/docs/reference.mdx new file mode 100644 index 0000000000000000000000000000000000000000..b133329a31e97a0ff36d242aa5f3ff9b4f4eab4d --- /dev/null +++ b/handbook/docs/reference.mdx @@ -0,0 +1,93 @@ +--- +id: reference +title: 2.7 手动搭建分层 +sidebar_label: 2.7 手动搭建分层 +description: 完全自定义项目分层结构 +--- + +:::tip 推荐使用脚手架 + +`Furion` 官方提供了非常灵活方便的脚手架,可以快速的创建多层架构项目。 + +推荐使用 《[2.6 官方脚手架](template.mdx)》代替本章节功能。 + +::: + +## 2.7.1 推荐分层设计 + +`Furion` 推荐采用多层项目设计架构,每一个项目层的依赖分别是: + +- `YourName.Application`:添加 `YourName.Core` 引用 +- **`YourName.Core`:添加 `Furion` 引用**,**SqlSugar 版本添加 `Furion.Pure`** 🎗 +- `YourName.Database.Migrations`:添加 `YourName.EntityFramework.Core` 引用 +- `YourName.EntityFramework.Core`:添加 `YourName.Core` 引用 +- `YourName.Web.Core`:添加 `YourName.Application`,`YourName.Database.Migrations` 引用 +- **`YourName.Web.Entry`:添加 `YourName.Web.Core` 引用 和 `Microsoft.EntityFrameworkCore.Tools` 包** + +:::important 特别说明 + +如果采用 `EFCore` 以外的 `ORM` 框架,如 `SqlSugar`,那么无需创建 `YourName.Database.Migrations` 和 `YourName.EntityFramework.Core` 层。 + +另外 `YourName.Web.Entry` 无需引用 `Microsoft.EntityFrameworkCore.Tools` 包。 + +::: + +## 2.7.2 集成 `Furion` 功能 + +项目搭建好之后,集成 `Furion` 框架只需要在 `Program.cs` 中 添加 `Inject()` 方法即可: + +- `.NET5` 版本: + +```cs showLineNumbers {18} +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace YourName.Web.Entry +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) + { + return Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.Inject() + .UseStartup(); + }); + } + } +} +``` + +- `.NET6` 版本 + +```cs showLineNumbers {1,3,11} +var builder = WebApplication.CreateBuilder(args).Inject(); + +builder.Services.AddControllers().AddInject(); + +var app = builder.Build(); + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.UseInject(); + +app.MapControllers(); + +app.Run(); +``` + +## 2.7.3 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/saas.mdx b/handbook/docs/saas.mdx new file mode 100644 index 0000000000000000000000000000000000000000..3f1b78d1da5a086e7cdd73ad103cd3b6f7a74a0c --- /dev/null +++ b/handbook/docs/saas.mdx @@ -0,0 +1,725 @@ +--- +id: saas +title: 11. SaaS 多租户 +sidebar_label: 11. SaaS 多租户 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +## 11.1 什么是 `SaaS` + +> SaaS 是 Software-as-a-Service(软件即服务)的简称,随着互联网技术的发展和应用软件的成熟, 在 21 世纪开始兴起的一种完全创新的软件应用模式。它与“on-demand software”,the application service provider(ASP,应用服务提供商),hosted software(托管软件)所具有相似的含义。 +> +> 它是一种通过 Internet 提供软件的模式,厂商将应用软件统一部署在自己的服务器上,客户可以根据自己实际需求,通过互联网向厂商定购所需的应用软件服务,按定购的服务多少和时间长短向厂商支付费用,并通过互联网获得厂商提供的服务。用户不用再购买软件,而改用向提供商租用基于 Web 的软件,来管理企业经营活动,且无需对软件进行维护,服务提供商会全权管理和维护软件,软件厂商在向客户提供互联网应用的同时,也提供软件的离线操作和本地数据存储,让用户随时随地都可以使用其定购的软件和服务。 +> +> 对于许多小型企业来说,SaaS 是采用先进技术的最好途径,它消除了企业购买、构建和维护基础设施和应用程序的需要。 + +## 11.2 什么是多租户 + +多租户技术或称多重租赁技术,简称 `SaaS`,是一种软件架构技术,是实现如何在多用户环境下(此处的多用户一般是面向企业用户)共用相同的系统或程序组件,并且可确保各用户间数据的隔离性。 + +**简单讲:在一台服务器上运行单个应用实例,它为多个租户(客户)提供服务。** 从定义中我们可以理解:多租户是一种架构,目的是为了让多用户环境下使用同一套程序,且保证用户间数据隔离。那么重点就很浅显易懂了,多租户的重点就是同一套程序下实现多用户数据的隔离。 + +## 11.3 实现多租户方案 + +### 11.3.1 独立数据库(基于 `Database` 的方式) + +这是第一种方案,即一个租户一个数据库,这种方案的用户数据隔离级别最高,安全性最好,但成本较高。 + +- **优点:** + 为不同的租户提供独立的数据库,有助于简化数据模型的扩展设计,满足不同租户的独特需求;如果出现故障,恢复数据比较简单。 + +- **缺点:** + 增多了数据库的安装数量,随之带来维护成本和购置成本的增加。 这种方案与传统的一个客户、一套数据、一套部署类似,差别只在于软件统一部署在运营商那里。如果面对的是银行、医院等需要非常高数据隔离级别的租户,可以选择这种模式,提高租用的定价。如果定价较低,产品走低价路线,这种方案一般对运营商来说是无法承受的。 + +### 11.3.2 共享数据库,独立 `Schema` (基于 `Schema` 的方式) + +这是第二种方案,即多个或所有租户共享 `Database`,但是每个租户一个 `Schema`(也可叫做一个 user)。底层库比如是:`SqlServer`、`Oracle` 等,一个数据库下可以有多个 `Schema`。 + +- **优点:** + 为安全性要求较高的租户提供了一定程度的逻辑数据隔离,并不是完全隔离;每个数据库可支持更多的租户数量。 + +- **缺点:** + 如果出现故障,数据恢复比较困难,因为恢复数据库将牵涉到其他租户的数据; 如果需要跨租户统计数据,存在一定困难。 + +### 11.3.3 共享数据表,共享 `Schema` (基于 `TenantId` 的方式) + +共享数据表 这是第三种方案,即租户共享同一个 `Database`、同一个 `Schema`,但在表中增加 **`TenantId`** 多租户的数据字段。这是共享程度最高、隔离级别最低的模式。 即每插入一条数据时都需要有一个客户的标识。这样才能在同一张表中区分出不同客户的数据。 + +- **优点:** + 三种方案比较,第三种方案的维护和购置成本最低,允许每个数据库支持的租户数量最多。 + +- **缺点:** + 隔离级别最低,安全性最低,需要在设计开发时加大对安全的开发量; 数据备份和恢复最困难,需要逐表逐条备份和还原。 + +## 11.4 多租户使用方案 + +`Furion` 框架支持以上三种多租户实现方案,使用简单且容易维护。下面分别使用三种不同方式演示多租户方案用法。 + +:::important 特别说明 + +一旦 `数据库上下文` 类继承了租户任意接口,则自动开始多租户功能支持。 + +::: + +## 11.5 基于 `TenantId` 的方式 + +此方式在中小型企业系统中最为常用,维护成本低,购置成本低。 + +### 11.5.1 创建租户数据库上下文 + +```cs showLineNumbers {6-7} title="Furion.EntityFramework.Core\DbContexts\MultiTenantDbContext.cs" +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore; + +namespace Furion.EntityFramework.Core +{ + [AppDbContext("Sqlite3ConnectionString", DbProvider.Sqlite)] + public class MultiTenantDbContext : AppDbContext + { + public MultiTenantDbContext(DbContextOptions options) : base(options) + { + } + } +} +``` + +:::important 特别注意 + +多租户操作建议单独一个数据库上下文,而且需指定 `MultiTenantDbContextLocator` 数据库上下文定位器。 + +::: + +### 11.5.2 注册多租户数据库上下文 + +```cs showLineNumbers {14} +using Furion.DatabaseAccessor; +using Microsoft.Extensions.DependencyInjection; + +namespace Furion.EntityFramework.Core +{ + [AppStartup(600)] + public sealed class FurEntityFrameworkCoreStartup : AppStartup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddDatabaseAccessor(options => + { + options.AddDbPool(); + options.AddDbPool(); + }); + } + } +} +``` + +### 11.5.3 添加 `Tenant` 种子数据 + +```cs showLineNumbers {8,12-28} title="Furion.EntityFramework.Core\SeedDatas\TenantSeedData.cs" +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; + +namespace Furion.EntityFramework.Core +{ + public class TenantSeedData : IEntitySeedData + { + public IEnumerable HasData(DbContext dbContext, Type dbContextLocator) + { + return new List + { + new Tenant + { + TenantId = Guid.Parse("383AFB88-F519-FFF8-B364-6D563BF3687F"), + Name = "默认租户", + Host = "localhost:44313", + CreatedTime = DateTime.Parse("2020-10-06 20:19:07") + }, + new Tenant + { + TenantId = Guid.Parse("C5798CB6-16D6-0F42-EB56-59C695353BC0"), + Name = "其他租户", + Host = "localhost:5000", + CreatedTime = DateTime.Parse("2020-10-06 20:20:32") + } + }; + } + } +} +``` + +:::note 特别说明 + +该步骤只在 `Code First` 方式执行,`Database First` 无需配置种子数据。 + +::: + +### 11.5.4 根据模型创建 `Tenant` 表 + +```shell showLineNumbers +Add-Migration add_tenant_table -Context MultiTenantDbContext +``` + +```shell showLineNumbers +Update-Database -Context MultiTenantDbContext +``` + +### 11.5.5 实现 `IMultiTenantOnTable` 接口 + +在需要多租户的数据库上下文中实现 `IMultiTenantOnTable` 接口,如: + +```cs showLineNumbers {8,14-17} +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore; +using System; + +namespace Furion.EntityFramework.Core +{ + [AppDbContext("Sqlite3ConnectionString", DbProvider.Sqlite)] + public class FurionDbContext : AppDbContext, IMultiTenantOnTable, IModelBuilderFilter + { + public FurionDbContext(DbContextOptions options) : base(options) + { + } + + public object GetTenantId() + { + return base.Tenant?.TenantId ?? Guid.Empty; + } + } +} +``` + +在 `GetTenantId()` 方法中,首先获取请求的 `主机地址`,然后根据主机地址查询对应的租户 `TenantId`,避免多次查询数据库,这里使用了 `IMemoryCache` 内存缓存。 + +:::important 特别说明 + +`base.Tenant` 只是 `Furion` 框架提供的默认租户实现方法,如果不能满足业务需求,只需要在 `GetTenantId` 里面写你的业务代码即可,也就是无需调用 `base.Tenant`。如: + +```cs showLineNumbers +public object GetTenantId() +{ + // 这里是你获取 TenantId 的逻辑 + return 你的 TenantId; +} +``` + +::: + +### 11.5.6 实现 `IModelBuilderFilter` 接口 + +`IModelBuilderFilter` 接口是全局查询过滤器实现接口,所以我们需要配置实体 `TenantId` 过滤器 + +```cs showLineNumbers {9,20-23} +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using System; + +namespace Furion.EntityFramework.Core +{ + [AppDbContext("Sqlite3ConnectionString", DbProvider.Sqlite)] + public class FurionDbContext : AppDbContext, IMultiTenantOnTable, IModelBuilderFilter + { + public FurionDbContext(DbContextOptions options) : base(options) + { + } + + public object GetTenantId() + { + return base.Tenant?.TenantId ?? Guid.Empty; + } + + public void OnCreating(ModelBuilder modelBuilder, EntityTypeBuilder entityBuilder, DbContext dbContext, Type dbContextLocator) + { + entityBuilder.HasQueryFilter(BuildTenantQueryFilter(entityBuilder, dbContext)); + } + } +} +``` + +### 11.5.7 重写 `SavingChangesEvent` 事件方法 + +通过上面的步骤,我们已经解决了 `查询` 租户过滤功能,但是 `新增` 和 `更新` 还未处理。 + +- `新增` 数据的时候自动设置 `TenantId` 的值 +- `更新` 数据的时候排除 `TenantId` 属性更新 + +实现上面的步骤很简单,只需要重写 `SavingChangesEvent` 事件方法即可。 + +```cs showLineNumbers {26-48} +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using System; +using System.Linq; + +namespace Furion.EntityFramework.Core +{ + [AppDbContext("Sqlite3ConnectionString", DbProvider.Sqlite)] + public class FurionDbContext : AppDbContext, IMultiTenantOnTable, IModelBuilderFilter + { + public FurionDbContext(DbContextOptions options) : base(options) + { + } + + public object GetTenantId() + { + return base.Tenant?.TenantId ?? Guid.Empty; + } + + public void OnCreating(ModelBuilder modelBuilder, EntityTypeBuilder entityBuilder, DbContext dbContext, Type dbContextLocator) + { + entityBuilder.HasQueryFilter(BuildTenantQueryFilter(entityBuilder, dbContext)); + } + + protected override void SavingChangesEvent(DbContextEventData eventData, InterceptionResult result) + { + // 获取当前事件对应上下文 + var dbContext = eventData.Context; + + // 获取所有新增、更新、删除的实体 + var entities = dbContext.ChangeTracker.Entries().Where(u => u.State == EntityState.Added || u.State == EntityState.Modified || u.State == EntityState.Deleted); + + foreach (var entity in entities) + { + switch (entity.State) + { + // 自动设置租户Id + case EntityState.Added: + entity.Property(nameof(Entity.TenantId)).CurrentValue = GetTenantId(); + break; + // 排除租户Id + case EntityState.Modified: + entity.Property(nameof(Entity.TenantId)).IsModified = false; + break; + // 删除处理 + case EntityState.Deleted: + break; + } + } + } + } +} +``` + + + +## 11.6 基于 `Database` 的方式 + +此方式在中大型企业系统中最为常用,一个租户(客户)一个独立的数据库。 + +### 11.6.1 创建租户数据库上下文 + +```cs showLineNumbers {6-7} title="Furion.EntityFramework.Core\DbContexts\MultiTenantDbContext.cs" +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore; + +namespace Furion.EntityFramework.Core +{ + [AppDbContext("Sqlite3ConnectionString", DbProvider.Sqlite)] + public class MultiTenantDbContext : AppDbContext + { + public MultiTenantDbContext(DbContextOptions options) : base(options) + { + } + } +} +``` + +:::important 特别注意 + +多租户操作建议单独一个数据库上下文,而且需指定 `MultiTenantDbContextLocator` 数据库上下文定位器。 + +::: + +### 11.6.2 注册多租户数据库上下文 + +```cs showLineNumbers {13,14} +using Furion.DatabaseAccessor; +using Microsoft.Extensions.DependencyInjection; + +namespace Furion.EntityFramework.Core +{ + [AppStartup(600)] + public sealed class FurEntityFrameworkCoreStartup : AppStartup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddDatabaseAccessor(options => + { + options.AddDb(); + options.AddDbPool(); + }); + } + } +} +``` + +:::caution 特别注意 + +需要 `Database` 多租户方案的数据库上下文需要采用 `AddDb` 注册,而不是 `AddDbPool`。原因是 `AddDbPool` 方式注册后续不支持 `OnConfiguring` 重写!!! + +::: + +### 11.6.3 添加 `Tenant` 种子数据 + +```cs showLineNumbers {8,12-30} title="Furion.EntityFramework.Core\SeedDatas\TenantSeedData.cs" +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; + +namespace Furion.EntityFramework.Core +{ + public class TenantSeedData : IEntitySeedData + { + public IEnumerable HasData(DbContext dbContext, Type dbContextLocator) + { + return new List + { + new Tenant + { + TenantId = Guid.Parse("383AFB88-F519-FFF8-B364-6D563BF3687F"), + Name = "默认租户", + Host = "localhost:44313", + CreatedTime = DateTime.Parse("2020-10-06 20:19:07"), + ConnectionString = "Data Source=./Furion.db" // 配置连接字符串 + }, + new Tenant + { + TenantId = Guid.Parse("C5798CB6-16D6-0F42-EB56-59C695353BC0"), + Name = "其他租户", + Host = "localhost:5000", + CreatedTime = DateTime.Parse("2020-10-06 20:20:32"), + ConnectionString = "Data Source=./Fur2.db" // 配置连接字符串 + } + }; + } + } +} +``` + +:::note 特别说明 + +该步骤只在 `Code First` 方式执行,`Database First` 无需配置种子数据。 + +::: + +### 11.6.4 根据模型创建 `Tenant` 表 + +```shell showLineNumbers +Add-Migration add_tenant_table -Context MultiTenantDbContext +``` + +```shell showLineNumbers +Update-Database -Context MultiTenantDbContext +``` + +### 11.6.5 实现 `IMultiTenantOnDatabase` 接口 + +在需要多租户的数据库上下文中实现 `IMultiTenantOnDatabase` 接口,如: + +```cs showLineNumbers {6,13-16} +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore; + +namespace Furion.EntityFramework.Core +{ + // 这里可不配置 + public class FurionDbContext : AppDbContext, IMultiTenantOnDatabase + { + public FurionDbContext(DbContextOptions options) : base(options) + { + } + + public string GetDatabaseConnectionString() + { + return base.Tenant?.ConnectionString??"默认链接字符串"; + } + } +} +``` + +:::important 特别说明 + +`base.Tenant` 只是 `Furion` 框架提供的默认租户实现方法,如果不能满足业务需求,只需要在 `GetDatabaseConnectionString` 里面写你的业务代码即可,也就是无需调用 `base.Tenant`。如: + +```cs showLineNumbers +public string GetDatabaseConnectionString() +{ + // 这里是你获取 DatabaseConnecionString 的逻辑 + return 你的 连接字符串; +} +``` + +::: + +### 11.6.6 重写 `OnConfiguring` 方法 + +在需要多租户的数据库上下文中重写 `OnConfiguring` 方法并配置连接字符串: + +```cs showLineNumbers {12-17} +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore; + +namespace Furion.EntityFramework.Core +{ + public class FurionDbContext : AppDbContext, IMultiTenantOnDatabase + { + public FurionDbContext(DbContextOptions options) : base(options) + { + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseSqlite(GetDatabaseConnectionString()); + + base.OnConfiguring(optionsBuilder); + } + + public string GetDatabaseConnectionString() + { + return base.Tenant?.ConnectionString??"默认链接字符串"; + } + } +} +``` + + + +:::caution 特别注意 + +基于 `Database` 方式做 `Code First` 的时候,需要手动指定迁移程序名称,如: + +```cs showLineNumbers +optionsBuilder.UseSqlite(GetDatabaseConnectionString(), options=> +{ + options.MigrationsAssembly("My.Migrations"); +}); +``` + +::: + +## 11.7 基于 `Schema` 的方式 + +此方式在中小型企业系统中也不少见,一个租户(客户)共享数据库且不同 `Schema`。 + +### 11.7.1 创建租户数据库上下文 + +```cs showLineNumbers {6-7} title="Furion.EntityFramework.Core\DbContexts\MultiTenantDbContext.cs" +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore; + +namespace Furion.EntityFramework.Core +{ + [AppDbContext("Sqlite3ConnectionString", DbProvider.Sqlite)] + public class MultiTenantDbContext : AppDbContext + { + public MultiTenantDbContext(DbContextOptions options) : base(options) + { + } + } +} +``` + +:::important 特别注意 + +多租户操作建议单独一个数据库上下文,而且需指定 `MultiTenantDbContextLocator` 数据库上下文定位器。 + +::: + +### 11.7.2 注册多租户数据库上下文 + +```cs showLineNumbers {14} +using Furion.DatabaseAccessor; +using Microsoft.Extensions.DependencyInjection; + +namespace Furion.EntityFramework.Core +{ + [AppStartup(600)] + public sealed class FurEntityFrameworkCoreStartup : AppStartup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddDatabaseAccessor(options => + { + options.AddDbPool(); + options.AddDbPool(); + }); + } + } +} +``` + +### 11.7.3 添加 `Tenant` 种子数据 + +```cs showLineNumbers {8,12-30} title="Furion.EntityFramework.Core\SeedDatas\TenantSeedData.cs" +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; + +namespace Furion.EntityFramework.Core +{ + public class TenantSeedData : IEntitySeedData + { + public IEnumerable HasData(DbContext dbContext, Type dbContextLocator) + { + return new List + { + new Tenant + { + TenantId = Guid.Parse("383AFB88-F519-FFF8-B364-6D563BF3687F"), + Name = "默认租户", + Host = "localhost:44313", + CreatedTime = DateTime.Parse("2020-10-06 20:19:07"), + Schema = "dbo" // Schema + }, + new Tenant + { + TenantId = Guid.Parse("C5798CB6-16D6-0F42-EB56-59C695353BC0"), + Name = "其他租户", + Host = "localhost:5000", + CreatedTime = DateTime.Parse("2020-10-06 20:20:32"), + Schema = "furion" // Schema + } + }; + } + } +} +``` + +:::note 特别说明 + +该步骤只在 `Code First` 方式执行,`Database First` 无需配置种子数据。 + +::: + +### 11.7.4 根据模型创建 `Tenant` 表 + +```shell showLineNumbers +Add-Migration add_tenant_table -Context MultiTenantDbContext +``` + +```shell showLineNumbers +Update-Database -Context MultiTenantDbContext +``` + +### 11.7.5 实现 `IMultiTenantOnSchema` 接口 + +在需要多租户的数据库上下文中实现 `IMultiTenantOnSchema` 接口,如: + +```cs showLineNumbers {6,12-15} +using Furion.DatabaseAccessor; +using Microsoft.EntityFrameworkCore; + +namespace Furion.EntityFramework.Core +{ + [AppDbContext("Sqlite3ConnectionString", DbProvider.Sqlite)] + public class FurionDbContext : AppDbContext, IMultiTenantOnSchema + { + public FurionDbContext(DbContextOptions options) : base(options) + { + } + + public string GetSchemaName() + { + return base.Tenant?.Schema??"dbo"; + } + } +} +``` + +:::important 特别说明 + +`base.Tenant` 只是 `Furion` 框架提供的默认租户实现方法,如果不能满足业务需求,只需要在 `GetSchemaName` 里面写你的业务代码即可,也就是无需调用 `base.Tenant`。如: + +```cs showLineNumbers +public string GetSchemaName() +{ + // 这里是你获取 Schema 的逻辑 + return 你的 Schema; +} +``` + +::: + +### 11.7.6 关于 `Code First 数据迁移` + +基于 `Schema` 方式比较特别,生成数据迁移的时候没办法获取租户信息,所以建议**分开多次迁移**,如: + +```cs showLineNumbers +public string GetSchemaName() +{ + return base.Tenant?.Schema?? "租户一Schema"; +} +``` + +```cs showLineNumbers +public string GetSchemaName() +{ + return base.Tenant?.Schema?? "租户二Schema"; +} +``` + +这样就可以在迁移的时候生成多次迁移了。 + +## 11.8 自定义 `Tenant` 类型 + +默认情况下,`Furion` 框架提供了内置的 `Tenant` 类型,方便大家快速实现 `SaaS` 多租户功能,如果需要自定义多租户 `Tenant` 类型,只需要启用以下配置即可: + +### 11.8.1 启动自定义多租户类型配置 + +```cs showLineNumbers {3} +services.AddDatabaseAccessor(options => +{ + options.CustomizeMultiTenants(); // 启用自定义多租户类型,有一个默认参数,配置多租户表字段名 + options.AddDbPool(); +}); +``` + +### 11.8.2 自定义租户类 + +```cs showLineNumbers {6} +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Furion.Core +{ + public class MyTenant : IEntity + { + [Key] + public Guid TenantId { get; set; } + + public string Name { get; set; } + + public string Host { get; set; } + } +} +``` + +如果需要查询该租户信息,可通过以下代码获取,如: + +```cs showLineNumbers +var tenantDbContext = Db.GetDbContext(); +var myTenant = tenantDbContext.Set(); +``` + +## 11.9 刷新租户缓存 + +`Furion` 框架会在租户上下文第一次查询时候将租户表缓存起来,避免频发查询数据库,如果更新了租户表,则需要手动刷新租户信息,如: + +```cs showLineNumbers +using Furion.DatabaseAccessor.Extensions; + +// 在更新租户信息后调用 +_repository.Context.RefreshTenantCache(); +``` + +## 11.10 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 diff --git a/handbook/docs/sensitive-detection.mdx b/handbook/docs/sensitive-detection.mdx new file mode 100644 index 0000000000000000000000000000000000000000..ad5981c2d59e9983bfdef8b33d9955d33d2520c4 --- /dev/null +++ b/handbook/docs/sensitive-detection.mdx @@ -0,0 +1,243 @@ +--- +id: sensitive-detection +title: 30. 脱敏处理 +sidebar_label: 30. 脱敏处理 +--- + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **问题修复** + + -  修复 脱敏处理如果字典存在重复词导致异常问题 4.8.8.4 ⏱️2023.04.23 [#I6Y19K](https://gitee.com/dotnetchina/Furion/issues/I6Y19K) + +- **其他更改** + + -  调整 脱敏处理 `sensitive-words.txt` 嵌入文件支持 `UTF8 BOM` 编码,感谢 [@man119](https://gitee.com/man119) 4.8.6.7 ⏱️2023.02.18 [#I6G1JN](https://gitee.com/dotnetchina/Furion/issues/I6G1JN) + +
+
+
+ +:::important 版本说明 + +以下内容仅限 `Furion 2.4.4 +` 版本使用。 + +::: + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +## 30.1 关于脱敏 + +引用百度百科: + +> 数据脱敏是指对某些敏感信息通过脱敏规则进行数据的变形,实现敏感隐私数据的可靠保护。在涉及客户安全数据或者一些商业性敏感数据的情况下,在不违反系统规则条件下,对真实数据进行改造并提供测试使用,如身份证号、手机号、卡号、客户号等个人信息都需要进行数据脱敏。数据安全技术之一,数据库安全技术主要包括:数据库漏扫、数据库加密、数据库防火墙、数据脱敏、数据库安全审计系统。 + +在 `Furion` 系统中,`脱敏处理` 指的是对不符合系统合法词汇检测验证。 + +## 30.2 如何使用 + +`Furion` 框架内置了一套默认的脱敏词汇脱敏处理机制,并且提供自定义操作。 + +### 30.2.1 注册 `脱敏词汇检测` 服务 + +```cs showLineNumbers {3} +public void ConfigureServices(IServiceCollection services) +{ + services.AddSensitiveDetection(); +} +``` + +### 30.2.2 创建 `sensitive-words.txt` 文件 + +在 `Web` 启动层项目中创建 `sensitive-words.txt` 文件,**确保采用 `UTF-8` 编码格式(`Furion 4.8.6.7+` 支持 `UTF8 BOM` 编码格式) 且设置为嵌入式资源!** + +`sensitive-words.txt` 内容格式为每一行标识一个脱敏词汇: + +```showLineNumbers +坏人 +无语 +滚开 +八嘎 +``` + +:::tip `Furion 3.8.9+` 版本 + +在 `Furion 3.8.9+` 版本支持 `|` 分隔符进行分割,也同时支持 `换行` 和 `|` 混用,如: + +```showLineNumbers +坏人|无语|滚开 +八嘎 +``` + +**推荐使用 `|` 方式,可以节省词库占用存储空间。** + +::: + +接下来设置为嵌入式资源: + + + +### 30.2.3 使用脱敏检测 + +- **实现数据验证脱敏检测 `[SensitiveDetection]`** + +`Furion` 框架提供了 `[SensitiveDetection]` 验证特性,可以对参数、属性进行脱敏验证,用法和 `[DataValidation]` 一致,如: + +```cs showLineNumbers {4,9} +// 在属性中使用 +public class Content +{ + [SensitiveDetection] + public string Text { get; set; } +} + +// 在 动态API/Controller 中使用 +public void Test([SensitiveDetection] string text) +{ + +} +``` + +- **通过 `ISensitiveDetectionProvider` 服务使用** + +`Furion` 框架也提供了 `ISensitiveDetectionProvider` 服务进行手动脱敏验证处理,如: + +```cs showLineNumbers {4,15,25,35} +public class FurionService +{ + private readonly ISensitiveDetectionProvider _sensitiveDetectionProvider; + public FurionService(ISensitiveDetectionProvider sensitiveDetectionProvider) + { + _sensitiveDetectionProvider = sensitiveDetectionProvider; + } + + /// + /// 获取所有脱敏词汇 + /// + /// + public async Task> GetWordsAsync() + { + return await _sensitiveDetectionProvider.GetWordsAsync(); + } + + /// + /// 判断是否是正常的词汇 + /// + /// + /// + public async Task VaildedAsync(string text) + { + return await _sensitiveDetectionProvider.VaildedAsync(text); + } + + /// + /// 替换非正常词汇 + /// + /// + /// + public async Task ReplaceAsync(string text) + { + return await _sensitiveDetectionProvider.ReplaceAsync(text, '*'); + } +} +``` + +### 30.2.4 脱敏词汇替换 + +`Furion` 框架也提供了替换脱敏词汇的特性支持,如: + +```cs showLineNumbers {4} +// 在属性中使用 +public class Content +{ + [SensitiveDetection('*')] + public string Text { get; set; } +} +``` + +:::caution 特别注意 + +在 `Furion 3.8.8+` 版本后支持方法特性直接替换 + +```cs showLineNumbers {1} +public void Test([SensitiveDetection('*')] string text) +{ +} +``` + +::: + +## 30.3 自定义脱敏词汇处理 + +`Furion` 框架除了内置了一套默认的 `脱敏处理` 程序,也支持自定义脱敏处理程序。 + +### 30.3.1 自定义 `ISensitiveDetectionProvider` 实现 + +```cs showLineNumbers {4,15,25,36} +/// +/// 自定义脱敏词汇检测器 +/// +public class YourSensitiveDetectionProvider : ISensitiveDetectionProvider +{ + // 支持构造函数注入 + public YourSensitiveDetectionProvider() + { + } + + /// + /// 返回所有脱敏词汇 + /// + /// + public async Task> GetWordsAsync() + { + // 这里写你脱敏词汇数据的来源(如从数据库读取),建议做好缓存操作 + } + + /// + /// 判断脱敏词汇是否有效 + /// + /// + /// + public async Task VaildedAsync(string text) + { + // 这里写你如何判断是正常的字符,返回 true 正常,返回 false 表示是个脱敏词汇 + } + + /// + /// 替换脱敏词汇 + /// + /// + /// + /// + public async Task ReplaceAsync(string text, char transfer = '*') + { + // 这里写你替换非正常字符为指定字符 + } +} +``` + +### 30.3.2 注册自定义脱敏提供器 + +```cs showLineNumbers {3} +public void ConfigureServices(IServiceCollection services) +{ + services.AddSensitiveDetection(); +} +``` + +之后系统将自动采用自定义的方式进行脱敏处理。 + +## 30.4 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: + +--- diff --git a/handbook/docs/serverun.mdx b/handbook/docs/serverun.mdx new file mode 100644 index 0000000000000000000000000000000000000000..12d5aee519a7ab5b8bf8f25958913317c8e235c6 --- /dev/null +++ b/handbook/docs/serverun.mdx @@ -0,0 +1,762 @@ +--- +id: serverun +title: 2.1 入门指南 +sidebar_label: 2.1 入门指南 +description: 学习 Furion 如何快速入门 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **新特性** + + -  新增 `Serve.IdleHost` 支持返回 `http` 和 `https` 协议 `Web` 地址(端口) 4.8.8 ⏱️2023.04.13 [fdf7885](https://gitee.com/dotnetchina/Furion/commit/fdf7885f282057599be6b1b3833373dd153db42a) + -  新增 `Serve.GetIdleHost([host])` 静态方法,可获取一个指定主机的 `Web` 地址(端口) 4.8.7.43 ⏱️2023.04.12 [fdf788](https://gitee.com/dotnetchina/Furion/commit/fdf7885f282057599be6b1b3833373dd153db42a) + -  新增 `Serve.IdleHost` 静态属性,可获取一个随机空闲 `Web` 主机地址(端口) 4.8.7.29 ⏱️2023.03.30 [e425063](https://gitee.com/dotnetchina/Furion/commit/e4250634246af612f052ec935416ee050b44d22e) + -  新增 `WinForm/WPF` 静态方法 `Serve.RunNative()` 可配置是否启用 `Web` 主机功能 4.8.7.26 ⏱️2023.03.29 [#I6R97L](https://gitee.com/dotnetchina/Furion/issues/I6R97L) + -  新增 **`WinForm/WPF` 快速注册静态方法:`Serve.RunNative()`** 4.8.7.23 ⏱️2023.03.27 [53d51c3](https://gitee.com/dotnetchina/Furion/commit/53d51c3645f4066f5d68d4726d78e389fd544560) + +- **其他更改** + + -  调整 `Serve.Run()` 迷你主机默认添加 `JSON` 中文乱码处理 4.8.6.3 ⏱️2023.02.15 [86b5f9f](https://gitee.com/dotnetchina/Furion/commit/86b5f9f7c2ace503312bf879dccd7add12bd93c4) + +- **问题修复** + + -  修复 `Serve.IdleHost` 获取随机端口的本地地址带 `$` 符号问题 4.8.8 ⏱️2023.04.13 [ed6f292](https://gitee.com/dotnetchina/Furion/commit/ed6f29263607f58fe0eafdf21dadfc33987309e1) + -  修复 `Serve.Run()` 因 [#I6G02W](https://gitee.com/dotnetchina/Furion/issues/I6G02W) 更改导致不配置端口时出现异常无法启动问题 4.8.6.10 ⏱️2023.02.20 [#I6G6AR](https://gitee.com/dotnetchina/Furion/issues/I6G6AR) + -  修复 `Serve.Run(urls: "端口")` 设置端口在 `.NET6/7` 下发布后始终是 `80` 端口问题 4.8.6.6 ⏱️2023.02.18 [#I6G02W](https://gitee.com/dotnetchina/Furion/issues/I6G02W) + +
+
+
+ +:::important 版本说明 + +以下内容仅限 `Furion 3.6.3 +` 版本使用。 + +::: + +:::tip 推荐使用脚手架 + +`Furion` 官方提供了非常灵活方便的脚手架,可以快速的创建多层架构项目。 + +推荐使用 《[2.6 官方脚手架](template.mdx)》代替本章节功能。 + +::: + +## 2.1.1 历史背景 + +相信从 `ASP.NET 5` 升级至 `ASP.NET 6` 的开发者都经历过这样变更: + +- 在 `ASP.NET 5` 中,我们这样创建 `Web 主机`: + +```cs showLineNumbers {1} title="Program.cs" +Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); +``` + +- 在 `ASP.NET 6` 中, 我们这样创建 `Web 主机`: + +```cs showLineNumbers {1} title="Program.cs" +var builder = WebApplication.CreateBuilder(args); +var app = builder.Build(); +``` + +试问,`ASP.NET 7`,`ASP.NET 8` ... `ASP.NET N` 呢?会不会每一个版本都有不同的创建方式,那后续项目如何无缝升级? + +**所以,为了保证一致的代码体验和后续无缝升级,推出了 `Serve.Run()`,即使未来创建方式变了,也不用担心,交给框架即可。** + +## 2.1.2 创建 `控制台` 项目 + +- 打开 `Visual Studio 2022` 并创建 `控制台` 项目 + + + +- 配置项目名称 + + + +- 选择 `.NET6` + + + +:::tip 使用命令行方式 + +```bash showLineNumbers +// 创建控制台项目 +dotnet new console -n HelloFurion +``` + +::: + +## 2.1.3 添加 `Furion` 依赖包 + + + +:::tip 使用命令行方式 + +```bash showLineNumbers +// 进入创建的目录 +cd HelloFurion +// 添加包 +dotnet add package Furion +``` + +::: + +## 2.1.4 一句话搞定 + +修改 `Program.cs` 代码为: + +```cs showLineNumbers title="Program.cs" +Serve.Run(); +``` + +对,你没看错,`Furion` 已经配置好了! + +:::tip 功能说明 + +`Serve.Run()` 已经包含了基本的 `WebAPI` 功能,包含动态 `WebAPI`,`跨域` 等等,如需完全自定义配置可使用 `Serve.Run(RunOptions.Default)`,之后 `AppStartup` 派生类自行配置。 + +::: + +## 2.1.5 启动浏览器 + +启动浏览器查看效果 + + + + + +是不是超级超级简单!!! + +## 2.1.6 编写第一个 `API` + +```cs showLineNumbers title="Program.cs" {3,6} +Serve.Run(); + +[DynamicApiController] +public class HelloService +{ + public string Say() + { + return "Hello, Furion"; + } +} +``` + +启动浏览器查看效果 + + + +## 2.1.7 `Serve.Run()` 更多配置 + +### 2.1.7.1 配置默认启动地址/端口 + +默认情况下,创建的 `Web` 主机端口为 `5000/5001` 端口,如需自定义配置,可通过第一个参数配置: + +```cs showLineNumbers +Serve.Run("https://localhost:8080"); + +// 或 +Serve.Run(RunOptions.Default, "https://localhost:8080"); +``` + +同时也支持 `dotnet run` 和 `dotnet watch run` 指定: + +```bash showLineNumbers +dotnet run --urls https://localhost:8080 + +# watch 方式 +dotnet watch run --urls https://localhost:8080 +``` + +也可以通过 `ConfigureBuilder` 方式配置: + +```cs showLineNumbers {1,7} +// .NET6+ +Serve.Run(RunOptions.Default.ConfigureBuilder(builder => +{ + builder.WebHost.UseUrls("https://localhost:8080"); // 也可以通过 builder.Configuration 读取 urls 配置 +})); + +// .NET5 +Serve.Run(RunOptions.ConfigureWebDefaults(webHostBuilder => +{ + return webHostBuilder.UseUrls("https://localhost:8080"); +})); +``` + +:::important 关于 `localhost` 和多端口 + +建议使用 `*` 代替 `localhost`,这样可以自适应主机地址,多个端口使用 `;` 分割,结尾无需 `;` + +::: + +:::tip 通过 `json` 方式配置 + +如需通过配置文件配置端口,需两个该步骤: + +1. 编辑控制台启动项目 `.csproj` 文件,修改 `Project` 节点为: + +```xml showLineNumbers + +``` + +也就是在原来的 `Sdk` 中添加 `.Web` 即可。 + +2. 在控制台启动项目中添加 `Properties` 文件夹并在此文件夹中创建 `launchSettings.json` 文件,同时写入以下内容: + +```json showLineNumbers {4,8} title="launchSettings.json" +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "启动项目名称": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "", + "applicationUrl": "https://localhost:8080;http://localhost:8081", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} +``` + +除了 `launchsettings.json` 的方式,还可以在 `appsettings.json` 简单配置 + +--- + +```json showLineNumbers {2} title="appsettings.json" +{ + "Urls": "http://localhost:8081" +} +``` + +::: + +- 还可以获取随机空闲端口启动 + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.7.29 +` 版本使用。 + +::: + +通过 `Serve.IdleHost` 静态属性可以获取随机的 `Web` 主机地址(端口),如:`http://localhost:随机端口`。 + +```cs showLineNumbers {1,4-7,10-13} +Serve.Run(urls: Serve.IdleHost.Urls); + +// 完整返回值 +var idleHost = Serve.IdleHost; +var port = idleHost.Port; // => 44322 +var urls = idleHost.Urls; // => http://localhost:44322 +var ssl_urls = idleHost.SSL_Urls; // => https://localhost:44322,Furion 4.8.7.43+ 支持 + +// 指定主机名,Furion 4.8.7.43+ 支持 +var idleHost = Serve.GetIdleHost("furion.baiqian.ltd"); // 不传参数默认 localhost +var port = idleHost.Port; // => 44322 +var urls = idleHost.Urls; // => http://furion.baiqian.ltd:44322 +var ssl_urls = idleHost.SSL_Urls; // => http://furion.baiqian.ltd:44322 +``` + +### 2.1.7.2 便捷服务注册 + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.0 +` 版本使用。 + +::: + +```cs showLineNumbers {1,7,13,19} +Serve.Run(additional: services => +{ + services.AddRemoteRequest(); +}); + +// 通用泛型主机方式 +Serve.RunGeneric(additional: services => +{ + services.AddRemoteRequest(); +}); + +// 还可以省去 additional +Serve.Run(services => +{ + services.AddRemoteRequest(); +}); + +// 通用泛型主机方式 +Serve.RunGeneric(services => +{ + services.AddRemoteRequest(); +}); +``` + +### 2.1.7.3 自定义配置 + +传入 `RunOptions` 对象相当于自由定义和控制,也就是除了默认集成了 `Furion` 以外,没有注册任何功能。 + +- 仅集成 `Furion` 的默认配置 + +```cs showLineNumbers +Serve.Run(RunOptions.Default); +``` + +- 配置更多服务/中间件 + +```cs showLineNumbers {1,2,7} +Serve.Run(RunOptions.Default + .ConfigureBuilder(builder => + { + builder.Services.AddControllers() + .AddInject(); + }) + .Configure(app => + { + app.UseRouting(); + app.UseInject(string.Empty); + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + })); +``` + +- `WebComponent` 方式 + +:::important 版本说明 + +以下内容仅限 `Furion 4.3.5 +` 版本使用。 + +::: + +```cs showLineNumbers {2,4,6} +Serve.Run(RunOptions.Default + .AddWebComponent()); + +public class XXXWebComponent : IWebComponent +{ + public void Load(WebApplicationBuilder builder, ComponentContext componentContext) + { + // .... + } +} +``` + +### 2.1.7.4 `Serve.Run` 和 `Startup` 最佳组合 + +默认情况下 `Serve.Run()` 内置了 `跨域`,`控制器`,`路由`,`规范化结果`、`静态文件` 服务/中间件。适合快速开始项目和编写测试代码。 + +**但不能对这些已注册服务/中间件进行自定义配置**,这时只需要配置 `RunOptions` 属性/方法即可,如: + +```cs showLineNumbers title="Program.cs" +Serve.Run(RunOptions.Default + .ConfigureBuilder(...) + .Configure(..)); +``` + +**但把所有服务/中间件都放在 `Program.cs` 中好吗?**答案是不好的,因为会导致后续迁移代码维护代码造成了一些困扰。 + +所以 `Furion` 推荐下面更加灵活且易维护的方式,`Program.cs` 只需一句话即可: + +:::tip 推荐使用组件启动 + +`Furion 3.7.3+` 官方提供了非常灵活方便的组件化启动配置服务。 + +推荐使用 《[3.1 组件化启动](./component)》代替 `AppStartup` 方式功能。 + +::: + +```cs showLineNumbers title="Program.cs" +Serve.Run(RunOptions.Default); +``` + +然后添加自定义 `Startup.cs` 文件,代码如下: + +```cs showLineNumbers {8,10,15} title="Startup.cs" +using Furion; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; + +namespace HelloFurion; + +public class Startup : AppStartup +{ + public void ConfigureServices(IServiceCollection services) + { + // .... + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + // .... + } +} +``` + +:::tip 小提示 + +正常情况下,自定义 `Startup.cs` 文件应该放在独立的 `YourProject.Web.Core` 层或其他层。 + +::: + +### 2.1.7.5 更多配置 + +如配置 `WebHost`... + +```cs showLineNumbers +Serve.Run(RunOptions.Default + .ConfigureBuilder(builder => { + builder.WebHost..... + })); +``` + +## 2.1.8 支持 `Furion` 所有功能 + +`Serve.Run()` 看似非常简单,实则非常灵活,而且支持 `Furion` 和 `.NET` 所有功能。 + +### 2.1.8.1 添加 `appsettings.json` + +创建 `appsettings.json` 文件,并设置 `属性` 为 `如果较新则复制` 和 `内容`(生成操作) + +```json showLineNumbers +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "Microsoft.EntityFrameworkCore": "Information" + } + }, + "AllowedHosts": "*" +} +``` + +在代码中读取配置: + +```cs showLineNumbers {10} +using Furion; + +Serve.Run(); + +[DynamicApiController] +public class HelloService +{ + public string Say() + { + return "Hello, Furion " + App.Configuration["Logging:LogLevel:Default"]; + } +} +``` + + + +### 2.1.8.2 添加自定义 `Startup` + +在 `Furion` 中可以派生自 `AppStartup` 可以实现更多配置,如: + +```cs showLineNumbers title="Program.cs" +Serve.Run(); +``` + +:::important 特别注意 + +如果您想自己配置 `Web` 项目服务,可通过 `Serve.Run(RunOptions.Default);` 方式,因为 `Serve.Run()` 已经包含了常用的 `Web` 可能会提示重复注册错误。 + +::: + +```cs showLineNumbers {8,10,12,15,17} title="MyStartup.cs" +using Furion; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; + +namespace HelloFurion; + +public class MyStartup : AppStartup +{ + public void ConfigureServices(IServiceCollection services) + { + Console.WriteLine("调用服务注册啦~~"); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + Console.WriteLine("调用中间件注册啦"); + } +} +``` + + + +### 2.1.8.3 将控制台项目变成 `Web` 项目 + +只需要编辑 `.csproj` 文件,将第一行 `Project` 节点的 `Sdk` + +```xml showLineNumbers + +``` + +修改为: + +```xml showLineNumbers + +``` + +即可完成转换,实际上只是追加了 `.Web`。 + +### 2.1.8.4 添加 `args` 启动参数 + +:::important 版本说明 + +以下内容仅限 `Furion 4.2.4 +` 版本使用。 + +::: + +```cs showLineNumbers {1,3,5} +Serve.Run(args: args); + +Serve.Run(RunOptions.Default.WithArgs(args)); + +Serve.Run(RunOptions.Main(args)); +``` + +### 2.1.8.5 还没看够? + +是不是非常强大啊,`Serve.Run()` 虽然简单,但是 100% 支持 `Furion` 和 `.NET` 所有功能。尽情去体验吧! + +## 2.1.9 `RunOptions`,`LegacyRunOptions` 和 `GenericRunOptions` + +`Serve.Run` 提供了 `RunOptions`,`LegacyRunOptions` 和 `GenericRunOptions` 重载参数类型,他们的主要区别: + +- `RunOptions`:使用的是 `WebApplication` 方式,**创建 `Web` 主机优先推荐方式** +- `LegacyRunOptions`:使用的是 `Host` 方式,但默认配置了 `Web` 主机 +- `GenericRunOptions`:使用的是 `Host` 方式,通用类型主机,可用于 `WorkerService` + +`LegacyRunOptions` 配置例子: + +```cs showLineNumbers {5-6,15-16} +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; + +Serve.Run(LegacyRunOptions.Default + // 配置 Web 主机 + .ConfigureWebDefaults(builder => builder.ConfigureServices(services => + { + // ... + }) + .Configure(app => + { + // ... + }); + }) + // 配置 Host 主机 + .ConfigureBuilder(builder => builder....)); +``` + +`GenericRunOptions` 配置例子: + +```cs showLineNumbers {2-3} +Serve.Run(GenericRunOptions.Default + // 配置 Host 主机 + .ConfigureBuilder(hostBuilder => hostBuilder....); +``` + +更多发布命令说明可查阅微软官方文档 [https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-publish](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-publish) + +## 2.1.10 在 `WinForm/WPF/Console` 中使用 + +在 `WinForm` 或 `WPF` 或 `Console` 中使用,请确保 `Serve.RunNative()` 在应用程序之前初始化: + +### 2.1.10.1 `WinForm` 初始化 + +```cs showLineNumbers {3,8} +namespace WinFormsApp2; + +internal static class Program +{ + [STAThread] + static void Main() + { + Serve.RunNative(); // Furion 4.8.7.26+ 支持 + + ApplicationConfiguration.Initialize(); + Application.Run(new Form1()); + } +} +``` + +:::tip `Serve.RunNative` 端口占用问题 + +默认情况下,`Serve.RunNative()` 会启动一个迷你小型 `Web` 主机,默认端口为 `5000`,如无需 `Web` 功能可配置 `includeWeb` 关闭,如: + +```cs showLineNumbers {2,5,8,11} +// 关闭迷你 Web 主机功能 +Serve.RunNative(includeWeb: false); + +// 或更换迷你 Web 主机端口 +Serve.RunNative(urls: "http://localhost:8080"); + +// 还可以完全自定义配置 Web 主机 +Serve.RunNative(RunOptions.Default); + +// 还可以随机获取一个空闲主机端口(推荐) +Serve.RunNative(urls: Serve.IdleHost.Urls); +``` + +::: + +### 2.1.10.2 `WPF` 初始化 + +```cs showLineNumbers {3,7} +namespace WpfApp1; + +public partial class App : Application +{ + public App() + { + Serve.RunNative(); // Furion 4.8.7.23+ + } +} +``` + +:::tip `Serve.RunNative` 端口占用问题 + +默认情况下,`Serve.RunNative()` 会启动一个迷你小型 `Web` 主机,默认端口为 `5000`,如无需 `Web` 功能可配置 `includeWeb` 关闭,如: + +```cs showLineNumbers {2,5,8,11} +// 关闭迷你 Web 主机功能 +Serve.RunNative(includeWeb: false); + +// 或更换迷你 Web 主机端口 +Serve.RunNative(urls: "http://localhost:8080"); + +// 还可以完全自定义配置 Web 主机 +Serve.RunNative(RunOptions.Default); + +// 还可以随机获取一个空闲主机端口(推荐) +Serve.RunNative(urls: Serve.IdleHost.Urls); +``` + +::: + +### 2.1.10.3 `Console` 初始化 + +```cs showLineNumbers {2} +Console.WriteLine("Hello World"); +Serve.RunNative(); // Furion 4.8.7.23+ +Console.ReadKey(); +``` + +### 2.1.10.4 注册服务 + +如果想注册服务,支持以下两种方式: + +- `Serve.RunNative` 方式 + +```cs showLineNumbers {1,3} +Serve.RunNative(services => +{ + services.AddRemoteRequest(); +}); +``` + +- `Startup.cs` 方式 + +```cs showLineNumbers {2,4} +// 在迷你 Web 主机基础上自定义 +Serve.RunNative(); + +// 或完全自定义 +Serve.RunNative(RunOptions.Default); +``` + +```cs showLineNumbers {6,8,10} title="YourStartup.cs" +using Furion; +using Microsoft.Extensions.DependencyInjection; + +namespace YourProject; + +public class YourStartup : AppStartup +{ + public void ConfigureServices(IServiceCollection services) + { + services.AddRemoteRequest(); + } +} +``` + +## 2.1.11 静默启动 + +默认情况下,`Serve.Run()` 使用阻塞线程方式启动,但有些时候我们不希望阻塞现有的代码,可使用**静默启动**的方式: + +```cs showLineNumbers {1} +Serve.Run(silence: true); + +Console.WriteLine("Hello, World!"); +Console.ReadKey(); +``` + +也可以通过 `RunOptions`,`LegacyRunOptions` 或 `GenericRunOptions` 方式,如: + +```cs showLineNumbers {2,5,8} +// RunOptions 方式 +Serve.Run(RunOptions.DefaultSilence); + +// LegacyRunOptions 方式 +Serve.Run(LegacyRunOptions.DefaultSilence); + +// GenericRunOptions 方式 +Serve.Run(GenericRunOptions.DefaultSilence); +``` + +## 2.1.12 `.NET5` 模式找不到 `Views` 视图路径 + +由于 `.NET5` 必须在使用 `.UseStartup<>` 配置启动项,所以 `Serve.Run()` 模式会提示找不到 `Views` 视图路径,这时候只需要在启动目录创建 `Startup.cs` 文件并通过泛型方式指定即可,如: + +```cs showLineNumbers title="Startup.cs" +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace YourProject.Web.Entity +{ + public class Startup + { + public void ConfigureServices(IServiceCollection _) + { + } + + public void Configure(IApplicationBuilder _) + { + } + } +} +``` + +将 `Startup` 类通过 `Serve.Run` 泛型指定: + +```cs showLineNumbers +Serve.Run(LegacyRunOptions.Default); +``` + +## 2.1.13 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/sesssion-state.mdx b/handbook/docs/sesssion-state.mdx new file mode 100644 index 0000000000000000000000000000000000000000..65d47164d4ef5110b7aaec47b9f880299b3bf193 --- /dev/null +++ b/handbook/docs/sesssion-state.mdx @@ -0,0 +1,246 @@ +--- +id: sesssion-state +title: 32. 会话和状态管理 +sidebar_label: 32. 会话和状态管理 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +## 32.1 关于会话和状态管理 + +`HTTP` 是无状态的协议。 默认情况下,`HTTP` 请求是不保留用户值的独立消息。但是我们可以通过以下几种方式保留请求用户数据: + +- `Cookie`:通常存储在客户端的数据,请求时带回服务端 +- `Session`:存储在服务端的数据(可以在存储在内存、进程等介质中) +- `Query Strings`:通过 `Http` 请求地址参数共享 +- `HttpContext.Items`:存储在服务端,只在请求声明周期内使用,请求结束自动销毁 +- `Cache`:服务端缓存,包括内存缓存、分布式内存缓存、IO 缓存、序列化缓存以及数据库缓存 +- `AsyncLocal`:通过异步控制流实现本地数据共享,跨线程 + +## 32.2 如何使用 + +### 32.2.1 `Cookie` 使用 + +使用 `Cookie` 非常简单,如: + +```cs showLineNumbers +// 读取 Cookies +var value = httpContext.Request.Cookies["key"]; + +// 设置 Cookies +var option = new CookieOptions(); +option.Expires = DateTime.Now.AddMilliseconds(10); +httpContext.Response.Cookies.Append(key, value, option); + +// 删除 Cookies +httpContext.Response.Cookies.Delete(key); +``` + +:::note 特别说明 + +`httpContext` 可以通过 `IHttpContextAccessor` 获取,也可以通过 `App.HttpContext` 获取。 + +::: + +我们还可以通过 `Cookie` 实现授权功能及单点登录(SSO):[网站共享 Cookie](https://docs.microsoft.com/zh-cn/aspnet/core/security/cookie-sharing?view=aspnetcore-5.0) + +### 32.2.2 `Session` 使用 + +在使用 `Session` 之前,必须注册 `Session` 服务:(如果 + +```cs showLineNumbers {1,5,7-12,32,37,39} +public class Startup +{ + public void ConfigureServices(IServiceCollection services) + { + // services.AddDistributedMemoryCache(); 框架内部已经默认注册 + + services.AddSession(options => + { + options.IdleTimeout = TimeSpan.FromSeconds(10); + options.Cookie.HttpOnly = true; + options.Cookie.IsEssential = true; + }); // 注意在控制器之前注册!!!! + + services.AddControllersWithViews(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + else + { + app.UseExceptionHandler("/Home/Error"); + app.UseHsts(); + } + + app.UseHttpsRedirection(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseSession(); + + app.UseEndpoints(endpoints => + { + endpoints.MapDefaultControllerRoute(); + endpoints.MapRazorPages(); + }); + } +} +``` + +:::important 中间件注册顺序 + +`app.UseSession()` 必须在 `app.UseRouting()` 和 `app.UseEndpoints()` **之间**注册! + +::: + +- 常见例子: + +```cs showLineNumbers +// 读取 Session +var byteArr = httpContext.Session.Get("key"); // 返回 byte[] +var str = httpContext.Session.GetString("key"); // 返回 string[] +var num = httpContext.Session.GetInt32("key"); // 返回 int + +// 设置 Session +httpContext.Session.SetString("key", "value"); // 设置字符串 +httpContext.Session.SetInt32("key", 1); // 设置 int 类型 +``` + +- **自定义设置任意类型拓展:** + +```cs showLineNumbers +public static class SessionExtensions +{ + public static void Set(this ISession session, string key, T value) + { + session.SetString(key, JsonSerializer.Serialize(value)); + } + + public static T Get(this ISession session, string key) + { + var value = session.GetString(key); + return value == null ? default : JsonSerializer.Deserialize(value); + } +} +``` + +- 防止 `Session ID` 改变或 `Session` 失效 + +在 `Startup.cs` 的 `ConfigureServices` 配置即可: + +```cs showLineNumbers +services.Configure(options => +{ +   options.CheckConsentNeeded = context => false; // 默认为true,改为false +   options.MinimumSameSitePolicy = SameSiteMode.None; +}); +``` + +### 32.2.3 `Query Strings` 使用 + +该方式使用非常简单,只需 `httpContext.Request.Query["key"]` 即可。 + +### 32.2.4 `HttpContext.Items` 使用 + +`HttpContext` 对象提供了 `Items` 集合属性,可以让我们在单次请求间共享数据,请求结束立即销毁,可以存储任何数据。使用也非常简单,如: + +```cs showLineNumbers +// 读取 +var value = httpContext.Items["key"]; + +// 添加 +httpContext.Items["key"] = "任何值包括对象"; + +// 删除 +httpContext.Items.Remove("key"); +``` + +### 32.2.5 `Cache` 方式 + +参见 [分布式缓存](/docs/cache) 文档 + +### 32.2.6 `AsyncLocal` 方式 + +`AsyncLocal` 可以说是进程内共享数据的大利器,可以通过该类实现跨线程、异步控制流中共享数据,如: + +```cs showLineNumbers +using System; +using System.Threading; +using System.Threading.Tasks; + +class Example +{ + static AsyncLocal _asyncLocalString = new AsyncLocal(); + + static ThreadLocal _threadLocalString = new ThreadLocal(); + + static async Task AsyncMethodA() + { + // Start multiple async method calls, with different AsyncLocal values. + // We also set ThreadLocal values, to demonstrate how the two mechanisms differ. + _asyncLocalString.Value = "Value 1"; + _threadLocalString.Value = "Value 1"; + var t1 = AsyncMethodB("Value 1"); + + _asyncLocalString.Value = "Value 2"; + _threadLocalString.Value = "Value 2"; + var t2 = AsyncMethodB("Value 2"); + + // Await both calls + await t1; + await t2; + } + + static async Task AsyncMethodB(string expectedValue) + { + Console.WriteLine("Entering AsyncMethodB."); + Console.WriteLine(" Expected '{0}', AsyncLocal value is '{1}', ThreadLocal value is '{2}'", + expectedValue, _asyncLocalString.Value, _threadLocalString.Value); + await Task.Delay(100); + Console.WriteLine("Exiting AsyncMethodB."); + Console.WriteLine(" Expected '{0}', got '{1}', ThreadLocal value is '{2}'", + expectedValue, _asyncLocalString.Value, _threadLocalString.Value); + } + + static async Task Main(string[] args) + { + await AsyncMethodA(); + } +} +// The example displays the following output: +// Entering AsyncMethodB. +// Expected 'Value 1', AsyncLocal value is 'Value 1', ThreadLocal value is 'Value 1' +// Entering AsyncMethodB. +// Expected 'Value 2', AsyncLocal value is 'Value 2', ThreadLocal value is 'Value 2' +// Exiting AsyncMethodB. +// Expected 'Value 2', got 'Value 2', ThreadLocal value is '' +// Exiting AsyncMethodB. +// Expected 'Value 1', got 'Value 1', ThreadLocal value is '' +``` + +为了简化操作,`Furion v2.18+` 版本实现了轻量级的 `CallContext` 静态类,内部使用 `AsyncLocal` 实现,使用如下: + +```cs showLineNumbers +CallContext.SetLocalValue("name", "Furion"); +CallContext.GetLocalValue("name"); + +CallContext.SetLocalValue("count", 1); +CallContext.GetLocalValue("count"); +``` + +了解更多 `AsyncLocal` 知识:[https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.asynclocal-1?redirectedfrom=MSDN&view=net-5.0](https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.asynclocal-1?redirectedfrom=MSDN&view=net-5.0) + +## 32.3 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 diff --git a/handbook/docs/settings/appsettings.mdx b/handbook/docs/settings/appsettings.mdx new file mode 100644 index 0000000000000000000000000000000000000000..fcca02ddfca196cdf94db5623b9e549f1789921c --- /dev/null +++ b/handbook/docs/settings/appsettings.mdx @@ -0,0 +1,72 @@ +--- +id: appsettings +title: 1. 应用配置 +sidebar_label: 1. 应用配置 +--- + +:::tip 配置智能提示和校验 + +如需编写配置的时候提供智能提示和校验,可查看 【[2.7 JSON Schema 使用](../jsonschema)】 + +只需要在 `.json` 文件头部添加下列配置即可: + +```json showLineNumbers {2} +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json" +} +``` + +::: + +## 1.1 关于配置 + +应用配置指的是 `Furion` 框架全局配置选项。 + +## 1.2 配置信息 + +- `AppSettings`:配置根节点 + - `InjectMiniProfiler`:是否注入 `MiniProfiler`,`bool` 类型,默认 `true`,**关闭 Swagger 左上角监听** + - `InjectSpecificationDocument`:是否启用 `Swagger` 文档,`bool` 类型,默认 `true`,**生产环境可关闭** + - `EnabledReferenceAssemblyScan`:是否启用通过 `dll` 方式添加的引用程序集扫描,`bool` 类型,默认 `false` + - `ExternalAssemblies`:配置外部程序集完整路径,支持动态加载,`string[]` 类型,默认 `[]` + - `ExcludeAssemblies`:排除扫描的程序集名称,`string[]` 类型,默认 `[]` + - `PrintDbConnectionInfo`:是否打印数据库连接信息到 `MiniProfiler` 中,`bool` 类型,默认 `true` + - `SupportPackageNamePrefixs`:配置支持的包前缀名,`string[]` 类型,默认 `[]` + - `OutputOriginalSqlExecuteLog`:是否输出原始 Sql 执行日志(ADO.NET),默认 `true` + - `VirtualPath`:配置虚拟目录,必须以 `/` 开头 + +## 1.3 配置示例 + +```json showLineNumbers +{ + "AppSettings": { + "InjectMiniProfiler": false + } +} +``` + +## 1.4 特别注意 + +默认情况下,`Furion` 框架会自动扫描根目录下的 `*.json` 和 `*.config.xml` 文件载入配置中,如需忽略个别文件,需在 `appsettings.json` 配置文件根节点下配置 `IgnoreConfigurationFiles` 节点即可,`string[]` 类型,如: + +```json showLineNumbers +{ + "IgnoreConfigurationFiles": ["runtime.json"] +} +``` + +如果需要自定义扫描目录(非根目录),需在 `appsettings.json` 中添加下面配置: + +:::important 支持版本 + +在 `v2.16.7+` 版本有效 + +::: + +```json showLineNumbers +{ + "ConfigurationScanDirectories": ["目录1名称", "目录1名称/子目录名称"] +} +``` + +**必须在 `appsettings.json` 文件中配置才有效** diff --git a/handbook/docs/settings/corsaccessorsettings.mdx b/handbook/docs/settings/corsaccessorsettings.mdx new file mode 100644 index 0000000000000000000000000000000000000000..f91df362a91d333a818b4bc9ef1c9a89fae53a01 --- /dev/null +++ b/handbook/docs/settings/corsaccessorsettings.mdx @@ -0,0 +1,60 @@ +--- +id: corsaccessorsettings +title: 2. 跨域配置 +sidebar_label: 2. 跨域配置 +--- + +:::tip 配置智能提示和校验 + +如需编写配置的时候提供智能提示和校验,可查看 【[2.7 JSON Schema 使用](../jsonschema)】 + +只需要在 `.json` 文件头部添加下列配置即可: + +```json showLineNumbers {2} +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json" +} +``` + +::: + +## 2.1 关于配置 + +跨域配置指的是 `Furion` 框架客户端跨域配置选项。 + +## 2.2 配置信息 + +- `CorsAccessorSettings`:配置根节点 + - `PolicyName`:跨域策略名,`string` 类型,必填,默认 `App.Cors.Policy` + - `WithOrigins`:允许跨域的域名列表,`string[]` 类型,默认 `*` + - `WithHeaders`:请求表头,没有配置则允许所有表头,`string[]` 类型 + - **`WithExposedHeaders`:设置客户端可获取的响应标头,`string[]` 类型,默认 `["access-token", "x-access-token"]`** + - **默认情况下,若后端输出特定的响应头 `Key`,那么需将该 `Key` 配置在数组中** + - `WithMethods`:设置跨域允许请求谓词,没有配置则允许所有,`string[]` 类型 + - `AllowCredentials`:是否允许跨域请求中的凭据,`bool` 类型,默认值 `true` + - `SetPreflightMaxAge`:设置预检过期时间,`int` 类型,默认值 `24小时` + - `FixedClientToken`:是否默认配置 `WithExposedHeaders`,`bool` 类型,默认 `true` + - `SignalRSupport`:是否启用 `SignalR` 跨域支持,`bool` 类型,默认 `false` + +## 2.3 配置示例 + +```json showLineNumbers +{ + "CorsAccessorSettings": { + "PolicyName": "MyPolicy", + "WithOrigins": ["http://localhost:4200", "http://furion.baiqian.ltd"] + } +} +``` + +## 2.4 使用 `axios` 前端注意事项 + +由于 `axios` 对跨域有特定的需要,需要响应报文中添加特定 `Header` 才能放行,如:`Access-Control-Expose-Headers: xxxxx`,所以,如果前端使用了 `axios` 请求,需要添加以下配置: + +```cs showLineNumbers +{ + "CorsAccessorSettings": { + "WithExposedHeaders": ["X-Pagination","access-token","x-access-token"] + } +} +``` diff --git a/handbook/docs/settings/dependencyinjectionsettings.mdx b/handbook/docs/settings/dependencyinjectionsettings.mdx new file mode 100644 index 0000000000000000000000000000000000000000..88d2dd6e293d898e677ac51d44f340b8d7acccf8 --- /dev/null +++ b/handbook/docs/settings/dependencyinjectionsettings.mdx @@ -0,0 +1,58 @@ +--- +id: dependencyinjectionsettings +title: 4. 依赖注入配置 +sidebar_label: 4. 依赖注入配置 +--- + +:::tip 配置智能提示和校验 + +如需编写配置的时候提供智能提示和校验,可查看 【[2.7 JSON Schema 使用](../jsonschema)】 + +只需要在 `.json` 文件头部添加下列配置即可: + +```json showLineNumbers {2} +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json" +} +``` + +::: + +## 4.1 关于配置 + +依赖注入配置指的是 `Furion` 框架依赖注入配置选项。 + +## 4.2 配置信息 + +- `DependencyInjectionSettings`:依赖注入配置根节点 + - `Definitions`:动态依赖注入配置节点,`ExternalService` 数组类型 + - `ExternalService`:配置单个依赖注入信息 + - `Interface`:配置依赖接口信息,格式:`程序集名称;接口完整名称`,如:`Furion.Application;Furion.Application.ITestService` + - `Service`:配置接口实现信息,格式同上 + - `RegisterType`:配置依赖注入的对象生存期,取值:`Transient`,`Scoped`,`Singleton` + - `Action`:注册行为,可选值:`Add`,`TryAdd`,参见 [依赖注入-特性配置](../dependency-injection#128-injection-特性配置) + - `Pattern`:注册选项,参见 [依赖注入-特性配置置](../dependency-injection#128-injection-特性配置) + - `Named`:注册别名,参见 [依赖注入-特性配置](../dependency-injection#128-injection-特性配置) + - `Order`:注册排序,参见 [依赖注入-特性配置](../dependency-injection#128-injection-特性配置) + - `Proxy`:配置代理拦截,格式:`程序集名称;代理类完整名称`,参见 [依赖注入-特性配置](../dependency-injection#128-injection-特性配置) + +## 4.3 配置示例 + +```json showLineNumbers +{ + "DependencyInjectionSettings": { + "Definitions": [ + { + "Interface": "Furion.Application;Furion.Application.ITestService", + "Service": "Furion.Application;Furion.Application.TestService", + "RegisterType": "Transient", + "Action": "Add", + "Pattern": "SelfWithFirstInterface", + "Named": "TestService", + "Order": 1, + "Proxy": "Furion.Application;Furion.Application.LogDispathProxy" + } + ] + } +} +``` diff --git a/handbook/docs/settings/dynamicapicontrollersettings.mdx b/handbook/docs/settings/dynamicapicontrollersettings.mdx new file mode 100644 index 0000000000000000000000000000000000000000..a4a396fe79048a4ade24eeab55b30eb0960a54bd --- /dev/null +++ b/handbook/docs/settings/dynamicapicontrollersettings.mdx @@ -0,0 +1,87 @@ +--- +id: dynamicapicontrollersettings +title: 5. 动态API配置 +sidebar_label: 5. 动态API配置 +--- + +:::tip 配置智能提示和校验 + +如需编写配置的时候提供智能提示和校验,可查看 【[2.7 JSON Schema 使用](../jsonschema)】 + +只需要在 `.json` 文件头部添加下列配置即可: + +```json showLineNumbers {2} +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json" +} +``` + +::: + +## 5.1 关于配置 + +动态 API 配置指的是 `Furion` 框架动态 API 配置选项。 + +## 5.2 配置信息 + +- `DynamicApiControllerSettings`:配置根节点 + - `DefaultRoutePrefix`:默认路由前缀,`string`,默认 `api` + - `DefaultHttpMethod`:默认请求谓词,`string`,默认:`POST` + - `DefaultModule`:默认模块名称(区域),可用作接口版本,`string`,默认:`v1` + - `LowercaseRoute`:小写路由格式,`bool`,默认:`true` + - `AsLowerCamelCase`:启用小驼峰命名(首字母小写),默认 `false` + - `KeepVerb`:是否保留动作谓词,`bool`,默认:`false` + - `KeepName`:是否保留默认名称,`bool`,默认:`fasle` + - `CamelCaseSeparator`:骆驼(驼峰)命名分隔符,`string`,默认:`-` + - `VersionSeparator`:版本分隔符,`string`,默认:`@` + - `ModelToQuery`:`GET/HEAD` 请求将 `类类型参数转查询参数`,`bool`,默认 `false` + - `SupportedMvcController`:是否支持 `Mvc Controller` 动态配置,`bool`,默认 `false` + - `UrlParameterization`:路由参数采用 `[FromQuery]` 化,默认 `false`(`[FromRoute]` 方式) + - `DefaultArea`:配置默认区域,默认 `null` + - `ForceWithRoutePrefix`:配置是否强制添加 `DefaultRoutePrefix`,当控制器自定义了 `[Route]` 有效,默认 `false`,**仅限 v3.4.1+版本有效** + - `AbandonControllerAffixes`:默认去除控制器名称前后缀列表名,`string[]`,默认: + - `AppServices` + - `AppService` + - `ApiController` + - `Controller` + - `Services` + - `Service` + - `AbandonActionAffixes`:默认去除动作方法名称前后缀列表名,`string[]`,默认: + - `Async` + - `VerbToHttpMethods`:复写默认方法名转 `[HttpMethod]` 规则,`string[][]` 二维数组类型,内置匹配规则为: + ```cs showLineNumbers + ["post"] = "POST", + ["add"] = "POST", + ["create"] = "POST", + ["insert"] = "POST", + ["submit"] = "POST", + ["get"] = "GET", + ["find"] = "GET", + ["fetch"] = "GET", + ["query"] = "GET", + ["put"] = "PUT", + ["update"] = "PUT", + ["delete"] = "DELETE", + ["remove"] = "DELETE", + ["clear"] = "DELETE", + ["patch"] = "PATCH" + ``` + - 复写示例 + ```json showLineNumbers + "DynamicApiControllerSettings": { + "VerbToHttpMethods": [ + [ "getall", "HEAD" ], // => getall 会被复写为 `[HttpHead]` + [ "other", "PUT" ] // => 新增一条新规则,比如,一 `[other]` 开头会转换为 `[HttpPut]` 请求 + ] + } + ``` + +## 5.3 配置示例 + +```json showLineNumbers +{ + "DynamicApiControllerSettings": { + "SupportedMvcController": true + } +} +``` diff --git a/handbook/docs/settings/friendlyexceptionsettings.mdx b/handbook/docs/settings/friendlyexceptionsettings.mdx new file mode 100644 index 0000000000000000000000000000000000000000..7b48b3f584a48ab77f71ee3547022759ff8ac9bc --- /dev/null +++ b/handbook/docs/settings/friendlyexceptionsettings.mdx @@ -0,0 +1,65 @@ +--- +id: friendlyexceptionsettings +title: 6. 友好异常配置 +sidebar_label: 6. 友好异常配置 +--- + +:::tip 配置智能提示和校验 + +如需编写配置的时候提供智能提示和校验,可查看 【[2.7 JSON Schema 使用](../jsonschema)】 + +只需要在 `.json` 文件头部添加下列配置即可: + +```json showLineNumbers {2} +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json" +} +``` + +::: + +## 6.1 关于配置 + +友好异常配置指的是 `Furion` 框架友好异常配置选项。 + +## 6.2 配置信息 + +- `FriendlyExceptionSettings`:配置根节点 + - `HideErrorCode`:隐藏错误码,`bool` 类型,默认 `false` + - `DefaultErrorCode`:默认错误码,`string` 类型 + - `DefaultErrorMessage`:默认错误消息,`string` 类型 + - `ThrowBah`:是否将 `Oops.Oh` 默认抛出为业务异常,`bool` 类型,默认 `false`,设置 `true` 之后 `Oops.Oh` 默认进入 `OnValidateFailed` 处理,而不是 `OnException` + - `LogError`:是否输出异常日志,`bool` 类型,默认 `true` + +## 6.3 配置示例 + +```json showLineNumbers +{ + "FriendlyExceptionSettings": { + "DefaultErrorMessage": "系统异常,请联系管理员" + } +} +``` + +## 6.4 异常消息配置 + +`Furion` 框架还为友好异常消息提供外部配置 + +### 6.4.1 配置信息 + +- `ErrorCodeMessageSettings`:配置根节点 + - `Definitions`:配置异常错误码消息类型,`[错误状态码,错误消息][]` 类型,如:`["5000", "{0} 不能小于 {1}"]` + +### 6.4.2 配置示例 + +```json showLineNumbers +{ + "ErrorCodeMessageSettings": { + "Definitions": [ + ["5000", "{0} 不能小于 {1}"], + ["5001", "我叫 {0} 名字", "百小僧"], + ["5002", "Oops! 出错了"] + ] + } +} +``` diff --git a/handbook/docs/settings/jwtsettings.mdx b/handbook/docs/settings/jwtsettings.mdx new file mode 100644 index 0000000000000000000000000000000000000000..9ed83a34a42d6727f664aa55f8bbb479429443e7 --- /dev/null +++ b/handbook/docs/settings/jwtsettings.mdx @@ -0,0 +1,70 @@ +--- +id: jwtsettings +title: 9. JWT 配置 +sidebar_label: 9. JWT 配置 +--- + +:::tip 配置智能提示和校验 + +如需编写配置的时候提供智能提示和校验,可查看 【[2.7 JSON Schema 使用](../jsonschema)】 + +只需要在 `.json` 文件头部添加下列配置即可: + +```json showLineNumbers {2} +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json" +} +``` + +::: + +## 9.1 关于配置 + +`JWT` 配置指的是生成 `JWT` token 配置。 + +## 9.2 配置信息 + +- `JWTSettings`:根节点 + - `ValidateIssuerSigningKey`:是否验证密钥,`bool` 类型,默认 `true` + - `IssuerSigningKey`:密钥,`string` 类型,必须是复杂密钥,长度大于 `16` + - `ValidateIssuer`:是否验证签发方,`bool` 类型,默认 `true` + - `ValidIssuer`:签发方,`string` 类型 + - `ValidateAudience`:是否验证签收方,`bool` 类型,默认 `true` + - `ValidAudience`:签收方,`string` 类型 + - `ValidateLifetim`:是否验证过期时间,`bool` 类型,默认 `true`,建议 `true` + - `ExpiredTime`:过期时间,`long` 类型,单位分钟,默认 `20` 分钟 + - `ClockSkew`:过期时间容错值,`long` 类型,单位秒,默认 `5` 秒 + - `Algorithm`:加密算法,`string` 类型,默认 `HS256`,可选算法有: + - `HS256` + - `HS384` + - `HS512` + - `PS256` + - `PS384` + - `PS512` + - `RS256` + - `RS384` + - `RS512` + - `ES256` + - `ES256K` + - `ES384` + - `ES512` + - `EdDSA` + +## 9.3 配置示例 + +```json showLineNumbers {4,6,8} +{ + "JWTSettings": { + "ValidateIssuerSigningKey": true, // 是否验证密钥,bool 类型,默认true + "IssuerSigningKey": "你的密钥", // 密钥,string 类型,必须是复杂密钥,长度大于16 + "ValidateIssuer": true, // 是否验证签发方,bool 类型,默认true + "ValidIssuer": "签发方", // 签发方,string 类型 + "ValidateAudience": true, // 是否验证签收方,bool 类型,默认true + "ValidAudience": "签收方", // 签收方,string 类型 + "ValidateLifetime": true, // 是否验证过期时间,bool 类型,默认true,建议true + "ExpiredTime": 20, // 过期时间,long 类型,单位分钟,默认20分钟 + "ClockSkew": 5, // 过期时间容错值,long 类型,单位秒,默认 5秒 + "Algorithm": "HS256" // 加密算法,string 类型,默认 HS256 + } +} +``` diff --git a/handbook/docs/settings/localizationsettings.mdx b/handbook/docs/settings/localizationsettings.mdx new file mode 100644 index 0000000000000000000000000000000000000000..9923380ee5ca7687912a45ed37126f64c7b1f9ed --- /dev/null +++ b/handbook/docs/settings/localizationsettings.mdx @@ -0,0 +1,43 @@ +--- +id: localizationsettings +title: 8. 多语言配置 +sidebar_label: 8. 多语言配置 +--- + +:::tip 配置智能提示和校验 + +如需编写配置的时候提供智能提示和校验,可查看 【[2.7 JSON Schema 使用](../jsonschema)】 + +只需要在 `.json` 文件头部添加下列配置即可: + +```json showLineNumbers {2} +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json" +} +``` + +::: + +## 8.1 关于配置 + +多语言配置指的是 `Furion` 框架全球化和本地化选项。 + +## 8.2 配置信息 + +- `LocalizationSettings` 多语言配置根节点 + - `ResourcesPath`:资源目录,`string` 类型,默认 `Resources` + - `SupportedCultures`:支持的语言区域码类别,`string[]` 类型 + - `DefaultCulture`:默认语言区域码,如果为空,则取 `SupportedCultures` 第一项 + - `LanguageFilePrefix`:配置资源文件前缀,`string` 类型,默认 `Lang` + - `AssemblyName`:配置资源文件存放程序集名,`string` 类型,默认 `启动层` 名称 + +## 8.3 配置示例 + +```json showLineNumbers {2,3} +{ + "LocalizationSettings": { + "SupportedCultures": ["zh-CN", "en-US"], // 配置支持的语言列表 + "DefaultCulture": "zh-CN" // 配置默认语言,如果不配置,取 SupportedCultures 的第一项 + } +} +``` diff --git a/handbook/docs/settings/specificationdocumentsettings.mdx b/handbook/docs/settings/specificationdocumentsettings.mdx new file mode 100644 index 0000000000000000000000000000000000000000..4374195b5e4e363ae803c1c0546379b493ff0feb --- /dev/null +++ b/handbook/docs/settings/specificationdocumentsettings.mdx @@ -0,0 +1,91 @@ +--- +id: specificationdocumentsettings +title: 7. 规范化文档配置 +sidebar_label: 7. 规范化文档配置 +--- + +:::tip 配置智能提示和校验 + +如需编写配置的时候提供智能提示和校验,可查看 【[2.7 JSON Schema 使用](../jsonschema)】 + +只需要在 `.json` 文件头部添加下列配置即可: + +```json showLineNumbers {2} +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json" +} +``` + +::: + +## 7.1 关于配置 + +规范化文档配置指的是 `Furion` 框架 Swagger 文档配置选项。 + +## 7.2 配置信息 + +- `SpecificationDocumentSettings`:配置根节点 + - `DocumentTitle`:文档标题,`string`,默认 `Specification Api Document` + - `DefaultGroupName`:默认分组名,`string`,默认 `Default` + - `EnableAuthorized`:是否启用权限控制,`bool`,默认 `true` + - `FormatAsV2`:采用 `Swagger 2.0` 版本,`bool`,默认 `false` **[已弃用](https://github.com/domaindrivendev/Swashbuckle.WebApi/issues/1393)** + - `RoutePrefix`:规范化文档地址,`string`,默认 `api`,**如果希望在首页,改为空字符串即可**。 + - `DocExpansionState`:文档显示方式,`DocExpansion`,默认 `List`,取值: + - `List`:列表式(展开子类),**默认值** + - `Full`:完全展开 + - `None`:列表式(不展开子类) + - `XmlComments`:程序集注释描述文件名(可带 `.xml`,`string`,默认 `Furion.Application, Furion.Web.Entry, Furion.Web.Core` + - `GroupOpenApiInfos`:分组信息配置,`SpecificationOpenApiInfo[]`,默认 `{ 'Group': 'Default'}` + - `SecurityDefinitions`:安全策略定义配置,`SpecificationOpenApiSecurityScheme[]`,默认 `[]` + - `Servers`:配置 Server 下拉列表,`OpenApiServer[]` 类型,默认 `[]`,如:`{Servers:[ { Url:"地址", Description:"描述"} ]}` + - `HideServers`:是否隐藏 Server 下拉列表,`bool` 类型,默认 `true` + - `RouteTemplate`:配置文档 `swagger.json` 路由模板,默认模板:`swagger/{documentName}/swagger.json`, `{documentName}` 代表分组名,**必须保留原样** + - `PackagesGroups`:配置模块化内置分组名称,`string[]` 类型,默认 `[]` + - `EnableEnumSchemaFilter`:启用枚举 Schema 筛选器,`bool` 类型,默认 `true` + - `EnableTagsOrderDocumentFilter`:启用标签排序筛选器,`bool` 类型,默认 `true` + - `ServerDir`:配置 `IIS` 添加 `Application` 部署名,`string` 类型,默认空,**仅在 Furion v3.2.0+` 有效** + - `LoginInfo`:配置 `Swagger` 是否需要登录才能访问,`SpecificationLoginInfo` 类型,默认 `null`,**仅在 Furion v3.3.3+` 有效** + - `Enabled`:是否启用登录授权,默认 `false` + - `CheckUrl`:检查登录状态的 `Url` 地址,**该地址必须是 `POST` 请求** + - `SubmitUrl`:提交登录的 `Url` 地址,**该地址必须是 `POST` 请求且只有一个 `SpecificationAuth` 类型参数**,成功登录返回 `200`,否则返回 `401`,支持相对地址,以 `/` 开头 + - `EnableAllGroups`:启用 `Swagger` 总分组功能,自动将所有分组的接口合并到 `All Groups` 中,`bool` 类型,默认 `false`,**仅在 Furion v3.3.4+` 有效** + - `EnumToNumber`:枚举类型生成值类型,`bool` 类型,默认 `false`,**仅在 Furion 4.8.8.35+` 有效** + +另外 `SpecificationOpenApiInfo` 内置配置如下: + +- `Group`:分组唯一标识,`string` 类型,必填 +- `Order`:分组排序,`int` 类型,数字越大排前面,默认 `0` +- `Visible`:配置分组是否可见,`bool` 类型,默认 `true` +- `Title`:配置分组标题,`string` 类型 +- `Description`:配置分组描述,`string` 类型 +- `Version`:配置分组版本,默认 `1.0` +- `TermsOfService`:配置相关链接地址,`Uri` 类型 +- `Contact`:配置联系方式,`OpenApiContact` 类型 +- `License`:配置协议,`OpenApiLicense` 类型 + +## 7.3 配置示例 + +```json showLineNumbers +{ + "SpecificationDocumentSettings": { + "GroupOpenApiInfos": [ + { + "Group": "Group1", + "Title": "分组标题", + "Description": "这里是分组描述", + "Version": "版本号", + "TermsOfService": "http://furion.baiqian.ltd", + "Contact": { + "Name": "百小僧", + "Url": "https://gitee.com/monksoul", + "Email": "monksoul@outlook.com" + }, + "License": { + "Name": "MIT", + "Url": "https://gitee.com/dotnetchina/Furion/blob/alpha/LICENSE" + } + } + ] + } +} +``` diff --git a/handbook/docs/settings/unifyresultsettings.mdx b/handbook/docs/settings/unifyresultsettings.mdx new file mode 100644 index 0000000000000000000000000000000000000000..769f10f889aac50fb093b0119900f6dc59bc20bc --- /dev/null +++ b/handbook/docs/settings/unifyresultsettings.mdx @@ -0,0 +1,45 @@ +--- +id: unifyresultsettings +title: 10. 规范化结果配置 +sidebar_label: 10. 规范化结果配置 +--- + +:::tip 配置智能提示和校验 + +如需编写配置的时候提供智能提示和校验,可查看 【[2.7 JSON Schema 使用](../jsonschema)】 + +只需要在 `.json` 文件头部添加下列配置即可: + +```json showLineNumbers {2} +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json" +} +``` + +::: + +## 10.1 关于配置 + +规范化配置指的是配置响应结果,响应状态码等配置 + +## 10.2 配置信息 + +- `UnifyResultSettings` 规范化配置根节点 + - `Return200StatusCodes`:配置返回 `200` 状态码的请求,`int[]` 类型,只支持 `400+(404除外)` 状态码篡改 + - `AdaptStatusCodes`:配置篡改状态码规则,`int[][]` 类型,只支持 `400+(404除外)` 状态码篡改 + - `SupportMvcController`:是否支持 `MVC` 控制台规范化处理,`bool` 类型,默认 `false` + +## 10.3 配置示例 + +```json showLineNumbers {2,3} +{ + "UnifyResultSettings": { + "Return200StatusCodes": [401, 403], + "AdaptStatusCodes": [ + [401, 200], + [403, 401] + ], + "SupportMvcController": true + } +} +``` diff --git a/handbook/docs/settings/validationTypemessagesettings.mdx b/handbook/docs/settings/validationTypemessagesettings.mdx new file mode 100644 index 0000000000000000000000000000000000000000..4045459bb16ac251d7c03e57a52b7c17d637cd85 --- /dev/null +++ b/handbook/docs/settings/validationTypemessagesettings.mdx @@ -0,0 +1,42 @@ +--- +id: validationTypemessagesettings +title: 3. 验证消息配置 +sidebar_label: 3. 验证消息配置 +--- + +:::tip 配置智能提示和校验 + +如需编写配置的时候提供智能提示和校验,可查看 【[2.7 JSON Schema 使用](../jsonschema)】 + +只需要在 `.json` 文件头部添加下列配置即可: + +```json showLineNumbers {2} +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json" +} +``` + +::: + +## 3.1 关于配置 + +验证消息配置指的是 `Furion` 框架数据校验中验证类型默认消息配置选项。 + +## 3.2 配置信息 + +- `ValidationTypeMessageSettings`:配置根节点 + - `Definitions`:配置验证类型对应的消息类型,`[类型名称,类型消息][]` 类型,如:`["Required", "值不能为空或Null"]` + +## 3.3 配置示例 + +```json showLineNumbers +{ + "ValidationTypeMessageSettings": { + "Definitions": [ + ["Required", "值不能为空或Null"], + ["Numeric", "必须是数值类型"], + ["StrongPassword", "密码太简单了!!!"] + ] + } +} +``` diff --git a/handbook/docs/signalr.mdx b/handbook/docs/signalr.mdx new file mode 100644 index 0000000000000000000000000000000000000000..4df0d54ce9b0520cc17ef1f35a307e234e42a499 --- /dev/null +++ b/handbook/docs/signalr.mdx @@ -0,0 +1,504 @@ +--- +id: signalr +title: 24. 即时通讯 +sidebar_label: 24. 即时通讯 +--- + +## 24.1 什么是即时通讯 + +即时通讯(Instant messaging,简称 IM)通常是指互联网上用以进行实时通讯的系统,允许两人或多人使用网络即时的传递文字信息、文档、语音与视频交流。 + +即时通讯不同于 E-mail 在于它的交谈是实时的。大部分的即时通讯服务提供了状态信息的特性 ── 显示联络人名单,联络人是否在线上与能否与联络人交谈。 + +在互联网上目前使用较广的即时通讯服务包括 Windows Live Messenger、AOL Instant Messenger、Skype、Yahoo! Messenger、NET Messenger Service、Jabber、ICQ 与 QQ 等。 + +## 24.2 即时通讯应用场景 + +即时通讯应用场景非常广泛,需要实时交互消息的都需要。如: + +- 聊天工具:QQ、WeChat、在线客服等 +- 手游网游:王者荣耀、魔兽等 +- 网络直播:腾讯课堂、抖音直播等 +- 订单推送:美团、餐饮下单系统等 +- 协同办公:公司内部文件分享、工作安排、在线会议等。 + +以上只是列举了比较常用的应用场景,但即时通讯的作用远不止于此。 + +文档紧急编写中,可以先看官方文档:https://docs.microsoft.com/zh-cn/aspnet/core/signalr/introduction?view=aspnetcore-5.0 + +## 24.3 关于 `SignalR` + +即时通讯技术实现是复杂且过于底层化,所以微软为了简化即时通讯应用程序,开发出了一个强大且简易使用的通信库:`SignalR`,通过该库我们可以轻松实现类似 QQ、微信这类 IM 聊天工具,也能快速实现消息推送、订单推送这样的系统。 + +### 24.3.1 微软官方介绍 + +ASP.NET Core SignalR 是一种开放源代码库,可简化将实时 web 功能添加到应用程序的功能。 实时 web 功能使服务器端代码可以立即将内容推送到客户端。 + +适用于 SignalR : + +- 需要从服务器进行高频率更新的应用。 示例包括游戏、社交网络、投票、拍卖、地图和 GPS 应用。 +- 仪表板和监视应用。 示例包括公司仪表板、即时销售更新或旅行警报。 +- 协作应用。 协作应用的示例包括白板应用和团队会议软件。 +- 需要通知的应用。 社交网络、电子邮件、聊天、游戏、旅行警报和很多其他应用都需使用通知。 + +目前 `SignalR` 已经内置在 `.NET 5 SDK` 中。同时 `SignalR` 支持 `Web、App、Console、Desktop` 等多个应用平台。 + +## 24.4 注册 `SignalR` 服务 + +在 `Furion` 框架中,任何服务功能都需要先注册后再使用,`SignalR` 也不例外。只需要在 `Startup.cs` 中添加注册即可: + +```cs showLineNumbers {1,15,22,25} +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Furion.Web.Core +{ + public sealed class Startup : AppStartup + { + public void ConfigureServices(IServiceCollection services) + { + // 其他代码... + + // 添加即时通讯 + services.AddSignalR(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + // 其他代码... + + app.UseEndpoints(endpoints => + { + // 注册集线器 + endpoints.MapHubs(); + + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} +``` + +## 24.5 `SignalR` 长连接和集线器 + +`SignalR` 包含两种用于在客户端和服务器之间进行通信的模型:`持久性连接`和 `集线器` 中心。 + +### 24.5.1 持久性连接 + +连接表示用于发送单接收方、分组或广播消息的简单终结点。 `持久性连接` (在 .NET 代码中由 PersistentConnection 类表示,在 ASP.NET Core SignalR 中 ,PersistentConnection 类已被删除。) 使开发人员能够直接访问 `SignalR` 公开的低级别通信协议。 使用基于连接的 Api (如 Windows Communication Foundation)的开发人员将对使用连接通信模型非常熟悉。 + +### 24.5.2 集线器 + +集线器是一种基于连接 API 构建的更高级别管道,**它允许客户端和服务器直接调用方法**。 `SignalR` 就像魔术一样处理跨机器边界的调度,使客户端能够像本地方法一样轻松地调用服务器上的方法,反之亦然。 如果开发人员已使用远程调用 (如 .NET 远程处理),则将对使用中心通信模型非常熟悉。 使用集线器还可以将强类型参数传递给方法,从而启用模型绑定。 + +:::note 小知识 + +想了解更多关于 `持久性连接` 和 `集线器中心` 可查阅 [SignalR 官方文档](https://docs.microsoft.com/zh-cn/aspnet/signalr/overview/getting-started/introduction-to-signalr#connections-and-hubs) + +::: + +## 24.6 集线器 `Hub` 定义 + +**在本章节中主要推荐使用集线器通信模型方式。**这里主要说明 `Hub` 定义,如果无法理解该通信模型的作用也没关系,接下来的例子会带大家慢慢熟悉并使用。 + +### 24.6.1 两种定义方式 + +定义集线器只需要继承 `Hub` 或 `Hub` 泛型基类即可,如: + +- `Hub` 方式 + +```cs showLineNumbers {9} +using Furion.InstantMessaging; +using Microsoft.AspNetCore.SignalR; + +namespace Furion.Core +{ + /// + /// 聊天集线器 + /// + public class ChatHub : Hub + { + // 定义一个方法供客户端调用 + public Task SendMessage(string user, string message) + { + // 触发客户端定义监听的方法 + return Clients.All.SendAsync("ReceiveMessage", user, message); + } + } +} +``` + +- `Hub` 类型方式 + +```cs showLineNumbers +public interface IChatClient +{ + Task ReceiveMessage(string user, string message); +} +``` + +```cs showLineNumbers {1} +public class StronglyTypedChatHub : Hub +{ + // 定义一个方法供客户端调用 + public async Task SendMessage(string user, string message) + { + // 触发客户端定义监听的方法 + await Clients.All.ReceiveMessage(user, message); + } +} +``` + +通过使用 `Hub` 可以对客户端方法进行编译时检查。 这可以防止由于使用神奇字符串而导致的问题,因为 `Hub` 只能提供对在接口中定义的方法的访问。 + +### 24.6.2 `[MapHub]` 配置连接地址 + +在 `SignalR` 库中要求每一个公开的集线器都需要配置客户端连接地址,所以,`Furion` 框架提供了更加 `[MapHub]` 配置,如: + +```cs showLineNumbers {1,11} +using Furion.InstantMessaging; +using Microsoft.AspNetCore.SignalR; +using System; +using System.Threading.Tasks; + +namespace Furion.Core +{ + /// + /// 聊天集线器 + /// + [MapHub("/hubs/chathub")] + public class ChatHub : Hub + { + // ... + } +} +``` + +:::important `SignalR` 原生配置方式 + +在 `Furion` 中推荐使用 `[MapHub]` 方式配置集线器客户端连接地址,当然也可以使用 `SignalR` 提供的方式,如在 `Startup.cs` 配置: + +```cs showLineNumbers {10} +public sealed class Startup : AppStartup +{ + // 其他代码 + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + // 其他代码... + app.UseEndpoints(endpoints => + { + // 注册集线器 + endpoints.MapHub("/hubs/chathub"); + }); + } +} +``` + +::: + +### 24.6.3 `Hub` 注册更多配置 + +有些时候,我们需要注册 `Hub` 时配置更多参数,比如权限、跨域等,这时只需要在 `Hub` 派生类中编写以下静态方法即可: + +```cs showLineNumbers {1,8,13,18} +using Furion.InstantMessaging; +using Microsoft.AspNetCore.SignalR; +using System; +using System.Threading.Tasks; + +namespace Furion.Core +{ + [MapHub("/hubs/chathub")] + public class ChatHub : Hub + { + // 其他代码 + + public static void HttpConnectionDispatcherOptionsSettings(HttpConnectionDispatcherOptions options) + { + // 配置 + } + + public static void HubEndpointConventionBuilderSettings(HubEndpointConventionBuilder Builder) + { + // 配置 + } + } +} +``` + +以上配置等价于 `SignalR` 在 `Startup.cs` 中的配置: + +```cs showLineNumbers +app.UseEndpoints(endpoints => +{ + var builder = endpoints.MapHub("/hubs/chathub", options => + { + // 配置 + }); +}); +``` + +## 24.7 获取 `Hub` 实例方式 + +`SignalR` 提供了几种方式进行获取 `Hub` 实例。 + +### 24.7.1 `IHubContext` 注入方式 + +`IHubContext` 默认注册为单例模式,可在任何地方直接获取实例。 + +```cs showLineNumbers {5,12} +public class HomeController : Controller +{ + private readonly IHubContext _hubContext; + + public HomeController(IHubContext hubContext) + { + _hubContext = hubContext; + } + + public async Task Index() + { + await _hubContext.Clients.All.SendAsync("Notify", $"Home page loaded at: {DateTime.Now}"); + return View(); + } +} +``` + +### 24.7.2 `HttpContext` 解析方式 + +```cs showLineNumbers {2} + var hubContext = context.RequestServices + .GetRequiredService>(); +``` + +### 24.7.3 `IHost` 中解析方式 + +```cs showLineNumbers {6} +public class Program +{ + public static void Main(string[] args) + { + var host = CreateHostBuilder(args).Build(); + var hubContext = host.Services.GetService(typeof(IHubContext)); + host.Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => { + webBuilder.UseStartup(); + }); +} +``` + +### 24.7.4 强类型 `IHubContext` 注入方式 + +默认情况下,`IHubContext` 非泛型实例返回的是 `dynamic` 动态类型对象,该类型对象无法获得编译期语法检查和 `IDE` 智能提示,所以我们可以传入一个和自定义 `Hub` 一样的方法签名接口,如: + +```cs showLineNumbers {5,12} +public class ChatController : Controller +{ + public IHubContext _strongChatHubContext { get; } + + public ChatController(IHubContext chatHubContext) + { + _strongChatHubContext = chatHubContext; + } + + public async Task SendMessage(string user, string message) + { + await _strongChatHubContext.Clients.All.ReceiveMessage(user, message); + } +} +``` + +### 24.7.5 `IHubContext` 泛型转换 + +正常情况下,我们获取的是 `IHubContext<>` 的实例,但在一些反射场景下,可以将 `IHubContext<>` 强制转换成 `IHubContext` 从而更易于操作,如: + +```cs showLineNumbers {2,4,6-7} +var myHubContext = context.RequestServices + .GetRequiredService>(); +var myOtherHubContext = context.RequestServices + .GetRequiredService>(); + +await CommonHubContextMethod((IHubContext)myHubContext); +await CommonHubContextMethod((IHubContext)myOtherHubContext); +``` + +## 24.8 服务端和客户端双工通信 + +### 24.8.1 触发所有客户端代码 + +```cs showLineNumbers +Clients.All.客户端方法(参数); +``` + +### 24.8.2 触发调用者客户端 + +```cs showLineNumbers +Clients.Caller.客户端方法(参数); +``` + +### 24.8.3 触发除了调用者以外的客户端 + +```cs showLineNumbers +Clients.Others.客户端方法(参数); +``` + +### 24.8.4 触发特定用户客户端 + +```cs showLineNumbers +Clients.User("用户").客户端方法(参数); +``` + +### 24.8.5 触发多个用户客户端 + +```cs showLineNumbers +Clients.Users("用户","用户2",...).客户端方法(参数); +``` + +### 24.8.6 触发分组内客户端 + +```cs showLineNumbers +Clients.Group("分组").客户端方法(参数); +``` + +### 24.8.7 触发多个分组客户端 + +```cs showLineNumbers +Clients.Groups("分组","分组2",...).客户端方法(参数); +``` + +### 24.8.8 触发分组外的客户端 + +```cs showLineNumbers +Clients.GroupExcept("分组").客户端方法(参数); +``` + +## 24.9 自定义用户唯一标识 + +默认情况下 `SignalR` 会为每一个链接创建 `ConnectionId`,但是这个 `ConnectionId` 并没有和我们系统的用户绑关联起来,所以需要采用自定义 `ConnectionId`,如: + +```cs showLineNumbers {1,3} +public class YourUserIdProvider : IUserIdProvider +{ + public virtual string GetUserId(HubConnectionContext connection) + { + // 你如何获取 UserId,可以通过 connection.User 获取 JWT 授权的用户 + } +} +``` + +然后在 `Startup.cs` 中注册即可: + +```cs showLineNumbers +builder.Services.AddSingleton(); +``` + +之后就可以通过自定义 `UserId` 发送消息: + +```cs showLineNumbers +Clients.User(userId).客户端方法(参数); +``` + +查看更多文档 [https://docs.microsoft.com/zh-cn/aspnet/core/signalr/authn-and-authz?view=aspnetcore-6.0#use-claims-to-customize-identity-handling](https://docs.microsoft.com/zh-cn/aspnet/core/signalr/authn-and-authz?view=aspnetcore-6.0#use-claims-to-customize-identity-handling) + +## 24.10 分组管理 + +整理中... + +## 24.11 各个客户端连接 API + +### 24.11.1 `JavaScript` 客户端 + +整理中... + +### 24.11.2 `TypeScript` 客户端 + +#### 在 `vue3.2+` 中使用 + +1. 安装微软的 `signalr typescript` 客户端包,主要用于调用服务端方法,如( `Hub` 中的 `SendMessage` 方法): + +```bash showLineNumbers +npm i @microsoft/signalr @types/node +``` + +2. 示例代码 + +```typescript showLineNumbers +import { HubConnectionBuilder } from "@microsoft/signalr"; + + +``` + +[参考文档](https://docs.microsoft.com/zh-cn/aspnet/core/tutorials/signalr-typescript-webpack?view=aspnetcore-6.0&tabs=visual-studio) + +### 24.11.3 `.NET` 客户端 + +整理中... + +### 24.11.4 `Java` 客户端 + +整理中... + +## 24.12 常见例子 + +### 24.12.1 实现消息广播、推送 + +整理中... + +### 24.12.2 实现聊天功能 + +整理中... + +### 24.12.3 实现 `你画我来猜` + +整理中... + +## 24.13 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: + +--- + +:::note 了解更多 + +想了解更多 `SignalR` 知识可查阅 [SignalR 官方文档](https://docs.microsoft.com/zh-cn/aspnet/signalr/) 或 [ASP.NET Core SignalR](https://docs.microsoft.com/zh-cn/aspnet/core/signalr/introduction?view=aspnetcore-5.0) 章节。 + +::: diff --git a/handbook/docs/singlefile.mdx b/handbook/docs/singlefile.mdx new file mode 100644 index 0000000000000000000000000000000000000000..4449c80515af0c3e20ef613431c1bf7574bdc006 --- /dev/null +++ b/handbook/docs/singlefile.mdx @@ -0,0 +1,280 @@ +--- +id: singlefile +title: 34.5. 单文件发布 +sidebar_label: 34.5. 单文件发布 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +:::important 版本说明 + +以下内容仅限 `Furion 3.5.2 +` 版本使用。 + +::: + +## 34.5.1 历史背景 + +自 `.NET Core 3` 起,微软就提供了单文件发布的技术支持,但实际上并不是 `.NET` 所有 `CLR` 都支持单文件发布,如 `Microsoft.Extensions.DependencyModel` 包本身不支持单文件发布,原因是内部使用了 `Assembley.CodeBase`。 + +**好巧不巧**,`Furion` 中招了,在过去两年中,`Furion` 依赖该包的 `DependencyContext.Default` 特性进行程序集扫描,所以单文件发布也就成了 `Furion` 不愿提起的痛!!! + +**终于,在 `Furion v3.5.2+` 版本想出了新的解决方案,自此彻底解决了单文件发布的问题。** + +:::tip `.NET` 官方单文件发布说明 + +[https://docs.microsoft.com/zh-cn/dotnet/core/deploying/single-file/overview](https://docs.microsoft.com/zh-cn/dotnet/core/deploying/single-file/overview) + +::: + +## 34.5.2 必要配置 + +在 `Furion v3.5.2+` 版本之后,新增了 `ISingleFilePublish` 接口。 + +- **编辑启动层 `.csproj` 文件,添加下列代码到 `` 节点中** + +```xml showLineNumbers +true +``` + +:::important 关于 `ReadyToRun` + +如果发布时未打勾 `ReadyToRun` 选项,则无需配置上述代码。 + +::: + +- **在 `Web 启动层` 创建类型并实现该接口**,如: + +```cs showLineNumbers {8,15,26,28-35} +using System.Reflection; + +namespace YourProject.Web.Entry; + +/// +/// 解决单文件发布问题 +/// +public class SingleFilePublish : ISingleFilePublish +{ + /// + /// 解决单文件不能扫描的程序集 + /// + /// 可同时配置 + /// + public Assembly[] IncludeAssemblies() + { + // 需要 Furion 框架扫描哪些程序集就写上去即可 + return Array.Empty(); + } + + /// + /// 解决单文件不能扫描的程序集名称 + /// + /// 可同时配置 + /// + public string[] IncludeAssemblyNames() + { + // 需要 Furion 框架扫描哪些程序集就写上去即可 + return new[] + { + "YourProject.Application", + "YourProject.Core", + "YourProject.EntityFramework.Core", + "YourProject.Web.Core", + "Furion.Extras.ObjectMapper.Mapster" // 解决 Mapster 单文件失效问题,v3.5.3+版本后无需配置 + }; + } +} +``` + +:::tip 配置说明 + +`IncludeAssemblies` 和 `IncludeAssemblyNames` 的区别是前者是开发者直接返回 `Assembley` 集合,后者是直接返回名称,`Furion` 会自动加载程序集,**可同时配置,也可以配置其中一个。** + +如果只配置启用一个,则另外一个返回 `Array.Empty()` 或 `Array.Empty()` 即可。 + +如果发布后出现 `Mapster` 不能映射问题,可将 `Furion.Extras.ObjectMapper.Mapster` 添加到 `IncludeAssemblyNames` 集合中即可。**`v3.5.3+` 版本后无需配置。** + +::: + +## 34.5.3 发布 + + + + + +:::tip 小知识 + +如无需生成 `.pdb` 调试包可在生成中禁用即可。 + + + +::: + +## 34.5.4 自定义启动端口 + +默认单文件发布监听的是 `https://localhost:5001`,如果需要修改,可在 `program.cs` 中配置: + +```cs showLineNumbers {2} +var builder = WebApplication.CreateBuilder(args).Inject(); +builder.WebHost.UseUrls("https://*:8089"); +var app = builder.Build(); +app.Run(); +``` + +这样就可以通过 `https://localhost:8089` 访问。 + +## 34.5.5 `pm2` 守护进程部署 + +### 34.5.5.1 运行弊端 + +正常情况下,将应用程序发布成单文件后,需点击 `XXXXX.exe` 进行启动,这时候程序自动打开终端(控制台),之后根据提示在浏览器上打开对应的地址即可。 + +**但是这种方式有以下问题:** + +- 必须保证终端/控制台一直运行 +- 终端/控制台有时候会出现假死的情况,导致应用程序无法访问 +- 无法实时监听应用程序资源使用情况(如 CPU,内存,日志等) +- 无法映射端口启动 +- 集群变得复杂 + +### 34.5.5.2 `pm2` 守护进程部署 + +为了解决上述问题,推荐 `NodeJS` 一个非常强大的工具 `pm2` [https://pm2.keymetrics.io/](https://pm2.keymetrics.io/),通过该工具可以解决上述的所有问题。 + +#### 必要条件 + +1. **系统必须安装 `NodeJS` 环境 [https://nodejs.org/en/](https://nodejs.org/en/)** + +相信大部分人电脑都已经安装。 + +2. **通过 `npm` 或 `yarn` 全局安装 `pm2` 工具** + +npm: + +```bash showLineNumbers +npm install pm2@latest -g +``` + +yarn: + +```bash showLineNumbers +yarn global add pm2 +``` + +3. **启动应用程序** + +使用 `pm2` 非常简单就可以启动守护进程应用程序。 + +```bash showLineNumbers +pm2 start --name pms PMS.Web.Entry.exe +``` + +:::important 指定端口 + +如需指定端口,可使用下列命令: + +```bash showLineNumbers +pm2 start --name pms PMS.Web.Entry.exe -- --urls=https://localhost:8089 +``` + +注意 `--` 后面可以写完整的 `dotnet` 命令。 + +::: + +:::tip 命令说明 + +`pms.exe` 为项目发布后的启动层名称,如果名称包含 `空格`,则使用双引号包裹,如 `"p ms.exe"`。 + +`--name` 配置应用程序在 `pm2` 中的唯一标识。 + +::: + +`start` 后面跟着是 `.exe` 文件,在 `linux/macos` 下无需指定后缀名。 + +**启动成功后即可通过浏览器访问指定端口,通常是 `http://localhost:5000`** + +```bash showLineNumbers +PS C:\Users\bqrjsoft\Desktop\pms> pm2 start --name pms PMS.Web.Entry.exe +[PM2] Starting C:\Users\bqrjsoft\Desktop\pms\PMS.Web.Entry.exe in fork_mode (1 instance) +[PM2] Done. +┌─────┬────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐ +│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │ +├─────┼────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤ +│ 0 │ pms │ default │ N/A │ fork │ 41764 │ 0s │ 0 │ online │ 0% │ 85.0mb │ bqrjsoft │ disabled │ +└─────┴────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘ +PS C:\Users\bqrjsoft\Desktop\pms> +``` + + + +### 34.5.5.3 `pm2` 常见操作 + +- **实时监听运行状态** + +```bash showLineNumbers +pm2 monit +``` + + + +- **显示运行日志** + +```bash showLineNumbers +pm2 logs +``` + + + +- **查看应用信息** + +```bash showLineNumbers +pm2 info pms +``` + +注意,`pms` 为您配置的 `--name` 名称。 + + + +- **随机启动** + +```bash showLineNumbers +pm2 startup +pm2 save +``` + +:::tip `Windows` 下随机启动 + +可查阅 [pm2-windows-startup](https://www.npmjs.com/package/pm2-windows-startup)。 + +```bash showLineNumbers +npm install pm2-windows-startup -g +pm2-startup install +pm2 save +``` + +::: + +- **其他操作** + +```bash showLineNumbers +// 重启应用 +pm2 restart app_name + +// 重载应用 +pm2 reload app_name + +// 停止应用 +pm2 stop app_name + +// 删除应用 +pm2 delete app_name +``` + +更多 `pm2` 文档可查阅 [https://pm2.keymetrics.io/docs/usage/quick-start/](https://pm2.keymetrics.io/docs/usage/quick-start/) + +## 34.5.6 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/source.mdx b/handbook/docs/source.mdx new file mode 100644 index 0000000000000000000000000000000000000000..1b579c64e8b80e232a312f2bbdbd3c13c8328b10 --- /dev/null +++ b/handbook/docs/source.mdx @@ -0,0 +1,30 @@ +--- +id: source +title: 1.3 示例架构说明 +sidebar_label: 1.3 示例架构说明 +description: 了解 Furion 示例代码架构说明 +--- + +## 1.3.1 示例架构 + +源码仓库中的 `samples` 示例采用多层分层设计,主要设计是 `自动抽象工厂` 和 `DDD 领域驱动设计` 相结合。 + +`Furion` 示例项目结构如下: + +- `Furion`:框架核心层 +- `Furion.Application`:业务应用层(业务代码主要编写层) +- `Furion.Core`:核心层(实体,仓储,其他核心代码) +- `Furion.Database.Migrations`:EFCore 架构迁移文件层 +- `Furion.EntityFramework.Core`:EF Core 配置层 +- `Furion.Web.Core`:Web 核心层(存放 Web 公共代码,如 过滤器、中间件、Web Helpers 等) +- `Furion.Web.Entry`:Web 入口层/启动层 + +示例地址:[https://gitee.com/dotnetchina/Furion/tree/v4/samples](https://gitee.com/dotnetchina/Furion/tree/v4/samples) + +## 1.3.2 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/specification-document.mdx b/handbook/docs/specification-document.mdx new file mode 100644 index 0000000000000000000000000000000000000000..ed42cae7326a68b8f3381dc96f3e32b763c36a82 --- /dev/null +++ b/handbook/docs/specification-document.mdx @@ -0,0 +1,1940 @@ +--- +id: specification-document +title: 6. 规范化接口文档 +sidebar_label: 6. 规范化接口文档 (Swagger) +--- + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **新特性** + + -  新增 规范化处理自动过滤 `SSE` 请求、文件请求、图片请求 4.9.1.6 ⏱️2023.11.22 [#I8IP6D](https://gitee.com/dotnetchina/Furion/issues/I8IP6D) + -  新增 规范化文档枚举支持 `[EnumToNumber]` 特性配置生成前端枚举定义代码是字符串值还是整数值类型,默认为字符串值 4.8.8.35 ⏱️2023.07.06 [#I7IZ7S](https://gitee.com/dotnetchina/Furion/issues/I7IZ7S) + -  新增 `Swagger` 分组信息可在任意配置文件中通过 `[openapi:分组名]` 进行配置 4.8.8.26 ⏱️2023.06.20 [a70eed3](https://gitee.com/dotnetchina/Furion/commit/a70eed3ec5f3081fbdc08312fdb4770f39f27cc0) + -  新增 `Swagger` 请求授权失败后自动退出授权状态 4.8.6.12 ⏱️2023.02.20 [!717](https://gitee.com/dotnetchina/Furion/pulls/717) + -  新增 `Swagger` 启用登录后配置 `CheckUrl` 可获取本地存储的 `Authorization` 请求报文头 4.8.6.2 ⏱️2023.02.10 [#I6E3LB](https://gitee.com/dotnetchina/Furion/issues/I6E3LB) + -  新增 `Swagger` 支持复制路由地址功能 4.8.4.13 ⏱️2023.01.11 [#I5VNJI](https://gitee.com/dotnetchina/Furion/issues/I5VNJI) + +- **问题修复** + + -  修复 `Swagger` 进行分组后 `Tags` 不能进行分组过滤问题 4.8.8.22 ⏱️2023.05.25 [#I78A55](https://gitee.com/dotnetchina/Furion/issues/I78A55) + -  修复 因 `4.8.7.22` 版本导致动态 `WebAPI` 类型注释丢失问题 4.8.7.27 ⏱️2023.03.29 [#I6QM23](https://gitee.com/dotnetchina/Furion/issues/I6QM23) + -  修复 `Swagger UI` 不显示 `ControllerBase` 派生类注释 4.8.7.22 ⏱️2023.03.27 [#I6QM23](https://gitee.com/dotnetchina/Furion/issues/I6QM23) + -  修复 启用规范化结果后导致 `WebSocket` 连接断开时出现异常 4.8.7.20 ⏱️2023.03.23 [#I6PI5E](https://gitee.com/dotnetchina/Furion/issues/I6PI5E) + -  修复 `Swagger` 接口排序同时指定 `Tag` 和 `Order` 之后无效 4.8.7.2 ⏱️2023.03.01 [#I6IQDI](https://gitee.com/dotnetchina/Furion/issues/I6IQDI) [#I6IP66](https://gitee.com/dotnetchina/Furion/issues/I6IP66) + -  修复 规范化结果不带 `mini-profiler` 版本启动登录 `UI` 后不能传递 `headers` 问题 4.8.6.11 ⏱️2023.02.20 [#I6G8IR](https://gitee.com/dotnetchina/Furion/issues/I6G8IR) + -  修复 **规范化结果不支持 `OData` 协议控制器** 4.8.5.5 ⏱️2023.02.01 [!571](https://gitee.com/dotnetchina/Furion/pulls/571) + -  修复 启用 `Swagger` 登录功能之后不能触发响应拦截器 4.8.5.5 ⏱️2023.02.01 [#I6C9A2](https://gitee.com/dotnetchina/Furion/issues/I6C9A2) [!702](https://gitee.com/dotnetchina/Furion/pulls/702) [!703](https://gitee.com/dotnetchina/Furion/pulls/703) + +- **其他调整** + + -  调整 规范化文档枚举生成 `json` 格式,由 `int32` 改为 `string` 4.8.8.34 ⏱️2023.07.02 [#I7HOPR](https://gitee.com/dotnetchina/Furion/issues/I7HOPR) + -  调整 规范化文档默认 `Title` 解析规则,不再自动添加空格 4.8.8.26 ⏱️2023.06.20 [24b7a47](https://gitee.com/dotnetchina/Furion/commit/24b7a4768471d312cbdff6a31739a0d9d4918c83) + +
+
+
+ +import useBaseUrl from "@docusaurus/useBaseUrl"; +import TabItem from "@theme/TabItem"; +import Tabs from "@theme/Tabs"; + +## 6.1 什么是接口文档 + +在现在移动为王、多端互辅、前端百花齐放的开放时代,不再是一人包揽式开发,大家各司其职,后端工程师负责接口开发,前端负责接口联调,也就是互联网现在流行的前后端分离架构。 + +所以就需要由前后端工程师共同定义接口,编写接口文档,之后大家按照这个接口文档进行开发、维护及开放给第三方。 + +## 6.2 为什么要写接口文档 + +- 能够让前端开发与后台开发人员更好的配合,提高工作效率 +- 项目迭代或者项目人员更迭时,方便后期人员查看和维护 +- 方便测试人员进行接口测试 + +## 6.3 为什么需要规范化文档 + +由于每个公司后端人员技术参差不齐,技术文档能力也不例外,导致接口定义及文档五花八门,不同项目或不同公司对接极其困难,而且体验糟糕。所以,无规矩不成方圆,为了开发人员间更好的配合,迫切需要整理出一套规范。 + +通常接口规范分为六个部分: + +### 6.3.1 协议规范 + +为了确保不同系统/模块间的数据交互,需要事先约定好通讯协议,如:TCP、HTTP、HTTPS 协议。为了确保数据交互安全,建议使用 HTTPS 协议 + +### 6.3.2 接口路径规范 + +作为接口路径,为了方便清晰的区分来自不同的系统,可以采用不同系统/模块名作为接口路径前缀,如:支付模块:`/pay/xxx`,订单模块:`/order/xxx` + +### 6.3.3 版本控制规范 + +为了便于后期接口的升级和维护,建议在接口路径中加入版本号,便于管理,实现接口多版本的可维护性。如:接口路径中添加类似"`v1`"、"`v2`"等版本号 + +### 6.3.4 接口命名规范 + +和 C# 命名规范一样,好的、统一的接口命名规范,不仅可以增强其可读性,而且还会减少很多不必要的口头/书面上的解释。可使用"驼峰命名法"按照实现接口的**业务类型、业务场景**等命名,有必要时可采取多级目录命名,但目录不宜过长,两级目录较为适宜 + +- `常见命名方式`: + - `接口名称动词前/后缀化`: 接口名称以接口数据操作的动词为前/后缀,常见动词有:`Add、Delete、Update、Query、Get、Send、Save、Detail、List`等,如:新建用户 `AddUser`、查询订单详情 `QueryOrderDetail`。 + - `接口名称动词 + 请求方式`:接口路径中包含具体接口名称的名词,接口数据操作动作以 HTTP 请求方式来区分。常用的 HTTP 请求方式有: + - `GET`:从服务器取出资源(一项或多项) + - `POST`:在服务器新建一个资源 + - `PUT`:在服务器更新资源(客户端提供改变后的完整资源) + - `PATCH`:在服务器更新资源(客户端提供改变的属性) + - `DELETE`:从服务器删除资源 + +### 6.3.5 请求参数规范 + +- `请求方式`:按照 `GET、POST、PUT` 等含义定义,避免出现不一致现象,对人造成误解、歧义 + - `请求头`:请求头根据项目需求添加配置参数。如:请求数据格式,`accept=application/json` 等。如有需要,请求头可根据项目需求要求传入用户 token、唯一验签码等加密数据 + - `请求参数/请求体`: 请求参数字段,尽可能与数据库表字段、对象属性名等保持一致,因为保持一致是最省事,最舒服的一件事 + +### 6.3.6 返回数据规范 + +统一规范返回数据的格式,对己对彼都有好处,此处以 json 格式为例。返回数据应包含:**返回状态码、返回状态信息、具体数据**。**返回数据中的状态码、状态信息,常指具体的业务状态,不建议和 HTTP 状态码混在一起**。HTTP 状态,是用来体现 HTTP 链路状态情况,如:404-Not Found。HTTP 状态码和 json 结果中的状态码,并存尚可,用于体现不同维度的状态。 + +## 6.4 什么是 Swagger + +相信无论是前端还是后端开发,都或多或少地被接口文档折磨过。前端经常抱怨后端给的接口文档与实际情况不一致。后端又觉得编写及维护接口文档会耗费不少精力,经常来不及更新。 + +其实无论是前端调用后端,还是后端调用后端,都期望有一个好的接口文档。但是这个接口文档对于程序员来说,就跟注释一样,经常会抱怨别人写的代码没有写注释,然而自己写起代码起来,最讨厌的,也是写注释。所以仅仅只通过强制来规范大家是不够的,随着时间推移,版本迭代,接口文档往往很容易就跟不上代码了。 + +**发现了痛点就要去找解决方案。解决方案用的人多了,就成了标准的规范,这就是 `Swagger` 的由来**。 + +通过这套规范,你只需要按照它的规范去定义接口及接口相关的信息。再通过 `Swagger` 衍生出来的一系列项目和工具,就可以做到生成各种格式的接口文档,生成多种语言的客户端和服务端的代码,以及在线接口调试页面等等。 + +这样,如果按照新的开发模式,在开发新版本或者迭代版本的时候,只需要更新 `Swagger` 描述文件,就可以自动生成接口文档和客户端服务端代码,做到调用端代码、服务端代码以及接口文档的一致性。 + +所以,Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化`RESTful` 风格的 `Web` 服务。 + +总体目标是使客户端和文件系统作为服务器以同样的速度来更新。文件的方法、参数和模型紧密集成到服务器端的代码,允许 API 来始终保持同步。`Swagger` 让部署管理和使用功能强大的 `API` 从未如此简单。 + +## 6.5 Swagger 使用 + +`Furion` 框架提供了非常方便且灵活的 `Swagger` 配置,无需增加额外学习成本。 + +### 6.5.1 注册服务 + +:::tip 小提示 + +`.AddInject[XXX]()` 已经包含了 `.AddSpecificationDocuments()` 注册,**无需再次注册**。 + +`.UseInject()` 已经包含了 `.UseSpecificationDocuments()` 注册,**无需再次注册**。 + +::: + +```cs showLineNumbers {13,21} title="Furion.Web.Core\Startup.cs" +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Furion.Web.Core +{ + [AppStartup(800)] + public sealed class FurWebCoreStartup : AppStartup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddSpecificationDocuments(); + services.AddControllers(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + // Other Codes + + app.UseSpecificationDocuments(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } +} +``` + +:::tip 小知识 + +`services.AddSpecificationDocuments()` 通常和 `.AddDynamicApiControllers()` 成对出现。 + +::: + +### 6.5.2 默认地址 + +在 `Furion` 框架中,默认 `规范化文档` 地址为 `/api` 目录,**支持自定义配置**。 + +如下图所示: + + + +可以通过两种方式配置: + +- `app.UseInject("路由")` 方式,如 + +```cs showLineNumbers +app.UseInject("testapi"); // 那么 /testapi 就是规范化地址 +``` + +- `配置文件配置`: + +```cs showLineNumbers {3} +{ + "SpecificationDocumentSettings": { + "RoutePrefix": "testapi" + } +} +``` + +**配置文件优先级大于 `UseInject()` 方式** + +### 6.5.3 默认分组 + +`Furion` 框架中默认分组名为 `Default`,**支持自定义配置**。 + +```cs showLineNumbers {3} +{ + "SpecificationDocumentSettings": { + "DefaultGroupName": "MyGroup" + } +} +``` + +### 6.5.4 文档注释 + +规范化文档默认扫描 `Furion.Application`,`Furion.Web.Core`,`Furion.Web.Entry` 三个程序集`.xml` 注释文件,**支持自定义配置**。 + +只支持 `///` 标识的注释语法,如:**类、方法、属性、参数、返回值、验证特性**。 + +```cs showLineNumbers {5-7,10-13,19-23} +using Furion.DynamicApiController; + +namespace Furion.Application +{ + /// + /// 类注释 + /// + public class FurionAppService : IDynamicApiController + { + /// + /// 方法注释 + /// + /// + public string Get() + { + return nameof(Furion); + } + + /// + /// 带 ID 参数的方法注释 + /// + /// + /// + public int Get(int id) + { + return id; + } + } +} +``` + +如下图所示: + + + +:::note 小提示 + +如果文档注释没有显示,请检查项目 `属性->生成->输出` 中 XML 文档是否配置输出路径。**注意:只有不带路径的 【项目名称.xml】 才会自动加载。** + +::: + +:::info 特别说明 + +`Debug` 模式下和 `Release` 模式下的注释文件是不通用的,所以导致很多开发者发布到服务器上发现没有显示注释。我们只需要在 `Visual Studio` 中切换 `Debug` 模式为 `Release`,然后重新配置一次即可。 + +这样不管是 `Debug` 还是 `Release` 模式都会显示注释了。 + +::: + +### 6.5.5 多分组支持 + +多分组是一个系统中必备功能,我们可以将系统划分为多个模块,每个模块都独立的 `api` 配置。在 `Furion` 框架中,实现多分组非常简单。如: + +```cs showLineNumbers {5,21,32} +using Furion.DynamicApiController; + +namespace Furion.Application +{ + [ApiDescriptionSettings("Group1")] + public class FurionAppService : IDynamicApiController + { + /// + /// 随父类 Group1 分组 + /// + /// + public string Post() + { + return nameof(Furion); + } + + /// + /// 在 Group1、Group3 都有我 + /// + /// + [ApiDescriptionSettings("Group1", "Group3")] + public string Get() + { + return nameof(Furion); + } + + /// + /// 我只在 Group2 出现 + /// + /// + /// + [ApiDescriptionSettings("Group2")] + public int Get(int id) + { + return id; + } + } +} +``` + +如下图所示: + + + +### 6.5.6 多分组排序 + + + + +**通过分组名称添加 `@整数` 进行排序** + +```cs showLineNumbers {5,19} +using Furion.DynamicApiController; + +namespace Furion.Application +{ + [ApiDescriptionSettings("Group1@1")] + public class FurionAppService : IDynamicApiController + { + public string Post() + { + return nameof(Furion); + } + + [ApiDescriptionSettings("Group1", "Group3")] + public string Get() + { + return nameof(Furion); + } + + [ApiDescriptionSettings("Group@2")] + public int Get(int id) + { + return id; + } + } +} +``` + +可以通过在分组名后面添加 `@整数` 进行排序,`整数` 越大排前面。如果分组名称多次指定且多次指定了 `@整数` ,则自动**取该分组最大的整数**进行排序。 + + + + +**通过配置文件配置排序** + +```json showLineNumbers {2-17} title="Furion.Web.Entry/appsettings.json" +{ + "SpecificationDocumentSettings": { + "GroupOpenApiInfos": [ + { + "Group": "Group1", + "Order": 1 + }, + { + "Group": "Group2", + "Order": 2 + }, + { + "Group": "Group3", + "Order": 0 + } + ] + } +} +``` + + + + +如下图所示: + + + +:::tip 排序说明 + +分组默认排序 `Order` 为 `0`。如果同时配置了 `@整数` 和 `appsettings.json` 配置文件,那么优先采用 `appsettings.json` 中的 `Order` + +::: + +### 6.5.7 多分组信息配置 + +`Furion` 框架提供了可通过 `appsetting.json` 配置分组信息: + +```json showLineNumbers {3-20} title="Furion.Web.Entry/appsettings.json" +{ + "SpecificationDocumentSettings": { + "GroupOpenApiInfos": [ + { + "Group": "Group1", + "Title": "分组标题", + "Description": "这里是分组描述", + "Version": "版本号", + "TermsOfService": "http://furion.baiqian.ltd", + "Contact": { + "Name": "百小僧", + "Url": "https://gitee.com/monksoul", + "Email": "monksoul@outlook.com" + }, + "License": { + "Name": "MIT", + "Url": "https://gitee.com/dotnetchina/Furion/blob/alpha/LICENSE" + } + } + ] + } +} +``` + +如下图所示: + + + +--- + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.8.26 +` 版本使用。 + +::: + +自该版本开始,可以在任何配置文件中添加 `[openapi:分组名]` 作为 `Key` 进行配置,而无需在 `SpecificationDocumentSettings:GroupOpenApiInfos` 中指定。 + +如果两种方式同时配置,那么 `GroupOpenApiInfos` 中的属性值只有在 `[openapi:分组名]` 配置中存在才会覆盖,不存在则保留。 + +```json showLineNumbers {2} +{ + "[openapi:Group1]": { + "Title": "我是自定义的标题", + "Order": 10, + "Description": "我是自定义的描述", + "Version": "2.0.0", + "TermsOfService": "https://furion.net", + "Contact": { + "Name": "Furion.NET", + "Url": "https://gitee.com/monksoul", + "Email": "support@furion.net" + }, + "License": { + "Name": "MIT", + "Url": "https://gitee.com/dotnetchina/Furion/blob/v4/LICENSE" + } + } +} +``` + +### 6.5.8 控制器和方法排序 + +有时候我们需要对控制器或者方法进行排序,框架提供了 `[ApiDescriptionSettings]` 特性的 `Order` 属性,**其值越大排序越靠前**,如: + +```cs showLineNumbers {1,9,15,22} +[ApiDescriptionSettings(Order = 10)] +public class FurionAppService : IDynamicApiController +{ + public string Post() + { + return nameof(Furion); + } + + [ApiDescriptionSettings(Order = 5)] + public string Get() + { + return nameof(Furion); + } + + [ApiDescriptionSettings(Order = 4)] + public int Get(int id) + { + return id; + } +} + +[ApiDescriptionSettings(Order = 9)] +public class Furion2AppService : IDynamicApiController +{ + // ... +} + +``` + +:::tip 排序说明 + +最终输出到 `Swagger` 界面时,`FurionAppService` 比 `Furion2AppService` 靠前,而 `FurionAppService` 中定义的方法排序时:`Get` > `Get(int id)` > `Post`。 + +::: + +### 6.5.9 组中组(标签) + +`Tag` 配置主要用于配置 `Swagger` 标签分组信息及合并标签。也就是 `组中组`: + + + + +#### 未贴标签之前 + +```cs showLineNumbers +using Furion.DynamicApiController; + +namespace Furion.Application +{ + public class FurionAppService : IDynamicApiController + { + public string Get() + { + return nameof(Furion); + } + + public int Get(int id) + { + return id; + } + } + + public class TestAppService : IDynamicApiController + { + public string Get() + { + return nameof(Furion); + } + + public int Get(int id) + { + return id; + } + } +} +``` + +#### 贴标签之后 + +```cs showLineNumbers {5,19} +using Furion.DynamicApiController; + +namespace Furion.Application +{ + [ApiDescriptionSettings(Tag = "分组一")] + public class FurionAppService : IDynamicApiController + { + public string Get() + { + return nameof(Furion); + } + + public int Get(int id) + { + return id; + } + } + + [ApiDescriptionSettings(Tag = "分组二")] + public class TestAppService : IDynamicApiController + { + public string Get() + { + return nameof(Furion); + } + + public int Get(int id) + { + return id; + } + } +} +``` + +如下图所示: + + + + + + +```cs showLineNumbers {5,19} +using Furion.DynamicApiController; + +namespace Furion.Application +{ + [ApiDescriptionSettings(Tag = "合并所有标签")] + public class FurionAppService : IDynamicApiController + { + public string Get() + { + return nameof(Furion); + } + + public int Get(int id) + { + return id; + } + } + + [ApiDescriptionSettings(Tag = "合并所有标签")] + public class TestAppService : IDynamicApiController + { + public string Get() + { + return nameof(Furion); + } + + public int Get(int id) + { + return id; + } + } +} +``` + +如下图所示: + + + + + + +:::tip 小知识 + +如果 `Tag` 名字一样,则会自动合并,否则只是命名。 + +::: + +### 6.5.10 默认展开所有文档 + +```json showLineNumbers {2-4} title="Furion.Web.Entry/appsettings.json" +{ + "SpecificationDocumentSettings": { + "DocExpansionState": "Full" + } +} +``` + +如下图所示: + + + +`DocExpansionState` 配置说明: + +- `List`:列表式(展开子类),**默认值** +- `Full`:完全展开 +- `None`:列表式(不展开子类) + +### 6.5.11 配置文档标题 + +```json showLineNumbers {2-4} title="Furion.Web.Entry/appsettings.json" +{ + "SpecificationDocumentSettings": { + "DocumentTitle": "我是自定义标题" + } +} +``` + +如下图所示: + + + +### 6.5.12 授权控制 + +**新版本 `Furion` 已经默认启用了 Bearer Token 授权配置,无需手动配置**,如需手动配置,可手动添加以下类似配置: + +```json showLineNumbers title="Furion.Web.Entry/appsettings.json" +{ + "SpecificationDocumentSettings": { + "EnableAuthorized": true, + + "SecurityDefinitions": [ + { + "Id": "Bearer", + "Type": "Http", + "Name": "Authorization", + "Description": "JWT Authorization header using the Bearer scheme.", + "BearerFormat": "JWT", + "Scheme": "bearer", + "In": "Header", + + "Requirement": { + "Scheme": { + "Reference": { + "Id": "Bearer", + "Type": "SecurityScheme" + }, + "Accesses": [] + } + } + } + ] + } +} +``` + +### 6.5.13 在线测试 + +如下图所示: + + + +### 6.5.14 性能监视 `MiniProfiler` + +规范化文档默认集成了 `MiniProfiler` 第三方性能组件,通过该组件可以方便查看请求性能、异常堆栈、数据库操作等信息。默认在 `Swagger` 首页左上角显示。 + +如下图所示: + + + +:::note 小提示 + +也可以通过 **`appsetting.json`** 中 **`AppSettings:InjectMiniProfiler`** 设为 **`false`** 关闭。 + +::: + +### 6.5.15 定义接口输出类型 + +```cs showLineNumbers {2,8-9} +using Furion.DynamicApiController; +using Microsoft.AspNetCore.Mvc; + +namespace Furion.Application +{ + public class FurionAppService : IDynamicApiController + { + [ProducesResponseType(201, Type = typeof(TestDto))] + [ProducesResponseType(400)] + public string Get() + { + return nameof(Furion); + } + } +} +``` + +如下图所示: + + + +### 6.5.16 隐藏特定分组 + +`Furion` 新版本提供了隐藏分组的 `Visible` 配置,设置为 `false` 之后该分组将不显示在规范化文档中,如: + +```json showLineNumbers title="appsetting.json" +{ + "SpecificationDocumentSettings": { + "GroupOpenApiInfos": [ + { + "Group": "Group1", + "Visible": false + } + ] + } +} +``` + +### 6.5.17 中文乱码问题 + +默认情况下,`.json` 文件并未采用 `utf-8` 编码,所以如果配置中文分组信息就会出现乱码情况,这时候,只需要修改 `.json` 文件编码为 `utf-8` 即可。 + +### 6.5.18 生产环境中关闭 `Swagger` + +如果不需要线上环境开启 `Swagger` 功能,只需要在 `appsetting.json` 配置即可: + +```json showLineNumbers {3} +{ + "AppSettings": { + "InjectSpecificationDocument": false + } +} +``` + +### 6.5.19 设置 `Example Value` 默认值 + +`Swagger` 会自动根据对象类型输入参数添加 `Example Value` 默认值,但是该默认值通常是对象属性的类型字符串或缺省值,如果我们需要自定义这些默认值,只需要添加 `/// 默认值` 注释即可。 + +如: + +```cs showLineNumbers {4} +/// +/// 年龄 +/// +/// 13 +[Required, Range(10, 110)] +public int Age { get; set; } +``` + +如下图所示: + + + +更多类型默认值设置示例: + +- `bool` 类型:`/// true` +- `string` 类型:`/// foobar` +- `number` 类型:`/// 123` +- `null` 类型: `/// null` +- `array` 类型:`/// [ 1, 2, 3 ]` + +:::important 关于 `object` 类型输入参数 + +默认情况下,`Example Value` 不会显示 `object` 类型的对象属性,因为 `Swagger` 认为这是不合理的定义。如果需要强制显示,只需要添加 `/// "object"` 注释即可。 + +::: + +### 6.5.20 自定义 `Swagger` 配置 + +`Furion` 框架除了内置了不少配置以外,还提供了直接配置 `Swagger` Api 的参数,如: + +```cs showLineNumbers {5} +public void ConfigureServices(IServiceCollection services) +{ + services.AddInject(options => + { + options.ConfigureSwaggerGen(gen => + { + // .... + }); + }); +} +``` + +```cs showLineNumbers {6,11} + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + // 如果使用 Furion 4.4.8+ 版本可以 app.UseInject(options => {}) 了,无需指定 configure: + app.UseInject(configure: options => + { + options.ConfigureSwagger(swg => + { + // .... + }); + + options.ConfigureSwaggerUI(ui => + { + // .... + }); + }); +} + +``` + +### 6.5.21 配置 `Swagger`的 `SchemaIds` + +`Furion` 框架默认只显示名称,如果需要自定义显示规则,只需要添加配置即可。 + +```cs showLineNumbers {4,6} +services.AddControllersWithViews() + .AddInject(options => + { + options.ConfigureSwaggerGen(gen => + { + gen.CustomSchemaIds(x => x.FullName); + }); + }); +``` + +### 6.5.22 自定义 `swagger.json` 路由模板 + +默认情况下,`Furion` 框架会生成统一的分组模板,如:`swagger/{documentName}/swagger.json`,`{documentName}` 会在运行时替换为分组名,如需自定义,配置 `RouteTemplate` 即可: + +```json showLineNumbers title="appsetting.json" +{ + "SpecificationDocumentSettings": { + "RouteTemplate": "myapp/{documentName}/xxxx.json" + } +} +``` + +### 6.5.23 关于 `application/x-www-form-urlencoded` 请求 + +默认情况下,`Swagger` 并未添加 `application/x-www-form-urlencoded` 支持,如需启用该配置,只需在方法顶部贴 `[Consumes]` 特性即可,如: + +```cs showLineNumbers {1,2} +[Consumes("application/x-www-form-urlencoded")] +public async Task Test([FromForm] TestRequest testRequest) +{ + // .... +} + +public class TestRequest +{ + public string TestValue { get; set; } +} +``` + +:::important 特别注意 + +参数必须贴 `[FromForm]` 特性。另外请求时将参数按 `URL` 地址拼接并放在 `Body` 中请求。 + +::: + +### 6.5.24 `Swagger` 出现 `CORS` 问题解决 + +如果 `Swagger` 出现以下错误,如图: + + + +则需要添加以下配置: + +```json showLineNumbers {2,3} +{ + "SpecificationDocumentSettings": { + "HideServers": true + } +} +``` + +### 6.5.25 `Swagger` 出现默认 `text/xml`/`text/plain` 参数问题解决 + +**产生此原因有两个必要条件:** + +1. 使用了 `Microsoft.AspNetCore.Mvc.NewtonsoftJson` 包并添加了 `AddNewtonsoftJson()` 注册。 +2. `.AddNewtonsoftJson()` 写在了 `.AddInjectWithUnifyResult()` 后面。 + +所以解决方法是,先注册 `.AddNewtonsoftJson()` 再注册规范化结果,如: + +```cs showLineNumbers {2} +services.AddControllers() + .AddNewtonsoftJson() + .AddInjectWithUnifyResult(); +``` + +### 6.5.26 `Swagger` 多语言支持 + +在 `Furion 2.9.0 + ` 版本已经支持了 `Swagger` 文档地址 `?culture=en-US` 参数多语言转发功能了,也就是 `Swagger` 地址带 `?culture=` 参数将自动添加到每一个请求的 `api` 地址中。 + +### 6.5.27 自定义逻辑控制 `Swagger` 每一个 `api` 可见性 + +有时候我们需要自定义 `Swagger` 接口可见性,比如根据权限,不同用户类型,各种逻辑控制,如: + +```cs showLineNumbers {4,6} +// 也可以用 .AddInjectWithUnifyResult +services.AddInject(options => +{ + options.ConfigureSwaggerGen(gen => + { + gen.DocInclusionPredicate((currentGroup, apiDescription) => + { + // Furion 内部检查,必须放第一行 + var isShow = SpecificationDocumentBuilder.CheckApiDescriptionInCurrentGroup(currentGroup, apiDescription); + + // 获取当前方法 + _ = apiDescription.TryGetMethodInfo(out var method); + + // 有了方法,这里做你想做的事情,isShow 设置 true 可见,设置 false 不可见 + + return isShow; + }); + }); +}); +``` + +### 6.5.28 配置 `MVC` 控制器支持规范化处理 + +```json showLineNumbers +{ + "UnifyResultSettings": { + "SupportMvcController": true + } +} +``` + +### 6.5.29 `Swagger` 刷新记住授权状态 + +默认情况下,`Swagger` 刷新浏览器后,授权状态将被重置,也就是需要重新登录,通过下面代码在 **用户登录成功后** 调用即可: + +```cs showLineNumbers +// ....验证用户名/密码.... + +_httpContextAccessor.HttpContext.SigninToSwagger("传入 token"); +``` + +### 6.5.30 带登录的 `Swagger` 文档 + +:::important 版本说明 + +以下内容仅限 `Furion v3.3.3+` 版本使用。 + +::: + +默认情况下,`Swagger` 是任何人都可以访问的,这样也暴露出一些安全问题,所以在该版本之后添加了登录功能,只需要配置 `SpecificationDocumentSettings` 的 `LoginInfo` 即可: + +```json showLineNumbers {2-6} +{ + "SpecificationDocumentSettings": { + "LoginInfo": { + "Enabled": true, + "CheckUrl": "/Home/CheckUrl", + "SubmitUrl": "/Home/SubmitUrl", + "UserName": "admin", + "Password": "admin" + } + } +} +``` + +#### 配置说明 + +- `LoginInfo`:配置 `Swagger` 是否需要登录才能访问,`SpecificationLoginInfo` 类型,默认 `null`,**仅在 Furion v3.3.3+` 有效** + - `Enabled`:是否启用登录授权,默认 `false` + - `CheckUrl`:检查登录状态的 `Url` 地址,**该地址必须是 `POST` 请求**,已授权返回 `200`,否则返回 `401`,支持相对地址,以 `/` 开头 + - `SubmitUrl`:提交登录的 `Url` 地址,**该地址必须是 `POST` 请求且只有一个 `SpecificationAuth` 类型参数**,成功登录返回 `200`,否则返回 `401`,支持相对地址,以 `/` 开头 + +#### 配置示例 + +```cs showLineNumbers {10-14,16-31} +using Furion.SpecificationDocument; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.ComponentModel.DataAnnotations; + +namespace Furion.Web.Entry.Controllers; + +public class HomeController : Controller +{ + [HttpPost, AllowAnonymous, NonUnify] + public int CheckUrl() + { + return 401; + } + + [HttpPost, AllowAnonymous, NonUnify] + public int SubmitUrl([FromForm] SpecificationAuth auth) + { + // 读取配置信息 + var userName = App.Configuration["SpecificationDocumentSettings:LoginInfo:UserName"]; + var password = App.Configuration["SpecificationDocumentSettings:LoginInfo:Password"]; + + if (auth.UserName == userName && auth.Password == password) + { + return 200; + } + else + { + return 401; + } + } +} +``` + + + +### 6.5.31 `inheritdoc` 实现注释继承 + +:::important 版本说明 + +以下内容仅限 `Furion v3.3.3+` 版本使用。 + +::: + +在过去我们在接口定义的时候编写了完整的成员注释,然后在派生成员中又要重复写一次,实际上做了很大无用功,现在 `Furion` 支持了注释继承了,同时 `Swagger` 中也能正确显示。 + +```cs showLineNumbers {5,8,14} +using Furion.DynamicApiController; + +namespace Furion.Application +{ + /// + public class TestInheritdoc : ITestInheritdoc, IDynamicApiController + { + /// + public string GetName() + { + return "Furion"; + } + + /// + public string GetVersion() + { + return "3.3.3"; + } + } + + /// + /// 测试注释继承 + /// + public interface ITestInheritdoc + { + /// + /// 获取名称 + /// + /// + string GetName(); + + /// + /// 获取版本 + /// + /// + string GetVersion(); + } +} + +``` + +显示效果: + + + +:::important 注意事项 + +`` 不指定 `cref` **仅限成员可用**且所在的类型必须指定 ``,这样才能自动识别。 + +::: + +### 6.5.32 启用 `All Groups` 分组功能 + +:::important 版本说明 + +以下内容仅限 `Furion v3.3.4+` 版本使用。 + +::: + +有时候我们为了更好的对接口进行归类,配置了 `Swagger` 多个分组的功能,但这样也对生成客户端请求代码造成了困扰,所以添加了新的配置: + +```json showLineNumbers {2-3} +{ + "SpecificationDocumentSettings": { + "EnableAllGroups": true + } +} +``` + +### 6.5.33 接口过时控制 + +:::important 版本说明 + +以下内容仅限 `Furion v3.3.5+` 版本使用。 + +::: + +有时候我们某个接口已经过时,提示尽早调用最新接口,只需要在方法上面贴 `[Obsolete]` 即可,如: + +```cs showLineNumbers {1,7} +[Obsolete("GetName() 已经过时,请调用 GetFrameworkName() 替代")] +public string GetName() +{ + return nameof(Furion); +} + +[Obsolete] +public string Other() +{ + // ... +} +``` + + + +### 6.5.34 单一接口更多描述 + +:::important 版本说明 + +以下内容仅限 `Furion v3.3.5+` 版本使用。 + +::: + +在该版本新增了 `[ApiDescriptionSettings]` 的 `Description` 属性,支持定义更多描述,如: + +```cs showLineNumbers {1} +[ApiDescriptionSettings(Description = "我是一段描述,显示更多内容 ")] +public string add() +{ + //.... +} +``` + + + +### 6.5.35 `Swagger` 异常/不能显示/错误处理 + +有时候可能因为错误的配置导致 `Swagger` 不能显示,这时候只需要复制提示的错误 `.json` 链接地址到浏览器中访问即可,如: + +```bash showLineNumbers +https://localhost:你的端口/swagger/Default/swagger.json +``` + +后面的 `/swagger/Default/swagger.json` 就是 `Swagger` 错误提示的 `.json` 链接地址。 + +这样就可以看到详细的错误了。 + + + + + +### 6.5.36 自定义 `Swagger` 的 `SchemaId` + +:::important 版本说明 + +以下内容仅限 `Furion v3.6.4+` 版本使用。 + +::: + +有时候,不同程序集会定义相同的类型名称 `Name`,这样就会导致生成 `Swagger` 的 `SchemaId` 出现冲突,这时只需要在类型上贴 `[SchemaId]` 特性即可,如: + +```cs showLineNumbers {3} +using Furion.SpecificationDocument; + +[SchemaId("Other_")] +public class PersonDto +{ + // ... +} +``` + +- `SchemaIdAttribute` 配置选项: + - `SchemaId`:自定义 `SchemaId`,字符串类型,只能是 `字母,数字,下划线` 组合 + - `Replace`:是否完全替换,`bool` 类型,默认 `false`,默认是作为前缀拼接,如上面的 `PersonDto` 会生成 `Other_PersonDto`,如果设置为 `true`,则直接使用 `Other_` + +### 6.5.37 自定义 `Swagger` 的 `OperationId` + +:::important 版本说明 + +以下内容仅限 `Furion 4.1.7+` 版本使用。 + +::: + +通过我们根据 `swagger.json` 生成前端代码时,`Swagger` 会自动根据路由地址生成调用的 `api` 名称,但这样的名称往往不易读,这时候可自定义 `[OperationId]` 来配置生成的前端名称。 + +```cs showLineNumbers {5} +using Furion.SpecificationDocument; + +public class PersonDto +{ + [OperationId("MyClientMethodName")] + public string TestMethod() + { + // ... + } +} +``` + +### 6.5.38 `Swagger` 接口文档支持完整的 `Markdown` + +在 `Furion` 最新版中,支持了完整的 `Markdown` 注册,如: + +````cs showLineNumbers {4,80} +/// +/// 测试 Markdown +/// +/// +/// # 先知 / Furion ([探索版](https://gitee.com/dotnetchina/Furion/tree/experimental/)) +/// +/// 一个应用程序框架,您可以将它集成到任何.NET/C# 应用程序中。 +/// +/// An application framework that you can integrate into any.NET/C# application. +/// +/// ## 安装 / Installation +/// +/// - [Package Manager] (https://www.nuget.org/packages/Furion) +/// +/// ```powershell +/// Install-Package Furion +/// ``` +/// +/// - [.NET CLI] (https://www.nuget.org/packages/Furion) +/// +/// ```powershell +/// dotnet add package Furion +/// ``` +/// +/// ## 例子 / Examples +/// +/// 我们在[主页](http://furion.baiqian.ltd)上有不少例子,这是让您入门的第一个: +/// +/// We have several examples [on the website] (http://furion.baiqian.ltd). Here is the first one to get you started: +/// +/// ```cs +/// Serve.Run(); +/// +/// [DynamicApiController] +/// public class HelloService +/// { +/// public string Say() +/// { +/// return "Hello, Furion"; +/// } +/// } +/// ``` +/// +/// 打开浏览器访问 `https://localhost:5001`。 +/// +/// Open browser access `https://localhost:5001`. +/// +/// ## 文档 / Documentation +/// +/// 您可以在[主页] (http://furion.baiqian.ltd)或[备份主页](http://furion.baiqian.ltd)找到 Furion 文档。 +/// +/// You can find the Furion documentation[on the website](http://furion.baiqian.ltd) or [on the backup website](http://furion.baiqian.ltd). +/// +/// ## 贡献 / Contributing +/// +/// 该存储库的主要目的是继续发展 Furion 核心,使其更快、更易于使用。 Furion 的开发在[Gitee](https://gitee.com/dotnetchina/Furion) 上公开进行,我们感谢社区贡献错误修复和改进。 +/// +/// 阅读[贡献指南] (http://furion.baiqian.ltd/docs/contribute)内容,了解如何参与改进 Furion。 +/// +/// The main purpose of this repository is to continue evolving Furion core, making it faster and easier to use.Development of Furion happens in the open on[Gitee] (https://gitee.com/dotnetchina/Furion), and we are grateful to the community for contributing bugfixes and improvements. +/// +/// Read[contribution documents] (http://furion.baiqian.ltd/docs/contribute) to learn how you can take part in improving Furion. +/// +/// ## 许可证 / License +/// +/// Furion 采用[MIT](https://gitee.com/dotnetchina/Furion/blob/v4/LICENSE) 开源许可证。 +/// +/// Furion uses the[MIT] (https://gitee.com/dotnetchina/Furion/blob/v4/LICENSE) open source license. +/// +/// ``` +/// Copyright© 2020-present 百小僧, Baiqian Co., Ltd. +/// Furion is licensed under Mulan PSL v2. +/// You can use this software according to the terms andconditions of the Mulan PSL v2. +/// You may obtain a copy of Mulan PSL v2 at: +/// https://gitee.com/dotnetchina/Furion/blob/v4/LICENSE +/// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUTWARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +/// See the Mulan PSL v2 for more details. +/// ``` +/// +/// +/// +public string Hello() +{ + return "Furion"; +} +```` + + + +### 6.5.39 第三方 `UI` 集成,如 `Knife4jUI` + +在 `Furion` 框架中,集成第三方 `SwaggerUI` 非常容易,比如集成 `IGeekFan.AspNetCore.Knife4jUI`: + +:::note 安装包 + +只需要在 `YourPoject.Web.Core` 层安装 `IGeekFan.AspNetCore.Knife4jUI` 即可。 + +::: + +**`Knife4jUI` 独立版本配置** + +```cs showLineNumbers {1,3,12} +var routePrefix = "api"; // 定义 swagger 路由地址,如果是跟目录,设置 string.Empty 即可 + +app.UseKnife4UI(options => +{ + options.RoutePrefix = routePrefix; // 配置 Knife4UI 路由地址 + foreach (var groupInfo in SpecificationDocumentBuilder.GetOpenApiGroups()) + { + options.SwaggerEndpoint("/" + groupInfo.RouteTemplate, groupInfo.Title); + } +}); + +app.UseInject(routePrefix); // 配置 Furion 路由地址 +``` + +**`Knife4jUI` 和 `Swagger` 共存版本配置** + +```cs showLineNumbers {1,3,10} +app.UseKnife4UI(options => +{ + options.RoutePrefix = "newapi"; // 配置 Knife4UI 路由地址,现在是 /newapi + foreach (var groupInfo in SpecificationDocumentBuilder.GetOpenApiGroups()) + { + options.SwaggerEndpoint("/" + groupInfo.RouteTemplate, groupInfo.Title); + } +}); + +app.UseInject(); // Furion 默认 api 地址为 /api +``` + + + +**如需实现登录之后自动将 `token` 添加到头部可在登录接口 `AfterScript` 执行以下代码:** + +```js +ke.global.setAllHeader( + "Authorization", + "Bearer " + ke.response.headers["access-token"] +); +``` + + + +### 6.5.40 添加 `Swagger` 请求响应拦截 + +有时候我们希望在 `Swagger` 请求之前进行拦截,比如修改 `Url` 参数,添加额外请求头等,又或者想在响应之后打印返回值到控制台或进行其他操作。这时候可以通过以下方式处理,如: + +```cs showLineNumbers {1,3,6,8} +app.UseInject(options => +{ + options.ConfigureSwaggerUI(ui => + { + // 请求拦截 + ui.UseRequestInterceptor("function(request) { return defaultRequestInterceptor(request); }"); + // 响应拦截 + ui.UseResponseInterceptor("function(response) { return defaultResponseInterceptor(response); }"); + }); +}); +``` + +注意,`UseRequestInterceptor` 和 `UseResponseInterceptor` 代码不能换行且须在 `defaultRequestInterceptor(request)` 和 `defaultResponseInterceptor(response)` 之前编写自定义代码,否则会报错。 + +下面给出请求添加自定义 `header` 示例: + +```cs showLineNumbers {6} +app.UseInject(options => +{ + options.ConfigureSwaggerUI(ui => + { + // 添加请求头 framework: Furion,注意代码不能换行,否则 Swagger 无法加载 !!!! + ui.UseRequestInterceptor("function(request) { request.headers['framework']='Furion'; return defaultRequestInterceptor(request); }"); + }); +}); +``` + +### 6.5.41 枚举类型值 + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.8.35 +` 版本使用。 + +::: + +默认情况下,枚举类型值会生成字符串类型,如需生成值类型则添加 `[EnumToNumber]` 特性,但还有一种情况,那就是**如果枚举项存在中文,会强制生成为值类型**。 + +- 缺省情况 + +```cs showLineNumbers +public enum NoticeTypeEnum +{ + NOTICE = 1, + ANNOUNCEMENT = 2, +} +``` + +默认生成 `TypeScript` 代码: + +```ts showLineNumbers {2-3} +export enum NoticeTypeEnum { + NOTICE = "NOTICE", + ANNOUNCEMENT = "ANNOUNCEMENT", +} +``` + +- 配置情况 + +```cs showLineNumbers {1} +[EnumToNumber] +public enum NoticeTypeEnum +{ + NOTICE = 1, + ANNOUNCEMENT = 2, +} +``` + +```ts showLineNumbers {2-3} +export enum NoticeTypeEnum { + NUMBER_1 = 1, + NUMBER_2 = 2, +} +``` + +**这种的 `NUMBER_` 规则 `Furion` 无法控制,这是 `Swagger` 生成的。** 还可以设置 `[EnumToNumber(false)]` 生成字符串类型。 + +- 存在中文情况 + +```cs showLineNumbers {5} +public enum NoticeTypeEnum +{ + NOTICE = 1, + ANNOUNCEMENT = 2, + 中文 +} +``` + +```ts showLineNumbers {2-4} +export enum NoticeTypeEnum { + NUMBER_1 = 1, + NUMBER_2 = 2, + NUMBER_3 = 3, +} +``` + +- 除了上面独立配置以外还支持全局配置: + +```json showLineNumbers {2-3} +{ + "SpecificationDocumentSettings": { + "EnumToNumber": true + } +} +``` + +## 6.6 `SpecificationDocumentSettings` 配置 + +除了上述例子外,`Furion` 提供了一些配置选项,如: + +- `DocumentTitle`:文档标题,`string`,默认 `Specification Api Document` +- `DefaultGroupName`:默认分组名,`string`,默认 `Default` +- `EnableAuthorized`:是否启用权限控制,`bool`,默认 `true` +- `FormatAsV2`:采用 `Swagger 2.0` 版本,`bool`,默认 `false` **[已弃用](https://github.com/domaindrivendev/Swashbuckle.WebApi/issues/1393)** +- `RoutePrefix`:规范化文档地址,`string`,默认 `api`,**如果希望在首页,改为空字符串即可**。 +- `DocExpansionState`:文档显示方式,`DocExpansion`,默认 `List`,取值: + - `List`:列表式(展开子类),**默认值** + - `Full`:完全展开 + - `None`:列表式(不展开子类) +- `XmlComments`:程序集注释描述文件名(可带 `.xml`,`string`,默认 `Furion.Application, Furion.Web.Entry, Furion.Web.Core` +- `GroupOpenApiInfos`:分组信息配置,`SpecificationOpenApiInfo[]`,默认 `{ 'Group': 'Default'}` +- `SecurityDefinitions`:安全策略定义配置,`SpecificationOpenApiSecurityScheme[]`,默认 `[]` +- `Servers`:配置 Server 下拉列表,`OpenApiServer[]` 类型,默认 `[]`,如:`{Servers:[ { Url:"地址", Description:"描述"} ]}` +- `HideServers`:是否隐藏 Server 下拉列表,`bool` 类型,默认 `true` +- `RouteTemplate`:配置文档 `swagger.json` 路由模板,默认模板:`swagger/{documentName}/swagger.json`, `{documentName}` 代表分组名,**必须保留原样** +- `PackagesGroups`:配置模块化内置分组名称,`string[]` 类型,默认 `[]` +- `EnableEnumSchemaFilter`:启用枚举 Schema 筛选器,`bool` 类型,默认 `true` +- `EnableTagsOrderDocumentFilter`:启用标签排序筛选器,`bool` 类型,默认 `true` +- `ServerDir`:配置 `IIS` 添加 `Application` 部署名,`string` 类型,默认空,**仅在 Furion v3.2.0+` 有效** +- `LoginInfo`:配置 `Swagger` 是否需要登录才能访问,`SpecificationLoginInfo` 类型,默认 `null`,**仅在 Furion v3.3.3+` 有效** + - `Enabled`:是否启用登录授权,默认 `false` + - `CheckUrl`:检查登录状态的 `Url` 地址,**该地址必须是 `POST` 请求**,已授权返回 `200`,否则返回 `401` + - `SubmitUrl`:提交登录的 `Url` 地址,**该地址必须是 `POST` 请求且只有一个 `SpecificationAuth` 类型参数**,成功登录返回 `200`,否则返回 `401`,支持相对地址,以 `/` 开头 +- `EnableAllGroups`:启用 `Swagger` 总分组功能,自动将所有分组的接口合并到 `All Groups` 中,`bool` 类型,默认 `false`,**仅在 Furion v3.3.4+` 有效** +- `EnumToNumber`:枚举类型生成值类型,`bool` 类型,默认 `false`,**仅在 Furion 4.8.8.35+` 有效** + +另外 `SpecificationOpenApiInfo` 内置配置如下: + +- `Group`:分组唯一标识,`string` 类型,必填 +- `Order`:分组排序,`int` 类型,数字越大排前面,默认 `0` +- `Visible`:配置分组是否可见,`bool` 类型,默认 `true` +- `Title`:配置分组标题,`string` 类型 +- `Description`:配置分组描述,`string` 类型 +- `Version`:配置分组版本,默认 `1.0` +- `TermsOfService`:配置相关链接地址,`Uri` 类型 +- `Contact`:配置联系方式,`OpenApiContact` 类型 +- `License`:配置协议,`OpenApiLicense` 类型 + +配置示例: + +```json showLineNumbers +{ + "SpecificationDocumentSettings": { + "GroupOpenApiInfos": [ + { + "Group": "Group1", + "Title": "分组标题", + "Description": "这里是分组描述", + "Version": "版本号", + "TermsOfService": "http://furion.baiqian.ltd", + "Contact": { + "Name": "百小僧", + "Url": "https://gitee.com/monksoul", + "Email": "monksoul@outlook.com" + }, + "License": { + "Name": "MIT", + "Url": "https://gitee.com/dotnetchina/Furion/blob/alpha/LICENSE" + } + } + ] + } +} +``` + +## 6.7 统一返回值模型/规范化结果/API 返回值 + +`Furion` 框架提供 `规范化结果` 功能,可以通过实现 `IUnifyResultProvider` 提供器实现统一规范化返回值定制,如: + +- **定义结果包装类型** + +```cs showLineNumbers {1-2} +// 必须是泛型类型 +public class YourRESTfulResult +{ + /// + /// 状态码 + /// + public int? StatusCode { get; set; } + + /// + /// 数据 + /// + public T Data { get; set; } + + /// + /// 执行成功 + /// + public bool Succeeded { get; set; } + + /// + /// 错误信息 + /// + public object Errors { get; set; } + + /// + /// 附加数据 + /// + public object Extras { get; set; } + + /// + /// 时间戳 + /// + public long Timestamp { get; set; } +} +``` + +- **定义 `IUnifyResultProvider` 实现类,并贴特性 `[UnifyModel(typeof(YourRESTfulResult<>))]`** + +```cs showLineNumbers {15-16,24,36,48,61} +using Furion.DataValidation; +using Furion.DependencyInjection; +using Furion.UnifyResult.Internal; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using System; +using System.Threading.Tasks; + +namespace YourProject.UnifyResult +{ + /// + /// RESTful 风格返回值 + /// + [UnifyModel(typeof(YourRESTfulResult<>))] + public class YourRESTfulResultProvider : IUnifyResultProvider + { + /// + /// 异常返回值 + /// + /// + /// + /// + public IActionResult OnException(ExceptionContext context, ExceptionMetadata metadata) + { + return new JsonResult(YourRESTfulResult(metadata.StatusCode, data: metadata.Data, errors: metadata.Errors) + , UnifyContext.GetSerializerSettings(context)); // 当前行仅限 Furion 4.6.6+ 使用 + } + + /// + /// 成功返回值 + /// + /// + /// + /// + public IActionResult OnSucceeded(ActionExecutedContext context, object data) + { + return new JsonResult(YourRESTfulResult(StatusCodes.Status200OK, true, data) + , UnifyContext.GetSerializerSettings(context)); // 当前行仅限 Furion 4.6.6+ 使用 + } + + /// + /// 验证失败返回值 + /// + /// + /// + /// + public IActionResult OnValidateFailed(ActionExecutingContext context, ValidationMetadata metadata) + { + return new JsonResult(YourRESTfulResult(metadata.StatusCode ?? StatusCodes.Status400BadRequest, data: metadata.Data, errors: metadata.ValidationResult) + , UnifyContext.GetSerializerSettings(context)); // 当前行仅限 Furion 4.6.6+ 使用 + } + + /// + /// 特定状态码返回值 + /// + /// + /// + /// + /// + public async Task OnResponseStatusCodes(HttpContext context, int statusCode, UnifyResultSettingsOptions unifyResultSettings) + { + // 设置响应状态码 + UnifyContext.SetResponseStatusCodes(context, statusCode, unifyResultSettings); + + switch (statusCode) + { + // 处理 401 状态码 + case StatusCodes.Status401Unauthorized: + await context.Response.WriteAsJsonAsync(YourRESTfulResult(statusCode, errors: "401 Unauthorized") + , App.GetOptions()?.JsonSerializerOptions); + break; + // 处理 403 状态码 + case StatusCodes.Status403Forbidden: + await context.Response.WriteAsJsonAsync(YourRESTfulResult(statusCode, errors: "403 Forbidden") + , App.GetOptions()?.JsonSerializerOptions); + break; + default: break; + } + } + + /// + /// 返回 RESTful 风格结果集 + /// + /// + /// + /// + /// + /// + private static YourRESTfulResult YourRESTfulResult(int statusCode, bool succeeded = default, object data = default, object errors = default) + { + return new YourRESTfulResult + { + StatusCode = statusCode, + Succeeded = succeeded, + Data = data, + Errors = errors, + Extras = UnifyContext.Take(), + Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() + }; + } + } +} +``` + +之后在 `Startup.cs` 中注册即可: + +```cs showLineNumbers +services.AddControllers() + .AddInjectWithUnifyResult(); +``` + +:::important 特别注意 + +默认情况下,规范化结果不会对 `401` 和 `403`、`404` 状态码进行规范化处理,如需启动该状态码处理,只需在 `Startup.cs` 中的 `Configure` 中启用接口: + +```cs showLineNumbers +public void Configure(IApplicationBuilder app, IWebHostEnvironment env) +{ + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + // 添加规范化结果状态码,需要在这里注册 + app.UseUnifyResultStatusCodes(); + // 其他注册... +} +``` + +::: + +### 6.7.1 排除规范化处理 + +有些时候,我们某些接口不需要进行规范化处理,这时,我们只需要帖 `[NonUnify]` 特性即可。 + +### 6.7.2 规范化结果添加额外数据 + +默认的规范化结果中包含 `extras` 属性,可以配置额外数据返回到客户端: + +```cs showLineNumbers +UnifyContext.Fill(new { Message = "操作成功" }); +``` + +### 6.7.3 自定义特别接口规范化结果 + +有些时候,我们特定接口需返回特定的接口类型,无需规范化处理,这时候我们只需要贴 `[UnifyResult(typeof(结果类))]` 或 `[ProducesResponseType(typeof(结果类),200)]`,如: + +```cs showLineNumbers +[UnifyResult(typeof(Person))] +public Person GetPerson(int id) +{ + // ... +} +``` + +## 6.8 支持多套规范化配置 + +:::important 版本说明 + +以下内容仅限 `Furion 4.4.4 +` 版本使用。 + +::: + +在一些情况下,我们可能需要针对特定的控制器(动态 WebAPI)或特定的方法使用不同的规范化处理机制,这时就需要多套规范化处理提供器,如: + +1. **定义规范化处理提供程序** + +```cs showLineNumbers {1-2,25} +[UnifyModel(typeof(MyResult<>))] +public class SpeciallyResultProvider : IUnifyResultProvider +{ + public IActionResult OnException(ExceptionContext context, ExceptionMetadata metadata) + { + // 参考上面的规范化处理写法 + } + + public IActionResult OnSucceeded(ActionExecutedContext context, object data) + { + // 参考上面的规范化处理写法 + } + + public IActionResult OnValidateFailed(ActionExecutingContext context, ValidationMetadata metadata) + { + // 参考上面的规范化处理写法 + } + + public async Task OnResponseStatusCodes(HttpContext context, int statusCode, UnifyResultSettingsOptions unifyResultSettings) + { + // 参考上面的规范化处理写法 + } +} + +public class MyResult +{ + /// + /// 数据 + /// + public T Data { get; set; } +} +``` + +2. **注册规范化处理提供器** + +```cs showLineNumbers +services.AddUnifyProvider("specially"); // 指定规范化唯一名称,如果不指定就会替代默认的 +``` + +3. **在控制器/动态 WebAPI 中使用** + +```cs showLineNumbers {3,8,14} +public class TestUnifyProvider : IDynamicApiController +{ + public string DefaultUnify() + { + return "test"; + } + + [UnifyProvider] + public string DefaultUnify2() + { + return "test"; + } + + [UnifyProvider("specially")] + public string SpeciallyUnify() + { + return "特别"; + } +} +``` + +## 6.9 针对特定控制器或特定方法配置序列化选项 + +很少开发者注意到 `new JsonResult(data)` 实际上有第二个参数的,也就是 `new JsonResult(data, serializerSettings)`,那么可以根据自己的逻辑传递第二个参数,如果不传递则采用全局配置的序列化选项。 + +`Furion` 框架提供两种处理方式。 + +### 6.9.1 通过 `JsonResult` 设置第二个参数 + +这种方式比较原始化,代码比较繁杂,也不利于维护。 + +```cs showLineNumbers {1,4,13-16} +[NonUnify] +public IActionResult SpecialApi() +{ + return new JsonResult(new YourRESTfulResult + { + StatusCode = 200, + Succeeded = true, + Data = new + { + Name = "Furion" + }, + Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() + }, new JsonSerializerOptions + { + PropertyNamingPolicy = null + }); +} +``` + +### 6.9.2 注册多套序列化配置选项 **(推荐)** + +:::important 版本说明 + +以下内容仅限 `Furion 4.6.6 +` 版本使用。 + +::: + +在 `Startup.cs` 中注册一套新的规则: + +```cs showLineNumbers {1} +services.AddUnifyJsonOptions("special", new JsonSerializerOptions // 如果使用 Newtonsoft.Json => new JsonSerializerSettings +{ + PropertyNamingPolicy = null +}); +``` + +代码使用: + +```cs showLineNumbers {1} +[UnifySerializerSetting("special")] +public object SpecialApi() +{ + return new + { + Name = "Furion" + } +} +``` + +也可以手动返回 `IActionResult` 方式: + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.7.1 +` 版本使用。 + +::: + +```cs showLineNumbers {3,6} +public IActionResult SpecialApi() +{ + return new JsonResult(new + { + Name = "Furion" + }, UnifyContext.GetSerializerSettings("special")); +} +``` + +:::warning 特别提醒 + +目前 `Swagger` 暂未提供个别的接口自定义 `schema` 序列化选项。 + +::: + +## 6.10 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/split-db.mdx b/handbook/docs/split-db.mdx new file mode 100644 index 0000000000000000000000000000000000000000..cfb2d71c15bd08d58ab013c452044a3ebb04437c --- /dev/null +++ b/handbook/docs/split-db.mdx @@ -0,0 +1,239 @@ +--- +id: split-db +title: 9.29 分表分库 +sidebar_label: 9.29 分表分库 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +:::important 特此声明 + +本章 `9.28.2` 至 `9.28.5` 小节挪用博主:[雨点的名字](https://www.cnblogs.com/qdhxhz/) 的 [分库分表 - 理论](https://www.cnblogs.com/qdhxhz/p/11608222.html) 博客内容。**特此声明。** + +::: + +## 9.29.1 应用场景 + +数据库中的数据量不一定是可控的,在未进行分库分表的情况下,随着时间和业务的发展,库中的表会越来越多,表中的数据量也会越来越大,相应地,数据操作增删改查的开销也会越来越大。 + +另外,由于无法进行分布式部署,而一台服务器的资源(CPU、磁盘、内存、IO 等)是有限的,最终数据库所能承载的数据量、数据处理能力都将遭遇瓶颈。 + +这个时候就需要对数据库或数据表进行拆分。 + +数据切分可以分为:`垂直切分` 和 `水平切分`。 + +## 9.29.2 垂直切分 + +垂直切分又可以分为: 垂直分库和垂直分表。 + +### 9.29.2.1 垂直分库 + +根据业务耦合性,将关联度低的不同表存储在不同的数据库。做法与大系统拆分为多个小系统类似,按业务分类进行独立划分。与"微服务治理"的做法相似,每个微服务使用单独的一个数据库。 + + + +:::note 说明 + +一开始我们是单体服务,所以只有一个数据库,所有的表都在这个库里。 + +后来因为业务需求,单体服务变成微服务治理。所以将之前的一个商品库,拆分成多个数据库。每个微服务对于一个数据库。 + +::: + +### 9.29.2.2 垂直分表 + +把一个表的多个字段分别拆成多个表,一般按字段的冷热拆分,热字段一个表,冷字段一个表。从而提升了数据库性能。 + + + +:::note 说明 + +一开始商品表中包含商品的所有字段,但是我们发现: + +1.商品详情和商品属性字段较长。2.商品列表的时候我们是不需要显示商品详情和商品属性信息,只有在点进商品商品的时候才会展示商品详情信息。 + +所以可以考虑把商品详情和商品属性单独切分一张表,提高查询效率。 + +::: + +### 9.29.2.3 优缺点 + +- 优点 + + - 解决业务系统层面的耦合,业务清晰 + - 与微服务的治理类似,也能对不同业务的数据进行分级管理、维护、监控、扩展等 + - 高并发场景下,垂直切分一定程度的提升 IO、数据库连接数、单机硬件资源的瓶颈 + +- 缺点 + + - 分库后无法 Join,只能通过接口聚合方式解决,提升了开发的复杂度 + - 分库后分布式事务处理复杂 + - 依然存在单表数据量过大的问题(需要水平切分) + +## 9.29.3 水平切分 + +当一个应用难以再细粒度的垂直切分或切分后数据量行数巨大,存在单库读写、存储性能瓶颈,这时候就需要进行水平切分了。 + +水平切分也可以分为:水平分库和水平分表。 + +### 9.29.3.1 水平分库 + +上面虽然已经把商品库分成 3 个库,但是随着业务的增加一个订单库也出现 QPS 过高,数据库响应速度来不及,一般 mysql 单机也就 1000 左右的 QPS,如果超过 1000 就要考虑分库。 + + + +### 9.29.3.2 水平分表 + +一般我们一张表的数据不要超过 1 千万,如果表数据超过 1 千万,并且还在不断增加数据,那就可以考虑分表。 + + + +### 9.29.3.3 优缺点 + +- 优点 + + - 不存在单库数据量过大、高并发的性能瓶颈,提升系统稳定性和负载能力 + - 应用端改造较小,不需要拆分业务模块 + +- 缺点 + + - 跨分片的事务一致性难以保证 + - 跨库的 Join 关联查询性能较差 + - 数据多次扩展难度和维护量极大 + +## 9.29.4 数据分片规则 + +我们考虑去水平切分表,将一张表水平切分成多张表,这就涉及到数据分片的规则,比较常见的有:Hash 取模分表、数值 Range 分表、一致性 Hash 算法分表。 + +### 9.29.4.1 Hash 取模分表 + +一般采用 Hash 取模的切分方式,例如:假设按 goods_id 分 4 张表。(goods_id%4 取整确定表) + + + +**优缺点** + +- 优点 + + - 数据分片相对比较均匀,不容易出现热点和并发访问的瓶颈。 + +- 缺点 + + - 后期分片集群扩容时,需要迁移旧的数据很难。 + - 容易面临跨分片查询的复杂问题。比如上例中,如果频繁用到的查询条件中不带 goods_id 时,将会导致无法定位数据库,从而需要同时向 4 个库发起查询, + 再在内存中合并数据,取最小集返回给应用,分库反而成为拖累。 + +### 9.29.4.2 数值 Range 分表 + +按照时间区间或 ID 区间来切分。例如:将 goods_id 为 1-1000 的记录分到第一个表,1000-2000 的分到第二个表,以此类推。 + + + +**优缺点** + +- 优点 + + - 单表大小可控 + - 天然便于水平扩展,后期如果想对整个分片集群扩容时,只需要添加节点即可,无需对其他分片的数据进行迁移 + - 使用分片字段进行范围查找时,连续分片可快速定位分片进行快速查询,有效避免跨分片查询的问题。 + +- 缺点 + + - 热点数据成为性能瓶颈。 + 例如按时间字段分片,有些分片存储最近时间段内的数据,可能会被频繁的读写,而有些分片存储的历史数据,则很少被查询 + +### 9.29.4.3 一致性 Hash 算法 + +一致性 Hash 算法能很好的解决因为 Hash 取模而产生的分片集群扩容时,需要迁移旧的数据的难题。具体原理可参考 [https://www.cnblogs.com/duhuo/p/4996105.html](https://www.cnblogs.com/duhuo/p/4996105.html) + +## 9.29.5 分库分表带来的问题 + +任何事情都有两面性,分库分表也不例外,如果采用分库分表,会引入新的的问题: + +### 9.29.5.1 分布式事务问题 + +使用分布式事务中间件解决,具体是通过最终一致性还是强一致性分布式事务,看业务需求,这里就不多说。 + +### 9.29.5.2 跨节点关联查询 Join 问题 + +切分之前,我们可以通过 Join 来完成。而切分之后,数据可能分布在不同的节点上,此时 Join 带来的问题就比较麻烦了,考虑到性能,尽量避免使用 Join 查询。 + +解决这个问题的一些方法: + +- **全局表** + +全局表,也可看做是 "数据字典表",就是系统中所有模块都可能依赖的一些表,为了避免跨库 Join 查询,可以将 这类表在每个数据库中都保存一份。这些数据通常很少会进行修改,所以也不担心一致性的问题。 + +- **字段冗余** + +利用空间换时间,为了性能而避免 join 查询。例:订单表保存 userId 时候,也将 userName 冗余保存一份,这样查询订单详情时就不需要再去查询"买家 user 表"了。 + +- **数据组装** + +在系统层面,分两次查询。第一次查询的结果集中找出关联数据 id,然后根据 id 发起第二次请求得到关联数据。最后将获得到的数据进行字段拼装。 + +### 9.29.5.3 跨节点分页、排序、函数问题 + +跨节点多库进行查询时,会出现 Limit 分页、Order by 排序等问题。分页需要按照指定字段进行排序,当排序字段就是分片字段时,通过分片规则就比较容易定位到指定的分片; + +当排序字段非分片字段时,就变得比较复杂了。需要先在不同的分片节点中将数据进行排序并返回,然后将不同分片返回的结果集进行汇总和再次排序,最终返回给用户。 + +### 9.29.5.4 全局主键避重问题 + +如果都用主键自增肯定不合理,如果用 UUID 那么无法做到根据主键排序,所以我们可以考虑通过[雪花 ID](https://www.cnblogs.com/qdhxhz/p/11372658.html) 来作为数据库的主键, + +### 9.29.5.5 数据迁移问题 + +采用双写的方式,修改代码,所有涉及到分库分表的表的增、删、改的代码,都要对新库进行增删改。同时,再有一个数据抽取服务,不断地从老库抽数据,往新库写, + +边写边按时间比较数据是不是最新的。 + +## 9.29.6 如何实现 + +:::caution 特别说明 + +由于分表分库不仅仅需要内置代码的支持,同时还需要集成数据库中间件,这里推荐 `MyCat` 中间件。[MyCat 官方网站](http://www.mycat.org.cn/) + +::: + +`Furion` 框架中提供了轻量级的 `分表分库` 支持: + +- **动态切换数据库** + +```cs showLineNumbers +// 直接改变数据库 +repository.ChangeDatabase("数据库连接字符串"); + +// 通过数据库上下文定位器切换 +repository.Change(); +``` + +如需跨库查询,需用到数据库技术,如 `SqlServer` 链接服务器或同义词。 + +- **动态切换数据库表** + +第一步、配置数据库上下文特性`[AppDbContext( Mode=DbContextMode.Dynamic)]` + +第二步、需要动态修改表名的实体继承 `IEntityMutableTable` 接口,并实现 `GetTableName()` 返回表名方法 + +最后通过 `BuildChange` 切换即可。 + +```cs showLineNumbers +var (rep, scoped) = repository.BuildChange(); +``` + +调用 `BuildChange` 方法之后会自动调用 `GetTableName()` 方法。 + +:::note 了解更多 + +想了解更多 `DynamicModelCacheKeyFactory` 知识可查阅 [EF Core - 多个模型之间交替](https://docs.microsoft.com/zh-cn/ef/core/modeling/dynamic-model) 章节。 + +::: + +## 9.29.7 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/sqlsugar-old.mdx b/handbook/docs/sqlsugar-old.mdx new file mode 100644 index 0000000000000000000000000000000000000000..bb859f76777ccb6fc1d0195e55aa7f607b174807 --- /dev/null +++ b/handbook/docs/sqlsugar-old.mdx @@ -0,0 +1,348 @@ +--- +id: sqlsugar-old +title: 10.1. SqlSugar 集成 +sidebar_label: 10.1. SqlSugar 集成 +--- + +:::warning 温馨提醒 + +在 `Furion` 包中默认集成了 `EFCore`,**如果不使用 `EFCore`,可安装纯净版 `Furion.Pure` 代替 `Furion`,这样可以忽略本章节直接按照 `SqlSugar` 官方文档使用**。 + +::: + +## 10.1.1 关于 SqlSugar + +`SqlSugar` 是 .NET/C# 平台非常优秀的 `ORM` 框架,目前 `NuGet` 总下载突破 700K,Github 关注量也高达 3.2K,是目前当之无愧的国产优秀 ORM 框架之一。 + +`SqlSugar` 官方地址:[http://www.donet5.com/](http://www.donet5.com/) + +`SqlSugar` 与 EF 优势: 性能比 EF 更快、语法上手更容易 + +`SqlSugar` 与 Dapper 优势: SqlSugar 功能更加丰富,性能丝毫不逊色于 Dapper,并且批量操作性能更好 + +## 10.1.2 如何集成 + +在 `Furion` 框架中,已经推出 `SqlSugar` 拓展包 [Furion.Extras.DatabaseAccessor.SqlSugar](https://www.nuget.org/packages/Furion.Extras.DatabaseAccessor.SqlSugar)。 + +### 10.1.2.1 注册 `SqlSugar` 服务 + +使用非常简单,只需要在 `Startup.cs` 中添加 `services.AddSqlSugar(config)` 即可。如: + +```cs showLineNumbers +// =====配置单库===== +services.AddSqlSugar(new ConnectionConfig +{ + ConnectionString = "Server=.xxxxx",//连接符字串 + DbType = DbType.SqlServer, + IsAutoCloseConnection = true, + InitKeyType = InitKeyType.Attribute//从特性读取主键自增信息 +}); + +// =====配置多库===== +List connectConfigList = new List(); +//数据库1 +connectConfigList.Add(new ConnectionConfig +{ + ConnectionString = "链接字符串1", + DbType = DbType.MySql, + IsAutoCloseConnection = true, + InitKeyType = InitKeyType.Attribute, + ConfigId = "0", + AopEvents = new AopEvents + { + //多库状态下每个库必须单独绑定打印事件,否则只会打印第一个库的sql日志 + OnLogExecuting = (sql, pars) => + { + Console.WriteLine(SqlProfiler.ParameterFormat(sql, pars)); + Console.WriteLine(); + } + } +}); +//数据库2 +connectConfigList.Add(new ConnectionConfig +{ + ConnectionString = "链接字符串2", + DbType = DbType.MySql, + IsAutoCloseConnection = true, + InitKeyType = InitKeyType.Attribute, + ConfigId = "1", + AopEvents = new AopEvents + { + //多库状态下每个库必须单独绑定打印事件,否则只会打印第一个库的sql日志 + OnLogExecuting = (sql, pars) => + { + Console.WriteLine(SqlProfiler.ParameterFormat(sql, pars)); + Console.WriteLine(); + } + } +}); +services.AddSqlSugar(connectConfigList.ToArray()); +``` + +同时也可以添加更多配置,如: +ps:多库状态下每个库必须单独绑定打印事件,否则只会打印第一个库的 sql 日志(参考上面的多库配置) + +```cs showLineNumbers +services.AddSqlSugar(connectConfigList.ToArray(), +db => +{ + //处理日志事务 + db.Aop.OnLogExecuting = (sql, pars) => + { + Console.WriteLine(sql); + Console.WriteLine(string.Join(",", pars?.Select(it => it.ParameterName + ":" + it.Value))); + Console.WriteLine(); + App.PrintToMiniProfiler("SqlSugar", "Info", sql + "\r\n" + db.Utilities.SerializeObject(pars.ToDictionary(it => it.ParameterName, it => it.Value))); + }; +}); +``` + +:::important 安装拓展包位置 + +在 `Furion` 框架中,推荐将拓展包 `Furion.Extras.DatabaseAccessor.SqlSugar` 安装到 `Furion.Core` 层中。 + +::: + +## 10.1.3 基本使用 + +在使用之前,我们可以通过构造函数注入 `ISqlSugarRepository` 接口,如: + +```cs showLineNumbers {1,6} +private readonly ISqlSugarRepository repository; // 仓储对象:封装简单的CRUD +private readonly SqlSugarClient db; // 核心对象:拥有完整的SqlSugar全部功能 +public PersonService(ISqlSugarRepository sqlSugarRepository) +{ + repository = sqlSugarRepository; + db = repository.Context; // 推荐操作 +} +``` + +## 10.1.4 数据库操作示例 + +```cs showLineNumbers +// ================== SqlSugarClient ================ + +//查询功能 +var data1 =db.Queryable().First(it=>it.Id==1); //db.GetById(1); + +var data2 = db.Queryable().ToList();// db.GetList(); + +// ================== 简单仓储 ================ + +//插入 +db.Insert(insertObj); +var id = db.InsertReturnIdentity(insertObj); +db.AsInsertable(insertObj).ExecuteCommand(); + +//删除 +db.Delete(insertObj); +db.Delete(it => it.Id == 1); + +//更新 +db.Update(insertObj); +db.Update(it => new Order() { Name = "a", }, it => it.Id == 1); + +//异步方法用法 +db.Insert(insertObj);//同步 +db.InsertAsync(insertObj);//异步 + +//切换仓储 +var orderRespository=db.GetSimpleClient(); +orderRespository.Insert(Order); +``` + +## 10.1.5 SqlSugarClient 操作示例 + +### 10.1.5.1 基础查询 + +```cs showLineNumbers +//查询所有 +var getAll = db.Queryable().ToList(); +//查询前10 +var top10= db.Queryable().Take(10).ToList(); +//查询单条 +var getFirst = db.Queryable().First(it=>it.Id==1); +//with nolock +var getAllNoLock = db.Queryable().With(SqlWith.NoLock).ToList(); +//根据主键查询 +var getByPrimaryKey = db.Queryable().InSingle(2); +//查询总和 +var sum = db.Queryable().Sum(it=>it.Id); +//是否存在 +var isAny = db.Queryable().Where(it=>it.Id==-1).Any(); +//模糊查 +var list2 = db.Queryable().Where(it =>it.Name.Contains("jack")).ToList(); +``` + +### 10.1.5.2 联表查询 + +```cs showLineNumbers +var list = db.Queryable((st, sc) => new JoinQueryInfos( + JoinType.Left,st.SchoolId==sc.Id)) + .Select((st,sc)=>new{Id=st.Id,Name=st.Name,SchoolName=sc.Name}).ToList(); + +生成的Sql如下: +SELECT [st].[ID] AS [id] , + [st].[Name] AS [name] , + [sc].[Name] AS [schoolName] FROM [STudent] st + Left JOIN School sc ON ( [st].[SchoolId] =[sc].[Id]) +``` + +### 10.1.5.3 分页查询 + +```cs showLineNumbers + int pageIndex = 1; + int pageSize = 20; + int totalCount=0; + var page = db.Queryable().ToPageList(pageIndex, pageSize, ref totalCount); +``` + +更多查询用法 : http://www.donet5.com/Home/Doc?typeId=1185 + +### 10.1.5.4 插入 + +```cs showLineNumbers +//可以是 类 或者 List<类> +db.Insertable(insertObj).ExecuteCommand(); + +//插入返回自增列 +db.Insertable(insertObj).ExecuteReturnIdentity(); + +//可以是 Dictionary 或者 List +var dc= new Dictionary(); + dt.Add("name", "1"); + dt.Add("CreateTime", null); +db.Insertable(dc).AS("student").ExecuteCommand(); + +//DataTable插入 +Dictionary dc= db.Utilities.DataTableToDictionary(dataTable);//转成字典就可以按上面的字典更新了 +db.Insertable(dc).AS("student").ExecuteReturnIdentity(); + +//实体可以配置主键和自增列 +public class Student +{ + [SugarColumn(IsPrimaryKey = true, IsIdentity = true)] + public int Id { get; set; } + public int? SchoolId { get; set; } + public string Name { get; set; } +} +``` + +更多插入用法: http://www.donet5.com/Home/Doc?typeId=1193 + +### 10.1.5.5 更新 + +实体配置参考插入,只需要配置主键即可 + +```cs showLineNumbers +//根据主键更新单条 参数 Class +var result= db.Updateable(updateObj).ExecuteCommand(); +//不更新 Name 和TestId +var result=db.Updateable(updateObj).IgnoreColumns(it => new { it.CreateTime,it.TestId }).ExecuteCommand() +//只更新 Name 和 CreateTime +var result=db.Updateable(updateObj).UpdateColumns(it => new { it.Name,it.CreateTime }).ExecuteCommand(); +//根据表达式更新 +var result71 = db.Updateable() + .SetColumns(it => it.Name == "a") + .SetColumnsIF(p!=null ,it => it.CreateTime == p.Value)//当p不等于null更新createtime列 + .Where(it => it.Id == 11).ExecuteCommand(); +``` + +更多更新用法: http://www.donet5.com/Home/Doc?typeId=1191 + +### 10.1.5.6 删除 + +实体配置参考插入,只需要配置主键即可 + +```cs showLineNumbers +//根据实体删除 +db.Deleteable().Where(new Student() { Id = 1 }).ExecuteCommand(); +//根据主键删除 +db.Deleteable().In(1).ExecuteCommand(); +//根据表达式删除 +db.Deleteable().Where(it => it.Id == 1).ExecuteCommand(); +``` + +更多删除用法: http://www.donet5.com/Home/Doc?typeId=1195 + +## 10.1.6 Sql 查询 + +```cs showLineNumbers +//sql分页 +var list = db.SqlQueryable("select * from student").ToPageList(1, 2,ref total); + +//原生Sql用法 +var dt=db.Ado.GetDataTable("select * from table where id=@id and name=@name",new List(){ + new SugarParameter("@id",1), + new SugarParameter("@name",2) +}); +//参数2 +var dt=db.Ado.GetDataTable("select * from table where id=@id and name=@name",new{id=1,name=2}); + +//存储过程用法 +var nameP= new SugarParameter("@name", "张三"); +var ageP= new SugarParameter("@age", null, true);//设置为output +var dt = db.Ado.UseStoredProcedure().GetDataTable("sp_school",nameP,ageP); + +``` + +| 方法名 | 描述 | 返回值 | +| :-------------: | :----------------------------: | :----------------: | +| SqlQuery\ | 查询所有返回实体集合 | List | +| SqlQuery\ | 可以返回 2 个结果集 | Tuple\ | +| SqlQuerySingle | 查询第一条记录 | T | +| GetDataTable | 查询所有 | DataTable | +| GetDataReader | 读取 DR 需要手动释放 DR | DataReader | +| GetDataSetAll | 获取多个结果集 | DataSet | +| ExecuteCommand | 返回受影响行数,一般用于增删改 | int | +| GetScalar | 获取首行首列 | object | +| GetString | 获取首行首列 | string | +| GetInt | 获取首行首列 | int | +| GetLong | 获取首行首列 | long | +| GetDouble | 获取首行首列 | Double | +| GetDecimal | 获取首行首列 | Decimal | +| GetDateTime | 获取首行首列 | DateTime | + +想了解更多 `SqlSugar` 知识可查阅 [SqlSugar 官网](http://donet5.com/)。 + +## 10.1.7 打印 `sql` 到 `Swagger` + +```cs showLineNumbers +services.AddSqlSugar(new ConnectionConfig +{ + ConnectionString = "Server=.xxxxx",//连接符字串 + DbType = DbType.SqlServer, + IsAutoCloseConnection = true, + InitKeyType = InitKeyType.Attribute//从特性读取主键自增信息 +}, +db => +{ + db.Aop.OnLogExecuting = (sql, pars) => + { + //方法一 + App.PrintToMiniProfiler("SqlSugar", "Info", sql + "\r\n" +string.Join(",", pars?.Select(it => it.ParameterName + ":" + it.Value))); + + //方法二:Furion对上述用法进行二次封装 建议方式 Sql参数会直接写入到生成Sql中 + App.PrintToMiniProfiler("SqlSugar","Info",SqlProfiler.ParameterFormat(sql,pars)); + }; +}); +``` + +## 10.1.8 `Oracle`注意事项 + +注意 .NET5 用户需要在 API 或者项目解决方案 `.csproj` 文件加以下一行代码: + +```cs showLineNumbers {3} + + net5.0 + true + +``` + +## 10.1.9 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/sqlsugar.mdx b/handbook/docs/sqlsugar.mdx new file mode 100644 index 0000000000000000000000000000000000000000..d5b233a64219ca2f34772a685662035164b07e07 --- /dev/null +++ b/handbook/docs/sqlsugar.mdx @@ -0,0 +1,382 @@ +--- +id: sqlsugar +title: 10.1 SqlSugar 集成 +sidebar_label: 10.1 SqlSugar 集成 +--- + +:::caution 关于拓展包 + +由于 `SqlSugar` 高速发展,新版本带来了诸多特性,而 `Furion.Extras.DatabaseAccessor.SqlSugar` 拓展包更新不及时导致不能第一时间体验新特性。 + +**所以,和 `SqlSugar` 作者商量后,决定全面推荐使用 `SqlSugar` 原生包即可。以下文档已更新**,[查看旧文档](./sqlsugar-old) + +::: + +:::warning 温馨提醒 + +在 `Furion` 包中默认集成了 `EFCore`,**如果不使用 `EFCore`,可安装纯净版 `Furion.Pure` 代替 `Furion`**。 + +::: + +## 10.1.1 SqlSugar ORM + +`SqlSugar` 是 `.NET/C#` 平台非常优秀的 `ORM` 框架,目前 `NuGet` 总下载突破 `1000K`,`Github` 关注量也高达 `3.7K`,是目前当之无愧的国产优秀 `ORM` 框架之一。 + +`SqlSugar` 高性能,具有百万级插入、更新大数据分表等特色功能。 + +## 10.1.2 功能介绍 + +- 支持 `SqlServer、MySql、PgSql、Oracle` 百万级插入和更新 +- 支持全自动分表 +- 支持多库事务 +- 支持 `CodeFirst` +- 支持联表查询、嵌套查询、导航查询、子查询和动态 `JSON` 查询等查询操作 +- 支持配置查询 +- 支持工具生成实体和代码生成实体 +- 支持数据库 MySql、SqlServer、Sqlite、Oracle、postgresql、达梦、人大金仓、神通数据库 + +## 10.1.3 官网文档 + +点击以下链接可以跳转到 `SqlSugar` 官网查看详细 `API` + +|入门 |查询 | 插入 | 更新 | 删 除| +| ----- | --------- | ----------- | ------- |------- | + 安装| [简单查询](https://www.donet5.com/Home/Doc?typeId=1187) | || | +[入门](https://www.donet5.com/home/Doc?typeId=1181)| 联表 | | | | | + +## 10.1.4 Furion 集成 + +1. 创建一个拓展类: + +```cs showLineNumbers {3,23} +public static class SqlsugarSetup +{ + public static void AddSqlsugarSetup(this IServiceCollection services, IConfiguration configuration, string dbName = "db_master") + { + //如果多个数数据库传 List + var configConnection=new ConnectionConfig() + { + DbType = SqlSugar.DbType.MySql, + ConnectionString = configuration.GetConnectionString(dbName), + IsAutoCloseConnection = true, + }; + + SqlSugarScope sqlSugar = new SqlSugarScope(configConnection, + db => + { + //单例参数配置,所有上下文生效 + db.Aop.OnLogExecuting = (sql, pars) => + { + //Console.WriteLine(sql);//输出sql + }; + }); + + services.AddSingleton(sqlSugar);//这边是SqlSugarScope用AddSingleton + } +} +``` + +使用注入 + +```cs showLineNumbers +//1.构造函数注入 +SqlSugar.ISqlSugarClient db; +public WeatherForecastController(ISqlSugarClient db) +{ + + this.db = db; +} + +//2.手动获取 +App.GetService(); +``` + +2. 在 `Startup.cs` 中注册: + +```cs showLineNumbers +services.AddSqlsugarSetup(App.Configuration); +``` + +:::tip 小知识 + +如果需要多库配置,可查看 [https://www.donet5.com/home/Doc?typeId=2246](https://www.donet5.com/home/Doc?typeId=2246) + +::: + +## 10.1.5 特色功能 + +### 10.1.5.1 联表查询 + +- `Linq/Lambda`: + +```cs showLineNumbers +var query5 = db.Queryable() + .LeftJoin ((o, cus) => o.CustomId == cus.Id) + .LeftJoin ((o, cus, oritem ) => o.Id == oritem.OrderId) + .Where(o => o.Id == 1) + .Select((o, cus) => new ViewOrder { Id = o.Id, CustomName = cus.Name }) + .ToList(); +``` + +- 生成 `SQL`: + +```sql showLineNumbers +SELECT + [o].[Id] AS [Id], + [cus].[Name] AS [CustomName] +FROM + [Order] o + Left JOIN [Custom] cus ON ([o].[CustomId] = [cus].[Id]) + Left JOIN [OrderDetail] oritem ON ([o].[Id] = [oritem].[OrderId]) +WHERE + ([o].[Id] = @Id0) +``` + +### 10.1.5.2 分页查询 + +```cs showLineNumbers +int pageIndex = 1; +int pageSize = 20; +int totalCount=0; +var page = db.Queryable().ToPageList(pageIndex, pageSize, ref totalCount); +``` + +### 10.1.5.3 动态表达式 + +- `Linq/Lambda`: + +```cs showLineNumbers +var names= new string [] { "a","b"}; +Expressionable exp = new Expressionable(); + +foreach (var item in names) +{ + exp.Or(it => it.Name.Contains(item.ToString())); +} + +var list= db.Queryable().Where(exp.ToExpression()).ToList(); +``` + +- 生成 `SQL`: + +```sql showLineNumbers +SELECT [Id],[Name],[Price],[CreateTime],[CustomId] + FROM [Order] WHERE ( + ([Name] like '%'+ CAST(@MethodConst0 AS NVARCHAR(MAX))+'%') OR + ([Name] like '%'+ CAST(@MethodConst1 AS NVARCHAR(MAX))+'%') + ) +``` + +### 10.1.5.4 仓储方法 + +新建一个仓储类,如果想扩展方法写到仓储类中 + +```cs showLineNumbers +public class Repository : SimpleClient where T : class, new() +{ + public Repository(ISqlSugarClient context = null) : base(context)//默认值等于null不能少 + { + base.Context = App.GetService();//用手动获取方式支持切换仓储 + } +} +``` + +继承仓储类就可以使用仓储API了 + +```cs showLineNumbers +//查询 +var data1 = base.GetById(1);//根据id查询 +var data4 = base.GetSingle(it => it.Id == 1);//查询单条记录,结果集不能超过1,不然会提示错误 +var data = base.GetFirst(it => it.Id == 1);//查询第一条记录 + +var data2 = base.GetList();//查询所有 +var data3 = base.GetList(it => it.Id == 1); //根据条件查询 + +var p = new PageModel() { PageIndex = 1, PageSize = 2 }; +var data5 = base.GetPageList(it => it.Name == "xx", p); +Console.Write(p.PageCount); + +var data6 = base.GetPageList(it => it.Name == "xx", p, it => it.Name, OrderByType.Asc); +Console.Write(p.PageCount); + +List conModels = new List(); +conModels.Add(new ConditionalModel(){FieldName="id",ConditionalType=ConditionalType.Equal,FieldValue="1"});//id=1 +var data7 = base.GetPageList(conModels, p, it => it.Name, OrderByType.Asc); +base.AsQueryable().Where(x => x.Id == 1).ToList(); + +//插入 +base.Insert(insertObj); +base.InsertRange(InsertObjs); +var id = base.InsertReturnIdentity(insertObj); +base.AsInsertable(insertObj).ExecuteCommand(); + +//删除 +base.Delete(insertObj); +base.DeleteById(1); +base.DeleteByIds(new object [] { 1, 2 }); //数组带是 ids方法 ,封装传 object [] 类型 +base.Delete(it => it.Id == 1); +base.AsDeleteable().Where(it => it.Id == 1).ExecuteCommand(); + +//更新 +base.Update(insertObj); +base.UpdateRange(InsertObjs); +base.Update(it => new Order() { Name = "a", }, it => it.Id == 1); +base.AsUpdateable(insertObj).UpdateColumns(it=>new { it.Name }).ExecuteCommand(); + +//高级操作 +base.AsSugarClient // 获取完整的db对象 +base.AsTenant // 获取多库相关操作 + +//切换仓储 +base.ChangeRepository>() //支持多租户和扩展方法,使用SqlSugarScope单例(或者SqlSugarClient Scope注入) +base.Change()//只支持自带方法和单库 +``` + +### 10.1.5.5 多库事务 + +```cs showLineNumbers +SqlSugarClient db = new SqlSugarClient(new List() +{ + new ConnectionConfig(){ ConfigId="0", DbType=DbType.SqlServer, ConnectionString=Config.ConnectionString, IsAutoCloseConnection=true }, + new ConnectionConfig(){ ConfigId="1", DbType=DbType.MySql, ConnectionString=Config.ConnectionString4 ,IsAutoCloseConnection=true} +}); + +var mysqldb = db.GetConnection("1"); // mysql db +var sqlServerdb = db.GetConnection("0"); // sqlserver db + +db.BeginTran(); + +mysqldb.Insertable(new Order() +{ + CreateTime = DateTime.Now, + CustomId = 1, + Name = "a", + Price = 1 +}).ExecuteCommand(); +mysqldb.Queryable().ToList(); +sqlServerdb.Queryable().ToList(); + +db.CommitTran(); +``` + +### 10.1.5.6 单例模式 + +```cs showLineNumbers +public static SqlSugarScope Db = new SqlSugarScope(new ConnectionConfig() + { + DbType = SqlSugar.DbType.SqlServer, + ConnectionString = Config.ConnectionString, + IsAutoCloseConnection = true + }, + db=> { + db.Aop.OnLogExecuting = (s, p) => + { + Console.WriteLine(s); + }; + }); + +using (var tran = Db.UseTran()) +{ + new Test2().Insert(XX); + new Test1().Insert(XX); + ..... + + tran.CommitTran(); +} +``` + +### 10.1.5.7 全局过滤器 + +```cs showLineNumbers +db.QueryFilter.Add(new TableFilterItem(it => it.Name.Contains("a"))); + +db.Queryable().ToList(); +// SELECT [Id],[Name],[Price],[CreateTime],[CustomId] FROM [Order] WHERE ([Name] like '%'+@MethodConst0+'%') + +db.Queryable((i, o) => i.OrderId == o.Id) + .Where(i => i.OrderId != 0) + .Select("i.*").ToList(); +// SELECT i.* FROM [OrderDetail] i ,[Order] o WHERE ( [i].[OrderId] = [o].[Id] ) AND +// ( [i].[OrderId] <> @OrderId0 ) AND ([o].[Name] like '%'+@MethodConst1+'%') +``` + +### 10.1.5.8 添加或者更新 + +```cs showLineNumbers +var x = Db.Storageable(list2).ToStorage(); +x.AsInsertable.ExecuteCommand(); +x.AsUpdateable.ExecuteCommand(); +``` + +```cs showLineNumbers +var x = Db.Storageable(list).SplitInsert(it => !it.Any()).ToStorage() +x.AsInsertable.ExecuteCommand(); +``` + +### 10.1.5.9 自动分表 + +```cs showLineNumbers +[SplitTable(SplitType.Year)] // Table by year (the table supports year, quarter, month, week and day) +[SugarTable("SplitTestTable_{year}{month}{day}")] +public class SplitTestTable +{ + [SugarColumn(IsPrimaryKey =true)] + public long Id { get; set; } + + public string Name { get; set; } + + //When the sub-table field is inserted, which table will be inserted according to this field. + //When it is updated and deleted, it can also be convenient to use this field to + //find out the related table + [SplitField] + public DateTime CreateTime { get; set; } +} +``` + +```cs showLineNumbers +var lis2t = db.Queryable() +.SplitTable(DateTime.Now.Date.AddYears(-1), DateTime.Now) +.ToPageList(1,2);  +``` + +### 10.1.5.10 大数据插入,更新,插入或者更新 + +```cs showLineNumbers +//Insert A million only takes a few seconds +db.Fastest().BulkCopy(GetList()); + +//update A million only takes a few seconds +db.Fastest().BulkUpdate(GetList());//A million only takes a few seconds完 +db.Fastest().BulkUpdate(GetList(),new string[]{"id"},new string[]{"name","time"})//no primary key + +//if exists update, else insert + var x= db.Storageable(data).ToStorage(); + x.BulkCopy(); + x.BulkUpdate(); + +//set table name +db.Fastest().AS("tableName").BulkCopy(GetList()) + +//set page +db.Fastest().PageSize(300000).BulkCopy(insertObjs); +``` + +### 10.1.5.11 更多功能 + +可查阅 [SqlSugar 官网](https://www.donet5.com/Home/Doc)。 + +## 10.1.6 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: + +--- + +:::note 了解更多 + +想了解更多 `SqlSugar` 知识可查阅 [SqlSugar 官网](https://www.donet5.com/Home/Doc)。 + +::: \ No newline at end of file diff --git a/handbook/docs/subscribe.mdx b/handbook/docs/subscribe.mdx new file mode 100644 index 0000000000000000000000000000000000000000..1db0fead9656bc1e8b914e5ff1b8865fdbd6a6dc --- /dev/null +++ b/handbook/docs/subscribe.mdx @@ -0,0 +1,105 @@ +--- +id: subscribe +title: 1.11 VIP 服务 +sidebar_label: 1.11 VIP 服务 ✨ +description: 开通 VIP 服务仅需 499 元/年,尊享 365 天项目无忧 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +:::note 服务背景 + +随着 Furion 框架在 NuGet 平台下载量突破 [1100 万](https://www.nuget.org/profiles/monk.soul),越来越多的企业和个人选择将 Furion 作为项目开发的首选框架。然而,随着用户数量的急剧增加,也带来了更多的使用问题。**尽管 Furion 提供了详细的文档和 Gitee 交流社区,但我们的技术团队面临着前所未有的压力。一方面,用户咨询问题的压力日益增加,另一方面,我们需要投入更多的研发资源和时间成本。** + +由于 Furion 框架的受众主要是程序员和软件企业用户,项目工期往往紧迫。为了赶进度,加班到很晚是常见的情况。**然而,在使用 Furion 框架时遇到无法解决的问题可能会对项目进度和个人绩效评估产生负面影响,甚至可能导致项目工期延误甚至法律风险。** + +尽管 Furion 框架提供了在线问题反馈平台,我们也力求在当天解决出现的 Bug 问题,但面对如此庞大的开发需求和企业规定的工期,仍然无法满足需求。 + +**因此,我们团队决定提供专业的 VIP 服务支持,并承诺以最短时间给出响应。这将极大地缩短企业的开发时间和试错成本。** + +::: + +--- + +## 服务说明 + +:::tip 期待与您建立长期的合作关系 + +**在保持 Furion 初衷不变的前提下**,我们努力尝试和探索 Furion 开源商业化的方式,以确保既满足原有开源的初衷,又能为企业或项目提供强有力的契约保证。**我们的目标是建立长期的合作关系,并共同实现成功!** + +**有了这笔资金,我们团队将能够加大研发投入,研发出一款能够显著提升企业开发效率并降低成本的优秀框架。 🤜🤛** + +**成功需要建立在契约上的朋友,Furion 与您做朋友。 🤝** + +::: + +感谢您选择我们基于 MIT 开源协议的框架,并对我们提供的 VIP 服务表达兴趣。我们致力于为您的企业或项目提供全面的技术支持和解决方案,以帮助您充分发挥我们框架的潜力。以下是我们的 VIP 服务内容: + +## 基础服务 ✅ + +- **以一对一的方式,为您提供快速入门框架的专业技术指导。✅** +- **提供优先级响应和解答,确保及时回复您在使用我们的框架时遇到的问题。✅** +- **您可通过电子邮件或在线聊天与技术支持团队联系,享受框架专业团队的优先支持。✅** +- **如发现框架中的 bug 或框架其他技术问题,我们承诺最快响应(1 小时内)并进行修复或答疑。✅** +- **作为 VIP 服务客户,您可提前获取框架最新版本和更新,确保始终使用框架的最新功能和改进。✅** +- **我们开发团队为企业或个人项目提供专注于框架技术支持交流的 QQ/微信群。✅** +- **我们承诺对开通 VIP 服务的企业或个人信息以及在服务期间涉及的相关项目资料进行保密。✅** +- **开通 VIP 服务的企业或个人将享受到与 Furion 框架专业团队进行一对一的框架技术指导特权。✅** +- **我们提供私密且安全的框架漏洞报告通道,以确保企业生产环境项目免受公开漏洞的入侵。✅** +- **每月提供不超过 3 次、每次不超过 30 分钟的有效远程框架问题排查技术协助服务。✅** +- 您可通过我们框架的 bug 报告系统([Gitee Issue](https://gitee.com/dotnetchina/Furion/issues))提交问题,并跟踪框架修复进度。 +- 我们定期发布框架的新版本和更新,修复漏洞、添加新功能和提升性能。 + +## 定制服务 + +如果您的企业或项目需要特定的定制化功能或开发需求,我们的专业团队可以提供相关支持,帮助您满足企业或项目特定的业务需求。 + +请注意,我们的定制服务费用将根据您的具体需求、服务级别和合同约定进行商议。我们非常愿意与您进一步讨论并提供详细的报价。 + +对于我们的定制服务,我们承诺为您提供高质量的技术支持和完善的解决方案。如果您对以上说明有任何问题或需要进一步了解,请随时与我们联系。 + +## 服务时间 + +**周一至周五(10:00\~12:0014:00\~18:00),国家法定节假日视情况提供服务。** + +## 服务范围 + +仅适用于项目初始时使用 Furion 框架模式开发的项目,不包括已包含大量业务代码并后续需要集成 Furion 框架的情况。 + +## 合作方式 + +**1. 微信扫码支付 499 元即可获得为期一年的 VIP 服务(备注:个人或企业名称)。✅** + + + +
+
+ + + (支持添加作者微信 ibaiqian 进行转账) + + +--- + +**2. 添加作者微信 ibaiqian 并加入 VIP 微信群(或为您提供独立的 QQ/微信 技术支持群)。 ✅** + +--- + +**3. 通过手机 QQ 扫码加入 VIP QQ 群(或为您提供独立的 QQ/微信 技术支持群)。✅** + + + +--- + +**4. 享受优先支持,获得最快响应(1 小时内)并提前获取最新版本和更新。✅** + +## 联系方式 + +- 作者微信:ibaiqian(添加时需备注个人或企业名称) +- 电子邮箱:monksoul@outlook.com + +--- + +**以上均为 Furion 框架增值服务,不影响开源社区的正常运营,再次感谢您对我们框架的选择。** + +--- diff --git a/handbook/docs/target.mdx b/handbook/docs/target.mdx new file mode 100644 index 0000000000000000000000000000000000000000..fda159bd6ea1ef09fd15998fc0a199b9cb9cd705 --- /dev/null +++ b/handbook/docs/target.mdx @@ -0,0 +1,53 @@ +--- +id: target +title: 1.8 路线图 +sidebar_label: 1.8 路线图/计划 +description: 了解 Furion 重构版本的状态和演变 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +## 项目背景 + +当前的 **👉 [Furion v4](https://gitee.com/dotnetchina/Furion/tree/v4/)** 版本已经逐步成熟且稳定,但仍存在以下问题: + +- **早期开发进展迅速,很多代码存在仓促上线的情况,缺乏充分思考和考虑** +- **所有模块都包含在一个项目中,无法按需加载和安装** +- **过度使用静态类和静态内存存储,不利于进行单元测试和数据隔离** +- **对于 .NET Core 的掌握程度有限,导致历史代码臃肿且高度耦合** +- 代码架构和设计模式缺乏统一性,可以说是一个大杂烩 +- 在早期对用户需求掌握不足,导致后期不断打补丁来进行改进,稍有改动就可能引发破坏性的修改 +- 模块、类型、属性、方法、属性等命名混乱,很难从字面上理解其功能含义 +- 模块功能封装过度,配置参数繁杂 +- 框架示例混乱,用户只能自行摸索最佳实践 +- 虽有单元测试,但是非常混乱 + +正是因为存在以上诸多问题,为了 Furion 能够长期发展,我们团队决定进行下一版本(v5)开发。 + +## 技术选择 + +Furion v5 版本采用 C# 12 和 .NET 8 进行开发。 + +## 设计指导 + +- **计划和分析**: 在开始编写代码之前,仔细规划和分析整个框架的需求和功能。确定好核心功能和结构,以及可能的扩展和变化。这样可以减少后续的重构工作。 +- **模块化设计**: 将整个框架划分为多个独立的模块,每个模块负责一个特定的功能。这样可以降低代码间的依赖性,方便后续的修改和调整。 +- **接口设计**: 设计清晰的接口和抽象层,以便将来的修改不会对其他模块产生过多的影响。好的接口设计可以提高代码的可维护性和可扩展性。 +- **设计模式**: 使用合适的设计模式来解决常见的问题,例如单例模式、观察者模式、策略模式等。这些设计模式可以提供灵活性和可扩展性,减少需要重构的风险。 +- **自动化测试**: 在开发过程中编写充分的自动化测试,覆盖各个模块的功能和边界情况。这样可以及早发现问题,并减少在重构时引入新的错误。 +- **审查和反馈**: 定期进行代码审查,并及时处理同事和用户的反馈意见。这有助于发现问题和改进,避免重复的重构工作。 +- **持续集成**: 将代码集成到一个持续集成系统中,并在每次提交代码时运行自动化测试。这可以及时发现潜在的问题,避免代码质量下降。 + +## 框架目标 + +- **实现完全无第三方依赖(除微软官方提供外)** +- **实现彻底模块化,每个模块都是独立的项目** +- **每个模块的单元测试覆盖率要达到 92% 以上** +- **确保每个类型、属性、字段、方法都有详细的注释** +- 尽可能避免使用静态内存存储 +- 所有模块都采用上下文和构建器模式进行设计 +- 所有模块都采用依赖注入/控制反转的设计模式 +- 所有模块都采用约定大于配置设计原则 +- 尽可能为每个模块提供看板功能 +- 提供所有模块最佳实践示例 +- 提供所有模块详细使用文档和 API 文档 diff --git a/handbook/docs/task-queue.mdx b/handbook/docs/task-queue.mdx new file mode 100644 index 0000000000000000000000000000000000000000..11a7da860a5afbe2ef127732d956d56c4bbeaeca --- /dev/null +++ b/handbook/docs/task-queue.mdx @@ -0,0 +1,248 @@ +--- +id: task-queue +title: 26.3 任务队列 +sidebar_label: 26.3 任务队列 +--- + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **新特性** + + -  新增 `Enqueue/EnqueueAsync` 支持 `Cron` 表达式 实例重载方法 4.8.4.10 ⏱️2023.01.09 [#I69HM4](https://gitee.com/dotnetchina/Furion/issues/I69HM4) + +
+
+
+ +import useBaseUrl from "@docusaurus/useBaseUrl"; + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.3 +` 版本使用。 + +::: + +:::tip 小知识 + +任务队列可取代旧版本定时任务的 `SpareTime.DoIt()` 和 `SpareTime.DoOnce` 功能。 + +::: + +## 26.3.1 关于任务队列 + +任务队列常用于管理后台工作,**通常这些后台工作在主线程响应之外,不会对主线程或当前线程响应阻塞**。任务队列的一个显著特定就是它是一个队列,入队的顺序决定它出队执行的先后。 + +任务队列使用 `Channel` + `Task` + `ThreadPool(线程池)` 实现,入队/出队速度非常快,吞吐量极高,内存和 `CPU` 占用几乎忽略不计。 + +任务队列应用场景:**对于可能需长时间运行的任务,或不是那么及时的需要立即反馈的任务。**比如发送邮件,发送短信等等。 + +## 26.3.2 与事件总线的区别 + +事件总线基于消息通讯,任务队列最显著的特点就是将操作依次加入队列,然后按照入队的顺序出队去执行。 + +前者(事件总线)是无序的,**只有完全匹配的消息 `Id` 才会触发执行操作,否则处于 “静待” 状态。** + +而后者(任务队列)则是将**可能耗时且一定会执行的操作放到队列中,之后依次出队执行**。 + +## 26.3.3 入门指南 + +任务队列使用非常简单,只需要注册 `services.AddTaskQueue()` 服务,之后通过依赖注入 `ITaskQueue` 服务或通过 `TaskQueued` 静态类使用即可, + +**1. 注册 `TaskQueue` 服务** + +```cs showLineNumbers +services.AddTaskQueue(); +``` + +**2. 使用 `ITaskQueue` 服务** + +```cs showLineNumbers {1,8,18,24,34,32,38,46,53,61,68,76,79,90,94} +using Furion.TaskQueue; + +namespace Your.Application; + +public class YourService : IYourService +{ + private readonly ITaskQueue _taskQueue; + public YourService(ITaskQueue taskQueue) + { + _taskQueue = taskQueue; + } + + /// + /// 同步入队 + /// + public void SyncTask() + { + _taskQueue.Enqueue(provider => + { + Console.WriteLine("我是同步的"); + }); + + // 如无需使用 provider 参数,可用 _ 替代 + _taskQueue.Enqueue(_ => {}); + } + + /// + /// 同步入队,延迟 3 秒触发 + /// + public void SyncTask2() + { + _taskQueue.Enqueue(provider => + { + Console.WriteLine("我是同步的,但我延迟了 3 秒"); + }, 3000); + + // 如无需使用 provider 参数,可用 _ 替代 + _taskQueue.Enqueue(_ => {}, 3000); + } + + /// + /// 异步入队 + /// + public async Task AsyncTask() + { + await _taskQueue.EnqueueAsync(async (provider, token) => + { + Console.WriteLine("我是异步的"); + await ValueTask.CompletedTask; + }); + + // 如无需使用 provider 和 token 参数,可用 _ 替代 + await _taskQueue.EnqueueAsync(async (_, _) => {}); + } + + /// + /// 异步入队,延迟 3 秒触发 + /// + public async Task AsyncTask2() + { + await _taskQueue.EnqueueAsync(async (provider, token) => + { + Console.WriteLine("我是异步的,但我延迟了 3 秒"); + await ValueTask.CompletedTask; + }, 3000); + + // 如无需使用 provider 和 token 参数,可用 _ 替代 + await _taskQueue.EnqueueAsync(async (_, _) => {}, 3000); + } + + /// + /// 同步入队,支持 Cron 表达式延迟 + /// + public void SyncTask3() + { + _taskQueue.Enqueue(provider => + { + Console.WriteLine("Cron ..."); + }, "* * * * *"); + + // 如无需使用 provider 参数,可用 _ 替代 + _taskQueue.Enqueue(_ => {}, "* * * * *", CronStringFormat.Default); + } + + /// + /// 异步入队,支持 Cron 表达式延迟 + /// + public async Task AsyncTask3() + { + await _taskQueue.EnqueueAsync(async (provider, token) => + { + Console.WriteLine("Cron ..."); + await ValueTask.CompletedTask; + }, "* * * * *"); + + // 如无需使用 provider 和 token 参数,可用 _ 替代 + await _taskQueue.EnqueueAsync(async (_, _) => {}, "* * * * *", CronStringFormat.Default); + } +} +``` + +:::important 注意事项 + +框架内置了一套简单的错误策略机制,也就是如果任务执行失败会默认重试 `3` 次,每次间隔 `1秒`,该策略配置暂不对外公开。 + +::: + +## 26.3.4 `TaskQueued` 静态类 + +框架还提供了 `TaskQueued` 静态类可在任何线程中操作,如: + +```cs showLineNumbers {2-3,6-7} +// 同步入队 +TaskQueued.Enqueue((provider) => {}, [delay]); +TaskQueued.Enqueue((provider) => {}, cronExpression, [format]); + +// 异步入队 +await TaskQueued.EnqueueAsync(async (provider, token) => {}, [delay]); +await TaskQueued.EnqueueAsynce(async (provider, token) => {}, cronExpression, [format]); +``` + +## 26.3.5 在处理程序中使用服务 + +如果在任务队列处理程序中使用了外部的服务,如: + +```cs showLineNumbers {4,17} +public class YourService : IYourService +{ + private readonly ITaskQueue _taskQueue; + private readonly ILogger _logger + + public YourService(ITaskQueue taskQueue + , ILogger logger) + { + _taskQueue = taskQueue; + _logger = logger; + } + + public void SyncTask() + { + _taskQueue.Enqueue(provider => + { + _logger.LogInformation("我使用了外部的 logger"); + }); + } +} +``` + +那么需要注意的是,**如果使用的外部服务是 **`单例`**服务,那么无需任何处理,但如果使用的服务属于 `瞬时` 或 `范围` 作用域,那么需要创建作用域**,如: + +```cs showLineNumbers {3-5,7-8} +_taskQueue.Enqueue(provider => +{ + // Repository 注册为范围,需创建作用域 + using var scoped = provider.CreateScope(); + var repository = scoped.ServiceProvider.GetService>(); + + // Logger 注册为单例,可以直接使用 + _logger.LogInformation("我使用了外部的 logger"); +}); +``` + +## 26.3.6 订阅执行任务意外异常 + +任务处理程序使用的是 `Task` 对象进行创建并执行,但可能存在一些意外且难以捕获的异常,这时候可以通过以下方式订阅: + +```cs showLineNumbers {4} +services.AddTaskQueue(builder => +{ + // 订阅 TaskQueue 意外未捕获异常 + builder.UnobservedTaskExceptionHandler = (obj, args) => + { + // .... + }; +}); +``` + +## 26.3.7 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/template.mdx b/handbook/docs/template.mdx new file mode 100644 index 0000000000000000000000000000000000000000..e4cdcff798e2adfdf4a0480d6c392df3a2ed5ebe --- /dev/null +++ b/handbook/docs/template.mdx @@ -0,0 +1,223 @@ +--- +id: template +title: 2.6 官方脚手架 +sidebar_label: 2.6 官方脚手架 +description: 快速创建生产级项目解决方案 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **突破性变化** + + -  升级 **脚手架支持创建 `.NET8 Preview.1` 项目** 4.8.7 ⏱️2023.02.22 + +- **问题修复** + + -  修复 `Blazor` 脚手架出现 `blazor.server.js` 不能加载问题(`404`) 4.8.7.13 ⏱️2023.03.16 [#I6NOBQ](https://gitee.com/dotnetchina/Furion/issues/I6NOBQ) + +- **其他更改** + + -  调整 `Blazor` + `WebAPI` 脚手架模板,默认添加授权支持 4.8.7.37 ⏱️2023.04.07 [#I6OM8O](https://gitee.com/dotnetchina/Furion/issues/I6OM8O) [544f80d](https://gitee.com/dotnetchina/Furion/commit/544f80dbd7c800e28d9c4137e1c3bfc289c14177) + -  调整 脚手架模板,默认启用主流文件类型 `MIME` 支持 4.8.7.5 ⏱️2023.03.07 [e35cdab](https://gitee.com/dotnetchina/Furion/commit/e35cdab592d1a00ff32b08c566c4ed5d6ddcff24) + +
+
+
+ +:::important 特别说明 + +**脚手架不是安装在项目中的!** 而是通过 `CMD` 或 `PowerShell` 安装到操作系统中的。 + +::: + +## 2.6.1 脚手架 + +`Furion` 官方提供了多种 `Web` 应用类型的脚手架,方便大家快速创建多层架构项目。目前支持以下应用脚手架: + +### 2.6.1.1 `Furion + EFCore` + +| 模板类型 | 名称 | 版本 | 关键词 | 描述 | +| :--------------------------------------------------------------------------------------------------------------------------------------: | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ---------------------- | +| [![nuget](https://shields.io/badge/-NuGet-yellow?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.Template.Mvc/) | Furion.Template.Mvc | [![nuget](https://img.shields.io/nuget/v/Furion.Template.Mvc.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.Template.Mvc/) | 👉 **furionmvc** | Mvc 模板 | +| [![nuget](https://shields.io/badge/-NuGet-yellow?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.Template.Api/) | Furion.Template.Api | [![nuget](https://img.shields.io/nuget/v/Furion.Template.Api.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.Template.Api/) | 👉 **furionapi** | WebApi 模板 | +| [![nuget](https://shields.io/badge/-NuGet-yellow?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.Template.App/) | Furion.Template.App | [![nuget](https://img.shields.io/nuget/v/Furion.Template.App.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.Template.App/) | 👉 **furionapp** | Mvc/WebApi 模板 | +| [![nuget](https://shields.io/badge/-NuGet-yellow?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.Template.Razor/) | Furion.Template.Razor | [![nuget](https://img.shields.io/nuget/v/Furion.Template.Razor.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.Template.Razor/) | 👉 **furionrazor** | RazorPages 模板 | +| [![nuget](https://shields.io/badge/-NuGet-yellow?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.Template.RazorWithWebApi/) | Furion.Template.RazorWithWebApi | [![nuget](https://img.shields.io/nuget/v/Furion.Template.RazorWithWebApi.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.Template.RazorWithWebApi/) | 👉 **furionrazorapi** | RazorPages/WebApi 模板 | +| [![nuget](https://shields.io/badge/-NuGet-yellow?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.Template.Blazor/) | Furion.Template.Blazor | [![nuget](https://img.shields.io/nuget/v/Furion.Template.Blazor.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.Template.Blazor/) | 👉 **furionblazor** | Blazor 模板 | +| [![nuget](https://shields.io/badge/-NuGet-yellow?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.Template.BlazorWithWebApi/) | Furion.Template.BlazorWithWebApi | [![nuget](https://img.shields.io/nuget/v/Furion.Template.BlazorWithWebApi.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.Template.BlazorWithWebApi/) | 👉 **furionblazorapi** | Blazor/WebApi 模板 | + +### 2.6.1.2 `Furion + SqlSugar` + +| 模板类型 | 名称 | 版本 | 关键词 | 描述 | +| :-----------------------------------------------------------------------------------------------------------------------------------------------: | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ---------------------- | +| [![nuget](https://shields.io/badge/-NuGet-yellow?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.SqlSugar.Template.Mvc/) | Furion.SqlSugar.Template.Mvc | [![nuget](https://img.shields.io/nuget/v/Furion.SqlSugar.Template.Mvc.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.SqlSugar.Template.Mvc/) | 👉 **fsmvc** | Mvc 模板 | +| [![nuget](https://shields.io/badge/-NuGet-yellow?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.SqlSugar.Template.Api/) | Furion.SqlSugar.Template.Api | [![nuget](https://img.shields.io/nuget/v/Furion.SqlSugar.Template.Api.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.SqlSugar.Template.Api/) | 👉 **fsapi** | WebApi 模板 | +| [![nuget](https://shields.io/badge/-NuGet-yellow?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.SqlSugar.Template.App/) | Furion.SqlSugar.Template.App | [![nuget](https://img.shields.io/nuget/v/Furion.SqlSugar.Template.App.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.SqlSugar.Template.App/) | 👉 **fsapp** | Mvc/WebApi 模板 | +| [![nuget](https://shields.io/badge/-NuGet-yellow?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.SqlSugar.Template.Razor/) | Furion.SqlSugar.Template.Razor | [![nuget](https://img.shields.io/nuget/v/Furion.SqlSugar.Template.Razor.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.SqlSugar.Template.Razor/) | 👉 **fsrazor** | RazorPages 模板 | +| [![nuget](https://shields.io/badge/-NuGet-yellow?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.SqlSugar.Template.RazorWithWebApi/) | Furion.SqlSugar.Template.RazorWithWebApi | [![nuget](https://img.shields.io/nuget/v/Furion.SqlSugar.Template.RazorWithWebApi.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.SqlSugar.Template.RazorWithWebApi/) | 👉 **fsrazorapi** | RazorPages/WebApi 模板 | +| [![nuget](https://shields.io/badge/-NuGet-yellow?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.SqlSugar.Template.Blazor/) | Furion.SqlSugar.Template.Blazor | [![nuget](https://img.shields.io/nuget/v/Furion.SqlSugar.Template.Blazor.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.SqlSugar.Template.Blazor/) | 👉 **fsblazor** | Blazor 模板 | +| [![nuget](https://shields.io/badge/-NuGet-yellow?cacheSeconds=604800)](https://www.nuget.org/packages/Furion.SqlSugar.Template.BlazorWithWebApi/) | Furion.SqlSugar.Template.BlazorWithWebApi | [![nuget](https://img.shields.io/nuget/v/Furion.SqlSugar.Template.BlazorWithWebApi.svg?cacheSeconds=10800)](https://www.nuget.org/packages/Furion.SqlSugar.Template.BlazorWithWebApi/) | 👉 **fsblazorapi** | Blazor/WebApi 模板 | + +## 2.6.2 安装脚手架 + +打开 `CMD` 或 `Powershell` 执行模板安装命令(旧版本 `install` 可换成 `--install`): + +### 2.6.2.1 `Furion + EFCore` 脚手架安装 + +```bash showLineNumbers +# Mvc 模板 +dotnet new install Furion.Template.Mvc::4.9.1.7 +# WebApi 模板 +dotnet new install Furion.Template.Api::4.9.1.7 +# Mvc/WebApi 模板 +dotnet new install Furion.Template.App::4.9.1.7 +# RazorPages 模板 +dotnet new install Furion.Template.Razor::4.9.1.7 +# RazorPages/WebApi 模板 +dotnet new install Furion.Template.RazorWithWebApi::4.9.1.7 +# Blazor 模板 +dotnet new install Furion.Template.Blazor::4.9.1.7 +# Blazor/WebApi 模板 +dotnet new install Furion.Template.BlazorWithWebAPI::4.9.1.7 +``` + +:::note 最新版安装 + +不带版本号总是安装最新的版本。 + +::: + +### 2.6.2.2 `Furion + SqlSugar` 脚手架安装 + +```bash showLineNumbers +# Mvc 模板 +dotnet new install Furion.SqlSugar.Template.Mvc::4.9.1.7 +# WebApi 模板 +dotnet new install Furion.SqlSugar.Template.Api::4.9.1.7 +# Mvc/WebApi 模板 +dotnet new install Furion.SqlSugar.Template.App::4.9.1.7 +# RazorPages 模板 +dotnet new install Furion.SqlSugar.Template.Razor::4.9.1.7 +# RazorPages/WebApi 模板 +dotnet new install Furion.SqlSugar.Template.RazorWithWebApi::4.9.1.7 +# Blazor 模板 +dotnet new install Furion.SqlSugar.Template.Blazor::4.9.1.7 +# Blazor/WebApi 模板 +dotnet new install Furion.SqlSugar.Template.BlazorWithWebAPI::4.9.1.7 +``` + +:::note 最新版安装 + +不带版本号总是安装最新的版本。 + +::: + +:::tip `SqlSugar` 脚手架基本使用 + +在 `SqlSugar` 脚手架中,`Core` 层包含了一个 `DbContext.cs` 类,可通过 `DbContext.Instance` 就可以获取到 `SqlSugarScope` 对象。 + +数据库链接字符串在启动层 `appsettings.json` 文件的 `ConnectionConfigs` 节点中配置,对应的是 `SqlSugar` 中的 `ConnectionConfig[]` 对象。 + +::: + +## 2.6.3 使用脚手架 + +```bash showLineNumbers {2,5-8,11-14} +# 命令模板如下 +dotnet new 关键词 -n 项目名称 -f .NET版本 + +# EFCore:支持创建 .NET5-8 版本 +dotnet new furionapi -n MyProject -f net5 +dotnet new furionapi -n MyProject -f net6 +dotnet new furionapi -n MyProject -f net7 +dotnet new furionapi -n MyProject -f net8 + +# SqlSugar:支持创建 .NET5-8 版本 +dotnet new fsapi -n MyProject -f net5 +dotnet new fsapi -n MyProject -f net6 +dotnet new fsapi -n MyProject -f net7 +dotnet new fsapi -n MyProject -f net8 +``` + +这样就可以生成项目代码了,**生成之后推荐将所有的 `nuget` 包更新到最新版本。** + +:::tip 关于项目名称和 `.NET` 版本 + +通过脚手架生成的项目名称不能包含 `短横线` 等特殊字符,如有该需求,可在生成之后通过 `Visual Studio` 进行手动修改。 + +`-f` 版本参数所有选项:`net5`,`net6`,`net7`,`net8`,默认是 `net8`。 + +::: + +:::important 特别提醒 + +`furionapi` 对应的是上面列表的 `关键词`,我们也可以通过 `dotnet new --list` 查看。 + +想了解更多可以使用 `dotnet new 关键词 --help` 查看更多参数。 + +::: + +## 2.6.4 脚手架更新 + +只需要重新安装最新版替换即可,如: + +```bash showLineNumbers +dotnet new install Furion.Template.Api::4.9.1.7 +``` + +:::note 最新版安装 + +不带版本号总是安装最新的版本。 + +::: + +## 2.6.5 `Visual Studio` 集成 + +通过命令安装脚手架后,升级 `Visual Studio` 到最新版可使用可视化方式创建哦。 + + + +## 2.6.6 `关于 MVC 添加区域出错问题` + +由于 `Furion` 底层依赖了 `Microsoft.CodeAnalysis.CSharp` 包,所以可能通过 `Viusal Studio` 的界面添加 `Area 区域` 时会出错,这时候只需要在 `Web.Entry` 层安装下面几个包即可: + +- `Microsoft.CodeAnalysis.CSharp.Features` +- `Microsoft.CodeAnalysis.CSharp.Scripting` +- `Microsoft.CodeAnalysis.VisualBasic.Features` +- `Microsoft.CodeAnalysis.Workspaces.MSBuild` +- `Microsoft.VisualStudio.Web.CodeGeneration.Design` + +## 2.6.7 搭建脚手架 + +脚手架可以极大的提高我们搭建新项目的速度,推荐两篇文章给大家学习: + +[https://www.cnblogs.com/laozhang-is-phi/p/10205495.html](https://www.cnblogs.com/laozhang-is-phi/p/10205495.html) + +[https://www.cnblogs.com/catcher1994/p/10061470.html](https://www.cnblogs.com/catcher1994/p/10061470.html) + +**推荐直接拷贝 `Furion` 脚手架文件夹进行修改,可避免很多问题。** + +[Furion 脚手架源码](https://gitee.com/dotnetchina/Furion/tree/v4/templates) + +## 2.6.8 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: + +--- + +:::note 了解更多 + +想了解更多 `模板知识` 知识可查阅 [dotnet-new 模板](https://docs.microsoft.com/zh-cn/dotnet/core/tools/dotnet-new) 章节。 + +::: diff --git a/handbook/docs/tran.mdx b/handbook/docs/tran.mdx new file mode 100644 index 0000000000000000000000000000000000000000..f191c2957bed3affdc0fe57915622afee00eeb8c --- /dev/null +++ b/handbook/docs/tran.mdx @@ -0,0 +1,384 @@ +--- +id: tran +title: 9.27 事务和工作单元 +sidebar_label: 9.27 事务和工作单元 (UnitOfWork) +--- + +import Tag from "@site/src/components/Tag.js"; +import useBaseUrl from "@docusaurus/useBaseUrl"; +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +
+ 📝 模块更新日志 +
+
+ +- **问题修复** + + -  修复 `Scoped.CreateUowAsync` 作用域工作单元异常无法回滚问题 4.8.8.44 ⏱️2023.09.23 [#I833I9](https://gitee.com/dotnetchina/Furion/issues/I833I9) + +
+
+
+ +## 9.27.1 事务 + +事务指作为单个逻辑工作单元执行的一系列操作,要么**完全地执行,要么完全地不执行**。 + +简单的说,事务就是并发控制的单位,是用户定义的一个操作序列。 而一个逻辑工作单元要成为事务,就必须满足 `ACID` 属性。 + +- `A`:原子性(Atomicity):事务中的操作要么都不做,要么就全做 +- `C`:一致性(Consistency):事务执行的结果必须是从数据库从一个一致性状态转换到另一个一致性状态 +- `I`:隔离性(Isolation):一个事务的执行不能被其他事务干扰 +- `D`:持久性(Durability):一个事务一旦提交,它对数据库中数据的改变就应该是永久性的 + +## 9.27.2 工作单元 + +简单来说,就是为了保证一次完整的功能操作所产生的一系列提交数据的完整性,起着事务的作用。在计算机领域中,工作单元通常用 `UnitOfWork` 名称表示。 + +通常我们保证用户的每一次请求都是处于在一个功能单元中,也就是工作单元。 + +## 9.27.3 如何使用 + +### 9.27.3.1 `[UnitOfWork]` 自动管理 + +在 `Furion` 框架中,我们只需要在控制器 Action 中贴 `[UnitOfWork]` 特性即可开启工作单元模式,保证了每一次请求都是一个 `工作单元`,要么同时成功,要么同时失败。 + +- **单库操作** + +下面方式支持所有关系型数据库类型 + +```cs showLineNumbers {1} +[UnitOfWork] // 由于出现错误,所以所有数据库变更都会自动回滚 +public async Task 测试环境事务(int id) +{ + // 各种奇葩数据库操作 + await _personRepository.DeleteNowAsync(id); + + // 其他数据库操作。。 + + // 故意出错 + var d = await _personRepository.SqlQueriesAsync("select * from persion2 d"); +} +``` + +- **多库操作** + +支持各种奇葩的 `ORM`,包括 `ADO.NET`,`EFCore` 等第三方,**支持所有关系型数据库类型但不支持 `Sqlite`** + +```cs showLineNumbers +[UnitOfWork(UseAmbientTransaction = true)] // 由于出现错误,所以所有数据库变更都会自动回滚 +public async Task 测试环境事务(int id) +{ + // 各种奇葩数据库操作 + await _personRepository.DeleteNowAsync(id); + + // 其他数据库操作。。 + + // 故意出错 + var d = await _personRepository.SqlQueriesAsync("select * from persion2 d"); +} +``` + +- `UnitOfWork` 内置配置: + - `UseAmbientTransaction`:是否开启分布式环境事务,`bool` 类型,默认 `false`,**不支持 `Sqlite`** + - `TransactionScope`:配置分布式环境事务范围,`TransactionScopeOption` 类型,当 `UseAmbientTransaction` 为 `true` 有效 + - `TransactionIsolationLevel`:配置分布式环境事务隔离级别,`IsolationLevel` 类型,当 `UseAmbientTransaction` 为 `true` 有效 + - `TransactionTimeout`:配置分布式环境事务执行超时时间,`int` 类型,当 `UseAmbientTransaction` 为 `true` 有效 + - `TransactionScopeAsyncFlow`:配置分布式环境事务异步流支持,`TransactionScopeAsyncFlowOption` 类型,当 `UseAmbientTransaction` 为 `true` 有效 + - `EnsureTransaction`:强制使字符串 `sql` 拓展事务有效,`bool` 类型,默认 `false` + +:::important 版本说明 + +以下内容仅限 `Furion 3.7.3 +` 版本使用。 + +::: + +如使用非 `EFCore` ORM 框架,可实现 `IUnitOfWork` 接口之后调用 `services.AddUnitOfWork()` 注册即可,如示例代码: + +```cs showLineNumbers {8,30,41,52,63} +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Furion.DatabaseAccessor; + +/// +/// SqlSugar 工作单元实现 +/// +public sealed class SqlSugarUnitOfWork : IUnitOfWork +{ + /// + /// SqlSugar 对象 + /// + private readonly ISqlSugarClient _sqlSugarClient; + + /// + /// 构造函数 + /// + /// + public SqlSugarUnitOfWork(ISqlSugarClient sqlSugarClient) + { + _sqlSugarClient = sqlSugarClient; + } + + /// + /// 开启工作单元处理 + /// + /// + /// + /// + public void BeginTransaction(FilterContext context, UnitOfWorkAttribute unitOfWork) + { + _sqlSugarClient.AsTenant().BeginTran(); + } + + /// + /// 提交工作单元处理 + /// + /// + /// + /// + public void CommitTransaction(FilterContext resultContext, UnitOfWorkAttribute unitOfWork) + { + _sqlSugarClient.AsTenant().CommitTran(); + } + + /// + /// 回滚工作单元处理 + /// + /// + /// + /// + public void RollbackTransaction(FilterContext resultContext, UnitOfWorkAttribute unitOfWork) + { + _sqlSugarClient.AsTenant().RollbackTran(); + } + + /// + /// 执行完毕(无论成功失败) + /// + /// + /// + /// + public void OnCompleted(FilterContext context, FilterContext resultContext) + { + _sqlSugarClient.Dispose(); + } +} +``` + +之后注册即可: + +```cs showLineNumbers +services.AddUnitOfWork(); +``` + +:::tip 小知识-如何判断是否开启了分布式环境事务 + +有时候我们自定义了工作单元之后,个别 `ORM` 不支持分布式环境事务,那么就会出现执行错误,我们可以通过 `System.Transactions.Transaction.Current != null` 来判断是否启用了分布式环境事务,不等于 `null` 则为启用,否则未启用。 + +::: + +### 9.27.3.2 `EnsureTransaction()` 方法 ✨ + +有些时候我们通过静态类或者其他方式不小心创建了新的 `DbContext` 实例,这时候贴了 `[UnitOfWork]` 也不见起效,这时候可以通过以下方法来确认事务是否有效: + +```cs showLineNumbers +repository.EnsureTransaction(); +``` + +**如果不喜欢手动方式也可以通过 `[UnitOfWork(true)]` 开启此功能。** + +该方法会将当前仓储添加到数据库上下文池中,并确保事务可用。 + +### 9.27.3.2 手动管理 + + + + +```cs showLineNumbers +// 开启事务 +using (var transaction = _testRepository.Database.BeginTransaction()) +{ + try + { + _testRepository.Insert(new Blog { Url = "http://blogs.msdn.com/dotnet" }); + _testRepository.SaveNow(); + + _testRepository.Insert(new Blog { Url = "http://blogs.msdn.com/visualstudio" }); + _testRepository.SaveNow(); + + var blogs = _testRepository.Entity + .OrderBy(b => b.Url) + .ToList(); + + // 提交事务 + transaction.Commit(); + } + catch (Exception) + { + // 回滚事务 + // transaction.RollBack(); // 新版本自动回滚了 + } +} +``` + + + + +```cs showLineNumbers +var options = new DbContextOptionsBuilder() + .UseSqlServer(new SqlConnection(connectionString)) + .Options; + +// 创建连接字符串 +using (var context1 = new DefaultDbContext(options)) +{ + // 开启事务 + using (var transaction = context1.Database.BeginTransaction()) + { + try + { + _testRepository.Insert(new Blog { Url = "http://blogs.msdn.com/dotnet" }); + _testRepository.SaveNow(); + + context1.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" }); + context1.SaveChanges(); + + // 创建新的连接对象 + using (var context2 = new DefaultDbContext(options)) + { + // 共享连接事务 + context2.Database.UseTransaction(transaction.GetDbTransaction()); + + var blogs = context2.Blogs + .OrderBy(b => b.Url) + .ToList(); + } + + // 提交事务 + transaction.Commit(); + } + catch (Exception) + { + // 回滚事务 + // transaction.RollBack(); // 新版本自动回滚了 + } + } +} +``` + + + + +```cs showLineNumbers {1-3} +// 开启分布式事务 +// 如果事务包裹的代码中包含异步 async/await,那么需要设置 TransactionScopeAsyncFlowOption.Enabled = true +using (var scope = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted })) +{ + using (var connection = new SqlConnection(connectionString)) + { + connection.Open(); + + try + { + // 这里是 Ado.NET 操作 + var command = connection.CreateCommand(); + command.CommandText = "DELETE FROM dbo.Blogs"; + command.ExecuteNonQuery(); + + // 创建EF Core 数据库上下文 + var options = new DbContextOptionsBuilder() + .UseSqlServer(connection) + .Options; + using (var context = new BloggingContext(options)) + { + context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" }); + context.SaveChanges(); + } + + // 框架封装的仓储 + _testRepository.Insert(new Blog { Url = "http://blogs.msdn.com/dotnet" }); + _testRepository.SaveChanges(); + + // 提交事务 + scope.Complete(); + } + catch (System.Exception) + { + // 自动回滚 + } + } +} +``` + + + + +### 9.27.3.3 `EnableRetryOnFailure` 错误处理 + +:::caution 特别注意 + +如果使用的是 `EFCore` 数据库且启用了 `EnableRetryOnFailure()` 功能,那么 `[UnitOfWork]` 将抛出以下错误: + +```bash showLineNumbers +InvalidOperationException: The configured execution strategy 'SqlServerRetryingExecutionStrategy' does not support user-initiated transactions. +Use the execution strategy returned by 'DbContext.Database.CreateExecutionStrategy()' to execute all the operations in the transaction as a retriable unit. +``` + +这时候需手动事务,并创建执行策略:`CreateExecutionStrategy`,详情请参考官方说明:[https://learn.microsoft.com/zh-cn/ef/core/miscellaneous/connection-resiliency](https://learn.microsoft.com/zh-cn/ef/core/miscellaneous/connection-resiliency) + +```cs showLineNumbers {2,4,8,16} +using var db = new BloggingContext(); +var strategy = db.Database.CreateExecutionStrategy(); + +strategy.Execute( + () => + { + using var context = new BloggingContext(); + using var transaction = context.Database.BeginTransaction(); + + context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" }); + context.SaveChanges(); + + context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/visualstudio" }); + context.SaveChanges(); + + transaction.Commit(); + }); +``` + +::: + +## 9.27.4 工作单元特性说明 + +### 9.27.4.1 `[UnitOfWork]` 特性 + +`[UnitOfWork]` 特性只能用于控制器的 `Action` 中,一旦贴了 `[UnitOfWork]` 特性后,那么该请求自动启用工作单元模式,要么成功,要么失败。 + +### 9.27.4.2 `[ManualCommit]` 特性 + +默认情况下,`Furion` 框架会在一次成功请求之后自动调用 `SaveChanges()` 方法,如果选择手动调用 `SaveChanges()` 方法,可以在控制器 `Action` 中贴 `[ManualCommit]` 特性即可。 + +## 9.27.5 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: + +--- + +:::note 了解更多 + +想了解更多 `事务` 知识可查阅 [EF Core - 使用事务](https://docs.microsoft.com/zh-cn/ef/core/saving/transactions) 章节。 + +::: diff --git a/handbook/docs/unittest.mdx b/handbook/docs/unittest.mdx new file mode 100644 index 0000000000000000000000000000000000000000..28ecddc0ddaecdb01ebc2c1008e49ab7b327b3ae --- /dev/null +++ b/handbook/docs/unittest.mdx @@ -0,0 +1,860 @@ +--- +id: unittest +title: 36.1 单元/集成测试 +sidebar_label: 36.1 单元/集成测试 +--- + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **问题修复** + + -  修复 `Furion.Xunit/Furion.Pure.Xunit` 单元测试依赖注入单例服务时不是同一实例问题 4.8.5.3 ⏱️2023.01.31 [305511e](https://gitee.com/dotnetchina/Furion/commit/305511ec6322019622c392a23957042d2dcae7fb) + +
+
+
+ +import useBaseUrl from "@docusaurus/useBaseUrl"; + +## 36.1.1 关于单元测试 + +引用自百度百科: + +> 单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如 C 语言中单元指一个函数,Java 里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。 + +## 36.1.2 单元测试好处 + +- **消灭低级错误** + +基本的单元测试,可以在系统测试之前,把大部分比较低级的错误都消灭掉,减少系统测试过程中的问题,这样也就减少了系统测试中定位和解决问题的时间成本了。 + +- **找出潜在的 bug** + +某些类型的 bug,靠系统测试是很难找到的。例如一些代码分支,平时 99%的场景基本上都走不到,但一旦走到了,如果没有提前测试好,那么可能就是一个灾难。 + +- **上线前的保证** + +加了新代码,上线前跑一把单元测试,都通过,说明代码可能没有影响到之前的逻辑,这样上线也比较放心。如果之前的单元测试跑不过,那么很有可能新的代码有潜在的问题,赶紧修复去吧。 + +- **重构代码的机会** + +写单元测试的过程中,你可能会顺手把一些 code 重构了,为什么?举例,一些长得非常像的代码,如果每次都要写一堆测试代码去测同样的 code,你会不会抓狂?不测吧,覆盖率又上不去,于是我就会想方设法把待测试的 code 改得尽量的精简,重复代码减少,这样覆盖率上去了,测试也好测了,代码也简洁了。如果没有单元测试和覆盖率的要求的话,坦白说可能一来自己不会发现这些重复的 code,另一方面即使发现了,可能也没有太大的动力去改进。 + +另外,由于单元测试中,你需要尝试去覆盖一些异常分支,这是系统测试常常走不到的地方,于是就会引起你的一些思考,例如这个异常分支是否真的需要?是否真的会发生?对于一些实际上绝对不会出错的函数,那么我觉得可能异常分支是没必要存在的。 + +- **重新 review 代码的机会** + +写 UT 的过程中,我总是会好好看哪些代码执行到了,哪些代码没有执行到,这其实也是一个 review 自己代码的机会,有些时候,并不是 UT 本身帮我找到 bug,而是回头 review 自己代码的时候发现的。 + +## 36.1.3 单元测试类型 + +- 基于 API 接口测试(白盒 + 浅度黑盒测试) +- 基于项目代码测试(深度白盒测试) + +## 36.1.4 主流的单元测试库 + +- `xUnit`(**最流行的库,推荐**) +- `NUnit` +- `MSTest` + +**在本章节,`Furion` 框架使用 `xUnit` 库进行单元测试。** + +## 36.1.5 第一个例子 + +### 36.1.5.1 创建 `xUnit` 单元测试项目 + + + +### 36.1.5.2 第一个测试方法 + +```cs showLineNumbers {1,7,10} +using Xunit; + +namespace TestProject1 +{ + public class UnitTest1 + { + [Fact] + public void Test1() + { + Assert.Equal(2, 1 + 1); + } + } +} +``` + +单元测试实际上是通过普通的类的方法进行模块功能测试,具体测试则是标记了 `[Fact]` 特性的方法,在方法中使用 `Assert` 类提供的静态方法进行 `断言`,`断言` 成功,则测试通过,否则测试不通过。 + +### 36.1.5.3 运行测试 + +在单元测试项目中 `右键` 选择 `运行测试` 并打开 `测试资源管理器` 即可查看测试结果。 + + + + + +### 36.1.5.4 多个测试方法测试 + + + +### 36.1.5.5 重复/回归测试 + +后续添加更多测试方法只需在 `测试资源管理器` 点击 `在视图中运行所有测试` 播放按钮即可,如下图 + + + +## 36.1.6 集成 `Furion` 强大功能 + +`Furion` 是跨平台、跨项目的开发框架,支持任意项目类型,包括单元测试项目。 + +### 36.1.6.1 安装 `Furion.Xunit` 包 + +:::note `Furion` 纯净版 + +如果使用的是 `Furion.Pure` 则安装 `Furion.Pure.Xunit` 这个拓展包。 + +::: + +打开 `NuGet` 程序包控制台,安装 `Furion.Xunit` 包 + + + +:::important 特别注意 + +`Furion.Xunit` 已经包含 `Furion` 无需再次安装 `Furion`。 + +::: + +### 36.1.6.2 添加初始配置类 + +在单元测试项目根目录下添加 `TestProgram.cs` 类,并写下以下代码: + +```cs showLineNumbers {6,13,18} title="TestProgram.cs" +using Furion.Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; + +// 配置启动类类型,第一个参数是 TestProgram 类完整限定名,第二个参数是当前项目程序集名称 +[assembly: TestFramework("TestProject1.TestProgram", "TestProject1")] + +namespace TestProject1; + +/// +/// 单元测试启动类 +/// +public class TestProgram : TestStartup +{ + public TestProgram(IMessageSink messageSink) : base(messageSink) + { + // 初始化 Furion + Serve.RunNative(); + } +} +``` + +:::tip 小提示 + +`TestProgram.cs` 名称可随意,只需要继承 `TestStartup` 类即可。**但类型必须是 `public` 公开的**。 + +::: + +### 36.1.6.3 使用 `Furion` 完整功能 + +`Furion` 是跨平台、跨项目的开发框架,下面在单元测试中演示 `远程请求` 并请求 `https://www.baidu.com` 数据,并测试是否请求成功。 + +`Furion` 提供了以下两种方式注册服务: + +- `Serve.RunNative` 方式(**推荐**) + +```cs showLineNumbers {1,4} +Serve.RunNative(services => +{ + // 注册远程服务 + services.AddRemoteRequest(); +}); +``` + +- `Startup.cs` 方式 + +```cs showLineNumbers {8,13} title="Startup.cs" +using Furion; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; + +namespace TestProject1; + +public class Startup : AppStartup +{ + public void ConfigureServices(IServiceCollection services) + { + // 注册远程服务 + services.AddRemoteRequest(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + } +} +``` + +- 编写测试方法 + +```cs showLineNumbers +[Fact] +public async Task 测试请求百度() +{ + var rep = await "https://www.baidu.com".GetAsync(); + Assert.True(rep.IsSuccessStatusCode); +} +``` + +- 查看测试结果 + + + +很神奇吧!`Furion` 支持任何项目类型,任何平台使用。 + +## 36.1.7 带参数的测试方法 + +上面例子中,测试方法都是没有参数的,有时候需要同一个方法输入多个不同的值进行测试,这时候就需要用到 `[Theory]` 和 `[InlineData]` 特性了。 + +如,下面测试两个数的和是 `奇数`,测试代码如下: + +```cs showLineNumbers {1-4} +[Theory] +[InlineData(1, 2)] +[InlineData(3, 4)] +[InlineData(5, 7)] +public void 带参数测试(int i, int j) +{ + Assert.NotEqual(0, (i + j) % 2); +} +``` + +测试结果: + + + +## 36.1.8 如何进行依赖注入 + +有些时候,我们需要测试某接口,或者进行依赖注入方式解析服务并调用,`Furion.Xunit` 提供完整的构造函数注入。 + +### 36.1.8.1 编写一个 `ICalcService` 接口及实现类 + +```cs showLineNumbers {5,10} +using Furion.DependencyInjection; + +namespace TestProject1.Services; + +public interface ICalcService +{ + int Plus(int i, int j); +} + +public class CalcService : ICalcService, ITransient // 支持任何生命周期 +{ + public int Plus(int i, int j) + { + return i + j; + } +} +``` + +### 36.1.8.2 在测试类中调用 + +```cs showLineNumbers {9,15,17} +using TestProject1.Services; +using Xunit; + +namespace TestProject1; + +public class UnitTest1 +{ + private readonly ICalcService _calcService; + public UnitTest1(ICalcService calcService) + { + _calcService = calcService; + } + + [Fact] + public void 测试两个数的和() + { + Assert.Equal(3, _calcService.Plus(1, 2)); + } +} +``` + + + +### 36.1.8.3 输出日志 + +如果在单元测试中想输出日志,只需要在构造函数注入 `ITestOutputHelper` 即可,如: + +```cs showLineNumbers {2,10,12,18} +using Xunit; +using Xunit.Abstractions; + +namespace TestProject1 +{ + public class UnitTest1 + { + private readonly ITestOutputHelper Output; + + public UnitTest1(ITestOutputHelper tempOutput) + { + Output = tempOutput; + } + + [Fact] + public void Test_String_Equal() + { + Output.WriteLine("哈哈哈哈,我是 Furion"); + Assert.NotEqual("Furion", "Fur"); + } + } +} +``` + + + +### 36.1.8.4 关于依赖注入作用域释放 + +`Furion` 会在创建单元测试实例时创建一个 `IServiceScope` 对象,等该实例所有测试案例执行完毕自动调用 `Dispose`,编写测试的开发者无需关注。 + +### 36.1.8.5 测试释放资源 + +有时候,我们需要测试成功后释放一些不能及时释放的对象,这时,只需要实现 `IDisposable` 接口即可: + +```cs showLineNumbers {6,14-17} +using System; +using Xunit; + +namespace TestProject1 +{ + public class UnitTest1 : IDisposable + { + [Fact] + public void Test_String_Equal() + { + Assert.NotEqual("Furion", "Fur"); + } + + public void Dispose() + { + // 释放你的对象 + } + } +} +``` + +### 36.1.8.6 `[AssemblyFixture]` 特性 + +有时候我们可能不需要对类进行依赖注册,或者无法通过外部进行注册,这时候可以通过 `[AssemblyFixture]` 特性实现构造函数注入任何类,如: + +:::important 有效范围说明 + +`[AssemblyFixture]` 方式对整个单元测试类构造函数都有效,如需个别单元测试类有效可使用 `IClassFixture<>` 或 `ICollectionFixture<>` + `[Collection]` 组合方式。 + +::: + +- **定义需要注入进单元测试构造函数中的类** + +```cs showLineNumbers {1} +public class MyAssemblyFixture : IDisposable +{ + public static int InstantiationCount; + + public MyAssemblyFixture() + { + InstantiationCount++; + } + + public void Dispose() + { + // 做一些释放工作 + } +} +``` + +- **在 `TestProgram.cs` 顶部全局注册** + +```cs showLineNumbers {11} title="TestProgram.cs" +using Furion.Xunit; +using TestProject1; +using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; + +// 配置启动类类型,第一个参数是 TestProgram 类完整限定名,第二个参数是当前项目程序集名称 +[assembly: TestFramework("TestProject1.TestProgram", "TestProject1")] + +// 支持多个 +[assembly: AssemblyFixture(typeof(MyAssemblyFixture))] +// [assembly: AssemblyFixture(typeof(XXXXFixture))] + +namespace TestProject1; + +/// +/// 单元测试启动类 +/// +public class TestProgram : TestStartup +{ + public TestProgram(IMessageSink messageSink) : base(messageSink) + { + Serve.Run(silence: true); + } +} +``` + +- **在测试类构造函数注入** + +```cs showLineNumbers {12,27} +using TestProject1.Services; +using Xunit; + +namespace TestProject1; + +public class UnitTest1 +{ + private readonly ICalcService _calcService; + private readonly MyAssemblyFixture _fixture; + + public UnitTest1(ICalcService calcService + , MyAssemblyFixture fixture) + { + _calcService = calcService; + _fixture = fixture; + } + + [Fact] + public void 测试两个数的和() + { + Assert.Equal(3, _calcService.Plus(1, 2)); + } + + [Fact] + public void EnsureSingleton() + { + Assert.Equal(1, MyAssemblyFixture.InstantiationCount); + } +} +``` + + + +### 36.1.8.7 `IClassFixture<>` 单个注入 + +通过上面 `[AssemblyFixture]` 方式我们知道此方式对全局的单元测试类都有效,但有时候我们只需要特定单元测试类有效,则可通过 `IClassFixture<>` 方式,如: + +- **定义需要注入进单元测试构造函数中的类** + +```cs showLineNumbers {1} +public class MyClassFixture : IDisposable +{ + public static int InstantiationCount; + + public MyClassFixture() + { + InstantiationCount++; + } + + public void Dispose() + { + // 做一些释放工作 + } +} +``` + +- **在测试类构造函数注入** + +```cs showLineNumbers {6,14,34} +using TestProject1.Services; +using Xunit; + +namespace TestProject1; + +public class UnitTest1 : IClassFixture +{ + private readonly ICalcService _calcService; + private readonly MyAssemblyFixture _fixture; + private readonly MyClassFixture _classFixture; + + public UnitTest1(ICalcService calcService + , MyAssemblyFixture fixture + , MyClassFixture classFixture) + { + _calcService = calcService; + _fixture = fixture; + _classFixture = classFixture; + } + + [Fact] + public void 测试两个数的和() + { + Assert.Equal(3, _calcService.Plus(1, 2)); + } + + [Fact] + public void EnsureSingleton() + { + Assert.Equal(1, MyAssemblyFixture.InstantiationCount); + } + + [Fact] + public void EnsureClassSingleton() + { + Assert.Equal(1, MyClassFixture.InstantiationCount); + } +} +``` + + + +### 36.1.8.8 `ICollectionFixture<>` 多个注入 + +`ICollectionFixture<>` 方式和 `IClassFixture<>` 方式最大的不同就是后者只能配置为单个测试类使用,而 `ICollectionFixture<>` 则通过 `[Collection]` 方式配置多个测试类有效,如: + +- **定义需要注入进单元测试构造函数中的类** + +:::important 特别注意 + +这里区别于 `IClassFixture<>` 方式,需定义配置器并实现 `ICollectionFixture<>` 接口。 + +::: + +```cs showLineNumbers {5,20-21} +using Xunit; + +namespace TestProject1; + +public class MyCollectionFixture : IDisposable +{ + public static int InstantiationCount; + + public MyCollectionFixture() + { + InstantiationCount++; + } + + public void Dispose() + { + // 做一些释放工作 + } +} + +[CollectionDefinition("MyCollection")] +public class MyCollection : ICollectionFixture +{ +} +``` + +- **在测试类构造函数注入** + +```cs showLineNumbers {6,17,44} +using TestProject1.Services; +using Xunit; + +namespace TestProject1; + +[Collection("MyCollection")] +public class UnitTest1 : IClassFixture +{ + private readonly ICalcService _calcService; + private readonly MyAssemblyFixture _fixture; + private readonly MyClassFixture _classFixture; + private readonly MyCollectionFixture _collectionFixture; + + public UnitTest1(ICalcService calcService + , MyAssemblyFixture fixture + , MyClassFixture classFixture + , MyCollectionFixture collectionFixture) + { + _calcService = calcService; + _fixture = fixture; + _classFixture = classFixture; + _collectionFixture = collectionFixture; + } + + [Fact] + public void 测试两个数的和() + { + Assert.Equal(3, _calcService.Plus(1, 2)); + } + + [Fact] + public void EnsureSingleton() + { + Assert.Equal(1, MyAssemblyFixture.InstantiationCount); + } + + [Fact] + public void EnsureClassSingleton() + { + Assert.Equal(1, MyClassFixture.InstantiationCount); + } + + [Fact] + public void EnsureCollectionSingleton() + { + Assert.Equal(1, MyCollectionFixture.InstantiationCount); + } +} +``` + + + +## 36.1.9 `Web` 集成测试 + +`Web` 集成测试有三种方式,通过这三种方式可以对项目进行全方位的测试,保证部署上线是测试期盼效果。 + +### 36.1.9.1 对现有项目进行集成测试 + +这种方式比较简单,也是最常用的方式,无需部署到服务器直接在本地即可测试,如: + +1. 创建 `Xunit` 单元测试项目 + + + +2. 添加 `Microsoft.AspNetCore.Mvc.Testing` 微软提供的集成测试拓展 + + + +3. 添加测试项目或使用已有的测试项目**引用** + + + +4. 配置 `Web` 项目启动层 + +- `.NET5` + +在需要测试的 `Web` 项目启动层添加 `FakeStarup.cs` 类 + +```cs showLineNumbers title="FakeStarup.cs" +namespace WebApplication1; + +/// +/// 供集成测试使用 +/// +public class FakeStartup +{ +} +``` + +- `.NET6+` + +(`.NET6+`)编辑 `WebApplication1.csproj`,添加 `` + +```xml showLineNumbers {2} + + + +``` + +(`.NET6+`)编辑 `Program.cs` 添加以下代码: + +```cs showLineNumbers +public partial class Program { } +``` + +5. 编写测试 `Web` 项目接口测试案例 + +```cs showLineNumbers {6,8-12,15,16} +using Microsoft.AspNetCore.Mvc.Testing; +using Xunit; + +namespace TestProject2; + +public class UnitTest1 : IClassFixture> +{ + private readonly WebApplicationFactory _factory; + public UnitTest1(WebApplicationFactory factory) + { + _factory = factory; + } + + [Theory] + [InlineData("/default")] + public async Task TestEnsureSuccessStatusCode(string url) + { + using var client = _factory.CreateClient(); + using var response = await client.GetAsync(url); + response.EnsureSuccessStatusCode(); + } +} +``` + +:::note 主机更多配置说明 + +如需添加更多配置,如环境变量设置,可通过以下方式: + +```cs showLineNumbers {11-14} +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; + +namespace TestProject2; + +public class UnitTest1 : IClassFixture> +{ + private readonly WebApplicationFactory _factory; + public UnitTest1(WebApplicationFactory factory) + { + _factory = factory.WithWebHostBuilder(builder => + { + builder.UseEnvironment("Stage"); // 设置环境 + }); + } + + // .... +} +``` + +::: + +:::tip `.NET6+` 泛型参数 + +如果是 `.NET6+(含)`,只需要将 `WebApplicationFactory` 泛型参数改为:`WebApplicationFactory` 即可。 + +::: + +`/default` 接口对应控制器定义如下: + +```cs showLineNumbers {10,12} +using Microsoft.AspNetCore.Mvc; + +namespace WebApplication1.Controllers +{ + [ApiController] + [Route("[controller]")] + public class DefaultController : ControllerBase + { + [HttpGet] + public string Get() + { + return "Furion 集成测试"; + } + } +} +``` + + + +6. 允许测试 + + + +### 36.1.9.2 独立主机方式测试 + +独立主机的方式就是利用单元测试的每一个测试案例构建主机进行测试。 + +1. 创建 `Xunit` 单元测试项目 + + + +2. 添加 `Microsoft.AspNetCore.Mvc.Testing` 微软提供的集成测试拓展 + + + +3. 各种创建主机方式示例 + +```cs showLineNumbers {15,33} +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; + +namespace TestProject3; + +public class UnitTest1 +{ + /// + /// 创建主机并注册服务 + /// + /// 可用来判断服务是否注册 + [Fact] + public void Test1() + { + var builder = WebApplication.CreateBuilder(); + + // 注册服务 + builder.Services.AddScoped(); + + using var app = builder.Build(); + var services = app.Services; + + services.GetRequiredService(); + } + + /// + /// 测试配置 + /// + /// 比如添加 JSON 文件配置后读取 + [Fact] + public void Test2() + { + var builder = WebApplication.CreateBuilder(); + var host = builder.Host; + host.ConfigureAppConfiguration(builder => + { + builder.Sources.Clear(); + }); + + var config = builder.Configuration["配置"]; + + // 判断不为空 + } +} +``` + +### 36.1.9.3 系统集成/环境/配置部署测试 + +有时候我们需要测试 `Web` 主机各种情况,比如端口是否有效,环境配置是否有效,系统集成情况等等,这时候只需要添加 `Microsoft.AspNetCore.TestHost` 拓展,然后在测试类顶部贴: + +```cs showLineNumbers +[assembly: HostingStartup(typeof(WebApplicationTests.TestHostingStartup))] +``` + +微软已经提供了非常详细的例子,这里直接放链接 [https://github.com/dotnet/aspnetcore/tree/main/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests](https://github.com/dotnet/aspnetcore/tree/main/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests) + +`WebApplicationTests.TestHostingStartup` 为您要测试的 `Web` 项目启动类。 + +### 36.1.9.4 集成 `Furion.Xunit` 拓展 + +`Web` 集成测试支持完整的 `Furion` 特性,参考上面单元测试集成 `Furion` 章节。 + +:::note `Furion` 纯净版 + +如果使用的是 `Furion.Pure` 则安装 `Furion.Pure.Xunit` 这个拓展包。 + +::: + +## 36.1.10 `Assert` 断言 + +`Assert` 是单元测试判定成功的依据,通常第一个参数为 `期望值`,第二个参数为 `实际值`,对比这两个值是否一致即可判断成功与否。详细的 `Assert` 静态方法可查阅官方库 [Assert 方法](https://github.com/xunit/assert.xunit) + +## 36.1.11 单元测试覆盖率 + +`Visual Studio` 提供了分析单元测试覆盖率工具,如: + + + +## 36.1.12 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: + +--- + +:::note 了解更多 + +想了解更多 `单元测试` 知识可查阅 [在 .NET 中测试](https://docs.microsoft.com/zh-cn/dotnet/core/testing/) 章节。 + +::: diff --git a/handbook/docs/upgrade.mdx b/handbook/docs/upgrade.mdx new file mode 100644 index 0000000000000000000000000000000000000000..11595b0296e461d2b61d3ceaa35e7c4ae9359788 --- /dev/null +++ b/handbook/docs/upgrade.mdx @@ -0,0 +1,5058 @@ +--- +id: upgrade +title: 1.6 更新日志 +sidebar_label: 1.6 更新日志 +description: 升级前先看看,会不会有您喜欢的特性 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; +import Assistance from "@site/src/components/Assistance"; +import Tag from "@site/src/components/Tag.js"; + +:::tip `Furion` 框架升级/发版规则 + +**升级前重点关注可能造成【破坏性】的标签类型**:修复调整移除升级 + +版本号规则:`主版本号.次版本号.修订版本号` + +- 只要【确认】为框架 `bug`,则当天修复,当天发版,修订版本号 `加 1`。 +- 只要 `.NET SDK` 版本更新,则当天升级,当天发版,修订版本号 `加 1`。 +- 如果 `.csproj` 文件有变更,则当天发版,修订版本号 `加 1`。 +- 如果新增 `拓展包`,为了版本号统一,则当天发版,修订版本号 `加 1`。 +- 如果涉及到代码重构,则当天发版,次版本号 `加 1`,修订版本号 `清 0`。 +- 如果 `.NET SDK` 主版本号升级,则当天发版,主版本号 `加 1`。 + +如有意外不能当天发版,则会在 `Issue` 中说明具体发版时间,正常不会超过 `3` 天。 + + + +::: + +## v4.9.1(当前版本,.NET8) + +:::important 版本细节 + +- `v4.9.1` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I8GHNK](https://gitee.com/dotnetchina/Furion/issues/I8GHNK) 2023.11.15 ~ 2023.11.24 🆕 + +**⭐️ `.NET8` 升级指南:[http://furion.baiqian.ltd/docs/net7-to-net8](http://furion.baiqian.ltd/docs/net7-to-net8) ⭐️** + +::: + + +- **新特性** + + -  新增 规范化处理自动过滤 `SSE` 请求、文件请求、图片请求 4.9.1.6 ⏱️2023.11.22 [#I8IP6D](https://gitee.com/dotnetchina/Furion/issues/I8IP6D) + -  新增 `[AppDbContext]` 特性支持 `UseSnakeCaseNaming` 属性配置表名使用蛇形命名 4.9.1.5 ⏱️2023.11.20 [#I8HGR2](https://gitee.com/dotnetchina/Furion/issues/I8HGR2) [!863](https://gitee.com/dotnetchina/Furion/pulls/863) + -  新增 时间戳模型绑定器将时间戳转换为 `DateTime/DateTimeOffset` 类型 4.9.1.5 ⏱️2023.11.20 [df3053c](https://gitee.com/dotnetchina/Furion/commit/df3053cf081d5e4d8eb63d567ed95c45267e0969) + -  新增 `Newtonsoft.Json` 自动将时间戳转换为 `DateTime/DateTimeOffset` 类型 4.9.1.3 ⏱️2023.11.17 [78a589d](https://gitee.com/dotnetchina/Furion/commit/78a589d99eb5985b576e4c96acd6e4890391d6ff) + -  新增 `System.Text.Json` 自动将时间戳转换为 `DateTime/DateTimeOffset` 类型 4.9.1.2 ⏱️2023.11.17 [abd5196](https://gitee.com/dotnetchina/Furion/commit/abd5196f5c5160a5df96dad80c7c5aa51b96d5b9) + -  新增 `IRepositoryFactory` 仓储功能,解决在 `Blazor` 中使用 `EFCore` 问题 4.9.1.1 ⏱️2023.11.16 [4285ec0](https://gitee.com/dotnetchina/Furion/commit/4285ec0b8debc2d71c7f978126cb3dc394a8ad30) [文档说明](https://learn.microsoft.com/zh-cn/aspnet/core/blazor/blazor-ef-core?view=aspnetcore-7.0) + -  新增 补偿策略模块功能 4.9.1 ⏱️2023.11.15 [【源码地址】](https://gitee.com/dotnetchina/Furion/tree/v4/framework/Furion/RescuePolicy) [dfc63e7](https://gitee.com/dotnetchina/Furion/commit/dfc63e7f0ffd2e03653f11a3e49a21646902ddc4) + +- **突破性变化** + + -  升级 `Serilog` 拓展包依赖至 `8.0.0` 版本,**移除 `.NET8.0` 的 `IWebHostBuilder.UseSerilogDefault` 拓展方法** 4.9.1.1 ⏱️2023.11.16 [5ab3e43](https://gitee.com/dotnetchina/Furion/commit/5ab3e43722a298db56a86792dde301adf1f3fe7f) + -  升级 **框架底层适配 `.NET8.0` 正式版** 4.9.1 ⏱️2023.11.15 + -  升级 **框架脚手架适配 `.NET8.0` 正式版** 4.9.1 ⏱️2023.11.15 + +- **问题修复** + + -  修复 定时任务设置触发器 `Result` 后作业执行异常不能重置问题 4.9.1.7 ⏱️2023.11.24 [147215f](https://gitee.com/dotnetchina/Furion/commit/147215f1631f58fca900f17cca5695f9431555e5) + -  修复 `JWTEncryption.GetJWTSettings()` 独立使用时无法获取自定义配置 4.9.1.4 ⏱️2023.11.18 [c045e08](https://gitee.com/dotnetchina/Furion/commit/c045e084670a98f71d5ea5ed55ca5cbbfc981e0b) + +- **文档** + + -  更新 仓储文档、`Db` 静态类文档、脚手架文档、`.NET7` 升级 `.NET8` 文档、`JSON` 序列化文档、`Docker` 部署文档、数据库上下文文档 + +- **贡献者** + + - zuohuaijun ([@zuohuaijun](https://gitee.com/zuohuaijun)) [!865](https://gitee.com/dotnetchina/Furion/pulls/865) + - anliuty ([@anliuty](https://gitee.com/www.fengyunmy.com)) [!863](https://gitee.com/dotnetchina/Furion/pulls/863) + - 风云明月 ([@www.fengyunmy.com](https://gitee.com/www.fengyunmy.com)) [!862](https://gitee.com/dotnetchina/Furion/pulls/862) + +--- + +## v4.8.8(已发布) + +:::important 版本细节 + +- `v4.8.8` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I6VF8V](https://gitee.com/dotnetchina/Furion/issues/I6VF8V) 2023.04.13 ~ 2023.11.09 🆕 + +::: + +- **新特性** + + -  新增 `Db.GetNewDbContext()` 多个重载方法,实现类似 `new DbContext()` 操作 4.8.8.55 ⏱️2023.11.09 [4157629](https://gitee.com/dotnetchina/Furion/commit/41576295473fefc47f6961154909a690d7c5ed58) + -  新增 控制台日志 `AddConsoleFormatter` 服务支持 `WriteFilter` 属性过滤 4.8.8.52 ⏱️2023.11.07 [516acb4](https://gitee.com/dotnetchina/Furion/commit/516acb455e9eae477cfce1052442fc30c9c4dfb9) + -  新增 监听日志 `LoggingMonitor` 支持打印输出 `requestHeaders` 请求头信息 4.8.8.50 ⏱️2023.10.27 [#I8BHM3](https://gitee.com/dotnetchina/Furion/issues/I8BHM3) + -  新增 多语言支持 `L.GetDefaultCulture()` 获取本地配置默认语言 4.8.8.49 ⏱️2023.10.25 [!858](https://gitee.com/dotnetchina/Furion/pulls/858) + -  新增 定时任务 `Http` 作业请求头 `Headers` 和作业分组 `Group` 和描述 `Description` 支持 4.8.8.46 ⏱️2023.10.09 [#I85Z7S](https://gitee.com/dotnetchina/Furion/issues/I85Z7S) + -  新增 定时任务看板列表支持作业分组名排序 4.8.8.43 ⏱️2023.09.14 [#I7YQ9V](https://gitee.com/dotnetchina/Furion/issues/I7YQ9V) + -  新增 验证特性 `[DataValidation]` 支持 `[Display]` 和 `[DisplayName]` 特性设置 `{0}` 4.8.8.42 ⏱️2023.09.01 [#I7XB3T](https://gitee.com/dotnetchina/Furion/issues/I7XB3T) + -  新增 监听日志 `LoggingMonitor` 支持配置日志输出级别 4.8.8.41 ⏱️2023.08.25 [#I7SRTP](https://gitee.com/dotnetchina/Furion/issues/I7SRTP) + -  新增 多语言支持 `L.GetString(name, culture)` 获取指定区域翻译 4.8.8.41 ⏱️2023.08.04 [044b0ed](https://gitee.com/dotnetchina/Furion/commit/044b0edfbd622c7c69d685267aafa9f5855a9167) + -  新增 粘土对象 `.ConvertTo` 支持自定义值提供器 4.8.8.40 ⏱️2023.08.03 [70d5888](https://gitee.com/dotnetchina/Furion/commit/70d58888b3cec88c5c2a8458654dca1881e2a88b) + -  新增 规范化文档枚举支持 `[EnumToNumber]` 特性配置生成前端枚举定义代码是字符串值还是整数值类型,默认为字符串值 4.8.8.35 ⏱️2023.07.06 [#I7IZ7S](https://gitee.com/dotnetchina/Furion/issues/I7IZ7S) + -  新增 定时任务作业计划 `OnChanged` 事件处理 4.8.8.29 ⏱️2023.06.25 [e4c4cf1](https://gitee.com/dotnetchina/Furion/commit/e4c4cf1d418f3cc2291eca7d7dd1c8b62d17b0e9) + -  新增 `Swagger` 分组信息可在任意配置文件中通过 `[openapi:分组名]` 进行配置 4.8.8.26 ⏱️2023.06.20 [a70eed3](https://gitee.com/dotnetchina/Furion/commit/a70eed3ec5f3081fbdc08312fdb4770f39f27cc0) + -  新增 `TP.WrapperRectangle` 绘制矩形日志模板 4.8.8.25 ⏱️2023.06.14 [60ffd76](https://gitee.com/dotnetchina/Furion/commit/60ffd76783633ac4fc7baaf845e14cb59518b795) + -  新增 `IServiceScope.CreateDefaultHttpContext` 拓展方法 4.8.8.24 ⏱️2023.06.07 [11a55e1](https://gitee.com/dotnetchina/Furion/commit/11a55e1796acae3318ea2c78cb3e88ba4d53c670) + -  新增 配置模块 `IgnoreConfigurationFiles` 支持完整的文件通配符 4.8.8.22 ⏱️2023.05.25 [#I78ABL](https://gitee.com/dotnetchina/Furion/issues/I78ABL) + -  新增 定时任务支持二级虚拟目录 `VisualPath` 配置部署 4.8.8.20 ⏱️2023.05.18 [#I740IA](https://gitee.com/dotnetchina/Sundial/issues/I740IA) + -  新增 **监听日志 `LoggingMonitor` 支持 `Razor Pages`** 4.8.8.16 ⏱️2023.05.15 [#I7332C](https://gitee.com/dotnetchina/Furion/issues/I7332C) + -  新增 定时任务作业处理程序工厂 `IJobFactory` 支持 4.8.8.13 ⏱️2023.05.08 [ad58dd3](https://gitee.com/dotnetchina/Furion/commit/ad58dd3141ed40e58cd486895ac6c1f21803797c) + -  新增 `AES` 支持对文件(含超大文件)进行加解密 4.8.8.11 ⏱️2023.05.05 [1d2265b](https://gitee.com/dotnetchina/Furion/commit/1d2265be04cfd7c6c2b9db932a77ebd620ef6054) + -  新增 动态 `WebAPI` 支持 `text/plain` 格式的 `Body` 参数 4.8.8.9 ⏱️2023.05.04 [b49fe50](https://gitee.com/dotnetchina/Furion/commit/b49fe5087cdf97b04b7c2c9d90231f1b9d5fc6ee) + -  新增 **插件化 `IDynamicApiRuntimeChangeProvider` 接口,可在运行时动态添加 `WebAPI/Controller`** 4.8.8.8 ⏱️2023.05.04 [322ea59](https://gitee.com/dotnetchina/Furion/commit/322ea599ed58b1804e9f8ab85d7ed44882b3e5a8) + +
+ 查看变化 +
+ +在一些特定的需求中,我们需要在运行时**动态编译代码,如动态编写 `WebAPI`**,之后能够在不重启主机服务的情况下即可有效。比如这里动态添加 `SomeClass` 动态 `WebAPI`,然后在 `Swagger/路由系统` 中立即有效: + +```cs showLineNumbers {10,21-30,36-39} +using Furion; +using Furion.DynamicApiController; +using Microsoft.AspNetCore.Mvc; + +namespace YourProject.Application; + +public class PluginApiServices : IDynamicApiController +{ + private readonly IDynamicApiRuntimeChangeProvider _provider; + public PluginApiServices(IDynamicApiRuntimeChangeProvider provider) + { + _provider = provider; + } + + /// + /// 动态添加 WebAPI/Controller + /// + /// + /// 可自行指定程序集名称 + /// + public string Compile([FromBody] string csharpCode, [FromQuery] string assemblyName = default) + { + // 编译 C# 代码并返回动态程序集 + var dynamicAssembly = App.CompileCSharpClassCode(csharpCode, assemblyName); + + // 将程序集添加进动态 WebAPI 应用部件 + _provider.AddAssembliesWithNotifyChanges(dynamicAssembly); + + // 返回动态程序集名称 + return dynamicAssembly.GetName().Name; + } + + /// + /// 移除动态程序集 WebAPI/Controller + /// + public void Remove(string assemblyName) + { + _provider.RemoveAssembliesWithNotifyChanges(assemblyName); + } +} +``` + +这时只需要请求 `api/plugin-api/compile` 接口同时设置请求 `Content-Type` 为 `text/plain`,接下来传入 `C# 代码字符串` 即可,如: + +```cs showLineNumbers title="动态C#代码字符串" +using Furion.DynamicApiController; + +namespace YourProject.Application; + +public class SomeClass : IDynamicApiController +{ + public string GetName() + { + return nameof(Furion); + } +} +``` + + + +之后刷新浏览器即可看到最新的 `API`: + + + +还可以在运行时动态卸载,使用 `DELETE` 请求 `api/plugin-api` 即可。 + +
+
+ +- -  新增 定时任务 `Schedular.CompileCSharpClassCode(code)` 支持动态编译作业处理程序代码 4.8.8.7 ⏱️2023.04.30 [fe1e8a1](https://gitee.com/dotnetchina/Furion/commit/fe1e8a1768c7020477684689b35a2a1349ec2b01) +- -  新增 `App.CompileCSharpClassCode(code)` 动态编译类定义代码 4.8.8.7 ⏱️2023.04.30 [fe1e8a1](https://gitee.com/dotnetchina/Furion/commit/fe1e8a1768c7020477684689b35a2a1349ec2b01) +- -  新增 粘土对象支持结构 `struct` 对象类型 4.8.8.7 ⏱️2023.04.30 [a0fa3aa](https://gitee.com/dotnetchina/Furion/commit/a0fa3aa7ae536e948740401b510d99cf45e251dc) +- -  新增 定时任务支持配置 `IJob` 执行异常 `FallbackAsync` 回退策略 4.8.8.6 ⏱️2023.04.25 [7671489](https://gitee.com/dotnetchina/Furion/commit/7671489a46ec7c957e92b7fbf9836e27f9077e24) +- -  新增 定时任务支持在非 `IOC/DI` 项目类型中使用 4.8.8.5 ⏱️2023.04.24 [#I6YJNB](https://gitee.com/dotnetchina/Sundial/issues/I6YJNB) +- -  新增 `RSA` 支持对超长字符(超 `245` 位)进行分段加解密 4.8.8.2 ⏱️2023.04.19 [!788](https://gitee.com/dotnetchina/Furion/pulls/788) 感谢 [@YaChengMu](https://gitee.com/YaChengMu) +- -  新增 `System.Text.Json` 和 `Newtonsoft.Json` 对粘土对象 `Clay` 支持 4.8.8.1 ⏱️2023.04.18 [#I6WKRZ](https://gitee.com/dotnetchina/Furion/issues/I6WKRZ) +- -  新增 粘土对象可反射转换成特定 `IEnumerable` 类型:`clay.ConvertTo()` 4.8.8 ⏱️2023.04.13 [5d54a65](https://gitee.com/dotnetchina/Furion/commit/5d54a6579be3d710649bb199dd985f60acaf9787) +- -  新增 `Serve.IdleHost` 支持返回 `http` 和 `https` 协议 `Web` 地址(端口) 4.8.8 ⏱️2023.04.13 [fdf7885](https://gitee.com/dotnetchina/Furion/commit/fdf7885f282057599be6b1b3833373dd153db42a) + +- **突破性变化** + + -  移除 **定时任务看板 `SyncRate` 配置,前后端采用最新的 `SSE` 推送技术替代** 4.8.8.29 ⏱️2023.06.25 [e4c4cf1](https://gitee.com/dotnetchina/Furion/commit/e4c4cf1d418f3cc2291eca7d7dd1c8b62d17b0e9) + -  调整 **监听日志 `WriteFilter` 和 `ConfigureLogger` 的 `ActionExecutingContext` 和 `ActionExecutedContext` 类型为 `FilterContext`** 4.8.8.16 ⏱️2023.05.15 [#I7332C](https://gitee.com/dotnetchina/Furion/issues/I7332C) + -  调整 **`IJsonSerializerProvider` 序列化接口,添加 `Deserialize` 反序列化方法** 4.8.8.15 ⏱️2023.05.15 [!815](https://gitee.com/dotnetchina/Furion/pulls/815) 感谢 [@YaChengMu](https://gitee.com/YaChengMu) + +
+ 查看变化 +
+ +添加 `25-32行` 接口方法: + +```cs showLineNumbers {25-32} +namespace Furion.JsonSerialization; + +/// +/// Json 序列化提供器 +/// +public interface IJsonSerializerProvider +{ + /// + /// 序列化对象 + /// + /// + /// + /// + string Serialize(object value, object jsonSerializerOptions = default); + + /// + /// 反序列化字符串 + /// + /// + /// + /// + /// + T Deserialize(string json, object jsonSerializerOptions = default); + + /// + /// 反序列化字符串 + /// + /// + /// + /// + /// + object Deserialize(string json, Type returnType, object jsonSerializerOptions = default); + + /// + /// 返回读取全局配置的 JSON 选项 + /// + /// + object GetSerializerOptions(); +} +``` + +如果使用 `Newtonsoft.Json` 则只需添加以下实现即可: + +```cs showLineNumbers {8-11} +/// +/// 反序列化字符串 +/// +/// +/// +/// +/// +public object Deserialize(string json, Type returnType, object jsonSerializerOptions = null) +{ + return JsonConvert.DeserializeObject(json, returnType, (jsonSerializerOptions ?? GetSerializerOptions()) as JsonSerializerSettings); +} +``` + +
+
+ +- **问题修复** + + -  修复 远程请求获取响应 `Cookies` 被截断问题 4.8.8.54 ⏱️2023.11.08 [#I8EV1Z](https://gitee.com/dotnetchina/Furion/issues/I8EV1Z) + -  修复 远程请求上传文件在其他编程语言获取文件名存在双引号问题 4.8.8.53 ⏱️2023.11.07 [#I8EF1S](https://gitee.com/dotnetchina/Furion/issues/I8EF1S) + -  修复 定时任务高频作业下持久化操作出现阻塞卡问题 4.8.8.51 ⏱️2023.11.06 [f1d0b4a](https://gitee.com/dotnetchina/Furion/commit/f1d0b4a9d7d65d5263109d5370b8d87705f4178b) + -  修复 定时任务看板中间件 `SSE` 请求不是长连接导致连接频繁初始化销毁 4.8.8.49 ⏱️2023.10.26 [1997f1b](https://gitee.com/dotnetchina/Furion/commit/1997f1b99043eb80accac4e6a0c60c4e33d77183) + -  修复 动态 `WebAPI` 不能正确移除 `AppService` 命名的 `Service` 问题 4.8.8.47 ⏱️2023.10.10 [#I86NL](https://gitee.com/dotnetchina/Furion/issues/I86NLO) + -  修复 审计日志不支持 `dynamic/JsonElement` 序列化问题 4.8.8.45 ⏱️2023.09.29 [#I84SD5](https://gitee.com/dotnetchina/Furion/issues/I84SD5) + -  修复 `Scoped.CreateUowAsync` 作用域工作单元异常无法回滚问题 4.8.8.44 ⏱️2023.09.23 [#I833I9](https://gitee.com/dotnetchina/Furion/issues/I833I9) + -  修复 模板引擎高并发读取缓存模板出现线程占用问题 4.8.8.43 ⏱️2023.09.14 [#I80ZKB](https://gitee.com/dotnetchina/Furion/issues/I80ZKB) + -  修复 使用刷新 `Token` 也能通过鉴权检查严重安全 `Bug` 4.8.8.42 ⏱️2023.08.28 [#I7TII4](https://gitee.com/dotnetchina/Furion/issues/I7TII4) + -  修复 粘土对象不支持枚举类型问题 4.8.8.41 ⏱️2023.08.25 [#I7VDDL](https://gitee.com/dotnetchina/Furion/issues/I7VDDL) + -  修复 定时任务因上一版本修改 [4e2615b](https://gitee.com/dotnetchina/Furion/commit/4e2615b00da0b2db756e4084be882c0362c442f5) 导致自定义作业触发器异常问题 4.8.8.36 ⏱️2023.07.06 [#I7J59D](https://gitee.com/dotnetchina/Furion/issues/I7J59D) + -  修复 审计日志解析 `DateTime` 类型参数不是本地时间问题 4.8.8.33 ⏱️2023.06.29 [#I7GW32](https://gitee.com/dotnetchina/Furion/issues/I7GW32) + -  修复 定时任务因上一版本修改 [4e2615b](https://gitee.com/dotnetchina/Furion/commit/4e2615b00da0b2db756e4084be882c0362c442f5) 导致 `Cron` 解析异常问题 4.8.8.32 ⏱️2023.06.28 [#I7GQ5I](https://gitee.com/dotnetchina/Furion/issues/I7GQ5I) + -  修复 定时任务设置额外数据不支持 `long/int64` 类型参数问题 4.8.8.31 ⏱️2023.06.28 [4e2615b](https://gitee.com/dotnetchina/Furion/commit/4e2615b00da0b2db756e4084be882c0362c442f5) + -  修复 定时任务休眠毫秒数大于 `int.MaxValue` 时出现 `ArgumentOutOfRangeException` 4.8.8.27 ⏱️2023.06.21 [#I7F6ZT](https://gitee.com/dotnetchina/Furion/issues/I7F6ZT) + -  修复 `Cron` 表达式步长解析器错误 4.8.8.25 ⏱️2023.06.14 [#I7D9XU](https://gitee.com/dotnetchina/TimeCrontab/issues/I7D9XU) + -  修复 修复 `ExpandoObject.ToDictionary()` 转换异常 4.8.8.25 ⏱️2023.06.14 [#I7BY0P](https://gitee.com/dotnetchina/Furion/issues/I7BY0P) + -  修复 配置友好异常 `FriendlyExceptionSettings:DefaultErrorMessage` 无效问题 4.8.8.23 ⏱️2023.05.31 [#I79LIG](https://gitee.com/dotnetchina/Furion/issues/I79LIG) + -  修复 `Swagger` 进行分组后 `Tags` 不能进行分组过滤问题 4.8.8.22 ⏱️2023.05.25 [#I78A55](https://gitee.com/dotnetchina/Furion/issues/I78A55) + -  修复 因 [9d8cb82](https://gitee.com/dotnetchina/Furion/commit/9d8cb82e4ce983839cf13c3c74640b08f258c325) 代码提交导致命名服务解析异常问题 4.8.8.21 ⏱️2023.05.18 [#I76JZR](https://gitee.com/dotnetchina/Furion/issues/I76JZR) + -  修复 因 [9d8cb82](https://gitee.com/dotnetchina/Furion/commit/9d8cb82e4ce983839cf13c3c74640b08f258c325) 代码提交导致服务 `AOP` 异常拦截问题 4.8.8.17 ⏱️2023.05.15 [#I73A8E](https://gitee.com/dotnetchina/Furion/issues/I73A8E) + -  修复 动态 `WebAPI` 自定义路由模板参数和自动拼接参数冲突问题 4.8.8.15 ⏱️2023.05.15 [#I72ZZ2](https://gitee.com/dotnetchina/Furion/issues/I72ZZ2) + -  修复 远程请求在被请求端返回非 `200` 状态码但实际请求已处理也抛异常问题 4.8.8.14 ⏱️2023.05.12 [b14a51f](https://gitee.com/dotnetchina/Furion/commit/b14a51fd6f85a905da50729d521a2232b5c9afc1) + -  修复 `App.CompileCSharpClassCode(code)` 运行时添加匿名程序集编译异常问题 4.8.8.8 ⏱️2023.05.04 [322ea59](https://gitee.com/dotnetchina/Furion/commit/322ea599ed58b1804e9f8ab85d7ed44882b3e5a8) + -  修复 `LoggingMonitor` 打印泛型类型如果存在多个泛型参数问题 4.8.8.8 ⏱️2023.05.04 [8d9cb74](https://gitee.com/dotnetchina/Furion/commit/8d9cb7457c736a91bc428ce61da553df40107960) + -  修复 脱敏处理如果字典存在重复词导致异常问题 4.8.8.4 ⏱️2023.04.23 [#I6Y19K](https://gitee.com/dotnetchina/Furion/issues/I6Y19K) + -  修复 远程请求 `Body` 参数为粘土对象 `Clay` 类型序列化有误 4.8.8.1 ⏱️2023.04.18 [#I6WKRZ](https://gitee.com/dotnetchina/Furion/issues/I6WKRZ) + -  修复 `Serve.IdleHost` 获取随机端口的本地地址带 `$` 符号问题 4.8.8 ⏱️2023.04.13 [ed6f292](https://gitee.com/dotnetchina/Furion/commit/ed6f29263607f58fe0eafdf21dadfc33987309e1) + +- **其他更改** + + -  调整 `[UnitofWork]` 支持在 `Class` 中指定,解决 `Pages` 应用警告问题 4.8.8.42 ⏱️2023.09.01 [#I7X51E](https://gitee.com/dotnetchina/Furion/issues/I7X51E) + -  调整 取消远程请求 `GET/HEAD` 不能传递 `Body` 的限制 4.8.8.39 ⏱️2023.08.02 [8113460](https://gitee.com/dotnetchina/Furion/commit/8113460ab8b23cbf392c49b79fe4eb77a89c8010) + -  调整 规范化文档枚举生成 `json` 格式,由 `int32` 改为 `string` 4.8.8.34 ⏱️2023.07.02 [#I7HOPR](https://gitee.com/dotnetchina/Furion/issues/I7HOPR) + -  调整 规范化文档默认 `Title` 解析规则,不再自动添加空格 4.8.8.26 ⏱️2023.06.20 [24b7a47](https://gitee.com/dotnetchina/Furion/commit/24b7a4768471d312cbdff6a31739a0d9d4918c83) + -  调整 组件 `Component` 模式支持 `[DependsOn]` 支持继承 4.8.8.16 ⏱️2023.05.15 [#I733RF](https://gitee.com/dotnetchina/Furion/issues/I733RF) + -  调整 定时任务 `GC` 回收逻辑,避免高频添加作业导致 `尾延迟` 问题 4.8.8.3 ⏱️2023.04.21 [#I6XIV8](https://gitee.com/dotnetchina/Furion/issues/I6XIV8) + -  调整 定时任务日志设计,减少不必要的日志输出 4.8.8.3 ⏱️2023.04.21 [#I6XI2L](https://gitee.com/dotnetchina/Furion/issues/I6XI2L) + +- **文档** + + -  新增 `Jwt` 身份验证过程监听文档 + -  新增 事件总线 `Redis` 集成文档 + -  更新 粘土对象文档、虚拟文件系统文档、序列化文档、事件总线文档、远程请求文档、数据加密文档、安全授权文档、动态 `WebAPI` 文档、定时任务文档、`JSON` 序列化文档、`App` 静态类文档、规范化文档、配置文档、数据库上下文文档、`Db` 静态类文档 + +- **贡献者** + + - Axin ([@lfuxin](https://gitee.com/lfuxin)) [!858](https://gitee.com/dotnetchina/Furion/pulls/858) + - 陶泥 ([@ncs48620](https://gitee.com/ncs48620)) [!848](https://gitee.com/dotnetchina/Furion/pulls/848) + - handsome_by ([@handsomeboyyl](https://gitee.com/handsomeboyyl)) [!842](https://gitee.com/dotnetchina/Furion/pulls/842) + - 拉风的 CC ([@LFDCC](https://gitee.com/zetaluoxin)) [!841](https://gitee.com/dotnetchina/Furion/pulls/841) + - Felix Hoi ([@felixhoi](https://gitee.com/zetaluoxin)) [!839](https://gitee.com/dotnetchina/Furion/pulls/839) + - zetaluoxin ([@zetaluoxin](https://gitee.com/zetaluoxin)) [!834](https://gitee.com/dotnetchina/Furion/pulls/834) + - SongXinXin ([@goodsxx](https://gitee.com/goodsxx)) [!832](https://gitee.com/dotnetchina/Furion/pulls/832) [!833](https://gitee.com/dotnetchina/Furion/pulls/833) + - 阿炬 ([@quejuwen](https://gitee.com/quejuwen)) [!813](https://gitee.com/dotnetchina/Furion/pulls/813) + - KaneLeung ([@KaneLeung](https://gitee.com/KaneLeung)) [!808](https://gitee.com/dotnetchina/Furion/pulls/808) + - 蒋状先生 ([@JiangZhuangXianSheng](https://gitee.com/JiangZhuangXianSheng)) [!806](https://gitee.com/dotnetchina/Furion/pulls/806) [!853](https://gitee.com/dotnetchina/Furion/pulls/853) + - NeoLu ([@neolu](https://gitee.com/neolu)) [!804](https://gitee.com/dotnetchina/Furion/pulls/804) + - 蓝色天空 ([@lds2013](https://gitee.com/lds2013)) [!796](https://gitee.com/dotnetchina/Furion/pulls/796) + - YaChengMu ([@YaChengMu](https://gitee.com/YaChengMu)) [!788](https://gitee.com/dotnetchina/Furion/pulls/788) [!815](https://gitee.com/dotnetchina/Furion/pulls/815) + +--- + +## v4.8.7(已发布) + +:::caution `.NET8 Preview.1` 发布 + +**🚀🎉🔥 2023 年 02 月 22 日,微软发布了 .NET8 首个预览版。** + +[https://devblogs.microsoft.com/dotnet/announcing-dotnet-8-preview-1/](https://devblogs.microsoft.com/dotnet/announcing-dotnet-8-preview-1/) + +`Furion` 第一时间完成了适配,**`v4` 版本开始一套代码支持 `.NET5-.NET8/N`,支持所有 `Furion` 版本升级**。 + +::: + +:::important 版本细节 + +- `v4.8.7` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I6GVN8](https://gitee.com/dotnetchina/Furion/issues/I6GVN8) 2023.02.22 + +::: + +- **新特性** + + -  新增 定时任务看板支持自定义刷新频率 `SyncRate` 功能 4.8.7.43 ⏱️2023.04.12 [703b465](https://gitee.com/dotnetchina/Furion/commit/703b465f41510d86976d325cd31d7f8eba3a31ec) + -  新增 `Serve.GetIdleHost([host])` 静态方法,可获取一个指定主机的 `Web` 地址(端口) 4.8.7.43 ⏱️2023.04.12 [fdf788](https://gitee.com/dotnetchina/Furion/commit/fdf7885f282057599be6b1b3833373dd153db42a) + -  新增 **粘土对象可配置访问不存在 `Key` 时是抛异常还是返回 `null`** 4.8.7.40 ⏱️2023.04.10 [e994d53](https://gitee.com/dotnetchina/Furion/commit/e994d53b64a825461673f48960df1716be44f192) + -  新增 定时任务看板支持完全自定义 `RequestPath` 入口地址功能 4.8.7.34 ⏱️2023.04.04 [24736f6](https://gitee.com/dotnetchina/Furion/commit/24736f6421dd5aa90289fbb9bc519e6ef55e667f) + -  新增 `App.GetServices(type)` 和 `App.GetServices()` 获取服务实例集合 4.8.7.33 ⏱️2023.04.03 [c3e9957](https://gitee.com/dotnetchina/Furion/commit/c3e9957fd276920b3a8366eda3e347500334458e) + -  新增 远程请求 `[HttpMethod]ToSaveAsync` 下载远程文件并保存到磁盘方法 4.8.7.32 ⏱️2023.04.02 [bfd02c1](https://gitee.com/dotnetchina/Furion/commit/bfd02c1a2ce4229e90fc825fe5657ada59e1892f) + -  新增 **定时任务一系列 `.AlterTo` 修改作业触发器触发时间便捷方法** 4.8.7.31 ⏱️2023.03.31 [0349017](https://gitee.com/dotnetchina/Furion/commit/0349017902835bed91041fb3ea1ee987b0a81bbb) + -  新增 多语言支持 `DateTime` 时间格式化配置节点 `DateTimeFormatCulture` 4.8.7.31 ⏱️2023.03.31 [#I6RUOU](https://gitee.com/dotnetchina/Furion/issues/I6RUOU) + -  新增 `Serve.IdleHost` 静态属性,可获取一个随机空闲 `Web` 主机地址(端口) 4.8.7.29 ⏱️2023.03.30 [e425063](https://gitee.com/dotnetchina/Furion/commit/e4250634246af612f052ec935416ee050b44d22e) + -  新增 `WinForm/WPF` 静态方法 `Serve.RunNative()` 可配置是否启用 `Web` 主机功能 4.8.7.26 ⏱️2023.03.29 [#I6R97L](https://gitee.com/dotnetchina/Furion/issues/I6R97L) + -  新增 **`WinForm/WPF` 支持依赖注入的 `Native.CreateInstance()` 静态方法** 4.8.7.23 ⏱️2023.03.27 [53d51c3](https://gitee.com/dotnetchina/Furion/commit/53d51c3645f4066f5d68d4726d78e389fd544560) + -  新增 **`WinForm/WPF` 快速注册静态方法:`Serve.RunNative()`** 4.8.7.23 ⏱️2023.03.27 [53d51c3](https://gitee.com/dotnetchina/Furion/commit/53d51c3645f4066f5d68d4726d78e389fd544560) + -  新增 远程请求支持 `Content-Type` 为 `text/html` 和 `text/plain` 处理 4.8.7.22 ⏱️2023.03.27 [#I6QMLR](https://gitee.com/dotnetchina/Furion/issues/I6QMLR) + -  新增 **粘土对象可转换成 `IEnumerable` 对象并实现 `Lambda/Linq` 操作** 4.8.7.19 ⏱️2023.03.22 [2b14ed9](https://gitee.com/dotnetchina/Furion/commit/2b14ed9da03699619b1fade6e053f65b77a5b0fe) + +
+ 查看变化 +
+ +```cs showLineNumbers {3-4,6-12} +dynamic clay = Clay.Parse("{\"Foo\":\"json\",\"Bar\":100,\"Nest\":{\"Foobar\":true},\"Arr\":[\"NOR\",\"XOR\"]}"); + +// 将 clay.Arr 转换成 IEnumerable +IEnumerable query = clay.Arr.AsEnumerator(); + +// 实现 Lambda/Linq 操作 +var result = query.Where(u => u.StartsWith("N")) + .Select(u => new + { + Name = u + }) + .ToList(); +``` + +
+
+ +- -  新增 `Crontab.IsValid(...)` 静态方法,判断 `Cron` 表达式是否有效 4.8.7.17 ⏱️2023.03.20 [#I6OHO4](https://gitee.com/dotnetchina/Furion/issues/I6OHO4) +- -  新增 **日志配置 `WithStackFrame`,可控制是否输出产生日志的程序集,类型和具体方法** 4.8.7.16 ⏱️2023.03.19 [5ad6ae2](https://gitee.com/dotnetchina/Furion/commit/5ad6ae241d1798ad788e42569a15d68686db4fa1) + +
+ 查看变化 +
+ +启用 `WithStackFrame` 日志配置后,可输出程序集,类型,方法签名信息。 + +```cs showLineNumbers {4,10,16} +// 控制台日志 +services.AddConsoleFormatter(options => +{ + options.WithStackFrame = true; +}); + +// 文件日志 +services.AddFileLogging(options => +{ + options.WithStackFrame = true; +}); + +// 数据库日志 +services.AddDatabaseLogging(options => +{ + options.WithStackFrame = true; +}); +``` + +日志输出如下: + +```bash showLineNumbers {2,5,8,11,14,17,20} +info: 2023-03-17 18:25:06.7988349 +08:00 星期五 L System.Logging.EventBusService[0] #1 + [Furion.dll] async Task Furion.EventBus.EventBusHostedService.ExecuteAsync(CancellationToken stoppingToken) + EventBus hosted service is running. +info: 2023-03-17 18:25:08.1393952 +08:00 星期五 L Microsoft.Hosting.Lifetime[14] #1 + [System.Private.CoreLib.dll] void System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start(ref TStateMachine stateMachine) + Now listening on: https://localhost:5001 +info: 2023-03-17 18:25:08.1620391 +08:00 星期五 L Microsoft.Hosting.Lifetime[14] #1 + [System.Private.CoreLib.dll] void System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start(ref TStateMachine stateMachine) + Now listening on: http://localhost:5000 +info: 2023-03-17 18:25:08.1972456 +08:00 星期五 L Microsoft.Hosting.Lifetime[0] #1 + [Microsoft.Extensions.Hosting.dll] void Microsoft.Extensions.Hosting.Internal.ConsoleLifetime.OnApplicationStarted() + Application started. Press Ctrl+C to shut down. +info: 2023-03-17 18:25:08.2456579 +08:00 星期五 L Microsoft.Hosting.Lifetime[0] #1 + [Microsoft.Extensions.Hosting.dll] void Microsoft.Extensions.Hosting.Internal.ConsoleLifetime.OnApplicationStarted() + Hosting environment: Development +info: 2023-03-17 18:25:08.2746134 +08:00 星期五 L Microsoft.Hosting.Lifetime[0] #1 + [System.Private.CoreLib.dll] void System.Threading.CancellationTokenSource.ExecuteCallbackHandlers(bool throwOnFirstException) + Content root path: D:\Workplaces\OpenSources\Furion\samples\Furion.Web.Entry +info: 2023-03-17 18:25:18.1917784 +08:00 星期五 L Furion.Application.TestLoggerServices[0] #16 + [Furion.Application.dll] void Furion.Application.TestLoggerServices.测试日志() + 我是一个日志 20 +``` + +这样就清楚地知道日志是哪个程序集、哪个类型、哪个方法输出的了。 + +
+
+ +- -  新增 定时任务看板 `UI` 作业列表 `最近执行时间` 列和优化显示效果 4.8.7.12 ⏱️2023.03.15 [26462a8](https://gitee.com/dotnetchina/Furion/commit/26462a84e553e39ce4cddd5128833ff732c85f3e) [cb5dd17](https://gitee.com/dotnetchina/Furion/commit/cb5dd17969244987b847fcd96825d28b243a5b9f) +- -  新增 定时任务作业计划/工厂立即执行 `RunJob` 方法 4.8.7.11 ⏱️2023.03.15 [#I6LD9X](https://gitee.com/dotnetchina/Furion/issues/I6LD9X) +- -  新增 定时任务看板 `UI` 提供立即执行功能 4.8.7.11 ⏱️2023.03.15 [#I6LD9X](https://gitee.com/dotnetchina/Furion/issues/I6LD9X) +- -  新增 远程请求 `HttpRequestMessage` 拓展方法 `AppendHeaders` 4.8.7.10 ⏱️2023.03.14 [#I6MVHT](https://gitee.com/dotnetchina/Furion/issues/I6MVHT) +- -  新增 定时任务作业执行上下文 `JobExecutionContext` 服务提供器 `ServiceProvider` 属性 4.8.7.10 ⏱️2023.03.14 [02586f8](https://gitee.com/dotnetchina/Furion/commit/02586f83edb4f98e4801ae65080c2d6aa5545763) +- -  新增 **定时任务 `HTTP` 作业,支持定时请求互联网 `URL` 地址** 4.8.7.7 ⏱️2023.03.11 [01d4466](https://gitee.com/dotnetchina/Furion/commit/01d446620c20e373f198195797896d8c96feeb15) + +
+ 查看变化 +
+ +```cs showLineNumbers {3} +services.AddSchedule(options => +{ + options.AddHttpJob(request => + { + request.RequestUri = "https://www.chinadot.net"; + request.HttpMethod = HttpMethod.Get; + // request.Body = "{}"; // 设置请求报文体 + }, Triggers.PeriodSeconds(5)); +}); +``` + +
+
+ +- -  新增 **定时任务作业触发器 `Trigger` 执行结果 `Result` 和执行耗时 `ElapsedTime` 属性** 4.8.7.7 ⏱️2023.03.11 [01d4466](https://gitee.com/dotnetchina/Furion/commit/01d446620c20e373f198195797896d8c96feeb15) +- -  新增 **定时任务作业看板支持查看作业触发器执行结果 `Result` 和执行耗时 `ElapsedTime` 属性** 4.8.7.7 ⏱️2023.03.11 [01d4466](https://gitee.com/dotnetchina/Furion/commit/01d446620c20e373f198195797896d8c96feeb15) +- -  新增 定时任务休眠时长和唤醒时机日志输出 4.8.7.6 ⏱️2023.03.08 [#I6LANE](https://gitee.com/dotnetchina/Furion/issues/I6LANE) +- -  新增 **`Sql` 高级拦截支持返回 `IEnumerable` 和 `T[]` 类型值** 4.8.7.5 ⏱️2023.03.07 [f2ca2d3](https://gitee.com/dotnetchina/Furion/commit/f2ca2d303ea06febcc3a50df56ed03895e43c639) + +
+ 查看变化 +
+ +过去版本如果**返回对象类型**只支持 `List`,`T` 和 `Tuple<>`,现已支持 `IEnumerable`、`T[]` 和 `Tuple<>` 混合体。 + +```cs showLineNumbers {4,7,16} +public interface ISql : ISqlDispatchProxy +{ + [SqlExecute("select * from person")] + Person[] GetPersons(); + + [SqlExecute("select * from person")] + IEnumerable GetPersons2(); + + // 更复杂的组合 + [SqlExecute(@" +select * from person where id = 1; +select * from person; +select * from person where id > 0; +select * from person where id > 0; +")] + (Person, List, Person[], IEnumerable) GetPersons(); +} +``` + +
+
+ +- -  新增 `.m3u8` 和 `.ts` 文件类型 `MIME` 支持 4.8.7.5 ⏱️2023.03.07 [#I6KKEM](https://gitee.com/dotnetchina/Furion/issues/I6KKEM) +- -  新增 审计日志 `LoggingMonitor` 支持对参数贴 `[SuppressMonitor]` 特性跳过记录 4.8.7.3 ⏱️2023.03.01 [#I6IVGW](https://gitee.com/dotnetchina/Furion/issues/I6IVGW) +- -  新增 审计日志 `LoggingMonitor` 监听 `TraceId`、`ThreadId`、`Accept-Language` 4.8.7.1 ⏱️2023.02.27 [df35201](https://gitee.com/dotnetchina/Furion/commit/df35201622f4d908ab423baff27caef856a23527) +- -  新增 规范化结果 `UnifyContext.GetSerializerSettings(string)` 静态方法 4.8.7.1 ⏱️2023.02.27 [#I6HM7T](https://gitee.com/dotnetchina/Furion/issues/I6HM7T) + +- **突破性变化** + + -  调整 **定时任务动态作业 `DynamicJob` 委托/方法签名** 4.8.7.10 ⏱️2023.03.14 [6d56b53](https://gitee.com/dotnetchina/Furion/commit/6d56b531f34c9d616202a6f53c31a10974065d56) + +
+ 查看变化 +
+ +减少记忆负担,统一动态作业和普通作业的 `ExecuteAsync` 方法签名,故做出调整。 + +由: + +```cs showLineNumbers {1,3} +options.AddJob((serviceProvider, context, stoppingToken) => +{ + serviceProvider.GetLogger().LogInformation($"{context}"); + return Task.CompletedTask; +}, Triggers.PeriodSeconds(5)); +``` + +调整为: + +```cs showLineNumbers {1,3} +options.AddJob((context, stoppingToken) => +{ + context.ServiceProvider.GetLogger().LogInformation($"{context}"); + return Task.CompletedTask; +}, Triggers.PeriodSeconds(5)); +``` + +
+
+ +- -  升级 **适配 `.NET8 Preview.1`** 4.8.7 ⏱️2023.02.22 +- -  升级 **脚手架支持创建 `.NET8 Preview.1` 项目** 4.8.7 ⏱️2023.02.22 + +- **问题修复** + + -  修复 远程请求获取 `Cookies` 时如果包含相同 `Key` 异常问题 4.8.7.44 ⏱️2023.04.12 [#I6V3T7](https://gitee.com/dotnetchina/Furion/issues/I6V3T7) + -  修复 粘土对象转换为 `Dictionary` 类型异常 4.8.7.41 ⏱️2023.04.11 [f96baeb](https://gitee.com/dotnetchina/Furion/commit/f96baebbb06b53fc481ea7925cbbfbcb191f9c10) + -  修复 `TP.Wrapper` 静态类不能准确识别多行内容问题 4.8.7.40 ⏱️2023.04.10 [#I6UAC8](https://gitee.com/dotnetchina/Furion/issues/I6UAC8) + -  修复 粘土对象不支持运行时动态设置携带特殊字符的 `Key` 键 4.8.7.39 ⏱️2023.04.10 [6572515](https://gitee.com/dotnetchina/Furion/commit/6572515abbd93c4572cc513da4dd5aa497d144d2) + -  修复 视图引擎模型为匿名泛型集合类型时出现类型转换异常 4.8.7.38 ⏱️2023.04.07 [!773](https://gitee.com/dotnetchina/Furion/pulls/773) + -  修复 定时任务通过作业 `Id` 删除作业不能删除作业触发器问题 4.8.7.35 ⏱️2023.04.05 [312ca35](https://gitee.com/dotnetchina/Furion/commit/312ca357cca1e59e1b6cc67ec499bf512f79dd0a) + -  修复 动态 `WebAPI` 去除叠词类型命名如 `ServiceService` 前后缀异常问题 4.8.7.32 ⏱️2023.04.02 [#I6SB3Z](https://gitee.com/dotnetchina/Furion/issues/I6SB3Z) + -  修复 因 `4.8.7.22` 版本导致动态 `WebAPI` 类型注释丢失问题 4.8.7.27 ⏱️2023.03.29 [#I6QM23](https://gitee.com/dotnetchina/Furion/issues/I6QM23) + -  修复 粘土对象遍历对象键值对因 `4.8.7.19` 版本更新导致异常 4.8.7.25 ⏱️2023.03.28 [#I6R4ZU](https://gitee.com/dotnetchina/Furion/issues/I6R4ZU) + -  修复 `Swagger UI` 不显示 `ControllerBase` 派生类注释 4.8.7.22 ⏱️2023.03.27 [#I6QM23](https://gitee.com/dotnetchina/Furion/issues/I6QM23) + -  修复 日志输出 `JSON` 格式漏掉了 `UseUtcTimestamp` 和 `TraceId` 键值 4.8.7.21 ⏱️2023.03.27 [5c90e65](https://gitee.com/dotnetchina/Furion/commit/5c90e652b20dc36450ea0322fe6d22cd2a39d5e6) + -  修复 启用规范化结果后导致 `WebSocket` 连接断开时出现异常 4.8.7.20 ⏱️2023.03.23 [#I6PI5E](https://gitee.com/dotnetchina/Furion/issues/I6PI5E) + -  修复 定时任务作业状态为 `积压:0` 和 `归档:6` 时调用立即执行后不能恢复上一次状态 4.8.7.18 ⏱️2023.03.21 [6f5aae8](https://gitee.com/dotnetchina/Furion/commit/6f5aae8dd1169b7111ff6801111691764b03ba29) + -  修复 使用达梦数据库执行 `sql` 不能自动修复命令参数前缀 4.8.7.18 ⏱️2023.03.21 [#I6OK4T](https://gitee.com/dotnetchina/Furion/issues/I6OK4T) + -  修复 `Cron` 表达式 `*` 符号解析器不够严谨,如:`*1111aaaaa` 也被解析为 `*` 4.8.7.17 ⏱️2023.03.20 [#I6OHO4](https://gitee.com/dotnetchina/Furion/issues/I6OHO4) + -  修复 定时任务更新作业 `null` 值默认被跳过问题 4.8.7.17 ⏱️2023.03.20 [#I6OHO4](https://gitee.com/dotnetchina/Furion/issues/I6OHO4) + -  修复 视图引擎不支持强制转换的 `(object)model` 类型 4.8.7.16 ⏱️2023.03.19 [#I6O3BD](https://gitee.com/dotnetchina/Furion/issues/I6O3BD) + -  修复 启用请求 `Body` 重复读且在授权之前读取导致非 `GET/HEAD/OPTION` 请求异常 4.8.7.15 ⏱️2023.03.19 [#I6NX9E](https://gitee.com/dotnetchina/Furion/issues/I6NX9E) + -  修复 定时任务生成 `SQL` 语句没有处理 `'` 转义问题 4.8.7.15 ⏱️2023.03.19 [#I6NXKA](https://gitee.com/dotnetchina/Furion/issues/I6NXKA) + -  修复 数据验证 `ValiationTypes.GUID_OR_UUID` 不支持大写问题 4.8.7.14 ⏱️2023.03.16 [#I6NP22](https://gitee.com/dotnetchina/Furion/issues/I6NP22) + -  修复 `Blazor` 脚手架出现 `blazor.server.js` 不能加载问题(`404`) 4.8.7.13 ⏱️2023.03.16 [#I6NOBQ](https://gitee.com/dotnetchina/Furion/issues/I6NOBQ) + -  修复 定时任务服务在停止进程时会卡住 `30秒` 问题 4.8.7.8 ⏱️2023.03.13 [#I6MI9I](https://gitee.com/dotnetchina/Furion/issues/I6MI9I) [#I6MHOU](https://gitee.com/dotnetchina/Furion/issues/I6MHOU) + -  修复 定时任务看板删除不存在的作业触发器出现空异常 4.8.7.7 ⏱️2023.03.11 [01d4466](https://gitee.com/dotnetchina/Furion/commit/01d446620c20e373f198195797896d8c96feeb15) + -  修复 日志消息没有处理 `\n` 换行符对齐问题 4.8.7.6 ⏱️2023.03.10 [759bcc5](https://gitee.com/dotnetchina/Furion/commit/759bcc5dca09017f37a5be6ad2beefad33214fae) + -  修复 审计日志 `LoggingMonitor` 对特定参数贴有 `[FromServices]` 特性依旧记录问题 4.8.7.3 ⏱️2023.03.01 [17b134e](https://gitee.com/dotnetchina/Furion/commit/17b134efd82baa31bff6a0e763f93839c767c364) + -  修复 `Swagger` 接口排序同时指定 `Tag` 和 `Order` 之后无效 4.8.7.2 ⏱️2023.03.01 [#I6IQDI](https://gitee.com/dotnetchina/Furion/issues/I6IQDI) [#I6IP66](https://gitee.com/dotnetchina/Furion/issues/I6IP66) + +- **其他更改** + + -  调整 `Blazor` + `WebAPI` 脚手架模板,默认添加授权支持 4.8.7.37 ⏱️2023.04.07 [#I6OM8O](https://gitee.com/dotnetchina/Furion/issues/I6OM8O) [544f80d](https://gitee.com/dotnetchina/Furion/commit/544f80dbd7c800e28d9c4137e1c3bfc289c14177) + -  调整 定时任务动态委托作业持久化逻辑,采用不触发持久化操作 4.8.7.36 ⏱️2023.04.06 [7bb58b6](https://gitee.com/dotnetchina/Furion/commit/7bb58b64407f899d5f7f128da64fa972cf4df61b) + -  调整 多语言中间件 `app.UseAppLocalization()` 添加 `Action` 委托参数 4.8.7.30 ⏱️2023.03.31 [#I6RUOU](https://gitee.com/dotnetchina/Furion/issues/I6RUOU) + -  调整 定时任务 `Http` 作业 `HttpMethod` 属性拼写错成 `HttpMedhod` 4.8.7.24 ⏱️2023.03.28 [!756](https://gitee.com/dotnetchina/Furion/pulls/756) + -  调整 粘土对象 `number` 类型处理,若含 `.` 转 `double` 类型,否则转 `long` 类型 4.8.7.24 ⏱️2023.03.28 [e82e883](https://gitee.com/dotnetchina/Furion/commit/e82e883d54282b749390ae5e93df8c3e7acaa97e) + -  调整 视图引擎默认程序集,追加 `System.Collections` 程序集 4.8.7.16 ⏱️2023.03.18 [#I6O3BD](https://gitee.com/dotnetchina/Furion/issues/I6O3BD) + -  调整 定时任务配置选项 `BuilSqlType` 属性命为 `BuildSqlType` 4.8.7.11 ⏱️2023.03.15 [92117b8](https://gitee.com/dotnetchina/Furion/commit/92117b842f7f8bdeb983bf3dac510f713d8410c2) + -  调整 **定时任务查看作业触发器运行记录由保存 `10条` 改为 `5条`** 4.8.7.7 ⏱️2023.03.07 [01d4466](https://gitee.com/dotnetchina/Furion/commit/01d446620c20e373f198195797896d8c96feeb15) + -  调整 脚手架模板,默认启用主流文件类型 `MIME` 支持 4.8.7.5 ⏱️2023.03.07 [e35cdab](https://gitee.com/dotnetchina/Furion/commit/e35cdab592d1a00ff32b08c566c4ed5d6ddcff24) + -  调整 审计日志 `LoggingMonitor` 返回值泛型字符串显示格式 4.8.7.1 ⏱️2023.02.27 [df35201](https://gitee.com/dotnetchina/Furion/commit/df35201622f4d908ab423baff27caef856a23527) + +- **文档** + + -  新增 **[发布桌面程序](/docs/bs-to-cs) 文档** + -  新增 **[Native](/docs/global/native) 全局静态类文档** + -  新增 **[ASP.NET 8 集成](/docs/get-start-net8) 文档** + -  新增 **[.NET7 升级 .NET8](/docs/net7-to-net8) 文档** + -  更新 定时任务文档、中间件文档、规范化结果文档、动态 `WebAPI` 文档、日志记录文档、事件总线文档、虚拟文件系统文档、`Sql` 高级代理文档、数据库实体文档、任务队列文档、跨域文档、配置选项文档、安全授权、脚手架文档、粘土对象文档、多语言文档 + +- **贡献者** + + - 柠檬苏打 ([@lemon_soda](https://gitee.com/lemon_soda)) [!778](https://gitee.com/dotnetchina/Furion/pulls/778) + - 拉风的 CC ([@LFDCC](https://gitee.com/LFDCC)) [!773](https://gitee.com/dotnetchina/Furion/pulls/773) + - 吴伟烈 ([@wuweilie](https://gitee.com/wuweilie)) [!772](https://gitee.com/dotnetchina/Furion/pulls/772) + - 缄默 ([@alianyone](https://gitee.com/alianyone)) [!765](https://gitee.com/dotnetchina/Furion/pulls/765) + - 写意 ([@xjj_0906](https://gitee.com/xjj_0906)) [!756](https://gitee.com/dotnetchina/Furion/pulls/756) + - lampon ([@lampon](https://gitee.com/lampon)) [!740](https://gitee.com/dotnetchina/Furion/pulls/740) + - family520 ([@family520](https://gitee.com/family520)) [!739](https://gitee.com/dotnetchina/Furion/pulls/739) + - kingling ([@kinglinglive](https://gitee.com/kinglinglive)) [!732](https://gitee.com/dotnetchina/Furion/pulls/732) [!729](https://gitee.com/dotnetchina/Furion/pulls/729) + - ksmy ([@ksmy](https://gitee.com/ksmy)) [!731](https://gitee.com/dotnetchina/Furion/pulls/731) + - handsome_by ([@handsomeboyyl](https://gitee.com/handsomeboyyl)) [!727](https://gitee.com/dotnetchina/Furion/pulls/727) + +--- + +## v4.8.6(已发布) + +:::important 版本细节 + +- `v4.8.6` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I6DQ57](https://gitee.com/dotnetchina/Furion/issues/I6DQ57) 2023.02.08 +- `v4.8.5` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I6BC6J](https://gitee.com/dotnetchina/Furion/issues/I6BC6J) 2023.01.28 + +::: + +- **新特性** + + -  新增 审计日志 `LoggingMonitor` 支持配置序列化属性命名规则 4.8.6.12 ⏱️2023.02.21 [#I6GPUP](https://gitee.com/dotnetchina/Furion/issues/I6GPUP) + -  新增 `Swagger` 请求授权失败后自动退出授权状态 4.8.6.12 ⏱️2023.02.20 [!717](https://gitee.com/dotnetchina/Furion/pulls/717) + +- -  新增 粘土对象支持任何字符作为 `JSON/XML` 键 4.8.6.9 ⏱️2023.02.19 [f99aee8](https://gitee.com/dotnetchina/Furion/commit/f99aee8dafddf3cfd148922166abd3998e8eb087) [#note_16329657](https://gitee.com/dotnetchina/Furion/commit/4961e01486f604db12a8d8d71f9bd563ed7d7d48#note_16329657) + -  新增 **动态 `WebAPI` 自动检查路由是否包含重复参数,如果有自动修正而不是抛异常** 4.8.6.5 ⏱️2023.02.17 [5f15ea1](https://gitee.com/dotnetchina/Furion/commit/5f15ea1edd2e7793d86dd074ffbbecbebf7f683f) + +
+ 查看变化 +
+ +在 `Furion 4.8.6.5` 之前,下列代码**会抛出异常**:`The route parameter name 'roleid' appears more than one time in the route template.` + +**原因是生成的路由包含了多个 `{roleId}`**:`/api/with-class/system/role/deptTree/{roleId}/{roleId}`。 + +```cs showLineNumbers {3} +public class WithClass : IDynamicApiController +{ + [HttpGet("system/role/deptTree/{roleId}")] // 过去版本抛异常,Furion 4.8.6.5+ 正常~ + public string GetResult2(string roleId) + { + return nameof(Furion); + } +} +``` + +新版本 `Furion 4.8.6.5+` 修正了该错误,**自动移除后面重复的路由参数且不再抛异常**,也就是最终生成路由为:`/api/with-class/system/role/deptTree/{roleId}` + +
+
+ +- -  新增 `byte[]` 类型 `MD5` 加密/比较重载方法 4.8.6.3 ⏱️2023.02.15 [#I6F1NT](https://gitee.com/dotnetchina/Furion/issues/I6F1NT) +- -  新增 动态 `WebAPI` 支持 `[RouteConstraint(":*")]` 路由约束 4.8.6.2 ⏱️2023.02.10 [#I6E6JA](https://gitee.com/dotnetchina/Furion/issues/I6E6JA) +- -  新增 `Swagger` 启用登录后配置 `CheckUrl` 可获取本地存储的 `Authorization` 请求报文头 4.8.6.2 ⏱️2023.02.10 [#I6E3LB](https://gitee.com/dotnetchina/Furion/issues/I6E3LB) +- -  新增 **多语言支持 `.json` 文件配置方式(推荐)** 4.8.6 ⏱️2023.02.08 [#I6DL71](https://gitee.com/dotnetchina/Furion/issues/I6DL71) [#I5DXKP](https://gitee.com/dotnetchina/Furion/issues/I5DXKP) +- -  新增 定时任务 `IScheduler.[Try]UpdateDetail(builder => {})` 和 `IScheduler.[Try]UpdateTrigger(triggerId, builder => {})` 重载方法 4.8.6 ⏱️2023.02.08 [6e43a54](https://gitee.com/dotnetchina/Furion/commit/6e43a542f1a7e10bf996c6e0179a40d434d8682e) + +
+ 查看变化 +
+ +- 更新作业信息 + +```cs showLineNumbers {2,8} +// 返回 ScheduleResult 类型 +var scheduleResult = Scheduler.TryUpdateDetail(jobBuilder => +{ + jobBuilder.SetDescription("~~~"); +}, out var jobDetail); + +// 无返回值 +scheduler.UpdateDetail(jobBuilder => +{ + jobBuilder.SetDescription("~~~"); +}); +``` + +- 更新作业触发器 + +```cs showLineNumbers {2,8} +// 返回 ScheduleResult 类型 +var scheduleResult = scheduler.TryUpdateTrigger("triggerId", triggerBuilder => +{ + triggerBuilder.SetDescription("~~"); +}, out var trigger); + +// 无返回值 +scheduler.UpdateTrigger("triggerId", triggerBuilder => +{ + triggerBuilder.SetDescription("~~"); +}); +``` + +
+
+ +- -  新增 审计日志 `LoggingMonitor` 支持 `[DisplayName]` 特性解析和 `Title` 属性记录 4.8.5.10 ⏱️2023.02.07 [#I6DHMF](https://gitee.com/dotnetchina/Furion/issues/I6DHMF) +- -  新增 远程请求配置 `SetHttpVersion(version)` 配置,可配置 `HTTP` 请求版本,默认为 `1.1` 4.8.5.8 ⏱️2023.02.06 [#I6D64H](https://gitee.com/dotnetchina/Furion/issues/I6D64H) +- -  新增 **动态 `WebAPI` 支持更加强大的路由组合功能** 4.8.5.7 ⏱️2023.02.03 [#I6CLPT](https://gitee.com/dotnetchina/Furion/issues/I6CLPT) + +
+ 查看变化 +
+ +```cs showLineNumbers {8,19,36} +using Furion.DynamicApiController; +using Microsoft.AspNetCore.Mvc; + +namespace WebApplication38; + +[Route("api/[controller]")] +[Route("api2/[controller]")] +public class Test1Service : IDynamicApiController +{ + [HttpGet("test")] + [HttpPost] + [AcceptVerbs("PUT", "PATCH")] + public async Task GetTestName() + { + await Task.CompletedTask; + } +} + +public class Test2Service : IDynamicApiController +{ + [HttpGet("/root/test")] + [HttpGet("test")] + [HttpGet(Name = "other-test")] + [HttpGet("template-test", Name = "other-test")] + [HttpPost] + [AcceptVerbs("PUT", "PATCH")] + public async Task GetTestName() + { + await Task.CompletedTask; + } +} + +[Route("api/[controller]")] +[Route("api2/[controller]/second")] +[Route("api3/[controller]/three")] +public class Test3Service : IDynamicApiController +{ + [HttpGet] + [HttpGet("get/[action]")] + [HttpPost] + [HttpPost("post/cus-version")] + public string GetVersion() + { + return "1.0.0"; + } +} +``` + + + +
+
+ +- -  新增 定时任务 `Dashboard` 可自定义入口地址 `/schedule` 4.8.5.6 ⏱️2023.02.02 [c5639f5](https://gitee.com/dotnetchina/Furion/commit/c5639f5b8d1ae5164bf812540aeb98f90487e855) +- -  新增 `App.GetServiceLifetime(type)` 获取服务注册生命周期类型 4.8.5.3 ⏱️2023.01.31 [4a573a8](https://gitee.com/dotnetchina/Furion/commit/4a573a8934784b1d7e11f7d1fa3cfa65b5ec2b3a) +- -  新增 审计日志 `LoggingMonitor` 记录 `HTTP` 响应状态码 4.8.5.2 ⏱️2023.01.30 [abb4cbd](https://gitee.com/dotnetchina/Furion/commit/abb4cbdab9f45f53cad8468352d1c14ac8c54b42) +- -  新增 定时任务执行上下文 `RunId` 属性,用于标识单次作业触发器执行 4.8.5.1 ⏱️2023.01.30 [1aac470](https://gitee.com/dotnetchina/Furion/commit/1aac4707d270cb0fe66eab67490d946029a9e41d) + +- **突破性变化** + + -  升级 适配 `.NET7.0.3` 和 `.NET6.0.14` 4.8.6.3 ⏱️2023.02.15 [eecbf83](https://gitee.com/dotnetchina/Furion/commit/eecbf83b4e3340ec83d54f675f0fe4f6623b0d11) + +- -  调整 **动态 `WebAPI` 生成路由 `[HttpMethod(template)]` 规则** 4.8.5.7 ⏱️2023.02.03 [#I6CLPT](https://gitee.com/dotnetchina/Furion/issues/I6CLPT) + +
+ 查看变化 +
+ +在过去,`TestMethod` 生成路由为:`/mytest` + +```cs showLineNumbers {1,4} +// 注意这里没有 [Route] 特性 +public class ClassService: IDynamicApiController +{ + [HttpPost("mytest")] + public void TestMethod() + { + } +} +``` + +新版本:`TestMethod` 生成路由为:`/api/class/mytest`,`TestMethod2` 生成路由为:`/mytest`。 + +```cs showLineNumbers {1,4,9} +// 注意这里没有 [Route] 特性 +public class ClassService: IDynamicApiController +{ + [HttpPost("mytest")] + public void TestMethod() + { + } + + [HttpPost("/mytest")] + public void TestMethod2() + { + } +} +``` + +也就是新版本如果不需要自动添加前缀,需在前面添加 `/`,旧版本不需要。 + +
+
+ +- **问题修复** + + -  修复 规范化结果不带 `mini-profiler` 版本启动登录 `UI` 后不能传递 `headers` 问题 4.8.6.11 ⏱️2023.02.20 [#I6G8IR](https://gitee.com/dotnetchina/Furion/issues/I6G8IR) + -  修复 `Serve.Run()` 因 [#I6G02W](https://gitee.com/dotnetchina/Furion/issues/I6G02W) 更改导致不配置端口时出现异常无法启动问题 4.8.6.10 ⏱️2023.02.20 [#I6G6AR](https://gitee.com/dotnetchina/Furion/issues/I6G6AR) + -  修复 动态 `WebAPI` 不支持嵌套继承 `[Route]` 特性问题 4.8.6.8 ⏱️2023.02.18 [#I6CLPT](https://gitee.com/dotnetchina/Furion/issues/I6CLPT) + +
+ 查看变化 +
+ +过去版本生成错误重复路由,如:`api/system/SystemDictionary/api/system/SystemDictionary/Add`,现已修正。 + +```cs showLineNumbers {1,3,16,17} +public class WithClass : IDynamicApiController +{ + [Route("Add")] + public void Add() + { + + } + + [Route("Edit")] + public void Edit() + { + + } +} + +[Route("api/system/SystemDictionary")] +public class SystemService : WithClass +{ + public void Some() + { + } +} +``` + +
+
+ +- -  修复 `Serve.Run(urls: "端口")` 设置端口在 `.NET6/7` 下发布后始终是 `80` 端口问题 4.8.6.6 ⏱️2023.02.18 [#I6G02W](https://gitee.com/dotnetchina/Furion/issues/I6G02W) +- -  修复 粘土对象不支持 `中文` 作为 `JSON/XML` 键问题 4.8.6.6 ⏱️2023.02.18 [4961e01](https://gitee.com/dotnetchina/Furion/commit/4961e01486f604db12a8d8d71f9bd563ed7d7d48) +- -  修复 远程请求代理模式配置了 `WithEncodeUrl = false` 无效问题 4.8.6.4 ⏱️2023.02.16 [89639ba](https://gitee.com/dotnetchina/Furion/commit/89639ba1db8a7df750d9bca66a887e252622b219) +- -  修复 动态 `WebAPI` 自定义 `[HttpMethod(template)]` 之后生成错误路由 4.8.6.1 ⏱️2023.02.08 [59fe53b](https://gitee.com/dotnetchina/Furion/commit/59fe53b5a9715ed139aff9075cb7fcaad01565b7) +- -  修复 由于 [#I6D64H](https://gitee.com/dotnetchina/Furion/issues/I6D64H) 导致远程请求出现 `Specified method is not supported.` 问题 4.8.5.9 ⏱️2023.02.07 [#I6DEEE](https://gitee.com/dotnetchina/Furion/issues/I6DEEE) [#I6D64H](https://gitee.com/dotnetchina/Furion/issues/I6D64H) +- -  修复 ~~优化远程请求 `ReadAsStringAsync` 底层方法,尝试修复 `Error while copying content to a stream.` 错误 4.8.5.8 ⏱️2023.02.06 [#I6D64H](https://gitee.com/dotnetchina/Furion/issues/I6D64H)~~ +- -  修复 **规范化结果不支持 `OData` 协议控制器** 4.8.5.5 ⏱️2023.02.01 [!571](https://gitee.com/dotnetchina/Furion/pulls/571) +- -  修复 启用 `Swagger` 登录功能之后不能触发响应拦截器 4.8.5.5 ⏱️2023.02.01 [#I6C9A2](https://gitee.com/dotnetchina/Furion/issues/I6C9A2) [!702](https://gitee.com/dotnetchina/Furion/pulls/702) [!703](https://gitee.com/dotnetchina/Furion/pulls/703) +- -  修复 在数据库日志的 `IDatabaseLoggingWriter` 实现类中依赖注入 `ILogger<>` 导致死循环 4.8.5.4 ⏱️2023.02.01 [#I6C6QU](https://gitee.com/dotnetchina/Furion/issues/I6C6QU) +- -  修复 `Furion.Xunit/Furion.Pure.Xunit` 单元测试依赖注入单例服务时不是同一实例问题 4.8.5.3 ⏱️2023.01.31 [305511e](https://gitee.com/dotnetchina/Furion/commit/305511ec6322019622c392a23957042d2dcae7fb) +- -  修复 数据库日志提供程序在应用程序终止时出现空异常问题 4.8.5 ⏱️2023.01.28 [#I6AZ8Y](https://gitee.com/dotnetchina/Furion/issues/I6AZ8Y) +- -  修复 实体拓展方式操作数据库出现空异常问题 4.8.5 ⏱️2023.01.28 [#I6AXU6](https://gitee.com/dotnetchina/Furion/issues/I6AXU6) + +- **其他更改** + + -  调整 脱敏处理 `sensitive-words.txt` 嵌入文件支持 `UTF8 BOM` 编码,感谢 [@man119](https://gitee.com/man119) 4.8.6.7 ⏱️2023.02.18 [#I6G1JN](https://gitee.com/dotnetchina/Furion/issues/I6G1JN) + -  调整 `Serve.Run()` 迷你主机默认添加 `JSON` 中文乱码处理 4.8.6.3 ⏱️2023.02.15 [86b5f9f](https://gitee.com/dotnetchina/Furion/commit/86b5f9f7c2ace503312bf879dccd7add12bd93c4) + +- **文档** + + -  新增 **多语言 `.json` 配置方式文档** + -  更新 日志文档、定时任务文档、动态 `WebAPI` 文档,规范化结果文档,`App` 静态类文档,`Oops` 静态类文档、虚拟文件系统文档 [!704](https://gitee.com/dotnetchina/Furion/pulls/704),远程请求文档,序列化文档、入门文档、脱敏模块文档 + +- **贡献者** + + - Andy ([@man119](https://gitee.com/man119)) + - liuhll ([@liuhll2](https://gitee.com/liuhll2)) + - 大柚 ([@big-pomelo](https://gitee.com/big-pomelo)) + - WR_YT ([@wr-yt](https://gitee.com/wr-yt)) + +--- + +## v4.8.4(已发布,全新定时任务) + + + +:::tip 更好的 `Furion`,更好的自己 + +在过去两年,实现 `Furion` 从无到有,编写文档已逾三百万字,过程心酸开源人自知。 + +这一路日夜兼程,嘲讽批评常伴眼耳,即便辛苦无奈、想过放弃,但为了那微不足道的成就感依然努力着。 + +当然,也收获了不少... 越来越多拥趸者,越发精湛技术能力,更高层次思维模式,还有许多跨界跨行朋友。 + +在 《[开源指北](https://gitee.com/opensource-guide/comments/)》中,我曾说道:**“开源如同人的脸,好坏一面便知,缺点可能会受到嘲讽批评,优点也会收获赞扬尊重。别担心,他们正在塑造更好的你。”** + +`.NET` 要在国内真正发展起来,必须得有一些追逐梦想的人在做着不计付出的事情,而我希望自己能贡献一份微薄之力。所以,这一次重新起航,重塑 `Furion` 重塑自己。也许未来在某个 IT 圈但凡有人谈起 `.NET` 还能瞟到 `Furion` 的身影。 + +::: + +:::important 版本细节 + +- `v4.8.4` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I68573](https://gitee.com/dotnetchina/Furion/issues/I68573) 2022.12.30 +- `v4.8.3` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I657O5](https://gitee.com/dotnetchina/Furion/issues/I657O5) 2022.12.08 +- `v4.8.2` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I63CTP](https://gitee.com/dotnetchina/Furion/issues/I63CTP) 2022.11.27 +- `v4.8.1` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I62RX3](https://gitee.com/dotnetchina/Furion/issues/I62RX3) 2022.11.24 +- `v4.8.0` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I62NZV](https://gitee.com/dotnetchina/Furion/issues/I62NZV) 2022.11.23 + +::: + +- **新特性** + + -  新增 **🎉 [全新的定时任务模块](/docs/job)** 4.8.0 【[查看源码](https://gitee.com/dotnetchina/Furion/tree/v4/framework/Furion/Schedule)】 + -  新增 **🎉 [全新的 `Cron` 表达式模块](/docs/cron)** 4.8.0 【[查看源码](https://gitee.com/dotnetchina/Furion/tree/v4/framework/Furion/TimeCrontab)】 + -  新增 **🎉 [全新的任务队列模块](/docs/task-queue)** 4.8.3 【[查看源码](https://gitee.com/dotnetchina/Furion/tree/v4/framework/Furion/TaskQueue)】 + -  新增 视图引擎支持无命名空间的强类型 4.8.4.16 ⏱️2023.01.15 [#I6ABN3](https://gitee.com/dotnetchina/Furion/issues/I6ABN3) [#I6A7SI](https://gitee.com/dotnetchina/Furion/issues/I6A7SI) [076bb17](https://gitee.com/dotnetchina/Furion/commit/076bb1781ab1a31b5b6a42a56910909b3528d25e) + -  新增 视图引擎支持匿名类型模型带集合类型属性 `@foreach` 遍历 4.8.4.15 ⏱️2023.01.13 [#I6A7SI](https://gitee.com/dotnetchina/Furion/issues/I6A7SI) + -  新增 `Swagger` 支持复制路由地址功能 4.8.4.13 ⏱️2023.01.11 [#I5VNJI](https://gitee.com/dotnetchina/Furion/issues/I5VNJI) + -  新增 动态 `WebAPI` 方法支持通过 `[ActionName(名称)]` 和 `[HttpMethod(Name=名称)]` 指定路由名称 4.8.4.12 ⏱️2023.01.10 [#I69AOJ](https://gitee.com/dotnetchina/Furion/issues/I69AOJ) [f699540](https://gitee.com/dotnetchina/Furion/commit/f699540989e688f25597c509249e024ec014dc4e) + -  新增 `BadPageResult.Status401Unauthorized` 等常见状态码 `401,403,404,500` 静态属性 4.8.4.11 ⏱️2023.01.09 [#I69KQF](https://gitee.com/dotnetchina/Furion/issues/I69KQF) + -  新增 `crontab.GetSleepTimeSpan(baseTime)` 实例方法 4.8.4.10 ⏱️2023.01.09 [#I69HM4](https://gitee.com/dotnetchina/Furion/issues/I69HM4) + -  新增 `Enqueue/EnqueueAsync` 支持 `Cron` 表达式 实例重载方法 4.8.4.10 ⏱️2023.01.09 [#I69HM4](https://gitee.com/dotnetchina/Furion/issues/I69HM4) + -  新增 `*.bcmap` 和 `.properties` 文件类型 `MIME` 支持 4.8.4.9 ⏱️2023.01.06 [!694](https://gitee.com/dotnetchina/Furion/pulls/694) + -  新增 定时任务 `Dashboard` 查看作业触发器最近运行记录功能 4.8.4.3 ⏱️2023.01.03 [e7d24d8](https://gitee.com/dotnetchina/Furion/commit/e7d24d84bcc448b0a13f8e0b76328669261af44b) + -  新增 定时任务作业触发器 `trigger.GetTimelines()` 获取最近 `10` 条运行记录列表 4.8.4.3 ⏱️2023.01.03 [e7d24d8](https://gitee.com/dotnetchina/Furion/commit/e7d24d84bcc448b0a13f8e0b76328669261af44b) + -  新增 **定时任务 `Dashboard` 看板** 4.8.4 ⏱️2022.12.30 [d3f9669](https://gitee.com/dotnetchina/Furion/commit/d3f966921dfa757c12c2bd071fb19fc166a2f24e) + -  新增 定时任务 `IScheduler.GetEnumerable()` 方法,可将作业计划转换成可枚举字典 4.8.4 ⏱️2022.12.30 [4d5235c](https://gitee.com/dotnetchina/Furion/commit/4d5235c37a9ef5e66e92847e65ef9786bcd7387c) + -  新增 `L.SetCurrentUICulture(culture)` 和 `L.GetCurrentUICulture()` 静态方法,可在运行时动态修改当前线程区域性 4.8.3.10 ⏱️2022.12.23 [#I66JWA](https://gitee.com/dotnetchina/Furion/issues/I66JWA) + -  新增 `L.SetCulture(culture, immediately)` 方法重载,可配置运行时修改多语言立即有效 4.8.3.10 ⏱️2022.12.23 [#I66JWA](https://gitee.com/dotnetchina/Furion/issues/I66JWA) + -  新增 定时任务配置选项 `options.JobDetail.LogEnabled` 配置,可自动输出执行日志 4.8.3.7 ⏱️2022.12.14 [58d2c20](https://gitee.com/dotnetchina/Furion/commit/58d2c20de05dc458b206863f7814e89866e7522b) + -  新增 `ValidationTypes` 更多常见验证格式(`手机机身码类型`,`统一社会信用代码`,`GUID/UUID`,`base64`) 4.8.3.6 ⏱️2022.12.13 [3680d7a](https://gitee.com/dotnetchina/Furion/commit/3680d7a7a53515dfb78625f2c6a393a788025685) + -  新增 **定时任务 `IScheduler` 对象每次操作后自动刷新和提供手动刷新 `Reload()` 方法** 4.8.3.3 ⏱️2022.12.09 [#I65EQ1](https://gitee.com/dotnetchina/Furion/issues/I65EQ1#note_15047484_link) + -  新增 定时任务间隔分钟作业触发器 `Triggers.PeriodMinutes(5)` 和 `[PeriodMinutes(5)]` 特性 4.8.2.8 ⏱️2022.12.01 [8e1f06f](https://gitee.com/dotnetchina/Furion/commit/8e1f06fa2161ee2bf8bcea29af8aaa5a60ef9db9) + -  新增 定时任务工作日作业触发器 `Triggers.Workday()` 和 `[Workday]` 特性 4.8.2.6 ⏱️2022.11.30 [28b2d20](https://gitee.com/dotnetchina/Furion/commit/28b2d20b3f6034a4cdf5827576c34412315fbb15) + -  新增 定时任务作业校对功能,可对误差进行校正 4.8.2.6 ⏱️2022.11.30 [f725a25](https://gitee.com/dotnetchina/Furion/commit/f725a252e6a89f9dea6489d4e54452077b1935e5) + -  新增 `Crontab.ParseAt(..)` 静态方法 4.8.2.6 ⏱️2022.11.30 [035cc23](https://gitee.com/dotnetchina/Furion/commit/035cc23a20045e9673a1406b94e72030f5f18375) + -  新增 `Crontab` 所有 `Macro At` 静态方法 4.8.2.6 ⏱️2022.11.30 [a15b69d](https://gitee.com/dotnetchina/Furion/commit/a15b69d0eeb4bdc8d0ec042cfec22ff0049dc89f) + -  新增 `Crontab.Workday` 表示周一至周五的 `Macro` 静态属性 4.8.2.6 ⏱️2022.11.30 [a15b69d](https://gitee.com/dotnetchina/Furion/commit/a15b69d0eeb4bdc8d0ec042cfec22ff0049dc89f) + -  新增 **定时任务 `Triggers` 所有带 `At` 的 `Cron` 表达式触发器构建器及特性** 4.8.2.5 ⏱️2022.11.29 [#I63PLR](https://gitee.com/dotnetchina/Sundial/issues/I63PLR) + -  新增 **`App.GetThreadId()` 和 `App.GetTraceId()` 获取线程 `Id` 和请求 `TraceId`** 4.8.2.4 ⏱️2022.11.29 [910fc1f](https://gitee.com/dotnetchina/Furion/commit/910fc1fbad9e40245c5694f30457cd4d2ca0d630) + -  新增 **`App.GetExecutionTime(() => { /*Your Code*/ })` 获取代码执行耗时** 4.8.2.4 ⏱️2022.11.29 [5ab4b19](https://gitee.com/dotnetchina/Furion/commit/5ab4b19786e53d8934de08fd2f5430fc66c3b9a1) + -  新增 定时任务批量添加 `SchedulerBuilder` 作业功能 4.8.2.4 ⏱️2022.11.29 [5faa67b](https://gitee.com/dotnetchina/Furion/commit/5faa67b7817459cb0ee0add86a6e53c17ff51a05) + -  新增 定时任务 `BuildSqlType` 配置,可设置生成不同数据库类型的 `SQL` 语句 4.8.2.3 ⏱️2022.11.29 [293f9bc](https://gitee.com/dotnetchina/Furion/commit/293f9bce34fc4f70eacae1043ed697d31da88409) [!675](https://gitee.com/dotnetchina/Furion/pulls/675) + -  新增 `JobDetail` 和 `Trigger` 自定义 `ConvertToSQL` 输出 `SQL` 配置 4.8.2 ⏱️2022.11.27 [0bb9d8f](https://gitee.com/dotnetchina/Furion/commit/0bb9d8f1f3606af145b44c2984b87fdc020f02e1) + -  新增 动态作业处理程序委托支持 4.8.1.8 ⏱️2022.11.27 [e02266c](https://gitee.com/dotnetchina/Furion/commit/e02266c44187dbc2b416abe1ca4112ce13c89180) + -  新增 **作业触发器 `ResetOnlyOnce` 属性,支持只运行一次的作业重新启动服务重复执行** 4.8.1.5 ⏱️2022.11.25 [a8be728](https://gitee.com/dotnetchina/Furion/commit/a8be728eac986ebc5f44718b08c67aaee8b89dc6) + -  新增 事件总线支持简单的 `Order` 编排规则 4.8.0 [833c0d4](https://gitee.com/dotnetchina/Furion/commit/833c0d4d069bca5f5304aa21cb32c7f902c20c69) + -  新增 远程请求代理模式对于基元类型参数支持自动获取参数名 4.8.0 [#I60OT6](https://gitee.com/dotnetchina/Furion/issues/I60OT6) + -  新增 动态 `WebAPI` 自动识别方法的接口参数是否是服务,如果是自动添加 `[FromServices]` 特性 4.8.0 [fae60a9](https://gitee.com/dotnetchina/Furion/commit/fae60a9c3097ec19ac2b3cd75ce1710ccd3e9294) + -  新增 远程请求 `[QueryString]` 特性添加时间格式化 `Format` 属性 4.8.1.2 [!670](https://gitee.com/dotnetchina/Furion/pulls/670) + -  新增 `Serve.Run` 模式的 `.ConfigureServices` 方法 4.8.0 [023391b](https://gitee.com/dotnetchina/Furion/commit/023391b428f4239d926c3abb6cb0e2b92a0d738a) + -  新增 `Serve.RunGeneric` 通用泛型主机方法 4.8.0 [6865f3d](https://gitee.com/dotnetchina/Furion/commit/6865f3d3d890ee3c90901eaa949ad058d34b477d) + -  新增 **`Serve.Run()` 的 `additional` 参数** 4.8.0 [023391b](https://gitee.com/dotnetchina/Furion/commit/023391b428f4239d926c3abb6cb0e2b92a0d738a) + +
+ 查看变化 +
+ +极速入门现在可以便捷注册服务,写测试例子的时候非常有用,无需编写 `Startup.cs`。 + +```cs showLineNumbers {1,7,13,19} +Serve.Run(additional: services => +{ + services.AddRemoteRequest(); +}); + +// 通用泛型主机方式 +Serve.RunGeneric(additional: services => +{ + services.AddRemoteRequest(); +}); + +// 还可以省去 additional +Serve.Run(services => +{ + services.AddRemoteRequest(); +}); + +// 通用泛型主机方式 +Serve.RunGeneric(services => +{ + services.AddRemoteRequest(); +}); +``` + +
+
+ +- -  新增 `Serve.Run` 主机返回值 `IHost` 4.8.0 [#I61XHV](https://gitee.com/dotnetchina/Furion/issues/I61XHV) + +
+ 查看变化 +
+ +在 `Winfom/WPF` 应用程序中,我们希望关闭窗体或退出应用程序时,能够关闭 `Serve` 主机: + +```cs showLineNumbers {1,5,8,11-17} +using Microsoft.Extensions.Hosting; + +public partial class App : Application +{ + private readonly IHost _host; + public App() + { + _host = Serve.Run(silence: true); + } + + protected override void OnExit(ExitEventArgs e) + { + _host.StopAsync(); + _host.Dispose(); + + base.OnExit(e); + } +} +``` + +
+
+ +- -  新增 **日志 `JSON` 自动美化格式化器 `LoggerFormatter.JsonIndented`** 4.8.0 [7b9268c](https://gitee.com/dotnetchina/Furion/commit/7b9268cc2428b5ee6cc5ed259e5c45a6830bddb1) +- -  新增 `LoggingMonitor` 的 `JsonIndented` 配置,可配置是否美化 `JSON` 4.8.0 [7b9268c](https://gitee.com/dotnetchina/Furion/commit/7b9268cc2428b5ee6cc5ed259e5c45a6830bddb1) + +
+ 查看变化 +
+ +默认情况下,配置输出 `JSON` 格式化 `LoggerFormatter.Json` 会浓缩到一行显示。 + +新版本支持 `LoggerFormatter.JsonIndented` 美化 `JSON` 配置: + +```cs showLineNumbers {2,4,8,10,14,16,20,22} +// 控制台日志 +services.AddConsoleFormatter(options => +{ + options.MessageFormat = LoggerFormatter.JsonIndented; +}); + +// 文件日志 +services.AddFileLogging("mytemplate.log", options => +{ + options.MessageFormat = LoggerFormatter.JsonIndented; +}); + +// 数据库日志 +services.AddDatabaseLogging(options => +{ + options.MessageFormat = LoggerFormatter.JsonIndented; +}); + +// LoggingMonitor 日志 +services.AddMonitorLogging(options => +{ + options.JsonIndented = true; +}); +``` + +
+
+ +- -  新增 **日志模块是否输出 `TraceId`,同一个请求的日志 `TraceId` 一致** 4.8.1.3 [#I62VGG](https://gitee.com/dotnetchina/Furion/issues/I62VGG) + +
+ 查看变化 +
+ +在生产环境中,日志的输出是非常频繁的,但是很难从日志文件中判断哪些日志是属于同一个请求输出的,所以新增 `WithTraceId` 配置。 + +```cs showLineNumbers {2,4,8,10,14,16,20,22} +// 控制台日志 +services.AddConsoleFormatter(options => +{ + options.WithTraceId = true; +}); + +// 文件日志 +services.AddFileLogging("mytemplate.log", options => +{ + options.WithTraceId = true; +}); + +// 数据库日志 +services.AddDatabaseLogging(options => +{ + options.WithTraceId = true; +}); +``` + +输出日志如下: + +```bash showLineNumbers {15,17} +info: 2022-11-24 14:34:55.1717549 +08:00 星期四 L System.Logging.EventBusService[0] #1 + EventBus Hosted Service is running. +info: 2022-11-24 14:34:55.2504015 +08:00 星期四 L System.Logging.ScheduleService[0] #1 + Schedule Hosted Service is running. +info: 2022-11-24 14:34:56.4280796 +08:00 星期四 L Microsoft.Hosting.Lifetime[14] #1 + Now listening on: https://localhost:5001 +info: 2022-11-24 14:34:56.4331170 +08:00 星期四 L Microsoft.Hosting.Lifetime[14] #1 + Now listening on: http://localhost:5000 +info: 2022-11-24 14:34:56.4384567 +08:00 星期四 L Microsoft.Hosting.Lifetime[0] #1 + Application started. Press Ctrl+C to shut down. +info: 2022-11-24 14:34:56.4408766 +08:00 星期四 L Microsoft.Hosting.Lifetime[0] #1 + Hosting environment: Development +info: 2022-11-24 14:34:56.4427659 +08:00 星期四 L Microsoft.Hosting.Lifetime[0] #1 + Content root path: D:\Workplaces\OpenSources\Furion\samples\Furion.Web.Entry +info: 2022-11-24 14:35:06.8507338 +08:00 Thursday L Furion.Application.TestLoggerServices[0] #17 '00-48df9ac5c8280de2f301faa44a23a10c-b75678d9f3883b0b-00' + 我是一个日志 20 +info: 2022-11-24 14:35:16.0373384 +08:00 星期四 L Furion.Application.TestLoggerServices[0] #17 '00-ff4fb15d6ff41a0411784e66400f0dfd-962bc25eff788b25-00' + 我是一个日志 20 +``` + +
+
+ +- **突破性变化** + + -  调整 **旧版本定时任务为 `弃用` 状态(2022 年 12 月 31 日彻底删除),如需取消警告在 `.csproj` 中添加 `0618`** 4.8.0 [旧版本文档](/docs/job-old) + -  移除 `JSON` 静态类所有方法的 `serviceProvider` 参数(无破坏) 4.8.0 [962fb16](https://gitee.com/dotnetchina/Furion/commit/962fb167aa13c689b688035bef26dcac73d3fca4) + -  移除 `Http` 静态类所有方法的 `serviceProvider` 参数(无破坏) 4.8.0 [962fb16](https://gitee.com/dotnetchina/Furion/commit/962fb167aa13c689b688035bef26dcac73d3fca4) + -  调整 **定时任务底层所有代码,日志,注释,文档** 4.8.1.10 ⏱️2022.12.05 + +- **问题修复** + + -  修复 定时任务 `StartAll` 出现个别作业显示 `无触发时间` 的状态 4.8.4.14 ⏱️2023.01.12 [#I6A08X](https://gitee.com/dotnetchina/Furion/issues/I6A08X) + -  修复 动态 `WebAPI` 配置 `[Consumes]` 特性后 `Swagger` 不显示问题 4.8.4.12 ⏱️2023.01.10 [daf25f8](https://gitee.com/dotnetchina/Furion/commit/daf25f8c91a24904381d3536c0c8825ef833e9c9) + -  修复 定时任务停止作业触发器后运行记录不能写入最新记录问题 4.8.4.8 ⏱️2023.01.05 [d4c553f](https://gitee.com/dotnetchina/Furion/commit/d4c553fb9b0037ab29942ccc2a25a386fc28c1db) + -  修复 数据库日志注册在一些特殊情况下丢失日志上下文问题 4.8.4.6 ⏱️2023.01.04 [#I68PDF](https://gitee.com/dotnetchina/Furion/issues/I68PDF) + -  修复 定时任务使用 `Furion.Pure` 包访问 `Dashboard` 出现 `404` 问题 4.8.4.2 ⏱️2023.01.02 [21977b7](https://gitee.com/dotnetchina/Furion/commit/21977b70ef84fd674bb306ab86ef032f7f28c7a7) + -  修复 在类中贴 `[SuppressMonitor]` 特性但 `LoggingMonitor` 依然输出问题 4.8.4 ⏱️2022.12.30 [#I6882I](https://gitee.com/dotnetchina/Furion/issues/I6882I) + -  修复 远程请求配置 `WithEncodeUrl(false)` 对 `application/x-www-form-urlencoded` 请求类型无效 4.8.4 ⏱️2022.12.30 [#I682DX](https://gitee.com/dotnetchina/Furion/issues/I682DX) + -  修复 `LoggingMonitor` 序列化 `IQueryable<>` 或 `OData` 返回值类型出现死循环问题 4.8.3.4 ⏱️2022.12.10 [7e8c9d0](https://gitee.com/dotnetchina/Furion/commit/7e8c9d0e3910c4d0cfa18a7a15cbe8415d34dd66) + -  修复 定时任务通过 `scheduler.RemoveTrigger(triggerId)` 报异常问题 4.8.3.3 ⏱️2022.12.09 [#I65EQ1](https://gitee.com/dotnetchina/Furion/issues/I65EQ1#note_15047484_link) + -  修复 定时任务作业触发器配置了 `EndTime` 和 `StartTime` 之后 `Status` 没有对应上 4.8.3.1 ⏱️2022.12.09 [52a5506](https://gitee.com/dotnetchina/Furion/commit/52a5506c2fda7c31df01b2e90af7ad6b0c5f94aa) + -  修复 定时任务通过 `scheduler.AddTrigger(triggerBuilder)` 无效的问题 4.8.3.1 ⏱️2022.12.09 [#I65EQ1](https://gitee.com/dotnetchina/Furion/issues/I65EQ1) + -  修复 作业拥有多个触发器时暂停作业后依然存在个别未暂停的清空(并发问题) 4.8.2.12 ⏱️2022.12.07 [#I655W9](https://gitee.com/dotnetchina/Furion/issues/I655W9) + -  修复 **通过 `Ctrl + C` 终止应用程序后获取 `TraceId` 出现对象已释放异常** 4.8.1.12 ⏱️2022.12.07 [55c3e49](https://gitee.com/dotnetchina/Furion/commit/55c3e4942b1f89e0548d4cf90453937ba4ea512c) + -  修复 **`cli.ps1` 脚本不支持 `EFCore 7.0` 问题** 4.8.1.12 ⏱️2022.12.07 [!676](https://gitee.com/dotnetchina/Furion/pulls/676) + -  修复 `EFCore` 实体监听器 `IEntityChangedListener` 问题 4.8.1.7 ⏱️2022.11.26 [#I61CTI](https://gitee.com/dotnetchina/Furion/issues/I61CTI) + -  修复 定时任务生成的 `SQL` 语句不支持 `MySQL` 问题 4.8.1.7 ⏱️2022.11.26 [#I638ZC](https://gitee.com/dotnetchina/Furion/issues/I638ZC) + -  修复 运行时启动/暂停作业无效问题 4.8.1.6 ⏱️2022.11.25 [#I6368M](https://gitee.com/dotnetchina/Furion/issues/I6368M) + -  修复 作业触发器不符合下一次执行规律但 `NextRunTime` 不为 `null` 情况 4.8.1.5 ⏱️2022.11.25 [a8be728](https://gitee.com/dotnetchina/Furion/commit/a8be728eac986ebc5f44718b08c67aaee8b89dc6) + -  修复 从 `.NET6/7` 降级回 `.NET5` 找不到 `.AddDateOnlyConverters()` 和 `.AddTimeOnlyConverters()` 拓展方法问题 4.8.0 [cdddf8d](https://gitee.com/dotnetchina/Furion/commit/cdddf8dbf628abb85e0816335ebae6bee96f6dc1) + -  修复 `Retry.InvokeAsync` 方法如果不传入 `fallbackPolicy` 参数报空异常问题 4.8.0 [21af847](https://gitee.com/dotnetchina/Furion/commit/21af847b02ae6044d344450620fa4e549480e387) + -  修复 动态 `WebAPI` 不支持在 `.NET7` 不声明 `[FromServices]` 自动注入问题 4.8.0 [#I62HP1](https://gitee.com/dotnetchina/Furion/issues/I62HP1) + -  修复 远程请求 `GetAsStreamAsync()` 报 `System.InvalidOperationException: Response Content-Length mismatch` 异常问题 4.8.1 [#I62QY4](https://gitee.com/dotnetchina/Furion/issues/I62QY4) + -  修复 `LoggingMonitor` 配置 `WriteFilter` 不起作用问题 4.8.1.2 [#I62P52](https://gitee.com/dotnetchina/Furion/issues/I62P52) [90bcfda](https://gitee.com/dotnetchina/Furion/commit/90bcfda4e3af24df399cc6bdb5c558c2bbed2536) + -  修复 `EFCore` 个别关系型数据库 `PostgreSQL/SqlServer/MySql` 出现短暂不能连接问题 4.8.1.3 [2c530ef](https://gitee.com/dotnetchina/Furion/commit/2c530ef3886327b572ffaa9a7ad43be5e629e854) + -  修复 日志模块因 `v4.8.0+` 版本导致写入数据库日志空异常问题 4.8.2.1 ⏱️2022.11.28 [8d9d72b](https://gitee.com/dotnetchina/Furion/commit/8d9d72b5bfcb8dfc730462c9313266ec0661d561) + +- **其他更改** + + -  调整 定时任务调度器时间精度,控制持续执行一年误差在 `100ms` 以内 4.8.2.9 ⏱️2022.12.01 [334d089](https://gitee.com/dotnetchina/Furion/commit/334d08989503bacd8bf2abb2cc87cf2031dc9da6) + -  调整 定时任务作业计划工厂 `GetNextRunJobs()` 方法逻辑 4.8.2.7 ⏱️2022.11.30 [#I63VS2](https://gitee.com/dotnetchina/Furion/issues/I63VS2) + -  调整 `LoggingMonitor` 解析授权逻辑,如果接口未授权则不打印授权信息 4.8.2.1 ⏱️2022.11.28 [#I63D2E](https://gitee.com/dotnetchina/Furion/issues/I63D2E) + +- **文档** + + -  新增 **[新版本定时任务文档](/docs/job)** + -  新增 **[Cron 表达式解析文档](/docs/cron)** + -  新增 **[任务队列文档](/docs/task-queue)** + -  新增 [Schedular](/docs/global/schedular) 全局静态类文档 + -  新增 [TaskQueued](/docs/global/taskqueued) 全局静态类文档 + -  新增 作业执行器实现超时文档 4.8.3.8 ⏱️2022.12.20 + -  新增 作业触发器 `ResetOnlyOnce` 文档 4.8.1.5 ⏱️2022.11.25 [a8be728](https://gitee.com/dotnetchina/Furion/commit/a8be728eac986ebc5f44718b08c67aaee8b89dc6) + -  新增 **通过 `Roslyn` 动态编译代码创建 `IJob` 类型文档** 4.8.1.5 ⏱️2022.11.25 [2c5e5be](https://gitee.com/dotnetchina/Furion/commit/2c5e5befc7a335d6ef0e75eea0061aee1e4dd061) + -  新增 自定义 `JobDetail` 和 `Trigger` 输出 `SQL` 文档 4.8.2 ⏱️2022.11.27 [0bb9d8f](https://gitee.com/dotnetchina/Furion/commit/0bb9d8f1f3606af145b44c2984b87fdc020f02e1) + -  新增 远程请求 `[QueryString]` 配置时间类型 `Format` 格式化文档 4.8.1.2 ⏱️2022.11.25 [!673](https://gitee.com/dotnetchina/Furion/pulls/673) + -  更新 `Serve.Run()` 入门文档文档、安全授权文档、前端接口代理文档、事件总线文档、日志文档、Worker Service 文档、数据库实体触发器文档、`App` 静态类文档、包管理工具文档 + +--- + +## v4.7.9(已发布,.NET7) + +:::caution .NET7 发布 + +**🚀🎉🔥 2022 年 11 月 08 日,微软发布了 .NET7 首个正式版。** + +`Furion` 第一时间完成了适配,**`v4` 版本开始一套代码支持 `.NET5-.NET7/N`,支持所有 `Furion` 版本升级**。 + +::: + +:::important 版本细节 + +- `v4.7.9` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I60MFK](https://gitee.com/dotnetchina/Furion/issues/I60MFK) 2022.11.11 +- `v4.7.7` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I60GZ8](https://gitee.com/dotnetchina/Furion/issues/I60GZ8) 2022.11.10 +- `v4.7.6` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I60591](https://gitee.com/dotnetchina/Furion/issues/I60591) 2022.11.08 +- `v4.7.5` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I600R4](https://gitee.com/dotnetchina/Furion/issues/I600R4) 2022.11.08 +- `v4.7.3` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I5Z9TI](https://gitee.com/dotnetchina/Furion/issues/I5Z9TI) 2022.11.03 +- `v4.7.2` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I5YG48](https://gitee.com/dotnetchina/Furion/issues/I5YG48) 2022.10.30 +- `v4.7.1` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I5Y6U3](https://gitee.com/dotnetchina/Furion/issues/I5Y6U3) 2022.10.28 +- `v4.7.0` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I5Y04N](https://gitee.com/dotnetchina/Furion/issues/I5Y04N) 2022.10.27 + +::: + +- **新特性** + + -  新增 日志模块时间格式化默认输出 `毫秒` 部分,针对并发比较高的场景 4.7.0 [c0dc36c](https://gitee.com/dotnetchina/Furion/commit/c0dc36c4a2294596cbf3f5cdbdb2589c447bf746) + -  新增 写入数据库日志死循环输出检测机制 4.7.0 [30dea0c](https://gitee.com/dotnetchina/Furion/commit/30dea0cebf8a946bea670fa0d2f82cde8321b847) + -  新增 `LoggingMonitor` 输出 `系统信息`,`.NET 架构` 和 `基础框架` 4.7.1 [aeda902](https://gitee.com/dotnetchina/Furion/commit/aeda90259ef0684bff6308e2f387d89ac9081d37) + -  新增 远程请求 `.SetQueries(obj, ignoreNullValue)` 重载方法 4.7.3 [#I5Z8KC](https://gitee.com/dotnetchina/Furion/issues/I5Z8KC) + -  新增 远程请求 `.GetCookies()` 和 `.GetSetCookies()` 拓展方法 4.7.5 [#I5ZY1L](https://gitee.com/dotnetchina/Furion/issues/I5ZY1L) + -  新增 事件总线 `.ReplaceStorerOrFallback` 自定义事件源存储器方法,可在自定义初始失败时回退到默认值 4.7.6 [#I602NU](https://gitee.com/dotnetchina/Furion/issues/I602NU) + -  新增 `LoggingMonitor` 输出 `启动信息`,`Cookies` 和 `请求端源` 信息 4.7.7 [3037b04](https://gitee.com/dotnetchina/Furion/commit/3037b04cac8cfa70d6e7f1dba62d48362f75e778) + -  新增 **`JSON` 序列化 `DateOnly` 和 `TimeOnly` 类型转换器:`.AddDateOnlyConverters()` 和 `.AddTimeOnlyConverters()`** 4.7.9 [!657](https://gitee.com/dotnetchina/Furion/pulls/657) [47a5fcb](https://gitee.com/dotnetchina/Furion/commit/47a5fcbe9dbcff4fb35addc178d2715dbb3544a2) + -  新增 **`HttpContext.ReadBodyContentAsync()` 拓展方法重复读取 `Body` 内容** 4.7.9 [#I60IYU](https://gitee.com/dotnetchina/Furion/issues/I60IYU) + +- **突破性变化** + + -  新增 **所有脚手架支持 `-f` 指定 `.NET` 版本** 4.7.6 [#I603AZ](https://gitee.com/dotnetchina/Furion/issues/I603AZ) + +
+ 查看变化 +
+ +```bash showLineNumbers {2,5,8} +# 创建 .NET5 版本 +dotnet new furionapi -n 项目名称 -f net5 + +# 创建 .NET6 版本 +dotnet new furionapi -n 项目名称 -f net6 + +# 创建 .NET7 版本 +dotnet new furionapi -n 项目名称 -f net7 +``` + +
+
+ +- -  升级 **适配 `.NET 6.0.11` 和 `.NET 7`** 4.7.5 [7df3195](https://gitee.com/dotnetchina/Furion/commit/7df31951cc565327ae101d416551c6de3afa401b) +- -  升级 **所有脚手架至 `.NET 7`** 4.7.5 [7df3195](https://gitee.com/dotnetchina/Furion/commit/7df31951cc565327ae101d416551c6de3afa401b) +- -  调整 **`LogContext` 类型的所有方法至 `Furion.Logging` 命名空间下,解决空异常问题** 4.7.3 [#I5YOT3](https://gitee.com/dotnetchina/Furion/issues/I5YOT3) + +
+ 查看变化 +
+ +由: + +```cs showLineNumbers +var value = logContext.Get("Key"); // 过去如果 logContext == null 报错 +``` + +改为: + +```cs showLineNumbers {1} +using Furion.Logging; + +var value = logContext.Get("Key"); // 新版本不会报错,且 value = null +``` + +
+
+ +- -  调整 **旧版本定时任务为 `弃用` 状态(一周内发布新版),如需取消警告在 `.csproj` 中添加 `0618`** 4.7.9 [0ff3ac0](https://gitee.com/dotnetchina/Furion/commit/0ff3ac0fcc54c510ce444b80e9f83eb2c81dc9ba) + +- **问题修复** + + -  修复 **生成 `JWT Token` 时间戳和自动刷新逻辑在高并发下检查有效性不够精确问题,原因是时间戳丢掉了毫秒部分** 4.7.0 [3c0c017](https://gitee.com/dotnetchina/Furion/commit/3c0c017ca43c9043bb5806b2951a5bcc7d571142) + -  修复 **在 `IDatabaseLoggingWriter` 实现类中输出日志导致死循环问题** 4.7.0 [30dea0c](https://gitee.com/dotnetchina/Furion/commit/30dea0cebf8a946bea670fa0d2f82cde8321b847) + -  修复 规范化结果 `OnResponseStatusCodes` 方法在 `Response` 已完成写入时设置出现异常问题 4.7.2 [#I5YBHL](https://gitee.com/dotnetchina/Furion/issues/I5YBHL) + -  修复 `L.SetCulture("zh-CN");` 在 `Response` 已完成写入时设置出现异常问题 4.7.2 [#I5YBHL](https://gitee.com/dotnetchina/Furion/issues/I5YBHL) + -  修复 动态 `WebAPI` 在类上配置 `[Route]` 特性且包含 `[action]` 模板导致生成错误接口路径 4.7.2 [#I5YEZQ](https://gitee.com/dotnetchina/Furion/issues/I5YEZQ) + -  修复 启用二级虚拟目录 `AppSettings:VirtualPath` 导致 `swagger` 的 `miniprofile` 加载失败 4.7.3 [#I5Z8RM](https://gitee.com/dotnetchina/Furion/issues/I5Z8RM) + -  修复 `LoggingMonitor` 监听带有 `[FromServices]` 的方法参数或接口类型参数出错 4.7.7 [3037b04](https://gitee.com/dotnetchina/Furion/commit/3037b04cac8cfa70d6e7f1dba62d48362f75e778) + -  修复 `HttpRequest` 通过 `.ReadBodyContentAsync()` 读取不到 `Body` 问题 4.7.9 [#I60IYU](https://gitee.com/dotnetchina/Furion/issues/I60IYU) + +- **其他更改** + + -  更新 `JSON Schema` 配置,新增日志更多参数提醒 4.7.0 [74bee56](https://gitee.com/dotnetchina/Furion/commit/74bee56ce2a6d305c3825d72e42f6dcaff102aaf) + -  调整 日志记录时间格式默认输出带 `7位` 的毫秒值 4.7.1 [aeda902](https://gitee.com/dotnetchina/Furion/commit/aeda90259ef0684bff6308e2f387d89ac9081d37) + -  调整 所有脚手架默认启用 `单文件/独立部署` 配置 4.7.7 [1277f53](https://gitee.com/dotnetchina/Furion/commit/1277f53085833001d241c713b1ec4b8e0a27843e) + +- **文档** + + -  新增 `IIS` 回收问题解决方案文档 + -  新增 远程请求获取 `Cookies` 文档 + -  新增 `LoggingMonitor` 写入数据库文档 + -  新增 JSON 序列化 `DateOnly` 和 `TimeOnly` 类型处理文档 + -  新增 `HttpContext` 读取 `Body` 内容文档 + -  新增 `PM2` 配置文件 `json` 部署文档 + -  更新 日志记录文档、定时任务文档、远程请求文档、脚手架文档 + +- **特别贡献** + + - [@YaChengMu](https://gitee.com/YaChengMu):[!657](https://gitee.com/dotnetchina/Furion/pulls/657) + - [@LiuDanK](https://gitee.com/LiuDanK_admin):[#I60MP2](https://gitee.com/dotnetchina/Furion/issues/I60MP2) + +--- + +## v4.6.9(已发布) + +:::important 版本细节 + +- `v4.6.9` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I5XKW4](https://gitee.com/dotnetchina/Furion/issues/I5XKW4) 2022.10.25 +- `v4.6.8` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I5X2Q0](https://gitee.com/dotnetchina/Furion/issues/I5X2Q0) 2022.10.22 +- `v4.6.7` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I5WQPP](https://gitee.com/dotnetchina/Furion/issues/I5WQPP) 2022.10.20 +- `v4.6.6` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I5WOIV](https://gitee.com/dotnetchina/Furion/issues/I5WOIV) 2022.10.20 +- `v4.6.5` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I5VPD1](https://gitee.com/dotnetchina/Furion/issues/I5VPD1) 2022.10.14 +- `v4.6.4` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I5VIAQ](https://gitee.com/dotnetchina/Furion/issues/I5VIAQ) 2022.10.13 +- `v4.6.3` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I5V99T](https://gitee.com/dotnetchina/Furion/issues/I5V99T) 2022.10.12 +- `v4.6.2` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I5V6UE](https://gitee.com/dotnetchina/Furion/issues/I5V6UE) 2022.10.12 +- `v4.6.1` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I5UYQW](https://gitee.com/dotnetchina/Furion/issues/I5UYQW) 2022.10.11 +- `v4.6.0` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I5UQZ7](https://gitee.com/dotnetchina/Furion/issues/I5UQZ7) 2022.10.10 + +::: + +- **新特性** + + -  新增 `LoggingMonitor` 支持 `FileResult` 类型监听 4.6.0 [bf9c0b1](https://gitee.com/dotnetchina/Furion/commit/bf9c0b1d7c695e800b3fdcca1ba578dd26e88d19) + -  新增 `LogMessage` 结构 `UseUtcTimestamp` 字段,解释日志记录时间格式是 `UTC` 还是 `LOCAL` 时间 4.6.1 [aab0371](https://gitee.com/dotnetchina/Furion/commit/aab0371d6473117e1e602eef1b9841c48d184e84) + -  新增 事件总线模块重试失败后支持回调 4.6.1 [#I5UVMV](https://gitee.com/dotnetchina/Furion/issues/I5UVMV) + -  新增 `LoggingMonitor` 支持序列化忽略指定属性名或属性类型 4.6.1 [81c6343](https://gitee.com/dotnetchina/Furion/commit/81c6343a2ddb3751e72de7c5943048c903c638b4) + -  新增 **`long` 序列化丢精度的 `JsonConvert` 内置转换器,`.AddLongTypeConverters()`** 4.6.5 [#I5VJHC](https://gitee.com/dotnetchina/Furion/issues/I5VJHC) [aded58d](https://gitee.com/dotnetchina/Furion/commit/aded58d0bc587d1a1844382c66ec1ab3de96be7c) + -  新增 `app.EnableBuffering()` 拓展,解决 `Request.Body` 不能重复读问题 4.6.5 [aded58d](https://gitee.com/dotnetchina/Furion/commit/aded58d0bc587d1a1844382c66ec1ab3de96be7c) + -  新增 **支持特别接口使用特定的序列化规则** 4.6.6 [797b0bf](https://gitee.com/dotnetchina/Furion/commit/797b0bfe7c702877b1e0f56ee1a97d91fc80c551) + -  新增 `LoggingMonitor` 自动解析 `JWT` 时间戳为时间格式 4.6.8 [9e31b0b](https://gitee.com/dotnetchina/Furion/commit/9e31b0b0cf9adc5fbbf92efd67c781ba8d003cc9) + +- **突破性变化** + + -  升级 **适配 `.NET 6.0.10` 和 `.NET 7 RC2`** 4.6.2 [6bb2fad](https://gitee.com/dotnetchina/Furion/commit/6bb2fadd6600edcebc1539faedba98d4db0e189e) + -  新增 内置 `Microsoft.AspNetCore.Mvc.NewtonsoftJson` 拓展,原因是太多人使用了 4.6.5 [aded58d](https://gitee.com/dotnetchina/Furion/commit/aded58d0bc587d1a1844382c66ec1ab3de96be7c) + -  移除 ~~`"some log".SetCategory(name)` 拓展方法~~ 4.6.0 [ec4838c](https://gitee.com/dotnetchina/Furion/commit/ec4838c218ed9c96ba92d7a80fd11539da59ca22) + -  移除 ~~`DateOnlyJsonConverter` 和 `DateOnlyOffsetJsonConverter` 处理~~ 4.6.5 [aded58d](https://gitee.com/dotnetchina/Furion/commit/aded58d0bc587d1a1844382c66ec1ab3de96be7c) + -  调整 **事件总线触发处理程序的逻辑,由过去的 `foreach` 改为 `Parallel.ForEach`,吞吐量提升近 4 倍** 4.6.4 [7384c9c](https://gitee.com/dotnetchina/Furion/commit/7384c9c3efb94883421379828565e61870f1640c) + -  调整 **~~`.AddDateFormatString()`~~ 名称为 `.AddDateTimeTypeConverters()`** 4.6.5 [aded58d](https://gitee.com/dotnetchina/Furion/commit/aded58d0bc587d1a1844382c66ec1ab3de96be7c) + -  调整 **重构日志模块设置上下文数据功能** 4.6.0 [1c198ee](https://gitee.com/dotnetchina/Furion/commit/1c198ee2701e0ca76bca1057df6fce6789344c35) + +
+ 查看变化 +
+ +由于过去版本设置日志上下文有多线程异常和堆内存溢出风险,所以重新设计了日志上下文的写法。 + +由: + +```cs showLineNumbers {1} +_logger.ScopeContext(ctx => ctx.Set("Name", "Furion").Set("UserId", 10)) + .LogInformation("我是一个日志 {id}", 20); +``` + +改为: + +```cs showLineNumbers {1,7} +using (var scope = _logger.ScopeContext(ctx => ctx.Set("Name", "Furion").Set("UserId", 10))) +{ + _logger.LogInformation("我是一个日志 {id}", 20); +} + +// 也可以简写 +using var scope = _logger.ScopeContext(ctx => ctx.Set("Name", "Furion").Set("UserId", 10)); +_logger.LogInformation("我是一个日志 {id}", 20); +``` + +
+
+ +- -  移除 ~~**远程请求 `.SetTimeout` 和 `[Timeout]` 配置方法,采用全局统一配置**~~ 4.6.4 [7384c9c](https://gitee.com/dotnetchina/Furion/commit/7384c9c3efb94883421379828565e61870f1640c) + +
+ 查看变化 +
+ +默认情况下,`HttpClient` 请求超时时间为 `100秒`,可根据实际情况进行设置: + +```cs showLineNumbers {4,10} +// 配置默认 HttpClient +options.AddHttpClient(string.Empty, c => +{ + c.Timeout = TimeSpan.FromMinutes(2); +}); + +// 配置特定客户端 +options.AddHttpClient("github", c => +{ + c.Timeout = TimeSpan.FromMinutes(2); +}); +``` + +
+
+ +- **问题修复** + + -  修复 `4.5.9+` 版本新增的 `IncludeScopes` 配置导致日志上下文失效 4.6.0 [4a76841](https://gitee.com/dotnetchina/Furion/commit/4a768415bb2effe47f1a6e12de0773d3bbd6f75c) + -  修复 多个 `sql` 共用 `DbParameters` 出现冲突问题 4.6.0 [#I5UO2H](https://gitee.com/dotnetchina/Furion/issues/I5UO2H) + -  修复 高频率写入日志导致堆内存溢出的异常问题 4.6.0 [#I5UJRS](https://gitee.com/dotnetchina/Furion/issues/I5UJRS) + -  修复 框架内部所有使用 `.CreateLogger` 创建的日志对象无法应用上下文问题 4.6.0 [ec4838c](https://gitee.com/dotnetchina/Furion/commit/ec4838c218ed9c96ba92d7a80fd11539da59ca22) + -  修复 远程请求不能在 `Worker Serivce` 中进行构造函数注入,原因是注册为 `Scope` 范围作用域 4.6.3 [974f835](https://gitee.com/dotnetchina/Furion/commit/974f835ccdcf84bc84dd6f7044589ae2a2e7a57b) + -  修复 **个别服务器的 `SQL Server` 不支持 `TLS 1.2` 协议问题** 4.6.3 [974f835](https://gitee.com/dotnetchina/Furion/commit/974f835ccdcf84bc84dd6f7044589ae2a2e7a57b) + -  修复 `.ToDictionary()` 拓展不支持 `JObject` 类型问题 4.6.5 [#I5VJHC](https://gitee.com/dotnetchina/Furion/issues/I5VJHC) [a11bf8d](https://github.com/MonkSoul/Furion/commit/a11bf8d8b6bb90b41b8394d8bca35aa3539239e6) + -  修复 `LoggingMonitor` 处理 `long` 类型丢精度问题 4.6.5 [#I5VJHC](https://gitee.com/dotnetchina/Furion/issues/I5VJHC) [aded58d](https://gitee.com/dotnetchina/Furion/commit/aded58d0bc587d1a1844382c66ec1ab3de96be7c) + -  修复 动态 `WebAPI` 在 `class` 类型上贴 `[ApiDescriptionSettings(false)]` 导致接口 `404` 问题 4.6.7 [#I5WQ18](https://gitee.com/dotnetchina/Furion/issues/I5WQ18) + -  修复 超高频率下发送事件总线消息,但是 `GC` 来不及回收导致内存和 `CPU` 爆掉问题 4.6.8 [dbc7935](https://gitee.com/dotnetchina/Furion/commit/dbc7935618ff60d7f41d82c0d49042cd189e58b9) + -  修复 **`JWT` 模块自动刷新 `Token` 达到临界值时导致自动刷新失败,并返回错误的 `401` 状态码** 4.6.8 [#I5WXHZ](https://gitee.com/dotnetchina/Furion/issues/I5WXHZ) + -  修复 自动生成 `vue/react/angular` 客户端工具库错误处理 `Token` 问题 4.6.8 [#I5WXHZ](https://gitee.com/dotnetchina/Furion/issues/I5WXHZ) + -  修复 远程请求没有正确处理 `数组和集合` 类型的 `url` 参数 4.6.9 [#I5XIQ4](https://gitee.com/dotnetchina/Furion/issues/I5XIQ4) + -  修复 自定义 `Tenant` 实体且包含 `TenantId` 属性且没有继承 `EntityBase/Entity` 基类出现 `The entity type 'Tenant' requires a primary key to be defined` 4.6.9 [#I4UM3E](https://gitee.com/dotnetchina/Furion/issues/I4UM3E) + +- **其他更改** + + -  调整 `LoggingMonitor` 返回值类型是 `泛型` 时获取 `FullName` 带程序集签名问题 4.6.2 [f0aaec6](https://gitee.com/dotnetchina/Furion/commit/f0aaec6a5ae358bab7f4b6ed58848de8a7f32f5e) + -  调整 优化远程请求性能,添加复用池的机制,避免频繁销毁创建 4.6.4 [7384c9c](https://gitee.com/dotnetchina/Furion/commit/7384c9c3efb94883421379828565e61870f1640c) + +- **文档** + + -  新增 远程请求设置客户端生命周期配置文档和新超时配置文档 + -  新增 `JSON` 序列化处理 `long` 类型说明文档 + -  新增 `JSON` 反序列化 `DateTimeOffset` 类型个别格式出错问题解决方案文档 + -  新增 `Worker Service` 实现 `串行` 操作文档 + -  新增 关闭 `.NET Core` 底层日志和远程请求日志文档 + -  新增 规范化结果支持特定接口配置独立序列化配置文档 + -  更新 日志记录文档、事件总线文档、数据库入门文档、`JSON` 序列化文档、远程请求文档、安全授权文档、生成前端请求代理文档 + +--- + +## v4.5.9(已发布) + +:::important 版本细节 + +- `v4.5.9` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I5ULWN](https://gitee.com/dotnetchina/Furion/issues/I5ULWN) 2022.10.09 +- `v4.5.8` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I5UGNS](https://gitee.com/dotnetchina/Furion/issues/I5UGNS) 2022.10.08 +- `v4.5.7` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I5U8Q9](https://gitee.com/dotnetchina/Furion/issues/I5U8Q9) 2022.10.06 +- `v4.5.6` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I5U4SG](https://gitee.com/dotnetchina/Furion/issues/I5U4SG) 2022.10.03 +- `v4.5.5` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I5U413](https://gitee.com/dotnetchina/Furion/issues/I5U413) 2022.10.02 +- `v4.5.4` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I5U3MK](https://gitee.com/dotnetchina/Furion/issues/I5U3MK) 2022.10.01 +- `v4.5.2` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I5TXON](https://gitee.com/dotnetchina/Furion/issues/I5TXON) 2022.09.30 +- `v4.5.1` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I5TLI6](https://gitee.com/dotnetchina/Furion/issues/I5TLI6) 2022.09.28 +- `v4.5.0` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I5TD4X](https://gitee.com/dotnetchina/Furion/issues/I5TD4X) 2022.09.27 + +::: + +- **新特性** + + -  新增 `.AddConsoleFormatter()` 拓展简化控制台日志模板配置 4.5.0 [#I5TCMO](https://gitee.com/dotnetchina/Furion/issues/I5TCMO) + -  新增 控制台和文件日志时间默认显示 `星期几` 4.5.1 [#I5TKL5](https://gitee.com/dotnetchina/Furion/issues/I5TKL5) + -  新增 控制台和文件日志支持配置 `options.DateFormat` 日期格式化 4.5.1 [#I5TKL5](https://gitee.com/dotnetchina/Furion/issues/I5TKL5) + -  新增 控制台日志带颜色输出,比如高亮 `日志级别` 4.5.1 [#I5TKL5](https://gitee.com/dotnetchina/Furion/issues/I5TKL5) + -  新增 控制台格式化配置 `options.WriteHandler` 完全自定义配置 4.5.2 [7fb3036](https://gitee.com/dotnetchina/Furion/commit/7fb30368f9640360017fc7f357df13eeb5031c2b) + -  新增 **日志输出 `JSON` 格式化配置** 4.5.2 [#I5TWC1](https://gitee.com/dotnetchina/Furion/issues/I5TWC1) [#I5OUT1](https://gitee.com/dotnetchina/Furion/issues/I5OUT1) + -  新增 数据库日志写入独立日志模板配置、独立日期格式配置 4.5.2 [#I5TWC1](https://gitee.com/dotnetchina/Furion/issues/I5TWC1) + -  新增 `LogMessage` 结构类 `LogDateTime`,`ThreadId`,`State` 属性 4.5.2 [#I5TWC1](https://gitee.com/dotnetchina/Furion/issues/I5TWC1) + -  新增 `LoggingMonitor` 可配置 `JsonWriterOptions` 属性 4.5.4 [#I5U375](https://gitee.com/dotnetchina/Furion/issues/I5U375) + -  新增 `Log.ScopeContext` 和 `"some log".ScopeContext` 拓展 4.5.4 [8129693](https://gitee.com/dotnetchina/Furion/commit/812969357a60d0b73becf105e6b77b46113b3800) + -  新增 `HttpContext.SetTokensOfResponseHeaders` 拓展 4.5.7 [3775e65](https://gitee.com/dotnetchina/Furion/commit/3775e659ca13615bc7f65b050fb8540daeed9433) + -  新增 新增远程请求支持 `Stream` 文件格式上传 4.5.8 [#I5UF3I](https://gitee.com/dotnetchina/Furion/issues/I5UF3I) + -  新增 日志模块可配置是否启用上下文功能 `IncludeScopes` 属性 4.5.9 [#I5UJRS](https://gitee.com/dotnetchina/Furion/issues/I5UJRS) + -  新增 `LoggingMonitor` 日志筛选 `WriteFilter` 配置 4.5.9 [6f06f12](https://gitee.com/dotnetchina/Furion/commit/6f06f12b2c115bd181f4832daa7537efe5cb3fe8) + +- **突破性变化** + + -  调整 **😊 `Furion` 框架文档地址为 [http://furion.baiqian.ltd](http://furion.baiqian.ltd)** 4.5.4 [2e3d80e](https://gitee.com/dotnetchina/Furion/commit/2e3d80ec0be830c33923ef9953ff504528120f06) + -  调整 `LoggingMonitor` 底层逻辑,移除原来的 `.ScopeContext` 存储监听信息设计 4.5.2 [#I5TWC1](https://gitee.com/dotnetchina/Furion/issues/I5TWC1) + -  移除 ~~**主机未启动时构建服务的操作权限,此操作会导致内存激增,受影响方法: `App.GetOptions` 系列和 `App.GetService` 和 `Scoped.Create`**~~ 4.5.4 [#I5U0A4](https://gitee.com/dotnetchina/Furion/issues/I5U0A4) [8129693](https://gitee.com/dotnetchina/Furion/commit/812969357a60d0b73becf105e6b77b46113b3800) + +
+ 查看变化 +
+ +近期发现许多开发者在主机还未启动时解析服务,这是非常不正确的行为,会导致启动时内存激增甚至溢出,常见的错误有: + +- 在启动的时候通过 `Scoped.Create` 创建作用域 +- **在启动的时候通过 `App.GetOptions` 获取选项对象** +- 在启动的时候通过 `App.GetService` 解析服务 + +正确的做法是,启动的时候禁止使用 `Scoped.Create` 和 `App.GetService`。 + +**如需启动时获取配置应该通过:`App.GetConfig("配置节点", true)` 替代 `App.GetOptions()`**。 + +
+
+ +- **问题修复** + + -  修复 字符串日志拓展带泛型方法不能正确显示 `CategoryName` 日志类别 4.5.0 [#I5TBKL](https://gitee.com/dotnetchina/Furion/issues/I5TBKL) + -  修复 控制台日志设置了 `.ScopeContext` 无效问题 4.5.2 [7fb3036](https://gitee.com/dotnetchina/Furion/commit/7fb30368f9640360017fc7f357df13eeb5031c2b) + -  修复 `LoggingMonitor` 同时配置了局部和全局日志监听触发两次问题 4.5.2 [a1a97e8](https://gitee.com/dotnetchina/Furion/commit/a1a97e817c43db4c20ba6e3aae03cea96d545a4b) + -  修复 `v4.4.8+` 版本更新导致远程请求在个别情况下出现并发问题 4.5.2 [#I5TWL3](https://gitee.com/dotnetchina/Furion/issues/I5TWL3) + -  修复 `LoggingMonitor` 配置了 `ReturnValueThreshold` 之后 `Json` 被截断引发有效性检测异常 4.5.4 [#I5U375](https://gitee.com/dotnetchina/Furion/issues/I5U375) + -  修复 `LoggingMonitor` 不支持 `DataTable`,`DataSet`,`Tuple` 等类型问题 4.5.5 [#I5U3VO](https://gitee.com/dotnetchina/Furion/issues/I5U3VO) + -  修复 **自 `v4.5.2+` 版本升级后出现启动时使用 `App.GetOptons` 异常问题** 4.5.6 [#I5U4OC](https://gitee.com/dotnetchina/Furion/issues/I5U4OC) [f9a6587](https://gitee.com/dotnetchina/Furion/commit/f9a6587e3f7893db88c905cf77c3c4ebed39b73c) + -  修复 `app.UseInject(action)` 导致死循环 4.5.7 [!608](https://gitee.com/dotnetchina/Furion/pulls/608) + -  修复 `LoggingMonitor` 报空引用异常问题 4.5.8 [#I5UGCA](https://gitee.com/dotnetchina/Furion/issues/I5UGCA) [!610](https://gitee.com/dotnetchina/Furion/pulls/610) + -  修复 并发情况下设置日志上下文出现偶然性空引用问题 4.5.9 [#I5UJRS](https://gitee.com/dotnetchina/Furion/issues/I5UJRS) + +- **其他更改** + + -  调整 文件日志默认模板,默认对日志时间进行格式化并显示星期几 4.5.1 [#I5TKL5](https://gitee.com/dotnetchina/Furion/issues/I5TKL5) + -  调整 脚手架代码,默认启用 `services.AddConsoleFormatter()` 4.5.1 [#I5TLI6](https://gitee.com/dotnetchina/Furion/issues/I5TLI6) + -  调整 `Serve.Run()` 代码,默认启用 `services.AddConsoleFormatter()` 4.5.1 [#I5TLI6](https://gitee.com/dotnetchina/Furion/issues/I5TLI6) + -  调整 减少 `MiniProfile` 不必要的监听,只在 `Swagger` 页面请求才监听 4.5.7 [697ef51](https://gitee.com/dotnetchina/Furion/commit/697ef51f4869a4d533a6debd2a17cb9f0c1000c3) + -  调整 日志模块所有日志时间默认为 `24小时制`,过去是 `12小时制` 4.5.9 [!612](https://gitee.com/dotnetchina/Furion/pulls/612) + +- **文档** + + -  新增 选项监听出现触发多次的解决方案 [#I5T9PR](https://gitee.com/dotnetchina/Furion/issues/I5T9PR) + -  更新 日志记录文档、动态 WebAPI 文档、选项文档、`HttpContext` 文档、远程请求文档 + +- **本期亮点** + +- 1. **支持日志配置 `JSON` 格式化输出** + +
+ 查看变化 +
+ +```cs showLineNumbers {2,4,8,10,14,16} +// 控制台 +services.AddConsoleFormatter(options => +{ + options.MessageFormat = LoggerFormatter.Json; +}); + +// 文件 +services.AddFileLogging("mytemplate.log", options => +{ + options.MessageFormat = LoggerFormatter.Json; +}); + +// 数据库 +services.AddDatabaseLogging(options => +{ + options.MessageFormat = LoggerFormatter.Json; +}); +``` + +
+
+ +- 2. **支持 `LoggingMonitor` 输出 `JSON` 格式** + +
+ 查看变化 +
+ +1. 全局/局部启用 `Json` 输出配置 + +```cs showLineNumbers {4,8} +// 全局 +services.AddMonitorLogging(options => +{ + options.JsonBehavior = Furion.Logging.JsonBehavior.OnlyJson; +}); + +// 局部 +[LoggingMonitor(JsonBehavior = Furion.Logging.JsonBehavior.OnlyJson)] +``` + +:::note 关于 `JsonBehavior` + +只有设置为 `JsonBehavior.OnlyJson` 时才不会输出**美观的**日志。 + +::: + +2. 写入存储介质 + +```cs showLineNumbers {14-18} +using Furion.Logging; + +namespace Your.Core; + +public class DatabaseLoggingWriter : IDatabaseLoggingWriter +{ + // 支持构造函数注入任何实例,会自动释放任何服务,比如注入 IRepository,或者 SqlSugarClient + public DatabaseLoggingWriter() + { + } + + public void Write(LogMessage logMsg, bool flush) + { + // 如果 JsonBehavior 配置为 OnlyJson 或者 All,那么 Context 就包含 loggingMonitor 的值 + // 如果 JsonBehavior 配置为 OnlyJson,那么可直接通过 logMsg.Message 获取结果就是 json 格式 + if (logMsg.LogName == "System.Logging.LoggingMonitor") + { + var jsonString = logMsg.Context.Get("loggingMonitor"); + } + + // 这里写你任何插入数据库的操作,无需 try catch + } +} +``` + +`Json` 输出格式如下: + +```json showLineNumbers +{ + "controllerName": "test-logger", + "controllerTypeName": "TestLoggerServices", + "actionName": "person", + "actionTypeName": "GetPerson", + "areaName": null, + "displayName": "Furion.Application.TestLoggerServices.GetPerson (Furion.Application)", + "localIPv4": "0.0.0.1", + "remoteIPv4": "0.0.0.1", + "httpMethod": "GET", + "requestUrl": "https://localhost:5001/api/test-logger/person/2", + "refererUrl": "https://localhost:5001/api/index.html?urls.primaryName=数据库操作演示", + "environment": "Development", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36 Edg/105.0.1343.53", + "requestHeaderAuthorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEsIkFjY291bnQiOiJhZG1pbiIsImlhdCI6MTY2NDQ1MDUwNSwibmJmIjoxNjY0NDUwNTA1LCJleHAiOjE2NjQ0NTE3MDUsImlzcyI6ImRvdG5ldGNoaW5hIiwiYXVkIjoicG93ZXJieSBGdXJpb24ifQ.-xocNcDQGoXClceoVU5QAHIkTcOZ7ZXo0hEbzghDfFI", + "timeOperationElapsedMilliseconds": 55, + "authorizationClaims": [ + { + "type": "UserId", + "valueType": "integer", + "value": "1" + }, + { + "type": "Account", + "valueType": "string", + "value": "admin" + }, + { + "type": "iat", + "valueType": "integer", + "value": "1664450505" + }, + { + "type": "nbf", + "valueType": "integer", + "value": "1664450505" + }, + { + "type": "exp", + "valueType": "integer", + "value": "1664451705" + }, + { + "type": "iss", + "valueType": "string", + "value": "dotnetchina" + }, + { + "type": "aud", + "valueType": "string", + "value": "powerby Furion" + } + ], + "parameters": [ + { + "name": "id", + "type": "System.Int32", + "value": 2 + } + ], + "returnInformation": { + "type": "Furion.UnifyResult.RESTfulResult`1[[System.Object, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]", + "actType": "Furion.Application.Persons.PersonDto", + "value": { + "StatusCode": 200, + "Data": { + "Id": 2, + "Name": null, + "Age": 0, + "Address": null, + "PhoneNumber": null, + "QQ": null, + "CreatedTime": "0001-01-01T00:00:00+00:00", + "Childrens": null, + "Posts": null + }, + "Succeeded": true, + "Errors": null, + "Extras": null, + "Timestamp": 1664450517341 + } + }, + "exception": { + "type": "System.DivideByZeroException", + "message": "Attempted to divide by zero.", + "stackTrace": " at Furion.Application.TestLoggerServices.测试日志监听8(Int32 id) in D:\\Workplaces\\OpenSources\\Furion\\samples\\Furion.Application\\TestLoggerServices.cs:line 78\r\n at lambda_method103(Closure , Object , Object[] )\r\n at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncObjectResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)\r\n at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.g__Logged|12_1(ControllerActionInvoker invoker)\r\n at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)" + }, + "validation": { + "errorCode": null, + "originErrorCode": null, + "message": "出错了啊。。。。" + } +} +``` + +
+
+ +- 3. **支持远程请求上传文件 `Stream` 流** + +
+ 查看变化 +
+ +1. 单文件上传 + +- 字符串方式 + +```cs showLineNumbers {1,5,9} +var fileStream = new FileStream("image.png", FileMode.Open); + +var result = await "https://localhost:44316/api/test-module/upload-file" + .SetContentType("multipart/form-data") + .SetFiles(HttpFile.Create("file", fileStream, "image.png")).PostAsync(); + +var fileName = await result.Content.ReadAsStringAsync(); + +await fileStream.DisposeAsync(); +``` + +- 代理方式 + +```cs showLineNumbers {1,3,6} +var fileStream = new FileStream("image.png", FileMode.Open); + +var result = await _http.TestSingleFileProxyAsync(HttpFile.Create("file", fileStream, "image.png")); +var fileName = await result.Content.ReadAsStringAsync(); + +await fileStream.DisposeAsync(); +``` + +2. 多文件上传 + +- 字符串方式 + +```cs showLineNumbers {1,5,8} +var fileStream = new FileStream("image.png", FileMode.Open); + +var result = await "https://localhost:44316/api/test-module/upload-muliti-file" + .SetContentType("multipart/form-data") + .SetFiles(HttpFile.CreateMultiple("files", (fileStream, "image1.png"), (fileStream, "image2.png"))).PostAsync(); +var fileName = await result.Content.ReadAsStringAsync(); + +await fileStream.DisposeAsync(); +``` + +- 代理方式 + +```cs showLineNumbers {1,3,6} +var fileStream = new FileStream("image.png", FileMode.Open); + +var result = await _http.TestMultiFileProxyAsync(HttpFile.CreateMultiple("files", (fileStream, "image1.png"), (fileStream, "image2.png"))); +var fileName = await result.Content.ReadAsStringAsync(); + +await fileStream.DisposeAsync(); +``` + +3. 还支持 `Bytes` 和 `Stream` 混合 + +```cs showLineNumbers {1-2,4-10,14,18} +var fileStream = new FileStream("image.png", FileMode.Open); +var bytes = File.ReadAllBytes("image.png"); + +var httpFile = new HttpFile +{ + Name = name, + Bytes = bytes, + FileStream = fileStream, + FileName = fileName +}; + +var result = await "https://localhost:44316/api/test-module/upload-file" + .SetContentType("multipart/form-data") + .SetFiles(httpFile).PostAsync(); + +var fileName = await result.Content.ReadAsStringAsync(); + +await fileStream.DisposeAsync(); +``` + +
+
+ +- 4. **`LoggingMonitor` 全局过滤** + +
+ 查看变化 +
+ +```cs showLineNumbers showLineNumbers {1,3,10} +services.AddMonitorLogging(options => +{ + options.WriteFilter = (context) => + { + // 获取控制器/操作描述器 + var controllerActionDescriptor = context.ActionDescriptor as ControllerActionDescriptor; + + // 你的逻辑....,不需要拦截返回 false,否则 true + + return true; + }; +}); +``` + +
+
+ +--- + +## v4.4.9(已发布) + +:::important 版本细节 + +- `v4.4.9` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I5SP37](https://gitee.com/dotnetchina/Furion/issues/I5SP37) 2022.09.23 +- `v4.4.8` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I5SKUE](https://gitee.com/dotnetchina/Furion/issues/I5SKUE) 2022.09.22 +- `v4.4.7` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I5SEFE](https://gitee.com/dotnetchina/Furion/issues/I5SEFE) 2022.09.21 +- `v4.4.6` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I5RSFD](https://gitee.com/dotnetchina/Furion/issues/I5RSFD) 2022.09.19 +- `v4.4.5` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I5RHQX](https://gitee.com/dotnetchina/Furion/issues/I5RHQX) 2022.09.16 +- `v4.4.4` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I5R5TI](https://gitee.com/dotnetchina/Furion/issues/I5R5TI) 2022.09.15 +- `v4.4.3` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I5QVH3](https://gitee.com/dotnetchina/Furion/issues/I5QVH3) 2022.09.13 +- `v4.4.2` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I5QDHX](https://gitee.com/dotnetchina/Furion/issues/I5QDHX) 2022.09.08 +- `v4.4.1` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I5Q3SX](https://gitee.com/dotnetchina/Furion/issues/I5Q3SX) 2022.09.07 +- `v4.4.0` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I5PQHR](https://gitee.com/dotnetchina/Furion/issues/I5PQHR) 2022.09.05 + +::: + +- **新特性** + + -  新增 友好异常可控制是否输出错误日志配置 `LogError: true` 4.4.0 [#I5PKJH](https://gitee.com/dotnetchina/Furion/issues/I5PKJH) + -  新增 `DateOnlyJsonConverter` 和 `DateOnlyOffsetJsonConverter` 序列化转换器 [!565](https://gitee.com/dotnetchina/Furion/pulls/565) + -  新增 事件总线 `LogEnabled` 配置,可控制是否输出服务日志 [#I5QLY5](https://gitee.com/dotnetchina/Furion/issues/I5QLY5) + -  新增 **可实现任何多套规范化结果功能,支持特定控制器,特定方法** [#I5QZ37](https://gitee.com/dotnetchina/Furion/issues/I5QZ37) + -  新增 `ILoggerFactory` 日志工厂动态批量添加文件日志拓展 [#I5R9PO](https://gitee.com/dotnetchina/Furion/issues/I5R9PO) + -  新增 `App.GetCommandLineConfiguration(args)` 解析命令行参数静态方法 [803542c](https://gitee.com/dotnetchina/Furion/commit/803542c3e21496e92d2bf83aaa2d00831fcb09bc) + -  新增 `Sql` 代理支持返回受影响行数 [#I5REJ9](https://gitee.com/dotnetchina/Furion/issues/I5REJ9) + -  新增 **任意自定义日志文件名支持滚动日志删除功能** [#I5RFBQ](https://gitee.com/dotnetchina/Furion/issues/I5RFBQ) + -  新增 `.pcd` 图片类型 `MIME` 为 `image/x-photo-cd` 支持 [5fafc84](https://gitee.com/dotnetchina/Furion/commit/5fafc8477a4d213d16db92cf030f409c758fab95) + -  新增 默认日志输出当前线程 `Environment.CurrentManagedThreadId` [b8fe2cd](https://gitee.com/dotnetchina/Furion/commit/b8fe2cdc49d1bd11e38ad37fa512acafc3d96417) + -  新增 `app.UseInject(Action)` 重载方法,简化配置 4.4.8 [0b645fe](https://gitee.com/dotnetchina/Furion/commit/0b645fede0ad81c4779a8b9b4b16d9c5d60c9662) + +- **突破性变化** + + -  调整 框架适配 **`.NET 6.0.9` 和 `.NET 7.0 RC1`** [be5b40](https://gitee.com/dotnetchina/Furion/commit/be5b4098bae2153f8d49cf9797e454afde0d0aab) [1eee77b](https://gitee.com/dotnetchina/Furion/commit/1eee77bff0954336dcc5402a09a3195667bb80f2) + -  调整 远程请求 `.SetBodyBytes` 为 `.SetFiles` [#I5PMS5](https://gitee.com/dotnetchina/Furion/issues/I5PMS5) [#I5PIYI](https://gitee.com/dotnetchina/Furion/issues/I5PIYI) + -  调整 `FS.InitialContentTypeProvider()` 名称为 `FS.GetFileExtensionContentTypeProvider()` [5fafc84](https://gitee.com/dotnetchina/Furion/commit/5fafc8477a4d213d16db92cf030f409c758fab95) + -  移除 ~~远程请求 `[BodyBytes]` 设计,采用 `HttpFile` 方式~~ [#I5PMS5](https://gitee.com/dotnetchina/Furion/issues/I5PMS5) [#I5PIYI](https://gitee.com/dotnetchina/Furion/issues/I5PIYI) + +
+ 查看变化 +
+ +```cs showLineNumbers {3-4,7-8,11-12} +public interface IHttp : IHttpDispatchProxy +{ + [Post("http://furion.baiqian.ltd/upload", ContentType = "multipart/form-data")] + Task PostXXXAsync(HttpFile file); + + // 支持多个文件 + [Post("http://furion.baiqian.ltd/upload", ContentType = "multipart/form-data")] + Task PostXXXAsync(HttpFile[] files); + + // 支持多个文件 + [Post("http://furion.baiqian.ltd/upload", ContentType = "multipart/form-data")] + Task PostXXXAsync(IList files); +} +``` + +```cs showLineNumbers {3,7} +// bytes 可以通过 File.ReadAllBytes(文件路径) 获取 +var res = await "http://furion.baiqian.ltd/upload".SetContentType("multipart/form-data") + .SetFiles(HttpFile.Create("file", bytes, "image.png")).PostAsync(); + +// 支持多个文件 +var res = await "http://furion.baiqian.ltd/upload".SetContentType("multipart/form-data") + .SetFiles(HttpFile.CreateMultiple("files", (bytes, "image1.png"), (bytes, "image2.png"))).PostAsync(); +``` + +
+
+ +- -  调整 所有的 `AddInject` 和 `UseInject` 参数设计 [#I5QCF0](https://gitee.com/dotnetchina/Furion/issues/I5QCF0) + +
+ 查看变化 +
+ +```cs showLineNumbers {3,5} +public void ConfigureServices(IServiceCollection services) +{ + services.AddInject(options => + { + options.ConfigureSwaggerGen(gen => + { + // ... + }); + }); +} +``` + +```cs showLineNumbers {3,5,10} + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseInject(configure: options => + { + options.ConfigureSwagger(swg => + { + // ... + }); + + options.ConfigureSwaggerUI(ui => + { + // ... + }); + }); +} +``` + +
+
+ +- -  调整 **远程请求所有 `xxxAsStreamAsync` 返回值** [#I5QVEB](https://gitee.com/dotnetchina/Furion/issues/I5QVEB) + +
+ 查看变化 +
+ +由: + +```cs showLineNumbers +var stream = await "http://furion.baiqian.ltd/".GetAsStreamAsync(); +``` + +改为: + +```cs showLineNumbers +var (stream, encoding) = await "http://furion.baiqian.ltd/".GetAsStreamAsync(); +``` + +
+
+ +- -  新增 调整 **`.Inject()` 支持配置更多参数,开放底层更多权限** 4.4.9 [1182283](https://gitee.com/dotnetchina/Furion/commit/1182283f6303b2d73ae4c82cdbc8b5b705d90030) + +
+ 查看变化 +
+ +```cs showLineNumbers {1} +.Inject((builder, options) => { + options.ConfigureAppConfiguration((context, config) => + { + + }); + options.ConfigureServices((context, services) => + { + + }); +}); +``` + +
+
+ +- **问题修复** + + -  修复 远程请求代理模式非泛型参数导致数组溢出问题 [#I5Q3SN](https://gitee.com/dotnetchina/Furion/issues/I5Q3SN) + -  修复 `LoggingMonitor` 客户端 `IP` 记录错误 [#I5QCU1](https://gitee.com/dotnetchina/Furion/issues/I5QCU1) [!562](https://gitee.com/dotnetchina/Furion/pulls/562) + -  修复 远程请求响应报文中包含 `charset=gbk` 进行序列化后乱码问题 [#I5QVEB](https://gitee.com/dotnetchina/Furion/issues/I5QVEB) + -  修复 文件日志断电时丢失日志问题 [ db7d51b](https://gitee.com/dotnetchina/Furion/commit/db7d51bba569001bc363727a6683ab3f31c3fc1d) + -  修复 动态 `WebAPI` 或控制台贴了 `[ApiDescriptionSettings(Tag = "")]` 标签之后导致注释丢失 [#I5REVF](https://gitee.com/dotnetchina/Furion/issues/I5REVF) [#I5RE4J](https://gitee.com/dotnetchina/Furion/issues/I5RE4J) + -  修复 **启用数据库日志但是没有配置配置文件出现空异常问题** [33817be](https://gitee.com/dotnetchina/Furion/commit/33817bed9d47c85a57c0198c0819ad5cf1c41d0b) + -  修复 **控制台日志过滤无法过滤默认主机日志问题** [33817be](https://gitee.com/dotnetchina/Furion/commit/33817bed9d47c85a57c0198c0819ad5cf1c41d0b) + -  修复 **脚手架错误的日志配置问题** [33817be](https://gitee.com/dotnetchina/Furion/commit/33817bed9d47c85a57c0198c0819ad5cf1c41d0b) + -  修复 **高频压测情况下写日志并设置日志上下文导致并发更新出现 `System.AggregateException` 异常问题** [#I5RFBQ](https://gitee.com/dotnetchina/Furion/issues/I5RFBQ) + -  修复 **日志文件名因 `Windows` 和 `Linux` 路径分隔符不一致导致日志文件创建失败问题,`Linux` 只支持 `/` 不支持 `\`** [#I5RFBQ](https://gitee.com/dotnetchina/Furion/issues/I5RFBQ) + -  修复 `Oops.Oh/Bah` 设置 `.WithData` 之后无效问题 [!580](https://gitee.com/dotnetchina/Furion/pulls/580) + -  修复 基于 `Redis` 重写事件存储器序列化 `IEventSource` 实例异常问题 4.4.7 [3e45020](https://gitee.com/dotnetchina/Furion/commit/3e45020eca5948d18d5cb405a665aae44088fd20) + -  修复 使用 `Log` 静态类超高频率下写日志导致 `CPU` 激增问题 4.4.7 [#I5SDK5](https://gitee.com/dotnetchina/Furion/issues/I5SDK5) + -  修复 远程请求超高频率下发送请求导致 `CPU` 激增问题和异常问题 4.4.8 [#I5SJJR](https://gitee.com/dotnetchina/Furion/issues/I5SJJR) + -  修复 集成第三方配置中心时获取的不是最新数据问题 4.4.9 [2cdef6b](https://gitee.com/dotnetchina/Furion/commit/2cdef6bd4274e5ea0328f209c34b9158dcded7ee) + +- **其他更改** + + -  调整 `JWTEncryption` 静态类,支持无需注册 `services.AddJwt()` 使用 [#I5PPKE](https://gitee.com/dotnetchina/Furion/issues/I5PPKE) [#I5POLZ](https://gitee.com/dotnetchina/Furion/issues/I5POLZ) + -  调整 事件总线默认日志类名为 `System.Logging.EventBusService` [#I5QLY5](https://gitee.com/dotnetchina/Furion/issues/I5QLY5) + +- **文档** + + -  新增 `.NET6` 升级 `.NET7` 文档 + -  新增 `ASP.NET 7` 集成文档 + -  新增 集成第三方配置中心文档 4.4.9 + -  新增 第三方事件总线和 `Furion` 集成文档 4.4.9 + -  新增 事件总线集成 `Kafka` 文档 [#I5P5UG](https://gitee.com/dotnetchina/Furion/issues/I5P5UG) + -  更新 友好异常文档、日志记录文档、远程请求文档、依赖注入文档、即时通讯文档、事件总线文档、Worker Service 文档、单元测试文档、入门指南文档、数据库新增文档 + +--- + +## v4.3.9(已发布) + +:::important 版本细节 + +- `v4.3.9` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I5PIWD](https://gitee.com/dotnetchina/Furion/issues/I5PIWD) 2022.09.03 +- `v4.3.8` 版本细节:[https://gitee.com/dotnetchina/Furion/issues/I5PCXK](https://gitee.com/dotnetchina/Furion/issues/I5PCXK) 2022.09.02 + +::: + +- **新特性** + + -  新增 `AppSettings` 配置的 `ExcludeAssemblies` 属性,支持忽略指定程序集扫描 [7b7747f](https://gitee.com/dotnetchina/Furion/commit/7b7747f38c84acfe7df3469599bebf417e5ad843) + -  新增 `Oops.Oh` 和 `Oops.Bah` 支持设置额外数据 `.WithData(data)` [#I5O38E](https://gitee.com/dotnetchina/Furion/issues/I5O38E) + -  新增 定时任务 `Crontab.GetSleepMilliseconds(baseTime)` 获取下一个发生时间的时间差 [d024fae](https://gitee.com/dotnetchina/Furion/commit/d024fae670b7ce3fd4bfd26aee70ed318a4c0383) + -  新增 **友好异常默认打印异常日志,避免生产环境漏掉重要异常信息 [6e3a5bd](https://gitee.com/dotnetchina/Furion/commit/6e3a5bdd0fd22a7f9ae618b7495cd64081a7f2e8)** + -  新增 日志静态类 `Log.CreateLoggerFactory()` 静态方法 [75c672a](https://gitee.com/dotnetchina/Furion/commit/75c672afc58b393313916c433cb9d92c779b9629) + -  新增 多语言 `SharedResource` 模式,避免硬编程 [18e80c7](https://gitee.com/dotnetchina/Furion/commit/18e80c7d7c2c2450c6ad429601716f546552e987) + -  新增 **事件总线 `MessageCenter` 静态类,解决从 `Fur v1.x` 版本升级问题 [a29fc7c](https://gitee.com/dotnetchina/Furion/commit/a29fc7cf63a3ea41b1617a6ad98a701a243e24f8)** + -  新增 组件化 `IWebComponent` 模式,支持 `.NET5+` [08a44c3](https://gitee.com/dotnetchina/Furion/commit/08a44c347a56c467527935a8caac8966585f5d1a) + -  新增 远程请求设置自己的 `HttpClient` 功能 [#I5PBR3](https://gitee.com/dotnetchina/Furion/issues/I5PBR3) [!545](https://gitee.com/dotnetchina/Furion/pulls/544) + -  新增 `LoggingMonitor` 支持添加更多自定义配置 [#I5PEPA](https://gitee.com/dotnetchina/Furion/issues/I5PEPA) + -  新增 `LoggingMonitor` 可配置 `WithReturnValue` 和 `ReturnValueThreshold` [#I5PFJ1](https://gitee.com/dotnetchina/Furion/issues/I5PFJ1) [#I5PFOW](https://gitee.com/dotnetchina/Furion/issues/I5PFOW) + -  新增 `LoggingMonitor` 可配置 `MethodsSettings` 更多信息 [#I5PFJ1](https://gitee.com/dotnetchina/Furion/issues/I5PFJ1) [#I5PFOW](https://gitee.com/dotnetchina/Furion/issues/I5PFOW) + +
+ 查看变化 +
+ +```cs showLineNumbers {2,4,6} +Serve.Run(RunOptions.Default + .AddWebComponent()); + +public class XXXWebComponent : IWebComponent +{ + public void Load(WebApplicationBuilder builder, ComponentContext componentContext) + { + // .... + } +} +``` + +
+
+ +- **突破性变化** + + -  新增 **`Furion` 程序集 `PublicKeyToken` 强签名** [26b12c0](https://gitee.com/dotnetchina/Furion/commit/26b12c0fd64b153a71496eb62110567e05450f20) + -  调整 **事件总线 `IEventBusFactory` 事件工厂方法 `AddSubscriber -> Subscribe`,`RemoveSubscriber -> Unsubscribe` [a29fc7c](https://gitee.com/dotnetchina/Furion/commit/a29fc7cf63a3ea41b1617a6ad98a701a243e24f8)** + -  调整 `.AddInject()` 和 `.UseInject()` 配置选项名称,移除 `Configure` 后缀 [b6953cd](https://gitee.com/dotnetchina/Furion/commit/b6953cd586936593e40ef626c3b8a1e770239e43) + -  调整 **远程请求 `请求拦截`、`响应拦截` 和 `异常拦截` 委托签名,新增 `HttpClient` 参数** [#I5OWBO](https://gitee.com/dotnetchina/Furion/issues/I5OWBO) + +
+ 查看变化 +
+ +```cs showLineNumbers {2,7,12} +[Interceptor(InterceptorTypes.Request)] +static void OnRequest(HttpClient client, HttpRequestMessage req) +{ +} + +[Interceptor(InterceptorTypes.Response)] +static void OnResponsing(HttpClien client, HttpResponseMessage res) +{ +} + +[Interceptor(InterceptorTypes.Exception)] +static void OnException(HttpClient client, HttpResponseMessage res, string errors) +{ +} +``` + +
+
+ +- **问题修复** + + -  修复 生成包含 `中文` 的 `JWT Token` 解密后出现乱码问题 [#I5O397](https://gitee.com/dotnetchina/Furion/issues/I5O397) + -  修复 `HttpRequestMessage` 拓展中追加查询参数时的空引用异常 [#I5PENW](https://gitee.com/dotnetchina/Furion/issues/I5PENW) [!547](https://gitee.com/dotnetchina/Furion/pulls/547) + -  修复 日志模块配置多个 `IDatabaseLoggingWriter` 只有一个生效 [#I5PFQ2](https://gitee.com/dotnetchina/Furion/issues/I5PFQ2) [#I5PFJ1](https://gitee.com/dotnetchina/Furion/issues/I5PFJ1) + +- **其他更改** + + -  调整 默认输出文件日志模板,使其更加美观 [#1518cf3](https://gitee.com/dotnetchina/Furion/commit/1518cf3be74524ed0d3f73360068a9a0ec6685d9) + -  调整 默认规范化结果验证处理也支持状态码设置 [2eb9390](https://gitee.com/dotnetchina/Furion/commit/2eb939074a14d29fcd3e4726937c8a8430765f48) + -  更新 `SqlSugarCore` 拓展包和脚手架至 `5.1.2.6` 版本 [#I5PCXK](https://gitee.com/dotnetchina/Furion/issues/I5PCXK) + -  更新 `JSON Schema` 关于 `LoggingMonitor` 更多配置 [#I5PFJ1](https://gitee.com/dotnetchina/Furion/issues/I5PFJ1) + +- **文档** + + -  新增 `RabbitMQ` 事件总线文档 + -  更新 `AppSettings` 配置文档、事件总线文档、多数据库配置文档、日志文档、定时任务文档、`MessageCenter` 文档、远程请求文档、组件化文档、入门指南、多语言文档。 + +--- + +## v4.2.13(已发布) + +- **新特性** + + -  新增 **事件总线工厂,支持运行时动态添加订阅程序和移除订阅程序** [#I5NNQX](https://gitee.com/dotnetchina/Furion/issues/I5NNQX) + -  新增 **事件总线 `[EventSubscribe]` 事件 `Id` 支持正则表达式匹配** [#I5NNQX](https://gitee.com/dotnetchina/Furion/issues/I5NNQX) + -  新增 **事件总线 `[EventSubscribe]` 支持局部失败重试配置** [#I5NNQX](https://gitee.com/dotnetchina/Furion/issues/I5NNQX) + -  新增 **`Log` 全局静态类,方便随时随地记录日志** [ba9b1f1](https://gitee.com/dotnetchina/Furion/commit/ba9b1f13a0b89b6670cf7078ac15abe0eed1f2c3) + -  新增 事件总线 `options.AddSubscriber(Type)` 重载 [42446078](https://gitee.com/dotnetchina/Furion/blob/424460780b630e1c71de4db84ad8fd14e33a09f5/framework/Furion.Pure/EventBus/Builders/EventBusOptionsBuilder.cs) + -  新增 `ValidationMetadata` 类型 `FirstErrorProperty` 和 `FirstErrorMessage` 属性 [#I5MFJT](https://gitee.com/dotnetchina/Furion/issues/I5MFJT) + -  新增 `Serve.Run()` 模式 `WithArgs(args)` 方法 [#I5MOJB](https://gitee.com/dotnetchina/Furion/issues/I5MOJB) + -  新增 `[UnitOfWork]` 分布式事务 `TransactionScope` 支持 [#I5MRTY](https://gitee.com/dotnetchina/Furion/issues/I5MRTY) + -  新增 16 位 `MD5` 加密支持 [#I5N8RC](https://gitee.com/dotnetchina/Furion/issues/I5N8RC) + +- **突破性变化** + + -  调整 `Scoped.Create(async (f,s) => {})` 异步创建作用域方法名称为 `CreateAsync`,避免一些情况下无法区分,**同步方法不变** [#I5N9XY](https://gitee.com/dotnetchina/Furion/issues/I5N9XY) + +
+ 查看变化 +
+ +由: + +```cs showLineNumbers {1-2} +// Scoped.CreateUow 一样 +await Scoped.Create(async (f, s) => {}); +``` + +改为: + +```cs showLineNumbers {1-2} +// Scoped.CreateUowAsync 一样 +await Scoped.CreateAsync(async (f, s) => {}); +``` + +
+
+ +- -  新增 `.NET 6.0.8` 和 `.NET 7 Preview 7` [842d4f7](https://gitee.com/dotnetchina/Furion/commit/842d4f739c92366e05fb1d2c619c9b2c2c2c21b7) +- -  调整 `[LoggingMonitor]` 命名空间为 `System`,因为使用频率越来越高 [b879861](https://gitee.com/dotnetchina/Furion/commit/b879861c9db5cf3cb0f4ae023d1e96b06fad3e46) +- -  新增 **在非 `Web` 环境中不正确使用字符串拓展方法检测机制** [6389cbd](https://gitee.com/dotnetchina/Furion/commit/6389cbdd69f5ca826fad25749a45ede079db98ce) +- -  调整 **所有 `.Default` 静态属性为 `.Default()` 方法** [6389cbd](https://gitee.com/dotnetchina/Furion/commit/6389cbdd69f5ca826fad25749a45ede079db98ce) +- -  调整 **工作单元 `IUnitOfWork` 所有方法参数类型,由 `ActionExecutingContext` 和 `ActionExecutedContext` 改为 `FilterContext`** [#I5MHX5](https://gitee.com/dotnetchina/Furion/issues/I5MHX5) + +
+ 查看变化 +
+ +```cs showLineNumbers {3,5,7,9} +public interface IUnitOfWork +{ + void BeginTransaction(FilterContext context, UnitOfWorkAttribute unitOfWork); + + void CommitTransaction(FilterContext resultContext, UnitOfWorkAttribute unitOfWork); + + void RollbackTransaction(FilterContext resultContext, UnitOfWorkAttribute unitOfWork); + + void OnCompleted(FilterContext context, FilterContext resultContext); +} +``` + +
+
+ +- **问题修复** + + -  修复 日志上下文数据多次写入被清空问题以及数据库日志出现异常后停止写入 [#I5LIWF](https://gitee.com/dotnetchina/Furion/issues/I5LIWF) + -  修复 个别情况下跨域默认配置的响应缓存导致嵌入式资源异常问题 [7a57efe](https://gitee.com/dotnetchina/Furion/commit/7a57efe15a9a2d76475d758f2b64395f96d94077) + -  修复 远程请求传入不合法的请求报文头数据触发校验失败问题 [#I5LPFE](https://gitee.com/dotnetchina/Furion/issues/I5LPFE) + -  修复 多线程中使用静态日志写数据库日志导致连接池耗光问题 [8d5cdd6](https://gitee.com/dotnetchina/Furion/commit/8d5cdd6ca04d55e33322000ecc176e47195b6f4d) + -  修复 `EFCore 6.0` 之后 `IModelCacheKeyFactory` 接口方法改变导致分表分库异常问题 [#I5MCZ6](https://gitee.com/dotnetchina/Furion/issues/I5MCZ6) [EFCore#25154](https://github.com/dotnet/efcore/issues/25154#issuecomment-868804532) [EFCore!3305](https://github.com/dotnet/EntityFramework.Docs/pull/3305) + -  修复 `ValidationMetadata` 对象 `Message` 字符串类型出现 `\"\"` 问题 [#I5MFJT](https://gitee.com/dotnetchina/Furion/issues/I5MFJT) + -  修复 `[IfException]` 覆盖 `Oops.Oh/Bah` 错误消息问题 [4bbd854](https://gitee.com/dotnetchina/Furion/commit/4bbd854c6779bf7eca26fd69b25c4979b8cec32d) + -  修复 数据库日志写入循环写入和频繁创建数据库连接池问题 [9ce214c](https://gitee.com/dotnetchina/Furion/commit/9ce214c9cf49eb1ff59f3c52dbecd789be4f45fd) + -  修复 `Razor Pages` 不支持全局异常拦截问题 [#I5MHX5](https://gitee.com/dotnetchina/Furion/issues/I5MHX5) + -  修复 `Razor Pages` 不支持全局数据验证问题 [#I5MHX5](https://gitee.com/dotnetchina/Furion/issues/I5MHX5) + -  修复 `Razor Pages` 不支持工作单元 `[UnitOfWork]` 问题 [#I5MHX5](https://gitee.com/dotnetchina/Furion/issues/I5MHX5) + -  修复 `Razor Pages` 不支持 `EFCore` 自动 `SaveChanges` 问题 [#I5MHX5](https://gitee.com/dotnetchina/Furion/issues/I5MHX5) + -  修复 `Blazor Server` 因 `v4.2.2` 版本更新导致的问题 [#I5MNFN](https://gitee.com/dotnetchina/Furion/issues/I5MNFN) + -  修复 `[IfException]` 不支持多语言配置问题 [#I5MPN7](https://gitee.com/dotnetchina/Furion/issues/I5MPN7) + -  修复 通过 `services.AddMvcFilter()` 方式注册无效问题 [8d1477d](https://gitee.com/dotnetchina/Furion/commit/8d1477d8b44ae111bddb8e4780672bcaf4e0e467) + -  修复 事件总线默认 `Channel` 管道初始化时机过晚问题,解决部分第三方依赖使用问题 [#I5MM3O](https://gitee.com/dotnetchina/Furion/issues/I5MM3O) + -  修复 主机停止时写入日志异常问题 [#I5N7S2](https://gitee.com/dotnetchina/Furion/issues/I5N7S2) + -  修复 数据库上下文手动释放导致 `AutoSaveChange` 特性出现释放异常问题 [#I5NFWC](https://gitee.com/dotnetchina/Furion/issues/I5NFWC) + -  修复 `[LoggingMonitor]` 循环引用序列化问题 [#I5NRT9](https://gitee.com/dotnetchina/Furion/issues/I5NRT9) + -  修复 远程请求传入 `null` Body 参数抛出空异常问题 [#I5NTUE](https://gitee.com/dotnetchina/Furion/issues/I5NTUE) + -  修复 事件总线默认开启模糊匹配(正则表达式)导致不必要的订阅 [#I5NVOP](https://gitee.com/dotnetchina/Furion/issues/I5NVOP) + +- **其他更改** + + -  调整 事件总线默认 `Channel` 管道初始化时机,解决部分第三方依赖使用问题 [#I5MM3O](https://gitee.com/dotnetchina/Furion/issues/I5MM3O) + -  更新 底层迭代改进优化 + -  新增 规范化文档获取控制器、方法分组、标签信息 [66d8d54](https://gitee.com/dotnetchina/Furion/commit/66d8d54b225b5294cd54aa76b548312f7c37903a) + +- **文档** + + -  新增 全局日志静态类 `Log` 文档 [ba9b1f1](https://gitee.com/dotnetchina/Furion/commit/ba9b1f13a0b89b6670cf7078ac15abe0eed1f2c3) + -  新增 `NuGet` 本地测试包文档 + -  更新 日志文档、静态类文档、数据校验文档、Worker Service 文档、工作单元文档、依赖注入文档 + +--- + +## v4.1.14(已发布,全新单元测试) + +:::tip 关于单元测试 + +单元测试和集成测试是保证一个系统能够持续维护和稳定运行的必备技能,但是目前现有的单元测试组件无法直接集成 `Furion` 的功能,最常用的就是如何在单元测试中读取配置,以及**如何进行依赖注入**。 + +在过去,`Furion` 只能不断的去调整,以至于适配第三方单元测试写法,搞得不伦不类! + +所以,这一次不再妥协,**`Furion` 推出自己的单元测试工具,可以让现有的单元测试如 `Xunit` 100% 支持 `Furion` 所有功能,全部保证一致的写法。** + +::: + +- **新特性** + + -  新增 **`Furion.Xunit` 拓展包,正式实现 `Xunit` 单元测试完整支持 `Furion` [063a034e](https://gitee.com/dotnetchina/Furion/commit/063a034edd089e88d501af4c09251611476fd238)** + -  新增 `services.AddMonitorLogging()` 日志监视器服务,支持非常灵活的日志操作 [81df742](https://gitee.com/dotnetchina/Furion/commit/81df742b2784a18fbf4060fe30cc5151909c3cab) + -  新增 **`Serve.Run(silence: true)` 等一系列强大的静默启动功能 [#I5JBSQ](https://gitee.com/dotnetchina/Furion/issues/I5JBSQ) [#I5J98T](https://gitee.com/dotnetchina/Furion/issues/I5J98T) [7cced4](https://gitee.com/dotnetchina/Furion/commit/7cced443ca1cdcb29226c71274e087ec2a6135ef)** + -  新增 `SpecificationDocumentBuilder.GetOpenApiGroups()` 方法获取底层的规范化接口分组信息 [4ff03c5](https://gitee.com/dotnetchina/Furion/commit/4ff03c5f8342c4d9b26fb1336cd78936ab189f5e) + -  新增 `logger.ScopeContext()` 配置日志上下文功能 [#I5JC0D](https://gitee.com/dotnetchina/Furion/issues/I5JC0D) + -  新增 跨域配置 `CorsAccessorSettings.SignalRSupport` 配置选项,支持配置 `SignalR` 跨域 [#I5JREM](https://gitee.com/dotnetchina/Furion/issues/I5JREM) + -  新增 事件总线 `UseUtcTimestamp` 选项配置,可选择使用 `DateTime.UtcNow` 还是 `DateTime.Now`,默认是 `DateTime.Now` [#I5JSEU](https://gitee.com/dotnetchina/Furion/issues/I5JSEU) + -  新增 规范化文档 `[OperationId]` 配置,解决自定义 `Swagger UI` 不能正确显示路由问题 [#I5K1IB](https://gitee.com/dotnetchina/Furion/issues/I5K1IB) + -  新增 远程请求 `IHttpDispatchProxy` 方式全局拦截支持多态(继承) [#I5K8FS](https://gitee.com/dotnetchina/Furion/issues/I5K8FS) + +- **突破性变化** + + -  新增 `Furion.Xunit` 拓展包,正式实现 `Xunit` 单元测试完整支持 `Furion` [063a034e](https://gitee.com/dotnetchina/Furion/commit/063a034edd089e88d501af4c09251611476fd238) + -  移除 ~~`Furion.Extras.DatabaseAccessor.SqlSugar` 拓展插件中的 `[SqlSugarUnitOfWork]` 工作单元特性,将使用通用工作单元替换~~ **[查看最新实现文档](./tran.mdx#92631-自动管理)** + -  移除 ~~`Inject.Create()` 方法,再也不需要了,框架提供了无敌强大的 `Serve.Run()` 静默启动方式~~ [200848e](https://gitee.com/dotnetchina/Furion/commit/200848eda8c2e419c0b5be83f7768a257f3c88bd) + -  调整 `Serve.Run` 的 `ConfigureConfiguration` 方法参数,由 `configuration => {}` 改为 `(environment, configuration) => {}` [83c97bb](https://gitee.com/dotnetchina/Furion/commit/83c97bb5a19d6fc4e51cfe05f635675d26067d45) + +
+ 查看变化 +
+ +```cs showLineNumbers {2,6} +// 由 +Serve.Run(RunOptions.Default.ConfigureConfiguration(configuration => { + +})); +// 改为: +Serve.Run(RunOptions.Default.ConfigureConfiguration((environment, configuration) => { + +})); +``` + +
+
+ +- **问题修复** + + -  修复 `[LoggingMonitor]` 异常消息日志级别为 `Information` 错误问题 [ab46cdf](https://gitee.com/dotnetchina/Furion/commit/ab46cdf534433f45d39ce4d3ee7c71ca84707140) + -  修复 新版本日志组件频繁提示文件占用问题,将文件独占锁改为共享锁 [#I5J3S6](https://gitee.com/dotnetchina/Furion/issues/I5J3S6) + -  修复 配置数据库日志读写器为 `EFCore` 时控制台出现无限打印问题 [#I5J474](https://gitee.com/dotnetchina/Furion/issues/I5J474) + -  修复 `[LoggingMonitor]` 针对 `byte[]` 类型参数输出过大问题 [5380f35](https://gitee.com/dotnetchina/Furion/commit/5380f3551de69f8607ca0fc33c950103c7ed8174) + -  修复 友好异常和规范化结果丢失了原始 `ErrorCode` 问题 [#I5IX2R](https://gitee.com/dotnetchina/Furion/issues/I5IX2R) + -  修复 新版本日志组件自定义数据库读写器注入 `IRepository` 仓储导致死循环问题 [#I5IX2R](https://gitee.com/dotnetchina/Furion/issues/I5IX2R) + -  修复 `Mvc` 默认手动验证和 `Furion` 全局验证冲突问题 [2a06c39](https://gitee.com/dotnetchina/Furion/commit/2a06c39c1d0a032bbc317e25a22c646babce2a60) + -  修复 `Serve.Run()` 模式不支持 `SuperSocket` 第三方包问题,原生是支持的。[186ca0a](https://gitee.com/dotnetchina/Furion/commit/186ca0a35d696f58d9e696094848a560074cdf6f) + -  修复 `SignalR` 跨域错误问题 [#I5JREM](https://gitee.com/dotnetchina/Furion/issues/I5JREM) + -  修复 `[LoggingMonitor]` 将 `Oops.Oh` 和 `Oops.Bah` 记录到了错误日志中,默认应该是 `Information` 且提供可配置 [#I5JZ1H](https://gitee.com/dotnetchina/Furion/issues/I5JZ1H) + -  修复 自定义 `Swagger UI` 之后个别 `UI` 要求必须配置 `operationId`,否则出现 `guid` 序号 [#I5K1IB](https://gitee.com/dotnetchina/Furion/issues/I5K1IB) + -  修复 主动抛出 `NotFoundResult` 和 `NotFoundObjectResult` 无效问题 [#I5KALZ](https://gitee.com/dotnetchina/Furion/issues/I5KALZ) + -  修复 `[LoggingMonitor]` 解析方法参数但前端未传入时出现错误问题 [#I5KC5P](https://gitee.com/dotnetchina/Furion/issues/I5KC5P) + -  修复 `[LoggingMonitor]` 无法序列化 `IQueryable` 返回值问题 [#I5KJD1](https://gitee.com/dotnetchina/Furion/issues/I5KJD1) + -  修复 `[LoggingMonitor]` 不能记录全局验证错误问题 [b44087d](https://gitee.com/dotnetchina/Furion/commit/b44087dcc7dbe9992b8f4518e0b0cf4ed61c56bb) + -  修复 `[LoggingMonitor]` 存在注册顺序差异问题 [b44087d](https://gitee.com/dotnetchina/Furion/commit/b44087dcc7dbe9992b8f4518e0b0cf4ed61c56bb) + +- **其他更改** + + -  新增 底层的规范化文档 `SpecificationDocumentBuilder` 部分方法,提供更加便捷的第三方 `Swagger UI` 集成 [10f0f01](https://gitee.com/dotnetchina/Furion/commit/10f0f01996586eedb138304c7b00c6f31282dfeb) + +- **文档** + + -  更新 单元测试文档、入门指南文档、Worker Services 文档 + +- **本期亮点** + +1. `Serve.Run()` 彻彻底底支持全平台,提供非常强大的静默模式 + +**启用静默模式可以实现无阻塞方式执行程序,而且还能体验完整的 `Furion` 功能。** + +
+ 查看变化 +
+ +:::tip 大革命 + +有了 `Serve.Run()` 静默模式后,`Furion` 彻彻底底支持全平台,不管你是 `控制台、Web、桌面、移动、单元测试,集成测试,基准测试等等应用程序`。 + +::: + +```cs showLineNumbers {1,3} +Serve.Run(silence: true); + +// 不会阻塞执行哦,而且从这里开始可以使用 Furion 任何功能,比如 App.Configuration.... +Console.WriteLine("Hello, World!"); +Console.ReadKey(); +``` + +还有更多静默模式。 + +```cs showLineNumbers {2,5,8} +// RunOptions 方式 +Serve.Run(RunOptions.DefaultSilence); + +// LegacyRunOptions 方式 +Serve.Run(LegacyRunOptions.DefaultSilence); + +// GenericRunOptions 方式 +Serve.Run(GenericRunOptions.DefaultSilence); +``` + +
+
+ +2. 强大的 `Furion.Xunit` 单元测试、集成测试 + +
+ 查看变化 +
+ +单元测试中初始化 `Furion` + +```cs showLineNumbers {6,13,18} +using Furion.Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; + +// 配置启动类类型,第一个参数是 TestProgram 类完整限定名,第二个参数是当前项目程序集名称 +[assembly: TestFramework("TestProject1.TestProgram", "TestProject1")] + +namespace TestProject1; + +/// +/// 单元测试启动类 +/// +public class TestProgram : TestStartup +{ + public TestProgram(IMessageSink messageSink) : base(messageSink) + { + // 初始化 Furion + Serve.Run(silence: true); + } +} +``` + +**测试类支持完整依赖注入** + +```cs showLineNumbers {9,15} +using TestProject1.Services; +using Xunit; + +namespace TestProject1; + +public class UnitTest1 +{ + private readonly ICalcService _calcService; + public UnitTest1(ICalcService calcService) + { + _calcService = calcService; + } + + [Fact] + public void 测试两个数的和() + { + Assert.Equal(3, _calcService.Plus(1, 2)); + } +} +``` + + + +**[查看新版本单元测试文档](./unittest.mdx)** + +
+
+ +3. 开放底层规范化文档分组接口,使得集成第三方 `Swagger UI` 更加容易,如集成 `IGeekFan.AspNetCore.Knife4jUI` 拓展: + +
+ 查看变化 +
+ +:::note 安装包 + +只需要在 `YourPoject.Web.Core` 层安装 `IGeekFan.AspNetCore.Knife4jUI` 即可。 + +::: + +3.1.1 **`Knife4jUI` 独立版本配置** + +```cs showLineNumbers {1,3,12} +var routePrefix = "api"; // 定义 swagger 路由地址,如果是跟目录,设置 string.Empty 即可 + +app.UseKnife4UI(options => +{ + options.RoutePrefix = routePrefix; // 配置 Knife4UI 路由地址 + foreach (var groupInfo in SpecificationDocumentBuilder.GetOpenApiGroups()) + { + options.SwaggerEndpoint("/" + groupInfo.RouteTemplate, groupInfo.Title); + } +}); + +app.UseInject(routePrefix); // 配置 Furion 路由地址 +``` + +3.1.2 **`Knife4jUI` 和 `Swagger` 共存版本配置** + +```cs showLineNumbers {1,3,10} +app.UseKnife4UI(options => +{ + options.RoutePrefix = "newapi"; // 配置 Knife4UI 路由地址,现在是 /newapi + foreach (var groupInfo in SpecificationDocumentBuilder.GetOpenApiGroups()) + { + options.SwaggerEndpoint("/" + groupInfo.RouteTemplate, groupInfo.Title); + } +}); + +app.UseInject(); // Furion 默认 api 地址为 /api +``` + + + +**如需实现登录之后自动将 `token` 添加到头部可在登录接口 `AfterScript` 执行以下代码:** + +```js +ke.global.setAllHeader( + "Authorization", + "Bearer " + ke.response.headers["access-token"] +); +``` + + + +4. 提供强大的日志上下文功能 + +```cs showLineNumbers {2,6,12} +// 写法一 +_logger.ScopeContext(ctx => ctx.Set("Name", "Furion").Set("UserId", 10)) + .LogInformation("我是一个日志 {id}", 20); + +// 写法二 +_logger.ScopeContext(new Dictionary { + { "Name", "Furion" }, + { "UserId", 10 } +}).LogInformation("我是一个日志 {id}", 20); + +// 写法三 +_logger.ScopeContext(new LogContext { + // .... +}).LogInformation("我是一个日志 {id}", 20) +``` + +
+
+ +--- + +## v4.0.0(重新起航)💖 + +:::tip 不忘初心,感恩遇见,感恩信任 + +2020 年 09 月 01 日,一个叫 `Fur` 的开源项目在 `Gitee` 的襁褓中悄然诞生,她的出生仿佛带着某种使命,没有包袱,无限可能。 + +她缓缓的张开双眼,干净雪亮的眼睛似乎对这个世界充满了好奇,任何事物在她眼前晃过都像是直击灵魂的思想碰撞,这些在她看来都是非常宝贵的财富。她貌似有用不完的精力,一路汲取知识,升级打怪,不断奔跑,乐此不疲。 + +记得 2020 年 11 月 11 日的单身节,她迎来了“一岁(v1.0.0)”生日,自那以后,IT 这个大银幕上频繁出现她的身影,越来越多 `.NET5` 开发者转粉,像是告诉这个世界,她就是 IT 界大明星。 + +每一个明星都有一个好听的艺名,她当然也不例外,2020 年 11 月 20 日,经纪人百小僧为她起名为 `Furion`。 + +2021 年 11 月 09 日起,她进入了每个孩子都经历过的叛逆期,年少轻狂喜新厌旧,抛弃了曾经支持她的 `.NET5` 粉丝们,投入到新的 `.NET6` 拥趸者怀抱中,自此过上了奢靡富足的生活。 + +但她过的不开心,时常在夜里想起 `.NET5` 的粉丝们,内心非常自责,但在双重工作压力下她毅然选择了忽视他们的诉求,仿佛他们就是累赘。 + +时间真的是好东西,曾经认为是对的,经过岁月的蹉跎历磨,渐渐的明白:不忘初心,方能始终。 + +这一次,不落下一人(`.NET5`,`.NET6`,...,`.NET N`),携手共进,重新起航,感恩遇见,感恩信任。 + +::: + +- **新特性** + + - **`v4.0.0` 支持 `.NET5`,`.NET6`,...,`.NET N`,所有的 `Furion` 项目都能够升级到该版本,重新起航,实现大统。** + +--- + +## v3.9.2(已发布,全新日志组件) + +:::tip 关于日志 + +日志模块是任何应用系统都必备的功能,可以说是最重要的模块!在 `.NET` 社区中有 `Log4NET`,`NLog`,`Serilog` 等日志组件,它们无一不是优秀的开源项目。 + +但由于这些日志组件历史悠久,内部兼容的 `.NET` 版本非常多,功能随着时间推移变得极其强大复杂,**在实际项目使用中,发现每一个日志组件配置总是不那么友好,特别是在使用上不够简单**。 + +`Furion` 作为全栈开发框架,**在过去版本并没有提供足以满足开发者需求的日志模块**,转而推荐大家集成第三方组件,如 `Serilog`,**导致后续无法实现自定义功能和也增加了不少维护成本。** + +**这一次,`Furion` 不再妥协,彻底重构了日志模块,实现日志功能/需求完全自主可控,提供给开发者几乎所有日志功能的需求!** + +`Furion` 的使用者们,**是时候“更换”掉第三方日志组件,让我们一起迭代出更强更好的日志组件吧!** 🍖 + +::: + +- **新特性** + + -  优化 日志模块,内置写入控制台、文件、数据库功能,再也无需引入第三方日志了![日志源码](https://gitee.com/dotnetchina/Furion/tree/v4/framework/Furion/Logging) + -  新增 **强大的调试日志 `[LoggingMonitor]` [32dfc1](https://gitee.com/dotnetchina/Furion/commit/32dfc11fd6b47551a81c09e676eb7f5d018ef737)** + +- **突破性变化** + + -  优化 **日志模块,内置写入控制台、文件、数据库功能,再也无需引入第三方日志了![日志源码](https://gitee.com/dotnetchina/Furion/tree/v4/framework/Furion/Logging)** + -  调整 **`TP.Wrapper` 规范化日志模板算法,由过去的 `[属性]` 改为 `##属性##`,解决 `JSON` 内容冲突问题 [394ecec](https://gitee.com/dotnetchina/Furion/commit/394ecec28629aee9945480155d502418d0b7a8f8)** + -  移除 ~~**未来 `Furion.Extras.Logging.Serilog` 拓展将不再继续维护,因为 `Furion` 有强大的日志组件了!**~~ + +- **问题修复** + + -  修复 脱敏模块模型绑定个别情况下空异常问题 [#I5IM5C](https://gitee.com/dotnetchina/Furion/issues/I5IM5C) + +- **文档** + + -  更新 日志文档、静态类文档、数据校验文档 + +- **本期亮点** + +1. **极易使用且强大的日志模块** + +
+ 查看变化 +
+ +```cs showLineNumbers +// 写入文件 +services.AddFileLogging("logs/application.log"); + +// 写入数据库 +services.AddDatabaseLogging(); +``` + +**[查看更多日志文档](./logging.mdx)** + +
+
+ +2. **强大的 `[LoggingMonitor]` 调试日志** + +
+ 查看变化 +
+ +```cs showLineNumbers {1,7} +using Furion.Logging; + +namespace Furion.Application; + +public class TestLoggerServices : IDynamicApiController +{ + [LoggingMonitor] + public PersonDto GetPerson(int id) + { + return new PersonDto + { + Id = id + }; + } +} +``` + +**支持控制器、操作或全局注册拦截**。 + +输出日志为: + +```bash showLineNumbers +┏━━━━━━━━━━━ Logging Monitor ━━━━━━━━━━━ +┣ Furion.Application.TestLoggerServices.GetPerson (Furion.Application) +┣ +┣ 控制器名称: TestLoggerServices +┣ 操作名称: GetPerson +┣ 路由信息: [area]: ; [controller]: test-logger; [action]: person +┣ 请求地址: https://localhost:44316/api/test-logger/person/11 +┣ 来源地址: https://localhost:44316/api/index.html +┣ 浏览器标识: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36 Edg/103.0.1264.62 +┣ 客户端 IP 地址: 0.0.0.1 +┣ 服务端 IP 地址: 0.0.0.1 +┣ 服务端运行环境: Development +┣ 执行耗时: 31ms +┣ ━━━━━━━━━━━━━━━ 授权信息 ━━━━━━━━━━━━━━━ +┣ JWT Token: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEsIkFjY291bnQiOiJhZG1pbiIsImlhdCI6MTY1ODcxNjc5NywibmJmIjoxNjU4NzE2Nzk3LCJleHAiOjE2NTg3MTc5OTcsImlzcyI6ImRvdG5ldGNoaW5hIiwiYXVkIjoicG93ZXJieSBGdXJpb24ifQ.VYZkwwqCwlUy3aJjuL-og62I0rkxNQ96kSjEm3VgXtg +┣ +┣ UserId (integer): 1 +┣ Account (string): admin +┣ iat (integer): 1658716797 +┣ nbf (integer): 1658716797 +┣ exp (integer): 1658717997 +┣ iss (string): dotnetchina +┣ aud (string): powerby Furion +┣ ━━━━━━━━━━━━━━━ 参数列表 ━━━━━━━━━━━━━━━ +┣ Content-Type: +┣ +┣ id (Int32): 11 +┣ ━━━━━━━━━━━━━━━ 返回信息 ━━━━━━━━━━━━━━━ +┣ 类型: Furion.Application.Persons.PersonDto +┣ 返回值: {"Id":11,"Name":null,"Age":0,"Address":null,"PhoneNumber":null,"QQ":null,"CreatedTime":"0001-01-01T00:00:00+00:00","Childrens":null,"Posts":null} +┗━━━━━━━━━━━ Logging Monitor ━━━━━━━━━━━ +``` + +
+
+ +--- + +## v3.8.9(已发布) + +- **新特性** + + -  新增 规范化结果 `ExceptionMetadata` 和 `ValidationMetadata` 都可以获取 `ErrorCode` 属性 [#I5GJ6D](https://gitee.com/dotnetchina/Furion/issues/I5GJ6D) + -  新增 `ValidationMetadata` 类 `StatusCode` 属性 [#I5HB5L](https://gitee.com/dotnetchina/Furion/issues/I5HB5L) + -  新增 远程请求对 `Url` 是否编码设置,`[Get(WithEncodeUrl = false)]` 和 `WithEncodeUrl(false)` [#I5GOBC](https://gitee.com/dotnetchina/Furion/issues/I5GOBC) + -  新增 更强大的 `JWTEncryption.SecurityReadJwtToken('token')` 读取解析 `Token` 静态方法 [574eeb6](https://gitee.com/dotnetchina/Furion/commit/574eeb601294378e68c3d57ceaf6cb17f36636e3) + -  新增 `ValiationTypes.Html` 验证 `Html` 标签 [#I5HBKC](https://gitee.com/dotnetchina/Furion/issues/I5HBKC) + -  新增 **[EFCore.NamingConventions](https://github.com/efcore/EFCore.NamingConventions) 支持,可自定义生成表名,字段名风格,比如小驼峰,蛇形命名等 [#I5HBEI](https://gitee.com/dotnetchina/Furion/issues/I5HBEI)** + -  新增 `INamedServiceProvider` 命名服务提供器,可解析接口多实现 [#I5HF98](https://gitee.com/dotnetchina/Furion/issues/I5HF98) + -  新增 脱敏处理模块方法参数单个值处理 [a22ec3c](https://gitee.com/dotnetchina/Furion/commit/a22ec3cc3565582eea67052936e788ede8c633cb) + -  新增 脱敏词库支持 `|` 分割词语 [3106b1d](https://gitee.com/dotnetchina/Furion/commit/3106b1d78b27b24f4141f052bf804a201a268ff7) + +- **突破性变化** + + -  升级 所有 `.NET` 依赖包至 `6.0.7` 版本 + -  升级 `Serilog.AspNetCore` 包至 `6.0.0` 版本 + -  优化 `JWT` `Token` 刷新逻辑 [#I5GXML](https://gitee.com/dotnetchina/Furion/issues/I5GXML) [574eeb6](https://gitee.com/dotnetchina/Furion/commit/574eeb601294378e68c3d57ceaf6cb17f36636e3) + -  调整 **`ExceptionMetadata` 命名空间为 `Furion.FriendlyException`** [3105d16](https://gitee.com/dotnetchina/Furion/commit/3105d168db10bfbe6e83fff2e435ba5d9ef62ed5) + -  调整 `Retry.Invoke(Func...)` 为:`Retry.InvokeAsync(Func...)` [3b78999](https://gitee.com/dotnetchina/Furion/commit/3b7899942bb37423c2c4ed313069fe361aabf682) + -  优化 脱敏处理模块,大大提高性能和准确率,同时支持方法参数单个值处理 [a22ec3c](https://gitee.com/dotnetchina/Furion/commit/a22ec3cc3565582eea67052936e788ede8c633cb) + +- **问题修复** + + -  修复 `Rider` 开发工具对同名脚手架 (`EFCore` 和 `SqlSugar`) 只显示一个问题 [!518](https://gitee.com/dotnetchina/Furion/pulls/518) + -  修复 `UnitOfWork` 工作单元在 `EFCore` 中失效问题 [#I5H0T3](https://gitee.com/dotnetchina/Furion/issues/I5H0T3) + -  修复 `JWT` 中 `Token` 如果存在数组类型的值时,刷新 `Token` 后丢失了历史值 [#I5GXML](https://gitee.com/dotnetchina/Furion/issues/I5GXML) + -  修复 远程请求 `WithEncodeUrl` 无法在 `[HttpMethod]` 设置问题 [574eeb6](https://gitee.com/dotnetchina/Furion/commit/574eeb601294378e68c3d57ceaf6cb17f36636e3) + -  修复 `Serve.Run()` 模式下添加自定义配置导致 `EFCore` 无法获取自定义配置文件问题 [#I5GZ0F](https://gitee.com/dotnetchina/Furion/issues/I5GZ0F) + -  修复 `Oops.Bah` 进入全局异常拦截器问题 [#I5H47S](https://gitee.com/dotnetchina/Furion/issues/I5H47S) + -  修复 `AddDbPool/AddDb` 扩展未根据配置 `Key` 路径读取问题 [#I5H6S4](https://gitee.com/dotnetchina/Furion/issues/I5H6S4) [!520](https://gitee.com/dotnetchina/Furion/pulls/520) + -  修复 `ValiationTypes.Url` 正则表达式覆盖不全问题 [#I5HBKC](https://gitee.com/dotnetchina/Furion/issues/I5HBKC) + -  修复 **`v3.5.x` 版本导致集成 [EFCore.NamingConventions](https://github.com/efcore/EFCore.NamingConventions) 失效问题 [#I5HBEI](https://gitee.com/dotnetchina/Furion/issues/I5HBEI)** + -  修复 `Swagger` 长路由不支持问题以及 `[Required]` 配置 `AllowEmptyStrings` 无效问题 [c014330](https://gitee.com/dotnetchina/Furion/commit/c0143300329b5a96a2fbd4d92de109f520674d33) + -  修复 远程请求上传文件时请求报文 `boundary` 和 `Content-Disposition` 设置不正确问题 [#I5HEF0](https://gitee.com/dotnetchina/Furion/issues/I5HEF0) + -  修复 脱敏模块替换敏感词汇出现多替换问题 [a22ec3c](https://gitee.com/dotnetchina/Furion/commit/a22ec3cc3565582eea67052936e788ede8c633cb) + +- **其他更改** + + -  调整 多语言默认处理逻辑,**允许不配置任何语言**,过去版本会报错 [#I5GRD9](https://gitee.com/dotnetchina/Furion/issues/I5GRD9) [5077c5d](https://gitee.com/dotnetchina/Furion/commit/5077c5dab9ee94733817f55ff8224b853d0001a3) + -  更新 规范化文档 `Swagger` 性能 + -  调整 `MongoDB` 仓储 `TDocument` 泛型约束 [3f49055](https://gitee.com/dotnetchina/Furion/commit/3f49055b6b80ef7861f58b0c6feabf5c87a32010) + +- **文档** + + -  更新 远程请求文档,日志记录文档、多数据库文档、PM2 部署文档、Visual Studio 高效率文档 + +--- + +## v3.7.11(已发布) + +- **新特性** + + -  新增 **`Minimal API` 应用支持:`.AddInjectMini()` [#I4KOQ5](https://gitee.com/dotnetchina/Furion/issues/I4KOQ5)** + -  新增 跨域 `WithExposedHeaders` 默认配置 `access-token` 和 `x-access-token` [42ebdfd](https://gitee.com/dotnetchina/Furion/commit/42ebdfd33a01353a0b3a801528de052990d2e4c9) + -  新增 脚手架默认启用 `app.UseHttpLogging()` `HTTP` 日志 [42ebdfd](https://gitee.com/dotnetchina/Furion/commit/42ebdfd33a01353a0b3a801528de052990d2e4c9) + -  新增 **`Furion` 和 `ASP.NET Core` 完整 `json` 配置的 `JSON Schema` 架构 [JSON Schema](https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json)** + -  新增 `Sql` 代理支持返回单个类类型参数 [1d7fb5b](https://gitee.com/dotnetchina/Furion/commit/1d7fb5b5330c5a30098056818a93a0879034fecd) + -  新增 `Sql` 代理支持返回 `ValueTuple` 单个类类型参数 [876a2f5](https://gitee.com/dotnetchina/Furion/commit/876a2f5f7e2d07fa3bbc3f5b99c0653893e0ada8) + -  新增 组件化设计模块,支持比 `AppStartup` 更灵活便捷的设计 [#components](https://gitee.com/dotnetchina/Furion/tree/v4/framework/Furion/Components) + -  新增 独立工作单元模块,支持任何第三方 `ORM` [a02413d](https://gitee.com/dotnetchina/Furion/commit/a02413d6887d258ad3a1ba972bb6a08d29291d0c) + -  新增 跨域 `FixedClientToken` 配置参数 [bd01638](https://gitee.com/dotnetchina/Furion/commit/bd016386681631a5539bcf215c068c2069bba15f) + -  新增 `throw Oops.Bah` 可以手动触发规范化验证失败处理 [83f0036](https://gitee.com/dotnetchina/Furion/commit/83f0036a4ce10c6b1cfc4a258dc61e197af38879) + -  新增 `FriendlyExceptionSettings` 的 `ThrowBah` 配置,可标记 `Oops.Oh` 不进入异常处理 [76ffa7f](https://gitee.com/dotnetchina/Furion/commit/76ffa7f18d3683ad36e37e5fc90cf54a4b04e520) + +- **突破性变化** + + -  新增 **`Minimal API` 应用支持:`.AddInjectMini()` [#I4KOQ5](https://gitee.com/dotnetchina/Furion/issues/I4KOQ5)** + -  新增 **`Furion` 和 `ASP.NET Core` 完整 `json` 配置的 `JSON Schema` 架构 [JSON Schema](https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json)** + -  新增 组件化设计模块,支持比 `AppStartup` 更灵活便捷的设计 [#components](https://gitee.com/dotnetchina/Furion/tree/v4/framework/Furion/Components) + -  新增 独立工作单元单元模块,支持任何第三方 `ORM` [a02413d](https://gitee.com/dotnetchina/Furion/commit/a02413d6887d258ad3a1ba972bb6a08d29291d0c) + -  优化 `DataValidationFilter` 和 `FriendlyExceptionFilter`,解决不支持手动抛出业务异常问题 [83f0036](https://gitee.com/dotnetchina/Furion/commit/83f0036a4ce10c6b1cfc4a258dc61e197af38879) + -  调整 **`.AddDb<>` 和 `.AddDbPool<>` 自定义委托参数签名,由 `Action` 改为:`Action`** + +
+ 查看变化 +
+ +```cs showLineNumbers {2,5} +// 由: +options.AddDbPool(DbProvider.MySql, opt => { +}); +// 改为 +options.AddDbPool(DbProvider.MySql, (services, opt)=> { +}) +``` + +
+
+ +- **问题修复** + + -  修复 自 `v3.6.3` 版本依赖,执行原生 `Sql` 添加了参数校验导致存储过程执行错误问题 [#I5ERMQ](https://gitee.com/dotnetchina/Furion/issues/I5ERMQ) + -  修复 `tools/cli.ps1` 脚本工具出现数据库链接被占用问题 + -  修复 `JWTSettings` 算法配置 `JSON Schema` 错误问题,感谢 [@gitwentao](https://gitee.com/gitwentao) [#I5G27B](https://gitee.com/dotnetchina/Furion/issues/I5G27B) [!516](https://gitee.com/dotnetchina/Furion/pulls/516) + -  修复 基于策略授权在不配置 `Policy` 的情况下出现空异常问题 [#I5EVF2](https://gitee.com/dotnetchina/Furion/issues/I5EVF2) + -  修复 启用数据库实体跟踪时导致新增实体多次查询数据库问题 [#I4J2LZ](https://gitee.com/dotnetchina/Furion/issues/I4J2LZ) + -  修复 不启用规范化结果导致验证失效,异常失效问题 [cdb3f57](https://gitee.com/dotnetchina/Furion/commit/cdb3f570ab72cff0a5327a717f85c48e153211be) + -  修复 验证异常和友好异常冲突问题 [83f0036](https://gitee.com/dotnetchina/Furion/commit/83f0036a4ce10c6b1cfc4a258dc61e197af38879) + -  修复 `CentOS 7.9` 系统部署无法指定命令 `--urls` 参数问题 [8cc8ee](https://gitee.com/dotnetchina/Furion/commit/8cc8eeff6b5b25de42367884b7c91d419557d054) + +- **其他更改** + + -  调整 脚手架所有 `.json` 文件,默认添加 `JSON Schema` 支持 + +- **文档** + + -  新增 组件化启动文档 + -  新增 `Vue/React/Angular` 请求代理文档 + -  新增 `JSON Schema` 文档,支持配置智能提示和验证 + -  更新 跨域文档、规范化文档、配置文档、日志文档、IIS 部署文档 + +- **本期亮点** + +1. **新增 `JSON Schema` 支持,所有 `.json` 文件支持智能提示和验证** + +
+ 查看变化 +
+ +```json showLineNumbers {2} +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "Microsoft.EntityFrameworkCore": "Information" + } + }, + "AllowedHosts": "*" +} +``` + + + +**[查看更多 `JSON Schema` 文档](./jsonschema)** + +
+
+ +2. **根据 `Swagger` 生成 `Vue/React/Angular` 前端请求代码** + +**[查看文档](./clientapi)** + +3. **`Sql` 代理支持返回单个类类型参数** + +
+ 查看变化 +
+ +```cs showLineNumbers {7-9} +public interface ISql : ISqlDispatchProxy +{ + // 集合类型 + [SqlExecute("select * from person")] + List GetPersons(); + + // 自 v3.7.3+ 版本支持返回单个类类型参数 + [SqlExecute("select * from person where id=@id")] + Person GetPerson(int id); +} +``` + +
+
+ +4. **`Sql` 代理支持返回 `ValueTuple` 单个类类型参数** + +
+ 查看变化 +
+ +```cs showLineNumbers {3-6} +public interface ISql : ISqlDispatchProxy +{ + [SqlExecute(@" + select * from person where id =@id; + select * from person")] + (Person, List) GetData(int id); // 注意返回值是 `(Person, List)` 组合 +} +``` + +
+
+ +5. **支持 `Minimal API` 应用** + +
+ 查看变化 +
+ +**[了解 `Minimal API` 应用](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/minimal-apis?view=aspnetcore-6.0)** + +```cs showLineNumbers {1,3-4,10,12-15} +var builder = WebApplication.CreateBuilder(args).Inject(); + +// 注册 Minimal 服务 +builder.Services.AddInjectMini(); + +var app = builder.Build(); + +app.UseHttpsRedirection(); + +app.UseInject(string.Empty); + +app.MapGet("/hello", () => +{ + return "Hello, Furion"; +}); + +app.Run(); +``` + +
+
+ +--- + +## v3.6.9(已发布,全新入口组件) + +:::tip 关于入口组件 + +相信从 `ASP.NET 5` 升级至 `ASP.NET 6` 的开发者都经历过微软多次变更初始化主机 `Program.cs` 和 `Startup.cs` 写法,甚至在 `.NET6+` 之后移除了 `Startup.cs` 的设计。 + +试问,`ASP.NET 7`,`ASP.NET 8` ... `ASP.NET N` 呢?会不会每一个版本都有不同的初始化方式,那后续项目如何无缝升级? + +**所以,为了保证一致的代码体验和后续无缝升级,推出了 `Serve.Run()`,即使未来创建方式变了,也不用担心,交给框架即可。** + +`Serve.Run` 模式也标记着 `Furion` 进入全新的极简入门时代。 + +::: + +- **新特性** + + -  新增 `Serve.Run()` 极简主机模式,真正实现极速入门。[95cac5b](https://gitee.com/dotnetchina/Furion/commit/95cac5b391b70d73bfc94147ee40eef529b2eec6) + -  新增 `TP.Wrapper(...)` 拓展方法,主要用来生成规范化的日志模板 [427999a](https://gitee.com/dotnetchina/Furion/commit/427999aba4847522ea91c42df6164e5fe69c5bc0) + -  新增 项目类型为 `` 的控制台项目 [fb08a65](https://gitee.com/dotnetchina/Furion/commit/fb08a6548d49921aa143d01389a2592b92e94e31) + -  新增 `BadPageResult` 错误页面类型 [!494](https://gitee.com/dotnetchina/Furion/pulls/494) + -  新增 `[SchemaId]` 特性,解决不同程序集相同的类名生成 `Swagger` 的 `SchemaId` 冲突问题 [#I5D3CU](https://gitee.com/dotnetchina/Furion/issues/I5D3CU) + -  新增 远程请求 `options.ApproveAllCerts()` 忽略所有客户端证书拓展 [eb7d18a](https://gitee.com/dotnetchina/Furion/commit/eb7d18ac183894d604cec1d4c8527466ef9c648a) + -  新增 判断是否是单文件环境部署静态属性 `App.SingleFileEnvironment` [de556f0](https://gitee.com/dotnetchina/Furion/commit/de556f0aaf87c41fe01cca2655f086890b2638a0) + -  新增 `WebApplicationBuilder.UseSerilogDefault()` 拓展 [e02524c](https://gitee.com/dotnetchina/Furion/commit/e02524c8a15fd660b6359285ef07a697878e6678) + +- **突破性变化** + + -  新增 项目类型为 `` 的控制台项目 [fb08a65](https://gitee.com/dotnetchina/Furion/commit/fb08a6548d49921aa143d01389a2592b92e94e31) + -  新增 `Serve.Run()` 极简主机模式,真正实现极速入门。[95cac5b](https://gitee.com/dotnetchina/Furion/commit/95cac5b391b70d73bfc94147ee40eef529b2eec6) + -  调整 未启用规范化结果时,`MVC` 验证失败返回 `BadPageResult()` 页面类型 [!494](https://gitee.com/dotnetchina/Furion/pulls/494) + +- **问题修复** + + -  修复 默认注册的 `services.AddResponseCaching();` 服务导致 `.axd` 内嵌资源请求错误问题 [!495](https://gitee.com/dotnetchina/Furion/pulls/495) + -  修复 `Oracle` 数据库执行 `sql` 必须要求命令参数和 `sql` 语言参数数量一致 [#I5D057](https://gitee.com/dotnetchina/Furion/issues/I5D057) + -  修复 `IHostService` 类型不能自动注册问题,之前只扫描了 `BackgroundService` 派生类 [968344](https://gitee.com/dotnetchina/Furion/commit/968344aa07348f4c06f914abf7b6f46174633d81) + -  修复 国产芯片主机不能识别 `dotnet run --urls` 参数问题 [6d4398](https://gitee.com/dotnetchina/Furion/commit/6d43983781e0c41228c4917a242141d011088bdb) + -  修复 远程请求上传文件不支持特定文件后缀问题,如 `.pem` 文件 [ba42198](https://gitee.com/dotnetchina/Furion/commit/ba42198c1cd58612f2b383349c37068aac78cdd7) + -  修复 一些程序集已破坏或程序集不完整导致主机无法启动问题 [d2dc3e4](https://gitee.com/dotnetchina/Furion/commit/d2dc3e4d09e726699c38ebb8cb7b83de0e97f46f) + -  修复 远程请求传入 `headers` 时类型为 `Dictionary` 导致转换异常问题 [#I5DHL9](https://gitee.com/dotnetchina/Furion/issues/I5DHL9) + -  修复 `Serilog` 单文件发布不生成日志文件 [I5DQ2B](https://gitee.com/dotnetchina/Furion/issues/I5DQ2B) + +- **其他更改** + + -  调整 远程请求默认客户端不检查 `SSL` 证书 [eb7d18a](https://gitee.com/dotnetchina/Furion/commit/eb7d18ac183894d604cec1d4c8527466ef9c648a) + -  调整 开放验证服务选项 `SuppressModelStateInvalidFilter` 属性为可配置 [!494](https://gitee.com/dotnetchina/Furion/pulls/495) + +- **文档** + + -  新增 `Serve.Run()` 文档 + -  新增 `HttpContext` 文档 + -  新增 `GlobalUsings` 文档 + -  新增 `TP` 全局静态类文档 + -  新增 中间件文档、筛选器文档、审计日志文档 + -  更新 跨域文档、远程请求文档 + +- **精彩贡献** + + - [!494](https://gitee.com/dotnetchina/Furion/pulls/494) 优秀 `Pull Request` 辩论典范 + +- **本期亮点** + +1. **极速入门** + +
+ 查看变化 +
+ +```cs showLineNumbers title="Program.cs" {3,6} +Serve.Run(); + +[DynamicApiController] +public class HelloService +{ + public string Say() + { + return "Hello, Furion"; + } +} +``` + +启动浏览器查看效果,惊呆了吗! + + + +
+
+ +2. **内置错误页** + +
+ 查看变化 +
+ +```cs showLineNumbers{1,7} +using Furion.FriendlyException; + +public IActionResult Add(Person person) +{ + if(!ModelState.IsValid) + { + return new BadPageResult(); + } +} +``` + + + +
+
+ +3. **`Swagger` 支持 `Markdown`** + +
+ 查看变化 +
+ +````cs showLineNumbers {4-62} +/// +/// 测试 Markdown +/// +/// +/// # 测试 `Markdown` 注释 +/// +/// ![](https://localhost:44316/images/logo.png) +/// +/// ```cs +/// Serve.Run(); +/// +/// [DynamicApiController +/// public class HelloService +/// { +/// public string Say() +/// { +/// return nameof(Furion); +/// } +/// } +/// ``` +/// +/// 功能还不错!!! +/// +/// | 商品 | 价格 | # 其他 | +/// |--------------|-----------|------------| +/// | Juicy Apples | 1.99 | *7* | +/// | Bananas | **1.89** | 5234 | +/// | Bananas | **1.89** | 5234 | +/// | Bananas | **1.89** | 5234 | +/// +/// ----- +/// +/// # Furion 探索版 +/// +/// > 在过去一年,实现 `Furion` 从无到有,编写文档已逾百万字,过程心酸开源人自知。 +/// > +/// > 这一路日夜兼程,嘲讽批评常伴眼耳,即便辛苦无奈、想过放弃,但为了那微不足道的存在感依然努力着。 +/// > +/// > 当然,也收获了不少...越来越多拥趸者,越发精湛技术能力,更高层次思维模式,还有许多跨界跨行朋友。 +/// > +/// > 在 《[开源指北] (https://gitee.com/opensource-guide/comments/)》中,我曾说道:“开源如同人的脸,好坏一面便知,缺点可能会受到嘲讽批评,优点也会收获赞扬尊重。别担心,他们正在塑造更好的你。” +/// > +/// > 所以,这一次重新起航,重塑 `Furion` 重塑自己。也许未来在某个 IT 圈但凡有人谈起 `.NET` 还能瞟到 `Furion` 的身影。 +/// +/// --- +/// +/// 🎉 探索 Furion 未来更多可能性,实现无第三方依赖的版本,所有模块功能按需安装,按需加载。 +/// +/// - 作者:[百小僧] (https://gitee.com/monksoul) +/// - 日期:2021 年 08 月 30 日 +/// +/// ## 环境 +/// +/// - IDE :[Microsoft Visual Studio Enterprise 2022 Preview(64 位) 版本 17.0.0 Preview 3.1] (https://visualstudio.microsoft.com/zh-hans/vs/preview/) +/// - SDK :[.NET SDK 6] (https://dotnet.microsoft.com/download/dotnet/6.0) +/// - 语言:[C# 10](https://docs.microsoft.com/zh-cn/dotnet/csharp/whats-new/csharp-10) +/// +/// ## 包说明 +/// +/// - `Furion.Core`:无第三方依赖,可在 `.NET 6` 所有项目类型中运行。 +/// - `Furion`:内部依赖 `Furion.Core` 且无第三方依赖,**聚焦 `Web` 应用**,采用共享框架 `` 模式 +/// +public void MarkdownTest() +{ + // .... +} +```` + + + +
+
+ +--- + +## v3.5.7(已发布) + +- **新特性** + + -  新增 `Options` 选项属性支持自定义 `Key` 名称,`[MapSettings("key")]` [#I5B2HN](https://gitee.com/dotnetchina/Furion/issues/I5B2HN) + -  新增 事件总线模块事件 `Id` 支持枚举类型 [2f328aa](https://gitee.com/dotnetchina/Furion/commit/2f328aa8213c8efe7a8480116985573cc6b7fce6) + -  新增 事件总线模块发布者 `PublishAsync` 和 `PublishDelayAsync` 重载 [2f328aa](https://gitee.com/dotnetchina/Furion/commit/2f328aa8213c8efe7a8480116985573cc6b7fce6) + -  新增 事件总线模块拓展方法:`Enum.ParseToString()` 和 `String.ParseToEnum()` [2f328aa](https://gitee.com/dotnetchina/Furion/commit/2f328aa8213c8efe7a8480116985573cc6b7fce6) + -  新增 **`Furion` 和 `SqlSugar` 脚手架** 🆕🆕🆕 [8d9293d](https://gitee.com/dotnetchina/Furion/commit/8d9293d1188670626f017ccea4ffb85ac315d2fc) + -  新增 `Dapper` 拓展全局配置委托 [#I5AYFX](https://gitee.com/dotnetchina/Furion/issues/I5AYFX) + -  新增 `sql` 转实体支持多种命名策略(纯大写,纯小写,带下划线分割等等),如 `Oracle` 数据库 [a90e245](https://gitee.com/dotnetchina/Furion/commit/a90e24516387e088b2c427e6b99d3dab937116c9) + -  新增 `FS.InitalContentTypeProvider()` 拓展方法,获取系统内所有支持的 `Content-Type` 文件提供器 [6099900](https://gitee.com/dotnetchina/Furion/commit/6099900472d93dab7012f0b091b05c914be11c4a) + +- **突破性变化** + + -  修复 彻底解决了 `Furion` 不能单文件发布的问题 [7e8e0b7](https://gitee.com/dotnetchina/Furion/commit/7e8e0b708bcdac670aa835dec5cd494d41ff3648) + +- **问题修复** + + -  修复 框架规范化文档 `Swagger` 不支持 `Controller` 派生类 `api` 路由问题,原生 `ASP.NET` 是支持的 [29e47bc](https://gitee.com/dotnetchina/Furion/commit/29e47bce3678767c4793ad254777704ab9dd7e03) + -  修复 基于 `Schema` 多租户配置无效问题 [6f820ce](https://gitee.com/dotnetchina/Furion/commit/6f820ce0f28dd27a6e265b969b5b4095de676106) + -  修复 指定实体 `[Table(schema:"dbo")]` 特性后 `Schema` 无效问题 [6f820ce](https://gitee.com/dotnetchina/Furion/commit/6f820ce0f28dd27a6e265b969b5b4095de676106) + -  修复 数据库视图不支持 `Schema` 配置问题 [6f820ce](https://gitee.com/dotnetchina/Furion/commit/6f820ce0f28dd27a6e265b969b5b4095de676106) + -  修复 规范化结果极端情况下出现 `空异常` 问题 [c9b0ef](https://gitee.com/dotnetchina/Furion/commit/c9b0ef09427418e2ccb88d3a4c02e7a29d9d510e) + +- **其他更改** + + -  调整 `axios-utils.ts` 和 `angular-utils.ts` ,新增请求拦截携带刷新 `Token` 的时机判断 [82f89bd](https://gitee.com/dotnetchina/Furion/commit/82f89bd95573aefa7075676af7f00c55507cb03b) + -  更新 规范化文档 `Swagger` 加载继承注释 `` 性能小优化 [5f06880](https://gitee.com/dotnetchina/Furion/commit/5f06880564ee8cd2e77caa5957ff18a0c489bdd2) + -  调整 脚手架模板,新增 `GlobalUsings.cs` 模式 + -  调整 对象映射默认支持忽略大小写 [!486](https://gitee.com/dotnetchina/Furion/pulls/486) + +- **文档** + + -  新增 `Furion` 单文件发布文档 + -  新增 `Furion + SqlSugar` 脚手架文档 + -  更新 事件总线文档、选项文档、即时通讯文档、`.NET5` 升级 `.NET6` 文档、依赖注入文档、跨域文档、数据加解密文档 + +- **本期亮点** + +1. **事件总线 `Id` 支持枚举类型** + +
+ 查看变化 +
+ +```cs showLineNumbers {7,10} +EventSubscribe("TO:DO")] // 字符串类型 +public async Task EventHandler1(EventHandlerExecutingContext context) +{ + // .... +} + +[EventSubscribe(YourEnum.Some)] // 枚举类型 +public async Task EventHandler2(EventHandlerExecutingContext context) +{ + var eventEnum = context.Source.EventId.ParseToEnum(); // 将事件 Id 转换成枚举对象 + // .... +} +``` + +
+
+ +2. **事件总线发布支持更简单调用** + +
+ 查看变化 +
+ +```cs showLineNumbers {5,6} +// 旧版本 +await _eventPublisher.PublishAsync(new ChannelEventSource("ToDo:Create", name)); + +// 新版本 +await _eventPublisher.PublishAsync("ToDo:Create", name); +await _eventPublisher.PublishAsync(YourEnum.Some); // 也支持枚举 +``` + +
+
+ +3. **选项支持属性自定义配置 `Key`** + +
+ 查看变化 +
+ +```json showLineNumbers {4} +"AppInfo": { + "Name": "Furion", + "Version": "1.0.0", + "Company_Name": "Baiqian" // 可以和属性不一样 +} +``` + +```cs showLineNumbers {6,7} +public class AppInfoOptions : IConfigurableOptions +{ + public string Name { get; set; } + public string Version { get; set; } + + [MapSettings("Company_Name")] // 支持自定义 + public string Company { get; set; } +} +``` + +
+
+ +4. **日志规范化模板** + +
+ 查看变化 +
+ +```cs showLineNumbers {2} +// 生成模板字符串 +var template = TP.Wrapper("Furion 框架", "让 .NET 开发更简单,更通用,更流行。", + "[作者] 百小僧", + "[当前版本] v3.5.3", + "[文档地址] http://furion.baiqian.ltd", + "[Copyright] 百小僧, 百签科技(广东)有限公司"); +Console.WriteLine(template); +``` + +输出结果 + +```bash showLineNumbers +┏━━━━━━━━━━━ Furion 框架 ━━━━━━━━━━━ +┣ 让 .NET 开发更简单,更通用,更流行。 +┣ +┣ 作者: 百小僧 +┣ 当前版本: v3.5.3 +┣ 文档地址: http://furion.baiqian.ltd +┣ Copyright: 百小僧, 百签科技(广东)有限公司 +┗━━━━━━━━━━━ Furion 框架 ━━━━━━━━━━━ + +``` + +
+
+ +--- + +## v3.4.2(已发布) + +- **新特性** + + -  新增 规范化文件 `EnableAllGroups` 功能,可以将多个分组合并到一个分组中 [9277b98](https://gitee.com/dotnetchina/Furion/commit/9277b982ce024bac8ab5117ba02c3bd96ad07972) + -  新增 `angular-utils` 客户端工具库,专门处理 `angular` 项目接口代理问题 [6c70584](https://gitee.com/dotnetchina/Furion/commit/6c705848a77fbf7234070d0ef9f053a85cc8838a) + -  新增 `Swagger` 支持单个接口更多描述功能(支持 `html`)[e5e1db0](https://gitee.com/dotnetchina/Furion/commit/e5e1db09710dab02966330063935bd5e5b7e4dc8) + -  新增 `Swagger` 接口 `[Obsolete]` 过时支持功能 [e5e1db0](https://gitee.com/dotnetchina/Furion/commit/e5e1db09710dab02966330063935bd5e5b7e4dc8) + -  新增 动态 `API` 的 `[ApiDescriptionSettings]` 特性 和`DynamicApiControllerSettings` 配置 的 `ForceWithRoutePrefix` 参数,支持强制复写 `[Route]` 特性并添加 `DefaultRoutePrefix` [#I59B74](https://gitee.com/dotnetchina/Furion/issues/I59B74) + +- **突破性变化** + + -  新增 默认内置 `GBK`,`Windows-1252`, `Shift-JIS`, `GB2312` 等编码支持 [c456ecb](https://gitee.com/dotnetchina/Furion/commit/c456ecb225b099e5d24add32024f16c359414532) + -  新增 `Furion` 和 `SqlSugar` 脚手架 + +- **问题修复** + + -  修复 `` 不能跨程序集问题 [3b9d39c](https://gitee.com/dotnetchina/Furion/commit/3b9d39ce691f9505c5541a790103fbb0ba6d35af) + -  修复 `` 不支持带参数,不支持隐式实现接口注释问题 [#I59A6W#note_10699021](https://gitee.com/dotnetchina/Furion/issues/I59A6W#note_10699021_link) + -  修复 `v3.3.1` 版本导致 `Swagger` 不能显示问题 [6763352](https://gitee.com/dotnetchina/Furion/commit/676335264478d68b99db009d32b65de781702605) + -  修复 远程请求、`JSON`以及 `Web` 页面不支持 `GBK`,`GB2312` 等国标编码问题 [c456ecb](https://gitee.com/dotnetchina/Furion/commit/c456ecb225b099e5d24add32024f16c359414532) + -  修复 远程请求响应报文设置了 `Content-Type:charset=` 不能自动转换编码问题 [c456ecb](https://gitee.com/dotnetchina/Furion/commit/c456ecb225b099e5d24add32024f16c359414532) + +- **其他更改** + + -  新增 `axios-utils.ts` 和 `angular-utils.ts` 多客户端支持 + +- **文档** + + -  新增 `GlobalUsings` 文档 [文档地址](http://furion.baiqian.ltd/blog/global-usings) + -  新增 请求大小/上传大小限制文档 [文档地址](http://furion.baiqian.ltd/docs/file-provider/#317-%E8%AF%B7%E6%B1%82%E5%A4%A7%E5%B0%8F%E6%8E%A7%E5%88%B6%E4%B8%8A%E4%BC%A0%E6%96%87%E4%BB%B6%E5%A4%A7%E5%B0%8F%E6%8E%A7%E5%88%B6) + -  更新 规范化文档,`Worker Service` 文档,动态 API 文档 + +- **本期亮点** + +1. **启用 `All Groups` 分组功能** + +
+ 查看变化 +
+ +有时候我们为了更好的对接口进行归类,配置了 `Swagger` 多个分组的功能,但这样也对生成客户端请求代码造成了困扰,所以添加了新的配置: + +```json showLineNumbers {2-3} +{ + "SpecificationDocumentSettings": { + "EnableAllGroups": true + } +} +``` + +
+
+ +2. **接口过时控制** + +
+ 查看变化 +
+ +当我们某个接口已经过时,提示尽早调用最新接口,只需要在方法上面贴 `[Obsolete]` 即可,如: + +```cs showLineNumbers {1,7} +[Obsolete("GetName() 已经过时,请调用 GetFrameworkName() 替代")] +public string GetName() +{ + return nameof(Furion); +} + +[Obsolete] +public string Other() +{ + // ... +} +``` + + + +
+
+ +3. **单一接口更多描述** + +
+ 查看变化 +
+ +在该版本新增了 `[ApiDescriptionSettings]` 的 `Description` 属性,支持定义更多描述,如: + +```cs showLineNumbers {1} +[ApiDescriptionSettings(Description = "我是一段描述,显示更多内容 ")] +public string add() +{ + //.... +} +``` + + + +
+
+ +--- + +## v3.3.3(已发布) + +- **新特性** + + -  新增 远程请求文件上传自动识别 `Content-Type` 和 `Mime` [#I57ZMN](https://gitee.com/dotnetchina/Furion/issues/I57ZMN) + -  新增 远程请求方法支持设置 `Content-Type` 和 `Encoding` [#I57ZMN](https://gitee.com/dotnetchina/Furion/issues/I57ZMN) + -  新增 根据文件名获取 `Content-Type` 和 `Mime` 类型 [#8f78184](https://gitee.com/dotnetchina/Furion/commit/8f78184f8661830744592c054b65d503346c1b27) + -  新增 规范化文档支持授权访问 [#32aa3b6](https://gitee.com/dotnetchina/Furion/commit/32aa3b6328d23a5885033837883c7b546e898d43) + -  新增 代码注释,规范化文档注释 `inheritdoc` 语法支持 ❤️️️️ [#159A6W](https://gitee.com/dotnetchina/Furion/issues/I59A6W) + -  新增 `Vue2/3`,`React 16.8+`,`Angular 9+` 前端请求工具库,实现后端 API 代理 [axios-utils](https://gitee.com/dotnetchina/Furion/tree/v4/clients/axios) + +- **突破性变化** + + -  新增 代码注释,规范化文档注释 `inheritdoc` 语法支持 ❤️️️️ [#159A6W](https://gitee.com/dotnetchina/Furion/issues/I59A6W) + -  更新 `.NET` 所有依赖包至 `v6.0.5` 版本 + +- **问题修复** + + -  修复 自定义全局异常 `Exception` 后导致获取错误行号,文件空异常问题 [#I53EGM](https://gitee.com/dotnetchina/Furion/issues/I53EGM) + -  修复 配置数据库上下文传递空委托导致空引用异常问题 [#I519AW](https://gitee.com/dotnetchina/Furion/issues/I519AW) + -  修复 字符串模板模板 `Render` 拓展方法返回 `void` 问题,应该返回 `string` [Github-#99](https://github.com/MonkSoul/Furion/issues/99#issuecomment-1073131906) + -  修复 远程请求文件上传出现空情况问题(原因是缺失 `Content-Type` )[I57ZMN](https://gitee.com/dotnetchina/Furion/issues/I57ZMN) + +- **其他更改** + + -  调整 框架源码引入 `GlobalUsings` 机制,减少代码体积 [#7e9cc1c](https://gitee.com/dotnetchina/Furion/commit/7e9cc1c205750906cddd540ad08a4c02f14efa3a) + -  调整 跨域请求的预检设置,如果未设置,则默认为 24 小时,主要解决前端多次发送 204 预检问题 [4a11e7c](https://gitee.com/dotnetchina/Furion/commit/4a11e7c9fa20b4419ac00f6ad21c078500d00791) + -  更新 视图引擎反射性能 + +- **文档** + + -  新增 粘土对象序列化 `JSON` 配置文档 + -  新增 前端解密 `JWT` 文档 + -  新增 将 `byte[]` 转 `url` 文档 + -  更新 二级虚拟目录部署文档,远程请求文档,文件上传文档,安全授权文档、规范化文档 + +- **本期亮点** + + - ❤️️️️ **根据文件名获取 `MIME` 或 `Content-Type` 类型** + +
+ 查看变化 +
+ +```cs showLineNumbers +var success = FS.TryGetContentType("image.png", out var contentType); // image/png +``` + +- ❤️️️️ **支持 `Swagger` 配置登录后才能访问** + +```json showLineNumbers {2-6} +{ + "SpecificationDocumentSettings": { + "LoginInfo": { + "Enabled": true, + "CheckUrl": "检查登录地址", + "SubmitUrl": "提交登录地址" + } + } +} +``` + + + +[查看详细文档](./specification-document#6529-带登录的-swagger-文档) + +
+
+ +- ❤️️️️ **支持代码注释继承,Swagger 文档注释也支持** + +
+ 查看变化 +
+ +```cs showLineNumbers {1,4,10} +/// +public class TestInheritdoc : ITestInheritdoc, IDynamicApiController +{ + /// + public string GetName() + { + return "Furion"; + } + + /// + public string GetVersion() + { + return "3.3.3"; + } +} + +/// +/// 测试注释继承 +/// +public interface ITestInheritdoc +{ + /// + /// 获取名称 + /// + /// + string GetName(); + + /// + /// 获取版本 + /// + /// + string GetVersion(); +} +``` + + + +[查看详细文档](./specification-document#6530-inheritdoc-实现注释继承) + +
+
+ +--- + +## v3.2.0(已发布) + +- **新特性** + + -  新增 `IFormFile` 拓展方法 `ToByteArray()` [da69640](https://gitee.com/dotnetchina/Furion/commit/da69640da2331e2c8582b88bbda965c5ad7ecbe0) + -  新增 规范化文档 `ServeDir` 虚拟目录配置功能,支持一键将一级目录切换至二级目录部署(IIS)[8718392](https://gitee.com/dotnetchina/Furion/commit/87183921ac8b6f9856db01b4de679b858a58e753) + +- **突破性变化** + + -  更新 所有依赖包至最新版 + -  优化 依赖注入模块核心代码,移除注册服务采用反射机制,减少反射性能损耗 [acdb315](https://gitee.com/dotnetchina/Furion/commit/acdb3157af92891610a1ba6d317b6af3f09e233f) + +- **问题修复** + + -  修复 `Swagger` 的 `schema` 类型如果是 `C# Object` 类型无法正确生成前端代码问题 [Swagger 官方 Issue](https://github.com/swagger-api/swagger-codegen-generators/issues/692) [1a25274](https://gitee.com/dotnetchina/Furion/commit/1a252747fd60fc87a8ed4425c8edf7803f96ce43) + -  修复 `Worker Service` 发布成 `Windows Services` 时日志绝对路径问题 感谢 [@jacoat](https://gitee.com/jacoat) [!467](https://gitee.com/dotnetchina/Furion/pulls/467) + -  修复 `Nginx` 和 `IIS` 对二级虚拟目录配置不同导致 `404` 问题 [8718392](https://gitee.com/dotnetchina/Furion/commit/87183921ac8b6f9856db01b4de679b858a58e753) + -  修复 远程请求模块未初始化 `OnRequestFailded` 导致空异常问题 [#I54PK7](https://gitee.com/dotnetchina/Furion/issues/I54PK7) + -  修复 依赖注入反射出现 `Not found Method` bug [#I546L1](https://gitee.com/dotnetchina/Furion/issues/I546L1) + +- **其他更改** + + -  调整 定时任务失败后异常处理逻辑,感谢 [@程小胜](https://gitee.com/cxs1992) [!463](https://gitee.com/dotnetchina/Furion/pulls/463) + +- **文档** + + -  更新 定时任务文档,日志文档 + -  新增 文件上传/下载 文档,包含单文件/多文件/Base64/Byte[] + +--- + +## v3.1.0(已发布) + +- **新特性** + + -  新增 远程请求模块异常 `Http` 状态码 [!462](https://gitee.com/dotnetchina/Furion/pulls/462) + -  新增 动态 WebAPI 支持小驼峰配置 [#I4W1R4](https://gitee.com/dotnetchina/Furion/issues/I4W1R4) + -  新增 远程请求 `SendAsByteArrayAsync` 等一系列方法,支持返回 `byte[]` [!452](https://gitee.com/dotnetchina/Furion/pulls/452) + -  新增 远程请求 `GZip` 压缩支持 [#I506S5](https://gitee.com/dotnetchina/Furion/issues/I506S5) + +- **突破性变化** + + -  升级 `.NET6` 依赖包全部升级至 `NuGet` 最新版 `v6.0.3` + +- **问题修复** + + -  修复 `.NET6 WebApplication` 模式二级虚拟目录问题 [#I4UZLM](https://gitee.com/dotnetchina/Furion/issues/I4UZLM) [#I4PZ0C](https://gitee.com/dotnetchina/Furion/issues/I4PZ0C) + -  修复 日期验证不支持 `2022-03-01 0:00:00`(现在支持小时域 `0` 和 `00`) 问题 [#I4Y3NT](https://gitee.com/dotnetchina/Furion/issues/I4Y3NT) + -  修复 环境配置和文件配置优先级问题 + -  修复 脱敏模块替换敏感词汇只替换最后一个 bug [#I4YFA0](https://gitee.com/dotnetchina/Furion/issues/I4YFA0) + -  修复 远程请求返回字符串个别情况出现中文乱码问题 [#I50GBD](https://gitee.com/dotnetchina/Furion/issues/I50GBD) + -  修复 `[DataValidate]` 配置 `AllowNullValue` 和 `AllowEmptyString` 无效问题 [#I4ZZBE](https://gitee.com/dotnetchina/Furion/issues/I4ZZBE) + +- **其他更改** + + - [过时] 标记 `Furion.Extras.Logging.Serilog` 拓展包 `IWebHost` 拓展为过时状态 + +- **文档** + + -  文档 优化文档体验,新增面包屑导航,重写文档缓存,提升文档访问速度 + -  文档 更新动态 API 文档、配置文档、远程请求文档 + -  文档 更新二级虚拟目录文档 + +- **本期亮点** + +1. 新增动态 `WebApi` 支持小驼峰路径,如 `GetMyName` -> `getMyName`: + +
+ 查看变化 +
+ +```json showLineNumbers +{ + "DynamicApiControllerSettings": { + "LowercaseRoute": false, + "KeepName": true, + "AsLowerCamelCase": true + } +} +``` + +
+
+ +2. 支持 `.NET6 WebApplication` 模式二级虚拟目录配置: + +
+ 查看变化 +
+ +```cs showLineNumbers {title="Progame.cs"} +app.UseVirtualPath(app => +{ + app.UseInject(String.Empty); // 注意 String.Empty 只是例子,可以不填或填其他的,见一分钟入门 + app.MapRouteControllers(); +}); +``` + +
+
+ +--- + +## v3.0.0(已发布,.NET6) + +:::warning v3+ 版本说明 + +**Furion v3.x 版本采用 .NET6 构建。** + +::: + +- **新特性** + + -  新增 远程请求支持 `GET` 请求自动转换 `类类型` 类型对象 [#I4HR5Q](https://gitee.com/dotnetchina/Furion/issues/I4HR5Q) + +- **突破性变化** + + -  升级 全面支持 `.NET6` 版本 + +- **问题修复** + + -  修复 开启规范化结果并自定义全局异常导致异常经过 `OnSucceeded` 过滤器 bug [#I4DTVL](https://gitee.com/dotnetchina/Furion/issues/I4DTVL) + -  修复 `.NET5.0.5+` 和 `.NET6` 微软底层修改了 `[ApiController]` 验证失败返回 `IActionResult` 类型 [#I4ISOK](https://gitee.com/dotnetchina/Furion/issues/I4ISOK) + -  修复 `EFCore 6.0` 适配 `SqlServer 2005+` 出错 [#I4ILA5](https://gitee.com/dotnetchina/Furion/issues/I4ILA5) + -  修复 .NET6 获取配置对象如果不存在返回 null 问题,.NET5 则返回初始对象 [94ae4d](https://gitee.com/dotnetchina/Furion/commit/94ae4d8c9b0fe7eb4d713a171f953c0d3c5a76ac) + -  修复 `Sql` 命令参数传入 `Clay` 类型异常问题 [#I4D21Q](https://gitee.com/dotnetchina/Furion/issues/I4D21Q) + -  修复 `Cron` 定时任务特性方式 bug [#I4OJQI](https://gitee.com/dotnetchina/Furion/issues/I4OJQI) + +- **其他更改** + + -  移除 ~~`ToPagedList` 泛型约束~~ [d0244d](https://gitee.com/dotnetchina/Furion/commit/d0244dc33a0e5236158cdcdff21d086e00a42ee7) + +- **文档** + + -  文档 查看 **[.NET6 一分钟入门](http://furion.baiqian.ltd/docs/get-start-net6)** + -  文档 查看 **[Furion v2 升级 v3](http://furion.baiqian.ltd/docs/net5-to-net6)** + +- **特别鸣谢** + + - [KaneLeung](https://gitee.com/KaneLeung) + +--- + +## v2.20(已发布,全新事件总线) + +:::warning v2.20+ 版本说明 + +**在 `Furion v2.20+` 版本后采用 [Jaina](https://gitee.com/dotnetchina/Jaina) 事件总线替换原有的 `EventBus`** + +::: + +- **新特性** + + -  新增 远程请求支持 `GET` 请求自动转换 `类类型` 类型对象 [#I4HR5Q](https://gitee.com/dotnetchina/Furion/issues/I4HR5Q) + +- **突破性变化** + + -  优化 `EventBus` 模块,采用 [Jaina](https://gitee.com/dotnetchina/Jaina) 方式 + +- **问题修复** + + -  修复 开启规范化结果并自定义全局异常导致异常经过 `OnSucceeded` 过滤器 bug [#I4DTVL](https://gitee.com/dotnetchina/Furion/issues/I4DTVL) + -  修复 `.NET5.0.5+` 微软底层修改了 `[ApiController]` 验证失败返回 `IActionResult` 类型 [#I4ISOK](https://gitee.com/dotnetchina/Furion/issues/I4ISOK) + -  修复 远程请求上传文件异常 [0c0752](https://gitee.com/dotnetchina/Furion/commit/0c0752c624799d7d3c7661a8f36a93983399bb59) + -  修复 框架启动不支持环境变量 `ASPNETCORE_HOSTINGSTARTUPASSEMBLIES` 配置 [!438](https://gitee.com/dotnetchina/Furion/pulls/438) + -  修复 定时任务内存和 CPU 占用及特殊情况下空异常问题 [12c65de](https://gitee.com/dotnetchina/Furion/commit/12c65debf552c57780679e6a567a9dd9fb077f46) + -  修复 默认控制器启用规范化结果无效 bug[c7a4a5e](https://gitee.com/dotnetchina/Furion/commit/c7a4a5ef8c3282d245cbe04124cf379d381d496f) + -  修复 依赖注入 `InjectionAttribute` 特性的 `ExceptInterfaces` 单词拼写错误问题 [!436](https://gitee.com/dotnetchina/Furion/pulls/436) + -  修复 `Sql` 命令参数传入 `Clay` 类型异常问题 [#I4D21Q](https://gitee.com/dotnetchina/Furion/issues/I4D21Q) + +- **其他更改** + + -  更新 `InjectionAttribute` 代码 [!435](https://gitee.com/dotnetchina/Furion/pulls/435) + -  移除 ~~`ToPagedList` 泛型约束~~ [d0244d](https://gitee.com/dotnetchina/Furion/commit/d0244dc33a0e5236158cdcdff21d086e00a42ee7) + +- **文档** + + -  新增 事件总线新文档 + +--- + +## v2.19(已发布) + +- **新特性** + + -  新增 定时任务监听器 `ISpareTimeListener` [#I468Q1](https://gitee.com/dotnetchina/Furion/issues/I468Q1) + -  新增 执行 `Sql` 支持 `JsonElement` 参数 [61985d6](https://gitee.com/dotnetchina/Furion/commit/61985d6a300485d553cbe8461b01f01bcd0936ef) + -  新增 `Swagger` 配置枚举及标签排序过滤器 [#I46LON](https://gitee.com/dotnetchina/Furion/issues/I46LON) [!404](https://gitee.com/dotnetchina/Furion/pulls/404) + -  新增 远程请求 `application/octet-stream` 类型默认支持 [d9bad03](https://gitee.com/dotnetchina/Furion/commit/d9bad0320cc4a204e24bc3a070517ebce4cdc5d7) + -  新增 远程请求代理模式请求报文头支持 `IDictionary` 类型。[0204c0a](https://gitee.com/dotnetchina/Furion/commit/0204c0afe2de5c28ebbd44b29131e701b93ae8b8) + -  新增 `MongoDB` 拓展类,添加更多常用操作方法 [!423](https://gitee.com/dotnetchina/Furion/pulls/423) + -  新增 `DateTimeOffset?` 转换 `DateTime` 拓展(包含互换) [!432](https://gitee.com/dotnetchina/Furion/pulls/432) + +- **问题修复** + + -  修复 `Scoped.Create` 在 `EFCore` 进行 `Add-Migration` 时候报空异常问题,原因是在 `PM` 环境中不存在根服务[0853e74](https://gitee.com/dotnetchina/Furion/commit/0853e74de90718fce9c0892e2ee4da597f62a918) + -  修复 定时任务执行异常后异常一直驻留内存问题,修正为执行成功自动清空过去异常 [197a62b](https://gitee.com/dotnetchina/Furion/commit/197a62bb4a7df34eb2c0dbda65121e9cf00d905c) + -  修复 `Jwt` 拓展包不正确的代码导致 IOptions 失效[#I46LUP](https://gitee.com/dotnetchina/Furion/issues/I46LUP) + -  修复 `Swagger` 枚举 `Schema` 过滤器不输出值问题 [#I46LON](https://gitee.com/dotnetchina/Furion/issues/I46LON) [!404](https://gitee.com/dotnetchina/Furion/pulls/404) + -  修复 `Swagger` 处理非 `int` 类型枚举转换 bug [#I46QJ9](https://gitee.com/dotnetchina/Furion/issues/I46QJ9) + -  修复 视图引擎编译模板生成 `dll` 后再次加载出现 `IL` 格式化错误问题 [ff52d38](https://gitee.com/dotnetchina/Furion/commit/ff52d383718b4d34968619f17c9d54d8718b4f3f) + -  修复 管道 `Channel` 读取器无法释放 `Handler` 对象问题 [10f4a90](https://gitee.com/dotnetchina/Furion/commit/10f4a900ee558a29f40ae21366a0eba83eceb3eb) + -  修复 `Worker Services` 下日志不输出问题 [c482548](https://gitee.com/dotnetchina/Furion/commit/c48254822c09092906ef77f04d54497e27665a92) + -  修复 远程请求 `multipart/form-data` 类型对接微信小程序上传文件 问题 [d9bad03](https://gitee.com/dotnetchina/Furion/commit/d9bad0320cc4a204e24bc3a070517ebce4cdc5d7) + -  修复 工作单元上下文在某些情况下共享事务失效问题 [006d439](https://gitee.com/dotnetchina/Furion/commit/006d439de3357d4d58ea6d7d3f9d51771a7b604e) + -  修复 `Swagger` 枚举值在 `GET` 请求中 `Schema` 显示不正确问题 [fb72fd7](https://gitee.com/dotnetchina/Furion/commit/fb72fd7c98de5bf2246dd1bf08200152bd7ab7a0) + -  修复 远程请求 `404` 不走异常过滤器问题 [!426](https://gitee.com/dotnetchina/Furion/pulls/423) + -  修复 自定义事件总线并发情况下调用完成后无法正确处理队列数据 [!429](https://gitee.com/dotnetchina/Furion/pulls/429) + -  修复 `v2.19+` 版本之后模块化开发加载外部程序集失效问题 [!433](https://gitee.com/dotnetchina/Furion/pulls/433) + -  修复 定时任务 SpareTime 频繁检查导致 CPU 增高问题 [aa0a2ee](https://gitee.com/dotnetchina/Furion/commit/aa0a2eec95f6ed2b74c681877498900726267d82) + +- **特别鸣谢** + + - [YaChengMu](https://gitee.com/YaChengMu) + +--- + +## v2.18(已发布) + +- **新特性** + + -  新增 `Furion.Tools.CommandLine` 拓展库 [查看源码](https://gitee.com/dotnetchina/Furion/tree/v4/tools/Furion.Tools/Furion.Tools.CommandLine) + -  新增 基于 `AsyncLocal` 的 `CallContext` 实现 [9057a21](https://gitee.com/dotnetchina/Furion/commit/9057a212aab8057b668086bd14369fa68ce120df) + -  新增 远程请求可配置请求异常重试策略 [656da87](https://gitee.com/dotnetchina/Furion/commit/656da87a667c2da7d82425cdcd47146e99602d65) + -  新增 远程请求 `OnRequestFailded` 事件 [4a3da4b](https://gitee.com/dotnetchina/Furion/commit/4a3da4ba2c69380fe5f8c2fda80054544c0a3468) + +- **突破性变化** + + -  移除 ~~`Scoped` 所有带返回值方法~~ [656da87](https://gitee.com/dotnetchina/Furion/commit/656da87a667c2da7d82425cdcd47146e99602d65) -  调整 **在 `ConfigureService` 中调用 `App.GetOptions<>()` 获取配置逻辑** [afa4ac3](https://gitee.com/dotnetchina/Furion/commit/afa4ac347152ccac37bd1d0f9af1e8ffb665a662) + +> 在过去,很多开发者总是喜欢在 `Startup.cs` 配置服务的 `ConfigureService` 方法中解析服务,这样导致内存存在溢出风险,GC 无法回收。 +> 正确的方式是尽可能的避免 `ConfigureService` 中解析服务。**如果需要在【启动时】获取 `配置选项`,请使用 `App.GetConfig(路径, true)` 代替 `App.GetOptions`**。 + +- **问题修复** + + -  修复 v2.16+ 版本重构 `AppDbContextBuilder` 之后写错实体类型 [#I45E6M](https://gitee.com/dotnetchina/Furion/issues/I45E6M) + -  修复 远程请求单个值序列化错误处理方式 [3282eba](https://gitee.com/dotnetchina/Furion/commit/3282eba2cecb505e339ef3f9c8e823f84dcb43f0) + -  修复 v2.17.3+ 单元测试创建 `TestServer` bug [#I45JR3](https://gitee.com/dotnetchina/Furion/issues/I45JR3) + -  修复 `Retry.Invoke` 正常方法死循环 bug [!392](https://gitee.com/dotnetchina/Furion/pulls/392) + -  修复 刷新 `Token` 生成新 `Token` 存在数组/集合类型导致 `Key` 重复异常问题 [aeea2b1](https://gitee.com/dotnetchina/Furion/commit/aeea2b1b19434f3171bd1c77be057ca36ecf9be2) + -  修复 远程请求序列化引用类型对象(不含 `string`)不正确的处理 [93cf63a](https://gitee.com/dotnetchina/Furion/commit/93cf63a023f3372b80edb5debc46271d2281318a) + -  修复 `AppDbContext` 默认租户属性受工作单元影响问题 [e51557f](https://gitee.com/dotnetchina/Furion/commit/e51557fdf37ae5646b2ea37c227c970eccdbed38) + +- **文档** + + -  新增 包管理工具文档 + -  更新 模板引擎、`Sql` 操作,`数据库上下文` 等等文档 + +--- + +## v2.17(已发布) + +- **新特性** + + -  新增 `IPC(Inter-Process Communication,进程间通信)` 模块功能,目前提供进程内通信和共享内存进程外通讯 [ProcessChannel](https://gitee.com/dotnetchina/Furion/tree/v4/framework/Furion/ProcessChannel) + -  新增 远程请求 `application/xml` 和 `text/xml` 默认支持 [4753a1a](https://gitee.com/dotnetchina/Furion/commit/4753a1aed527a6282fe6c05036de9d50bd3b3dd8) + -  新增 控制台全局异常拦截 [4a4fe1f](https://gitee.com/dotnetchina/Furion/commit/4a4fe1f40e1856ea36a0c0d19ca625d3f7bf95b7) + -  新增 支持自定义 `.json` 配置文件扫描目录 [3e2910a](https://gitee.com/dotnetchina/Furion/commit/3e2910a8b775fb6323e293b020bbe7cdfb4c6436) + -  新增 支持数据库实体接口显式实现接口配置 [9610a0a](https://gitee.com/dotnetchina/Furion/commit/9610a0a481f4f78770bc2fc3ed4cabbef2a8f937) + -  新增 控制台应用程序全局拦截 `[IfException]` 支持 [4a4fe1f](https://gitee.com/dotnetchina/Furion/commit/4a4fe1f40e1856ea36a0c0d19ca625d3f7bf95b7) + -  新增 依赖注入模块接口可以限制实现类生存周期,实现类也支持复写生存周期 [d2ce089](https://gitee.com/dotnetchina/Furion/commit/d2ce089130300cdd8b1bc6792f325c5d38ee9404) + +- **突破性变化** + + -  新增 `Oops.Retry()` 重试策略功能至新类:`Retry.Invoke()` [6a7bbd0](https://gitee.com/dotnetchina/Furion/commit/6a7bbd0b30a653b9a42d340a63520485aa6bbfa4) + -  移除 ~~`IHttpContextAccessor.SigninToSwagger()` 拓展~~,请使用 `IHttpContextAccessor.HttpContext.SigninToSwagger()`,退出也一样 + -  移除 ~~全局处理 `Request Body` 重复读处理 `Request.EnableBuffering()`~~ [d92c24b](https://gitee.com/dotnetchina/Furion/commit/d92c24bdb43bfb01643007ebb6a4ee42a5c738e9) + +- **问题修复** + + -  修复 规范化状态码过滤逻辑错误问题 [#I44JYS](https://gitee.com/dotnetchina/Furion/issues/I44JYS) + -  修复 非关系型数据库(内存数据库)注册及操作异常 [e167651](https://gitee.com/dotnetchina/Furion/commit/e1676512a54374427bedbde17cd8cb59d7852557) + -  修复 远程请求默认序列化问题 [a55603b](https://gitee.com/dotnetchina/Furion/commit/a55603bf7ed109296375dbeffc31591a6f8f8e49) + -  修复 定时任务零点/整点提前一秒触发问题 [#I4321L](https://gitee.com/dotnetchina/Furion/issues/I4321L) + -  修复 友好异常在子类重写抽象类方法内部抛异常无法获取的问题 [4a4fe1f](https://gitee.com/dotnetchina/Furion/commit/4a4fe1f40e1856ea36a0c0d19ca625d3f7bf95b7) + -  修复 非 `Web` 项目抛异常问题 [4a4fe1f](https://gitee.com/dotnetchina/Furion/commit/4a4fe1f40e1856ea36a0c0d19ca625d3f7bf95b7) + -  修复 数据库实体模型贴 `[NotMapper]` 特性无效 [#I44MNO](https://gitee.com/dotnetchina/Furion/issues/I44MNO) + +- **其他更改** + + -  调整 Swagger 生成泛型 SchemaIds 默认连接符,由 `Of` 改为 `_` [81946b6](https://gitee.com/dotnetchina/Furion/commit/81946b64e81d9e290f80cd5bcebdb69c99001153) + +--- + +## v2.16(已发布) + +- **新特性** + + -  新增 **`MVC` 控制器支持规范化处理 [#I427Z2](https://gitee.com/dotnetchina/Furion/issues/I427Z2)** + -  新增 `throw Oops.Bah()` 抛出业务异常(状态码 `400`) + -  新增 `UnifyResultSettings` 规范化 `json` 配置选项 [#I42NY7](https://gitee.com/dotnetchina/Furion/issues/I42NY7) + -  新增 多语言自定义配置资源文件名及自定义程序集 [#I434YJ](https://gitee.com/dotnetchina/Furion/issues/I434YJ) + +- **突破性变化** + + -  调整 规范化结果 `IUnifyResultProvider` 参数 **(破坏性更改)** [#I427Z2](https://gitee.com/dotnetchina/Furion/issues/I427Z2) + -  调整 `IJsonSerializerProvider` 接口参数,去掉 `inherit` 参数 [a55603b](https://gitee.com/dotnetchina/Furion/commit/a55603bf7ed109296375dbeffc31591a6f8f8e49) + +
+ 查看变化 +
+ +:::important 新版本自定义规范化结果 + +```cs showLineNumbers {15-16} +using Furion.DataValidation; +using Furion.DependencyInjection; +using Furion.UnifyResult.Internal; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using System; +using System.Threading.Tasks; + +namespace Furion.UnifyResult +{ + /// + /// RESTful 风格返回值 + /// + [SuppressSniffer, UnifyModel(typeof(RESTfulResult<>))] + public class RESTfulResultProvider : IUnifyResultProvider + { + /// + /// 异常返回值 + /// + /// + /// + /// + public IActionResult OnException(ExceptionContext context, ExceptionMetadata metadata) + { + return new JsonResult(RESTfulResult(metadata.StatusCode, errors: metadata.Errors)); + } + + /// + /// 成功返回值 + /// + /// + /// + /// + public IActionResult OnSucceeded(ActionExecutedContext context, object data) + { + return new JsonResult(RESTfulResult(StatusCodes.Status200OK, true, data)); + } + + /// + /// 验证失败返回值 + /// + /// + /// + /// + public IActionResult OnValidateFailed(ActionExecutingContext context, ValidationMetadata metadata) + { + return new JsonResult(RESTfulResult(StatusCodes.Status400BadRequest, errors: metadata.ValidationResult)); + } + + /// + /// 特定状态码返回值 + /// + /// + /// + /// + /// + public async Task OnResponseStatusCodes(HttpContext context, int statusCode, UnifyResultSettingsOptions unifyResultSettings) + { + // 设置响应状态码 + UnifyContext.SetResponseStatusCodes(context, statusCode, unifyResultSettings); + + switch (statusCode) + { + // 处理 401 状态码 + case StatusCodes.Status401Unauthorized: + await context.Response.WriteAsJsonAsync(RESTfulResult(statusCode, errors: "401 Unauthorized") + , App.GetOptions()?.JsonSerializerOptions); + break; + // 处理 403 状态码 + case StatusCodes.Status403Forbidden: + await context.Response.WriteAsJsonAsync(RESTfulResult(statusCode, errors: "403 Forbidden") + , App.GetOptions()?.JsonSerializerOptions); + break; + default: break; + } + } + + /// + /// 返回 RESTful 风格结果集 + /// + /// + /// + /// + /// + /// + private static RESTfulResult RESTfulResult(int statusCode, bool succeeded = default, object data = default, object errors = default) + { + return new RESTfulResult + { + StatusCode = statusCode, + Succeeded = succeeded, + Data = data, + Errors = errors, + Extras = UnifyContext.Take(), + Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() + }; + } + } +} +``` + +::: + + + + +- **问题修复** + + -  修复 `MVC` 控制器启用规范化处理后返回 `new Json({})` 对象为 `null` 问题 [#I4354S](https://gitee.com/dotnetchina/Furion/issues/I4354S) + +- **其他更改** + + -  更新 多语言底层设计,取消需要创建 `Lang.cs` 空类的要求 [#I434YJ](https://gitee.com/dotnetchina/Furion/issues/I434YJ) + -  更新 `MiniProfiler` 性能,减少不必要的监听 + +--- + +## v2.15(已发布) + +- **新特性** + + -  新增 `Db.GetDbRepository<定位器>()` 静态方法 [#I41MZP](https://gitee.com/dotnetchina/Furion/issues/I41MZP) + -  新增 远程请求缺省序列化配置选项 [#I41PBW](https://gitee.com/dotnetchina/Furion/issues/I41PBW) + +- **问题修复** + + -  修复 自动扫描接口进行依赖注入获取首个接口错误 bug [#I41D1M](https://gitee.com/dotnetchina/Furion/issues/I41D1M) + -  修复 `IRepository` 没有实现 `IRepository` 问题 [#I41MZP](https://gitee.com/dotnetchina/Furion/issues/I41MZP) + -  修复 远程请求缺省序列化 bug [#I41PBW](https://gitee.com/dotnetchina/Furion/issues/I41PBW) + -  修复 `AppDbContext.Tenant` 多租户空异常问题 [#I421DA](https://gitee.com/dotnetchina/Furion/issues/I421DA) + -  修复 `Worker Service` 多个 `Worker` 定时任务阻塞问题 [82a79cc](https://gitee.com/dotnetchina/Furion/commit/82a79cce0a3d9b09f4090b7363c3b78327c76846) + -  修复 `Jwt` 自动刷新机制时区处理问题,主要针对国外用户 [#I41UB1](https://gitee.com/dotnetchina/Furion/issues/I41UB1) [82a79cc](https://gitee.com/dotnetchina/Furion/commit/82a79cce0a3d9b09f4090b7363c3b78327c76846) + +- **其他更改** + + -  更新 **系统启动性能,从 106M 减少到 84M** + -  更新 **大量底层代码,包大小从 391Kb 减少到 350Kb(不带注释版本仅 64Kb)** + +--- + +## v2.13/v2.14(已发布) + +- **新特性** + + -  新增 简易字符串模板功能,支持远程请求、数据库模块、日志模块、事件总线模块、定时任务模块、异常模块、数据校验模块 [#I402BL](https://gitee.com/dotnetchina/Furion/issues/I402BL) + -  新增 `404` 状态码规范化默认处理 [#I408F5](https://gitee.com/dotnetchina/Furion/issues/I408F5) + -  新增 定时任务 `ISpareTimeWorker` 声明方式支持异步方法 [#I40KWR](https://gitee.com/dotnetchina/Furion/issues/I40KWR) + -  新增 自动配置二级虚拟目录 [!354](https://gitee.com/dotnetchina/Furion/pulls/354) + +- **突破性变化** + + -  升级 **框架依赖 `SDK` 为 `.NET 5.0.8` 版本** + -  移除 ~~`Db.GetNewDbContext()` 静态方法~~ [#I400BK](https://gitee.com/dotnetchina/Furion/issues/I400BK) + -  移除 ~~数据库模块时态表拓展支持~~ [#I405HI](https://gitee.com/dotnetchina/Furion/issues/I405HI) + -  调整 `IJsonSerializerProvider` 接口参数,新增 `inherit` 参数 [#I3ZQU5](https://gitee.com/dotnetchina/Furion/issues/I3ZQU5) + -  调整 `AppSettings` 配置的 `LogEntityFrameworkCoreSqlExecuteCommand` 名称为 `OutputOriginalSqlExecuteLog` [#I40VVE](https://gitee.com/dotnetchina/Furion/issues/I40VVE) + +- **问题修复** + + -  修复 `Worker Services` 定时任务边界值问题导致跳过单次任务 [#I405NI](https://gitee.com/dotnetchina/Furion/issues/I405NI) + -  修复 `Worker Services` 独立发布后程序集扫描失效 bug [#I3ZH3X](https://gitee.com/dotnetchina/Furion/issues/I3ZH3X) + -  修复 远程请求如果配置了 `Client` 客户端但传入了空 `RequestUrl` 地址导致异常问题 [#I40BC6](https://gitee.com/dotnetchina/Furion/issues/I40BC6) + -  修复 规范化结果篡改非短路端状态码出现异常 bug [#I408F5](https://gitee.com/dotnetchina/Furion/issues/I408F5) + +- **其他更改** + + -  更新 `App.GetServiceProvider(type)` 解析服务性能 [#I40KXN](https://gitee.com/dotnetchina/Furion/issues/I40KXN) + -  调整 视图引擎保存成文件流默认缓存区大小,从 `4096` 提升至 `8192` [#I40KH5](https://gitee.com/dotnetchina/Furion/issues/I40KH5) + +--- + +## v2.10/2.11/2.12 (已发布) + +> 该版本有多个破坏性更改,更新时请认真查看。 + +- **新特性** + + -  新增 `App.Configuration.Reload()` 拓展 [#I3XYI8](https://gitee.com/dotnetchina/Furion/issues/I3XYI8) + -  新增 `ISubscribeHandler` 支持异步方法定义 [#I3XYHJ](https://gitee.com/dotnetchina/Furion/issues/I3XYHJ) + -  新增 `app.UseUnifyResultStatusCodes()` 可配置修改返回状态码 [#I3VZQH](https://gitee.com/dotnetchina/Furion/issues/I3VZQH) + -  新增 远程请求添加默认 `User-Agent` 头 [#I3W17C](https://gitee.com/dotnetchina/Furion/issues/I3W17C) + -  新增 支持 `Sql` 高级代理切换数据库上下文定位器 [#I3XFP6](https://gitee.com/dotnetchina/Furion/issues/I3XFP6) [#I3XDCR](https://gitee.com/dotnetchina/Furion/issues/I3XDCR) + -  新增 定时任务 `CronFormat` 自动识别 [#I3Y7GT](https://gitee.com/dotnetchina/Furion/issues/I3Y7GT) + -  新增 `Sql 高级代理` 拦截功能 [#I3YHG4](https://gitee.com/dotnetchina/Furion/issues/I3YHG4) + -  新增 拦截远程请求所有异常处理 [#I3YPDE](https://gitee.com/dotnetchina/Furion/issues/I3YPDE) + -  新增 远程请求配置 `Timeout` 超时时间 [#I3YPPK](https://gitee.com/dotnetchina/Furion/issues/I3YPPK) + -  新增 `RSA` 加密算法 [#I3YZNU](https://gitee.com/dotnetchina/Furion/issues/I3YZNU) [!345](https://gitee.com/dotnetchina/Furion/pulls/345) + -  新增 `DataTable` 和 `DataSet` 支持不指定强类型返回 [#I3Z6RI](https://gitee.com/dotnetchina/Furion/issues/I3Z6RI) + -  新增 `Sql` 字符串拓展方法设置 `Timeout` 超时时间 [#I3ZKWF](https://gitee.com/dotnetchina/Furion/issues/I3ZKWF) + -  新增 `Sql` 高级代理 `[Timeout]` 特性,设置超时时间 [#I3ZKWF](https://gitee.com/dotnetchina/Furion/issues/I3ZKWF) + +- **突破性变化** + + -  移除 **`FakeDelete` 假删除/软删除所有功能 [#I3XKII](https://gitee.com/dotnetchina/Furion/issues/I3XKII)** + -  移除 **`PBKDF2` 加密算法 [#I3Z0IO](https://gitee.com/dotnetchina/Furion/issues/I3Z0IO)** + -  调整 **远程请求设置超时时间单位由 `分钟` 改为 `秒` [#I3YPPK](https://gitee.com/dotnetchina/Furion/issues/I3YPPK)** + -  调整 **`IJsonSerializerProvider` 接口参数,新增 `inherit` 参数 [#I3ZQU5](https://gitee.com/dotnetchina/Furion/issues/I3ZQU5)** + -  调整 `[NonAutomatic]` 特性名称为 `[Manual]` [#I3XKKX](https://gitee.com/dotnetchina/Furion/issues/I3XKKX) + -  调整 `[NotChangedListener]` 特性名称为 `[SuppressChangedListener]` [#I3XKLZ](https://gitee.com/dotnetchina/Furion/issues/I3XKLZ) + -  调整 `[ManualSaveChanges]` 名称为 `[ManualCommit]` [#I3XKNP](https://gitee.com/dotnetchina/Furion/issues/I3XKNP) + -  调整 **`DbContext.TenantIdQueryFilterExpression` 名称为 `DbContext.BuildTenantQueryFilter` [#I3XKTB](https://gitee.com/dotnetchina/Furion/issues/I3XKTB)** + -  调整 `[SkipScan]` 名称为 `[SuppressSniffer]` [#I3XN5N](https://gitee.com/dotnetchina/Furion/issues/I3XN5N) + -  调整 `[SkipProxy]` 名称为 `[SuppressProxy]` [#I3XN7O](https://gitee.com/dotnetchina/Furion/issues/I3XN7O) + -  优化 `Sql` 执行,性能提升 20% [#I3W33U](https://gitee.com/dotnetchina/Furion/issues/I3W33U) + +- **问题修复** + + -  修复 动态 WebAPI 扫描控制器没有屏蔽没有注册的第三方控制器 [#I3Y7TJ](https://gitee.com/dotnetchina/Furion/issues/I3Y7TJ) + -  修复 `AppDbContext` 设置 `TablePrefix` 无效: [#I3Y57Q](https://gitee.com/dotnetchina/Furion/issues/I3Y57Q) + -  修复 定时任务使用异步委托导致程序终止 bug [#I3XVZ0](https://gitee.com/dotnetchina/Furion/issues/I3XVZ0) + -  修复 事件总线一个 `消息id` 对应多个 `Handler` 只触发第一个[#I3XYP0](https://gitee.com/dotnetchina/Furion/issues/I3XYP0) + -  修复 `.ToPagedList()` 分页方法传入小于或等于 0 的页码 [#I3XNAN](https://gitee.com/dotnetchina/Furion/issues/I3XNAN) + -  修复 `JSON` 序列化默认 `DateTimeOffset` 异常 [#I3XMOL](https://gitee.com/dotnetchina/Furion/issues/I3XMOL) + -  修复 继承 `Serilog` 日志在 `Worker Service` 生成重复日志 bug [#I3WA0L](https://gitee.com/dotnetchina/Furion/issues/I3WA0L) [!331](https://gitee.com/dotnetchina/Furion/pulls/331) + -  修复 `粘土对象` 动态添加 `Clay` 类型 bug [#I3W9LW](https://gitee.com/dotnetchina/Furion/issues/I3W9LW) + -  修复 `ValidationTypes.Numeric` 校验数值类型正则表达式错误 [#I3WADS](https://gitee.com/dotnetchina/Furion/issues/I3WADS) + -  修复 数据库命令参数 `DbParameter` 的 `Value` 是 `object` 类型的时候且不指定 [#I3YKM6](https://gitee.com/dotnetchina/Furion/issues/I3YKM6) + -  修复 `Oracle` 数据库存储过程 `游标参数` 报错问题 [#I3ZBYE](https://gitee.com/dotnetchina/Furion/issues/I3ZBYE) + -  修复 `Worker Services` 采用独立发布后无法执行问题 [#I3ZH3X](https://gitee.com/dotnetchina/Furion/issues/I3ZH3X) + -  修复 远程请求如果无返回值序列化异常问题 [!348](https://gitee.com/dotnetchina/Furion/pulls/348) + +- **其他更改** + + -  新增 支持 `appsettings.json` 等自定义配置文件中文命名 [#I3YBFD](https://gitee.com/dotnetchina/Furion/issues/I3YBFD) + -  更新 远程请求配置命名客户端 `BaseAddress` 地址兼容处理 [#I3YCRH](https://gitee.com/dotnetchina/Furion/issues/I3YCRH) + -  移除 框架无用代码、优化代码 + -  更新 `Furion` 在 `非 Web` 环境下性能 + +- **文档变化** + + -  新增 会话和状态管理 文档 [#I3YI3G](https://gitee.com/dotnetchina/Furion/issues/I3YI3G) + -  更新 远程请求、日志、数据库上下文、远程请求、`Sql` 高级代理文档 + -  更新 配置文件 [#I3Y2EV](https://gitee.com/dotnetchina/Furion/issues/I3Y2EV) + +- **问答答疑** + + -  答疑 `dapper` 多个数据源如何继承 [#I3WUOI](https://gitee.com/dotnetchina/Furion/issues/I3WUOI) + -  答疑 关于 `SpareTime` 多次执行问题[#I3XEQU](https://gitee.com/dotnetchina/Furion/issues/I3XEQU) + -  答疑 选项更改通知(热更新):数据库里的数据更改了如何通知选项进行改变? [#I3XYI8](https://gitee.com/dotnetchina/Furion/issues/I3XYI8) + -  答疑 `SaaS` 多租户添加时无法获取租户`Id` [#I3Y5CF](https://gitee.com/dotnetchina/Furion/issues/I3Y5CF) + -  答疑 获取 `_httpContextAccessor.HttpContext` 为空[#I3Y6BI](https://gitee.com/dotnetchina/Furion/issues/I3Y6BI) + -  答疑 `Ubuntu` 中使用 `App.Configuration` 方法读取不到值 [#I3Y74H](https://gitee.com/dotnetchina/Furion/issues/I3Y74H) + -  答疑 数据库上下文作用域问题 [#I3YHXP](https://gitee.com/dotnetchina/Furion/issues/I3YHXP) + -  答疑 使用 `UnitofWork` 提交事务,可以提交成功,但是系统会有错误 [#I3YIWU](https://gitee.com/dotnetchina/Furion/issues/I3YIWU) + -  答疑 数据库读写分离--非默认主库的从库随机该如何配置? [#I3YVR7](https://gitee.com/dotnetchina/Furion/issues/I3YVR7) + +--- + +## v2.9.0 (已发布) + +- **新特性** + + -  新增 **应用全局未托管资源监听,并实现特定时机释放非托管资源** [#I3VXAU](https://gitee.com/dotnetchina/Furion/issues/I3VXAU) + -  新增 不包含 `EntityFramework.Core` 版本的 `Furion.Pure` 包[#I3VGW8](https://gitee.com/dotnetchina/Furion/issues/I3VGW8) + -  新增 swagger 支持设置多语言方式,设置的语言自动添加到 api 地址后面 [#I3VDTD](https://gitee.com/dotnetchina/Furion/issues/I3VDTD) + -  新增 动态 WebAPI 支持 `[FromRoute]` 非必填(选填)参数设置 [#I3VFIM](https://gitee.com/dotnetchina/Furion/issues/I3VFIM) + -  新增 动态 WebAPI 参数支持配置路由约束 [#I3VFIR](https://gitee.com/dotnetchina/Furion/issues/I3VFIR) + -  新增 `MD5` 和 `DESC` 加密支持 `大写` 输出 [#326](https://gitee.com/dotnetchina/Furion/pulls/326) + +- **突破性变化** + + -  新增 `Furion` 所有包生成 `.snupkg` 包,支持开发阶段直接调试 `Furion` 所有包源码 [#I3VFIX](https://gitee.com/dotnetchina/Furion/issues/I3VFIX) + -  调整 `repository.BuildChange()` 方法的返回值,多返回一个 `IServiceScope` 对象 [#I3VX3D](https://gitee.com/dotnetchina/Furion/issues/I3VX3D) + -  调整 `JWT` 刷新 `Token` 方法 `AutoRefreshToken` 参数 `days` 改为 `minutes` [#I3VXNB](https://gitee.com/dotnetchina/Furion/issues/I3VXNB) + +- **问题修复** + + -  修复 `App.GetOptionsSnapshot<>` 从根服务解析异常 bug [#I3VS2X](https://gitee.com/dotnetchina/Furion/issues/I3VS2X) + -  修复 远程请求如果出现异常,返回 `Stream` 为 null 导致异常的问题 [#I3VSTU](https://gitee.com/dotnetchina/Furion/issues/I3VSTU) + -  修复 如果实体被跟踪后,无法执行删除操作 [#I3W08P](https://gitee.com/dotnetchina/Furion/issues/I3W08P) + +- **其他更改** + + -  更新 运行时内存,实现请求结束自动释放未托管资源 [#I3VXAU](https://gitee.com/dotnetchina/Furion/issues/I3VXAU) + +- **文档变化** + + -  更新 `App` 静态类文档、远程请求文档、分表分库文档 + +- **问答答疑** + + -  答疑 动态 WebAPI,自定义根据方法名生成 [HttpMethod] 规则报错 [#I3VKQG](https://gitee.com/dotnetchina/Furion/issues/I3VKQG) + -  答疑 `InsertAsync` 的时候提示 `ID` 为空 [#I3VS7E](https://gitee.com/dotnetchina/Furion/issues/I3VS7E) + -  答疑 `FirstOrDefault` 自动过滤了 `TanantId` 字段 [#I3W0VH](https://gitee.com/dotnetchina/Furion/issues/I3W0VH) + -  答疑 对方接口返回 `HttpConnectionResponseContent` 远程请求拿不到返回值 [#I3W17C](https://gitee.com/dotnetchina/Furion/issues/I3W17C) + -  答疑 查询方法 `FindOrDefault` 报错 [#I3W830](https://gitee.com/dotnetchina/Furion/issues/I3W830) + -  答疑 `SqlNonQuery` 在 `UnitOfWork` 循环执行[#I3W8WW](https://gitee.com/dotnetchina/Furion/issues/I3W8WW) + -  答疑 因 `Swagger` 配置问题,导致 `Swagger` 中不能自动携带 token 授权的问题 [#I3W934](https://gitee.com/dotnetchina/Furion/issues/I3W934) + -  答疑 远程请求 `SetBody` 参数识别不了[#I3WBM1](https://gitee.com/dotnetchina/Furion/issues/I3WBM1) + -  答疑 `Scoped.Create` 里执行 `sql.SqlNonQuery()` 或者 `obj.insert()` 问题[#I3WB5O](https://gitee.com/dotnetchina/Furion/issues/I3WB5O) + -  答疑 调用函数或存储过程,怎么出参数据自定义对象?如 `Oracle`数据库的数组或记录 [#I3W71W](https://gitee.com/dotnetchina/Furion/issues/I3W71W) + +--- + +## v2.7.0/2.8.0 (已发布) + +- **新特性** + + -  新增 `throw Oops.On("异常消息")` 应用多语言支持 [#I3UYC2](https://gitee.com/dotnetchina/Furion/issues/I3UYC2) + -  新增 `Db.GetMSRepository()` 获取主从库仓储静态方法 [#I3UBSJ](https://gitee.com/dotnetchina/Furion/issues/I3UBSJ) + -  新增 工作单元特性,支持静态类强制性开启共享事务 [#I3S9N8](https://gitee.com/dotnetchina/Furion/issues/I3S9N8) + -  新增 `EFCore` 执行 `sql` 模式打印日志 [#I3SE8X](https://gitee.com/dotnetchina/Furion/issues/I3SE8X) + -  新增 远程请求支持默认 `HttpClient` 配置 [#I3SI17](https://gitee.com/dotnetchina/Furion/issues/I3SI17) + -  新增 `短 ID` 生成功能 [#I3T7JP](https://gitee.com/dotnetchina/Furion/issues/I3T7JP) + -  新增 `[SensitiveDetection]` 支持配置替换敏感词汇 [#I3THIA](https://gitee.com/dotnetchina/Furion/issues/I3THIA) + -  新增 `SpecificationDocumentBuilder.DocumentGroups` 和 `SpecificationDocumentBuilder.CheckApiDescriptionInCurrentGroup(currentGroup, apiDescription)` 公开方法[#I3UDSY](https://gitee.com/dotnetchina/Furion/issues/I3UDSY) + +- **突破性变化** + + -  优化 自动扫描 `.json` 和 `.xml` 文件并加载到配置中的代码和规则,同时移除默认 `.xml` 文件加载,只保留 `.json` 文件 [#I3UJ3L](https://gitee.com/dotnetchina/Furion/issues/I3UJ3L) + -  优化 分布式连续 `GUID` 代码 [#I3UBK0](https://gitee.com/dotnetchina/Furion/issues/I3UBK0) + -  调整 **`Scoped.CreateUnitOfWork` 名称为 `Scoped.CreateUow` [#I3SJPU](https://gitee.com/dotnetchina/Furion/issues/I3SJPU)** + -  调整 `JWTEncryption.Validate` 返回值,支持返回 `TokenValidationResult` [#I3S2ND](https://gitee.com/dotnetchina/Furion/issues/I3S2ND) + +- **问题修复** + + -  修复 `[DataValidation]` 和 `[SensitiveDetection]` 多语言应用失效 [#I3UH6U](https://gitee.com/dotnetchina/Furion/issues/I3UH6U) + -  修复 `Scoped` 系列方法异步出现 `Task is cancel` 情况 [#I3SJF6](https://gitee.com/dotnetchina/Furion/issues/I3SJF6) + -  修复 `Mysql` 数据库的 `ToPagedList` 方法返回的结果进行遍历出现 `MySqlConnection is aleady use` 问题 [#I3SJQ3](https://gitee.com/dotnetchina/Furion/issues/I3SJQ3) + -  修复 `tool/cli.psl` 没有包含项目名称 [#I3S1T6](https://gitee.com/dotnetchina/Furion/issues/I3S1T6) + -  修复 远程请求做上传文件时,没有传入 `Body`,程序直接跳过 [#I3TKFH](https://gitee.com/dotnetchina/Furion/issues/I3TKFH) + -  修复 远程请求 `multipart/form-data` 内容分割符缺失 [#I3TNO9](https://gitee.com/dotnetchina/Furion/issues/I3TNO9) + -  修复 远程请求代理拦截方式返回 `HttpResponseMessage` 问题 [#I3V161](https://gitee.com/dotnetchina/Furion/issues/I3V161) + -  修复 `repository.Database.SetCommandTimeout(600)` 无法生效[#I3VAQS](https://gitee.com/dotnetchina/Furion/issues/I3VAQS) + +- **其他更改** + + -  更新 支持规范化结果中间件判断是否跳过规范化结果 [#I3T2AA](https://gitee.com/dotnetchina/Furion/issues/I3T2AA) + -  调整 更新部分列 `UpdateIncludeNowAsync` 具有二义性 [#I3RW9Q](https://gitee.com/dotnetchina/Furion/issues/I3RW9Q) + -  更新 **框架底层性能,大大减少内存占用和溢出情况,启动内存从之前 `136M` 下将到 `86M`** + -  优化 删除无用代码,优化不规范命名等 + +- **文档变化** + + -  新增 `Inject` 说明文档 [#I3TITA](https://gitee.com/dotnetchina/Furion/issues/I3TITA) + -  更新 4.2.9 的示例代码文档,方法没有放在 class 中 [#I3S9T5](https://gitee.com/dotnetchina/Furion/issues/I3S9T5) + -  修复 规范化结果 6.5.6 多分组排序图片引用错误 [#I3UBOQ](https://gitee.com/dotnetchina/Furion/issues/I3UBOQ) + -  更新 静态类 `Scoped` 文档 + +- **问答答疑** + + -  答疑 默认 `MasterDbContextLocator` 不随自定义的参数生成 [#I3SDBB](https://gitee.com/dotnetchina/Furion/issues/I3SDBB) + -  答疑 事件总线中订阅处理程序类获取不到用户信息,这个正常吗 [#I3SS0U](https://gitee.com/dotnetchina/Furion/issues/I3SS0U) + -  答疑 在有多租户过滤器的情况下,是否有一种方式查询全量的数据 [#I3T0VI](https://gitee.com/dotnetchina/Furion/issues/I3T0VI) + -  答疑 mysql 使用 `&"tools/cli.ps1"` 页面化加载表结构失败 [#I3T4F8](https://gitee.com/dotnetchina/Furion/issues/I3T4F8) + -  答疑 其他 Web 层的 Startup 优先执行 [#I3T8IP](https://gitee.com/dotnetchina/Furion/issues/I3T8IP) + -  答疑 辅助角色服务实现建议 [#I3T906](https://gitee.com/dotnetchina/Furion/issues/I3T906) + -  答疑 开启 `easy connection` 后同一内网地址浏览器可以正常访问,远程请求则无法访问[#I3TA2U](https://gitee.com/dotnetchina/Furion/issues/I3TA2U) + -  答疑 `scope.ServiceProvider.GetService`不存在 [#I3TQMV](https://gitee.com/dotnetchina/Furion/issues/I3TQMV) + -  答疑 能否在 WPF 项目中使用呢? [#I3TMCC](https://gitee.com/dotnetchina/Furion/issues/I3TMCC) + -  答疑 `Dapper` 多个数据源 [#I3TM9B](https://gitee.com/dotnetchina/Furion/issues/I3TM9B) + -  答疑 `L.GetSelectCulture()` 方法异常 [#I3TQS4](https://gitee.com/dotnetchina/Furion/issues/I3TQS4) + -  答疑 循环中使用 `IDGen.NextID()` 得到的结果并不是连续的 [#I3UAF6](https://gitee.com/dotnetchina/Furion/issues/I3UAF6) + -  答疑 模块化动态加载插件支持通配符匹配.dll [#I3UDT8](https://gitee.com/dotnetchina/Furion/issues/I3UDT8) + -  答疑 `MVC` 模式,在 `Controller` 里快捷方式创建 `View` 页面出错 [#I3UFGB](https://gitee.com/dotnetchina/Furion/issues/I3UFGB) + -  答疑 数据库迁移没有种子数据 [#I3UI7G](https://gitee.com/dotnetchina/Furion/issues/I3UI7G) + -  答疑 `SpareTimeAttribute` 中 根据 Cron 表达式 自动匹配 Cron 表达式格式化方式 [#I3UTKQ](https://gitee.com/dotnetchina/Furion/issues/I3UTKQ) + -  答疑 使用 `workService` 集成 `SqlSugar` 报错 [#I3V8HJ](https://gitee.com/dotnetchina/Furion/issues/I3V8HJ) + -  答疑 `sqlserver 2008` 分页报错如何解决呢 [#I3VF96](https://gitee.com/dotnetchina/Furion/issues/I3VF96) + +--- + +## v2.5.0/2.6.0 (已发布) + +- **新特性** + + -  新增 虚拟文件服务,支持物理文件和嵌入资源文件 [#I3RBR9](https://gitee.com/dotnetchina/Furion/issues/I3RBR9) + -  新增 读写分离/主从复制仓储 `IMSRepository` 和 `IMSRepository` 仓储,可进行随机或自定义获取从库 + -  新增 数据脱敏处理 [#I3R5ZF](https://gitee.com/dotnetchina/Furion/issues/I3R5ZF) + +- **突破性变化** + + -  移除 **`InsertOrUpdate` 一系列数据库操作方法** [#I3RI9L](https://gitee.com/dotnetchina/Furion/issues/I3RI9L) + -  移除 所有包含 `Exists` 单词的数据库操作方法 [#I3RJ0T](https://gitee.com/dotnetchina/Furion/issues/I3RJ0T) + -  调整 分布式 GUID `IDGenerater` 静态类名称为 `IDGen` [#I3RGUA](https://gitee.com/dotnetchina/Furion/issues/I3RGUA) + +- **问题修复** + + -  修复 远程调用方法错误,请求报文头 `Headers` 不能添加到 `IHttpDispatchProxy` 的子接口上 [#I3RAF7](https://gitee.com/dotnetchina/Furion/issues/I3RAF7) + +- **其他更改** + + -  更新 应用启动性能,减少内存分配 + +- **文档变化** + + -  新增 脱敏处理文档 [#I3R6WZ](https://gitee.com/dotnetchina/Furion/issues/I3R6WZ) + -  新增 文件系统文档、`FS` 静态类文档 [#I3RCC4](https://gitee.com/dotnetchina/Furion/issues/I3RCC4) + -  更新 读写分离/主从复制、数据库仓储文档、`Db` 静态类 [#I3R3B6](https://gitee.com/dotnetchina/Furion/issues/I3R3B6) + +- **问答答疑** + + -  答疑 关于 `Furion` 集群部署 [#I3R3J4](https://gitee.com/dotnetchina/Furion/issues/I3R3J4) + -  答疑 升级最新框架以后, 数据库生成模型报错 [#I3R7TP](https://gitee.com/dotnetchina/Furion/issues/I3R7TP) + -  答疑 数据库上下文事务执行中,`SaveNow` 执行后有警告 [#I3RAJI](https://gitee.com/dotnetchina/Furion/issues/I3RAJI) + -  答疑 `Hangfire` 使用事务出现错误 [#I3ROQ5](https://gitee.com/dotnetchina/Furion/issues/I3ROQ5) + -  答疑 如何实现 cli 不执行某些表的迁移,web 请求可以正常操作呢? [#I3ROU5](https://gitee.com/dotnetchina/Furion/issues/I3ROU5) + -  答疑 在使用定时任务时候出现的问题:继承 `ISpareTimeWorker` [#I3RRZS](https://gitee.com/dotnetchina/Furion/issues/I3RRZS) + -  答疑 `MySql` 时间差 8 小时处理 [#I3RSCO](https://gitee.com/dotnetchina/Furion/issues/I3RSCO) + -  答疑 `Db.GetRepository<>` 方法结合 `[UnitOfWork]` 后不可用 [#I3RUK5](https://gitee.com/dotnetchina/Furion/issues/I3RUK5) + -  答疑 事务开启失败问题 [#I3RYJY](https://gitee.com/dotnetchina/Furion/issues/I3RYJY) + -  答疑 支持 `DbProvider` 可动态配置 [#I3RYPE](https://gitee.com/dotnetchina/Furion/issues/I3RYPE) + -  答疑 `WorkService` 依赖注入 `ISingleton` 问题 [#I3RZ1L](https://gitee.com/dotnetchina/Furion/issues/I3RZ1L) + -  答疑 `ISpareTimeWorker` 运行期动态修改 [#I3S33Q](https://gitee.com/dotnetchina/Furion/issues/I3S33Q) + +--- + +## v2.4.0 (已发布) + +- **新特性** + + -  新增 支持自动加载模块化/插件 `.xml` 注释文件 [#I3Q7XY](https://gitee.com/dotnetchina/Furion/issues/I3Q7XY) + -  新增 `AppDbContext.FailedAutoRollback` 属性,可配置事务是否自动回滚 [#I3QOUS](https://gitee.com/dotnetchina/Furion/issues/I3QOUS) + +- **突破性变化** + + -  升级 **.NET 5 SDK 为 5.0.6 版本** + -  新增 `IJsonSerializerProvider.GetSerializerOptions()` 接口方法 [#I3QIJN](https://gitee.com/dotnetchina/Furion/issues/I3QIJN) + +- **问题修复** + + -  修复 通过 `services.AddInject()` 方式注册,模块化/插件不加载 [#I3Q7XH](https://gitee.com/dotnetchina/Furion/issues/I3Q7XH) + -  修复 种子数据返回 `null` 报空异常 [#I3QCM5](https://gitee.com/dotnetchina/Furion/issues/I3QCM5) + -  修复 通过 `Clay.Object` 创建粘土对象后属性变小写问题 [#I3QRV3](https://gitee.com/dotnetchina/Furion/issues/I3QRV3) + +- **其他更改** + + -  更新 `Furion` 框架底层性能,减少内存占用,提高应用初始化速度 [92f8cc1](https://gitee.com/dotnetchina/Furion/commit/92f8cc1) + +- **文档变化** + + -  更新 JSON 序列化文档、规范化结果文档、数据库上下文文档 + +- **问答答疑** + + -  答疑 `InsertOrUpdateNowAsync` 报错 [#I3QKO5](https://gitee.com/dotnetchina/Furion/issues/I3QKO5) + +--- + +## v2.3.0 (已发布) + +- **新特性** + + -  新增 `Furion.Extras.DatabaseAccessor.MongoDB` 拓展包支持 [#I3PKST](https://gitee.com/dotnetchina/Furion/issues/I3PKST) + -  新增 动态粘土类型直接转 `object` 或 `dynamic` 类型 [#I3OY27](https://gitee.com/dotnetchina/Furion/issues/I3OY27) + -  新增 `Oops.Retry` 方法,支持设置方法调用失败进行重试 [#I3PJKQ](https://gitee.com/dotnetchina/Furion/issues/I3PJKQ) + -  新增 `JWTSettings` 配置节点 `Algorithm`,用于配置加密算法 [#I3PQGV](https://gitee.com/dotnetchina/Furion/issues/I3PQGV) + -  新增 `repository.EnsureTransaction()` 方法确保工作单元事务有效 [#I3PVF1](https://gitee.com/dotnetchina/Furion/issues/I3PVF1) + +- **突破性变化** + + -  新增 支持 .NET 6.0.0 Preview 3 版本 [#I3P2C7](https://gitee.com/dotnetchina/Furion/issues/I3P2C7) + +- **问题修复** + + -  修复 使用数据库生成模型 `tools/cli.ps1`,从数据库表生成的实体异常 [#I3PL18](https://gitee.com/dotnetchina/Furion/issues/I3PL18) + -  修复 贴了 `[NonUntify]` 特性后,`Swagger` 的 `Example Value` 没有匹配正确 [#I3PK0L](https://gitee.com/dotnetchina/Furion/issues/I3PK0L) + -  修复 `SpareTimer.Tally` 在 `Cron` 表达式中计数无效 [#I3PWSE](https://gitee.com/dotnetchina/Furion/issues/I3PWSE) + +- **其他更改** + + -  更新 框架默认序列化应该从配置中读取,而非手动编写 [#I3P1SJ](https://gitee.com/dotnetchina/Furion/issues/I3P1SJ) + -  更新 `SqlSugar` 拓展库,支持非泛型仓储获取上下文操作对象 [#I3PK2N](https://gitee.com/dotnetchina/Furion/issues/I3PK2N) + -  更新 支持分布式内存缓存可配置化 [#I3POKD](https://gitee.com/dotnetchina/Furion/issues/I3POKD) + +- **文档变化** + + -  文档 添加 `JWTSettings` 配置独立文档 [#I3PQGW](https://gitee.com/dotnetchina/Furion/issues/I3PQGW) + +- **问答答疑** + + -  答疑 软删除如果数据不存在,则报错 [#I3PTVB](https://gitee.com/dotnetchina/Furion/issues/I3PTVB) + -  答疑 多个类集成测试会造成数据库定位器多次注册,无法运行所有测试,只能一个类一个类的运行 [#I3PXGY](https://gitee.com/dotnetchina/Furion/issues/I3PXGY) + +--- + +## v2.2.0 (已发布) + +- **新特性** + + -  新增 `Clay` 粘土类型,支持让 `C#` 创建一个弱类型对象并操作弱类型 [#I3O2QQ](https://gitee.com/dotnetchina/Furion/issues/I3O2QQ) + -  新增 `Scoped.Create` 带返回值重载 [#I3O47J](https://gitee.com/dotnetchina/Furion/issues/I3O47J) + -  新增 支持 `Scoped.Create()` 一系列方法支持传入作用域工厂 [#I3OAP5](https://gitee.com/dotnetchina/Furion/issues/I3OAP5) + -  新增 支持事件总线同步执行方式 [#I3OAW2](https://gitee.com/dotnetchina/Furion/issues/I3OAW2) + -  新增 `[DataValidation]` 跳过空字符串和空值验证 [#I3OGEN](https://gitee.com/dotnetchina/Furion/issues/I3OGEN) + -  新增 `Worker Service` 可配置是否自动注册 `Worker` [#I3OLW4](https://gitee.com/dotnetchina/Furion/issues/I3OLW4) + +- **突破性变化** + +- **问题修复** + + -  修复 定时任务设置 `cancelInNoneNextTime: false` 一次也不执行 [#I3O3N0](https://gitee.com/dotnetchina/Furion/issues/I3O3N0) + -  修复 SpareTime 自定义下次执行时间出现空异常 [#I3O46X](https://gitee.com/dotnetchina/Furion/issues/I3O46X) + -  修复 `MiniProfiler` 设置为 `false` 时,数据库上下文提交拦截器未添加 [#I3OAWX](https://gitee.com/dotnetchina/Furion/issues/I3OAWX) + -  修复 `[Consumes("application/x-www-form-urlencoded")]` 和 `ModelQuery` 配置同时配置导致空引用问题 [#I3ODUR](https://gitee.com/dotnetchina/Furion/issues/I3ODUR) + -  修复 在 Grpc 中使用 jwt 授权出现空异常 [#I3OW3I](https://gitee.com/dotnetchina/Furion/issues/I3OW3I) + +- **其他更改** + + -  更新 支持发布后代码精简配置,减少不必要的文件夹输出 [#I3OAPF](https://gitee.com/dotnetchina/Furion/issues/I3OAPF) + -  更新 自动刷新 Token 机制,新增容错值处理,解决并发 Token 刷新失败问题 [#I3OGYF](https://gitee.com/dotnetchina/Furion/issues/I3OGYF) + +- **文档变化** + + -  新增 粘土对象文档 [#I3OG18](https://gitee.com/dotnetchina/Furion/issues/I3OG18) + +- **问答答疑** + + -  答疑 动态 WebAPI 如何获取接收文件 [#I3O29B](https://gitee.com/dotnetchina/Furion/issues/I3O29B) + -  答疑 定时任务使用 `Scope.CreateUow` 引发的问题 [#I3O2CD](https://gitee.com/dotnetchina/Furion/issues/I3O2CD) + -  答疑 单文件发布程序工作不正常 [#I3O4D8](https://gitee.com/dotnetchina/Furion/issues/I3O4D8) + -  答疑 同时配置租户过滤器和软删除过滤器,最终的 sql 只生成了一种过滤条件 [#I3OB0A](https://gitee.com/dotnetchina/Furion/issues/I3OB0A) + -  答疑 HTTP 重定向 HTTPS 后跨域失效 [#I3OB8R](https://gitee.com/dotnetchina/Furion/issues/I3OB8R) + -  答疑 在 PostgreSql 数据库使用 `rep.FirstOrDefault(u => u.Id == UserId);` 引起异常 [#I3O5OF](https://gitee.com/dotnetchina/Furion/issues/I3O5OF) + -  答疑 定时任务有时能触发有时不能触发 [#I3ORBE](https://gitee.com/dotnetchina/Furion/issues/I3ORBE) + +--- + +## v2.1.0 (已发布) + +- **新特性** + + -  新增 新增定时任务 `ISpareTimeWorker` 方式支持 `[SpareTime("{配置路径}}]` 方式 [#I3NTUX](https://gitee.com/dotnetchina/Furion/issues/I3NTUX) + -  新增 定时任务支持异步委托 [#I3NP96](https://gitee.com/dotnetchina/Furion/issues/I3NP96) + -  新增 轻量级分布式连续 GUID 生成器 [#I3NKLZ](https://gitee.com/dotnetchina/Furion/issues/I3NKLZ) + -  新增 `ClayObject` 模块,处理 `ExpandoObject` 及 `IDictionary` 类型 [#I3N3J4](https://gitee.com/dotnetchina/Furion/issues/I3N3J4) + -  新增 `Scoped.CreateUow(handler)` 创建作用域并自动提交数据库更改方法 [#I3NU3G](https://gitee.com/dotnetchina/Furion/issues/I3NU3G) + +- **突破性变化** + + -  调整 规范化结果接口 `OnResponseStatusCodes` 方法,新增 `UnifyResultStatusCodesOptions` 参数 [#I3NDB9](https://gitee.com/dotnetchina/Furion/issues/I3NDB9) + -  移除 **雪花 ID 实现代码 [#I3NKLZ](https://gitee.com/dotnetchina/Furion/issues/I3NKLZ)** + +- **问题修复** + + -  修复 `Swagger` 不能支持非 int 类型的枚举 [#I3NQM8](https://gitee.com/dotnetchina/Furion/issues/I3NQM8) + -  修复 数据库线程池多线程并发问题 [#I3NR4L](https://gitee.com/dotnetchina/Furion/issues/I3NR4L) + -  修复 自定义控制器路由后且为方法参数指定了 `[ApiSeat]` 后生成路由重复 [#I3NRF6](https://gitee.com/dotnetchina/Furion/issues/I3NRF6) + +- **其他更改** + + -  更新 支持应用启动的时候迁移种子数据 [#I3NH3M](https://gitee.com/dotnetchina/Furion/issues/I3NH3M) + +- **文档变化** + + -  新增 分布式 ID 生成文档 [#I3B6CX](https://gitee.com/dotnetchina/Furion/issues/I3B6CX) + -  新增 新增模块化开发文档 [#I3NSUS](https://gitee.com/dotnetchina/Furion/issues/I3NSUS) + -  更新 20.4 字符串拓展方式 > 错误`ToAESDecrypt` 写成了 `ToToAESDecrypt` [#](https://gitee.com/dotnetchina/Furion/issues/I3NNKV) + +- **问答答疑** + + -  答疑 有关【定时任务/委托】的疑问 [#I3N3EW](https://gitee.com/dotnetchina/Furion/issues/I3N3EW) + -  答疑 统一返回格式支持自定义 [#I3NU1G](https://gitee.com/dotnetchina/Furion/issues/I3NU1G) + +--- + +## v2.0.0 (已发布) + +- **新特性** + + -  新增 控制台应用程序及 Worker Services 支持 [#I3K4DG](https://gitee.com/dotnetchina/Furion/issues/I3K4DG) + -  新增 完整任务调度功能 [#I3IRUX](https://gitee.com/dotnetchina/Furion/issues/I3IRUX) + -  新增 `Cron` 表达式解析 [#I3IQ9Y](https://gitee.com/dotnetchina/Furion/issues/I3IQ9Y) + -  新增 支持 `Swagger` 自定义配置 `swagger.json` 地址模板 [#I3IHMX](https://gitee.com/dotnetchina/Furion/issues/I3IHMX) + -  新增 支持配置动态 WebApi 区域 [#I3IJAZ](https://gitee.com/dotnetchina/Furion/issues/I3IJAZ) + -  新增 远程请求新增支持传入服务提供器 `IServiceProvider` [#I3IVBL](https://gitee.com/dotnetchina/Furion/issues/I3IVBL) + -  新增 全局配置选型 `SupportPackageNamePrefixs` 配置,支持配置包前缀 [#I3K0SN](https://gitee.com/dotnetchina/Furion/issues/I3K0SN) + -  新增 应用启动时支持 `referenceassembly` 类型程序集扫描 [#I3K0SN](https://gitee.com/dotnetchina/Furion/issues/I3K0SN) + -  新增 依赖注入 `AOP` 拦截获取方法真实特性 [#I3LZBX](https://gitee.com/dotnetchina/Furion/issues/I3LZBX) + -  新增 EFCore 手动 `SaveChanges()` 特性 [#I3N01Y](https://gitee.com/dotnetchina/Furion/issues/I3N01Y) + -  新增 支持 `Cors` 跨域更多配置 [#I3N2J0](https://gitee.com/dotnetchina/Furion/issues/I3N2J0) + +- **突破性变化** + + -  优化 完整任务调度功能 [#I3IRUX](https://gitee.com/dotnetchina/Furion/issues/I3IRUX) + -  优化 日志模块功能 [#I3J2K0](https://gitee.com/dotnetchina/Furion/issues/I3J2K0) + -  优化 模板引擎功能 [#I3J46E](https://gitee.com/dotnetchina/Furion/issues/I3J46E) + -  优化 底层 `EFCoreRepository` 仓储 [#I3J6W5](https://gitee.com/dotnetchina/Furion/issues/I3J6W5) + -  优化 sql 字符串拓展底层代码 [#I3IVCE](https://gitee.com/dotnetchina/Furion/issues/I3IVCE) + -  优化 底层 `SqlRepository` 所有逻辑代码 [#I3J6V6](https://gitee.com/dotnetchina/Furion/issues/I3J6V6) + -  优化 数据库实体拓展方法 [#I3J609](https://gitee.com/dotnetchina/Furion/issues/I3J609) + -  调整 事件事件总线同步执行为异步方式执行 [#I3J0WA](https://gitee.com/dotnetchina/Furion/issues/I3J0WA) + -  移除 框架底层 `HttpContext.IsAjaxRequest()` 拓展 [#I3IVAA](https://gitee.com/dotnetchina/Furion/issues/I3IVAA) + -  移除 `ValidationTypes.Required` 验证 [#I3KR85](https://gitee.com/dotnetchina/Furion/issues/I3KR85) + +- **问题修复** + + -  修复 关闭 `InjectMiniProfiler` 参数后内存缓存无效 [#I3IHLR](https://gitee.com/dotnetchina/Furion/issues/I3IHLR) + -  修复 在多租户中调用 `Tenant` 属性出现偶然性数据库上下文被释放的情况 [#I3IC70](https://gitee.com/dotnetchina/Furion/issues/I3IC70) + -  修复 Sql 代理中如果返回基元类型抛出不能将 object 转换成对应类型的异常 [#I3IC84](https://gitee.com/dotnetchina/Furion/issues/I3IC84) + -  修复 存储过程多返回值的时候,outputvalues 的 name 不是定义的 MSG 的 name,是 Msg 类型。 [#I3IC7Y](https://gitee.com/dotnetchina/Furion/issues/I3IC7Y) + -  修复 PhoneNumber 手机号验证正则表达式错误 [#I3ID10](https://gitee.com/dotnetchina/Furion/issues/I3ID10) + -  修复 依赖注入 AOP 拦截无法捕获内部异常 [#I3IGCC](https://gitee.com/dotnetchina/Furion/issues/I3IGCC) + -  修复 全局拦截标记异常已被处理后异常过滤器依然执行 [#I3J463](https://gitee.com/dotnetchina/Furion/issues/I3J463) + -  修复 自定义全局异常拦截器不起作用 [#I3K1SJ](https://gitee.com/dotnetchina/Furion/issues/I3K1SJ) + -  修复 在 WorkerService 模式下,还是使用 WebHostEnvironment 来判断 Host 环境,会导致错误 [#I3LCQY](https://gitee.com/dotnetchina/Furion/issues/I3LCQY) + -  修复 定时任务 `DoOnce` 抛空异常 bug [#I3M0ZT](https://gitee.com/dotnetchina/Furion/issues/I3M0ZT) + +- **其他更改** + + -  更新 启动时程序集扫描类型 [#I3K0SN](https://gitee.com/dotnetchina/Furion/issues/I3K0SN) + -  更新 `App.GetConfig<>("key")` 不支持获取单个值问题 [#I3ILF1](https://gitee.com/dotnetchina/Furion/issues/I3ILF1) + -  更新 UrlEncode 应该用 `Uri.EscapeDataString()` 而不是 `HttpUtility.UrlEncode` [#I3ICTK](https://gitee.com/dotnetchina/Furion/issues/I3ICTK) + +- **文档变化** + + -  新增 定位任务、后台任务文档 [#I3JHHG](https://gitee.com/dotnetchina/Furion/issues/I3JHHG) + -  新增 辅助角色服务文档 [#I3K5GN](https://gitee.com/dotnetchina/Furion/issues/I3K5GN) + -  更新 动态 WebAPI、规范化文档、数据库上下文文档 + +- **问答答疑** + + -  答疑 数据校验,自定义 ErrorMessage 无效问题 [#I3ICL3](https://gitee.com/dotnetchina/Furion/issues/I3ICL3) + -  答疑 最新 issue 中新增的“新增常用的 JSON 序列化方法” 会导致 AOP 拦截异常 [#I3I7VE](https://gitee.com/dotnetchina/Furion/issues/I3I7VE) + -  答疑 Furion.DatabaseAccessor.PrivateEntityBase 中的 TenantId 数据类型设置为 object [#I3IQV6](https://gitee.com/dotnetchina/Furion/issues/I3IQV6) + -  答疑 有关异常拦截和处理的疑问 [#I3IUFZ](https://gitee.com/dotnetchina/Furion/issues/I3IUFZ) + -  答疑 `DataValidation` 在空值的情况下被忽略掉了[#I3IWSM](https://gitee.com/dotnetchina/Furion/issues/I3IWSM) + -  答疑 日志文档没有更新 [#I3J1DX](https://gitee.com/dotnetchina/Furion/issues/I3J1DX) + -  答疑 对于 webapi 简单类型参数,是否可以以 json 方式提交 [#I3J18I](https://gitee.com/dotnetchina/Furion/issues/I3J18I) + -  答疑 `IUnifyResultProvider` 实现中如果 `UnifyModel` 的 type 不是范型会报错 [#I3JBXF](https://gitee.com/dotnetchina/Furion/issues/I3JBXF) + -  答疑 如何模块化开发新功能? [#I3J7ZZ](https://gitee.com/dotnetchina/Furion/issues/I3J7ZZ) + -  答疑 建议增加微服务中间件的集成 [#I3JTZQ](https://gitee.com/dotnetchina/Furion/issues/I3JTZQ) + -  答疑 二级虚拟目录部署的 swagger 的 MiniProfiler js 报错 [#I3IWLR](https://gitee.com/dotnetchina/Furion/issues/I3IWLR) + +--- + +## v1.19.0 (已发布) + +- **新特性** + + -  新增 `EFCore 5.0` 支持 **SqlServer 2005-2008** 数据库 [#I3HZZ6](https://gitee.com/dotnetchina/Furion/issues/I3HZZ6) + -  新增 `Sql` 高级代理支持模板替换了 [#I3HHWU](https://gitee.com/dotnetchina/Furion/issues/I3HHWU) [#I3HH2T](https://gitee.com/dotnetchina/Furion/issues/I3HH2T) + -  新增 `PBKDF2` 加密 [#I3HN7A](https://gitee.com/dotnetchina/Furion/issues/I3HN7A) + -  新增 常用的 `JSON` 操作方法 [#I3HUYO](https://gitee.com/dotnetchina/Furion/issues/I3HUYO) + -  新增 所有解析服务的方法都支持传入 `IServiceProvidier` 参数 [#I3HXEU](https://gitee.com/dotnetchina/Furion/issues/I3HXEU) + +- **突破性变化** + + -  升级 .NET 5 SDK 至 5.0.5 版本 + +- **问题修复** + + -  修复 远程请求 `application/x-www-form-urlencoded` 自动被转码了 [#I3HDPC](https://gitee.com/dotnetchina/Furion/issues/I3HDPC) + -  修复 `ISqlDispatchProxy` 调用带返回值的存储过程出错 [#I3HISS](https://gitee.com/dotnetchina/Furion/issues/I3HISS) + -  修复 多数据库工作单元异常无法回滚数据 [#I3I2KN](https://gitee.com/dotnetchina/Furion/issues/I3I2KN) [#I3HYN5](https://gitee.com/zuohuaijun/Admin.NET/issues/I3HYN5) + -  修复 Serilog 日志生成太多文件 [#I3I2PN](https://gitee.com/dotnetchina/Furion/issues/I3I2PN) + -  修复 `1.18.0` 版本数据库连接池存在连接泄漏问题 [#I3I5KO](https://gitee.com/dotnetchina/Furion/issues/I3I5KO) + -  修复 Sqlite 提示事务已完成异常 [#I3I9F2](https://gitee.com/dotnetchina/Furion/issues/I3I9F2) + +- **其他更改** + + -  更新 视图模板功能,默认支持可枚举泛型类型 [#I3GYEE](https://gitee.com/dotnetchina/Furion/issues/I3GYEE) + -  更新 开发阶段 MiniProfiler 打印数据库相关信息 [#I3I8VQ](https://gitee.com/dotnetchina/Furion/issues/I3I8VQ) + -  更新 EFCore 5.0 未提供 Sqlite 数据库 DataAdapter 的支持 [#I3I9FC](https://gitee.com/dotnetchina/Furion/issues/I3I9FC) + +- **文档变化** + + -  更新 数据库上下文、多租户、仓储、日志、序列化等文档。 + +- **问答答疑** + + -  答疑 建议 MVC 模式下增加 Furion 的功能 [#I3GY4R](https://gitee.com/dotnetchina/Furion/issues/I3GY4R) + -  答疑 数据库关联操作 [#I3H5QP](https://gitee.com/dotnetchina/Furion/issues/I3H5QP) + -  答疑 1.17.5 版本 suagger 无法生成 swagger.json [#I3HGPZ](https://gitee.com/dotnetchina/Furion/issues/I3HGPZ) + -  答疑 Serilog 扩展+dll 启动与 swagger 的 MiniProfiler 冲突 [#I3HWJM](https://gitee.com/dotnetchina/Furion/issues/I3HWJM) + -  答疑 Sql 高级代理返回 DataTable 时,结果为空取不到记录 [#I3HUWG](https://gitee.com/dotnetchina/Furion/issues/I3HUWG) + -  答疑 Task.Run 操作数据库问题 [#I3HZ9D](https://gitee.com/dotnetchina/Furion/issues/I3HZ9D) + +--- + +## v1.18.0 (已发布) + +- **新特性** + + -  新增 `Oracle` 11 版本支持 [#I3EVL5](https://gitee.com/dotnetchina/Furion/issues/I3EVL5) + -  新增 `Mysql` 官方包 `MySql.EntityFrameworkCore` 支持 [#I3E6J1](https://gitee.com/dotnetchina/Furion/issues/I3E6J1) + -  新增 全局配置 `WebApi` 参数 `[FromQury]` 化 [#I3EFYJ](https://gitee.com/dotnetchina/Furion/issues/I3EFYJ) + -  新增 公开框架底层依赖注入扫描注册拓展 `services.AddRisterTypes(types)` [#I3EIV3](https://gitee.com/dotnetchina/Furion/issues/I3EIV3) + -  新增 SqlSugar 工作单元特性 [#I3EJO5](https://gitee.com/dotnetchina/Furion/issues/I3EJO5) + +- **突破性变化** + +- **问题修复** + + -  修复 数据库上下文池一旦有上下文操作失败还数据库上下文出现二次提交数据库的问题 [#I3EIJJ](https://gitee.com/dotnetchina/Furion/issues/I3EIJJ) + -  修复 不同数据库命令参数前缀都添加了 `@` 处理 [#I3EBJP](https://gitee.com/dotnetchina/Furion/issues/I3EBJP) + -  修复 尝试修复事件总线线程安全问题 [#I3EGSB](https://gitee.com/dotnetchina/Furion/issues/I3EGSB) [#PR236](https://gitee.com/dotnetchina/Furion/pulls/236) + -  修复 `HttpContextExtensions` 的 `SignoutToSwagger` 方法无效 [#I3EHNQ](https://gitee.com/dotnetchina/Furion/issues/I3EHNQ) + -  修复 如果动态 WebApi 贴了 `[ApiController]` 特性后,导致路由参数重复生成 [#I3EOQQ](https://gitee.com/dotnetchina/Furion/issues/I3EOQQ) + -  修复 如果没有任何 webapi 控制器时,文档报错 [#I3EVLB](https://gitee.com/dotnetchina/Furion/issues/I3EVLB) + -  修复 依赖注入泛型类型注册失败 [#I3EX66](https://gitee.com/dotnetchina/Furion/issues/I3EX66) + +- **其他更改** + + -  调整 SqlSugar 拓展库仓储 `Context` 属性类型未 `SqlSugarClient` [#I3EHXA](https://gitee.com/dotnetchina/Furion/issues/I3EHXA) + -  更新 刷新 Token 黑名单 Redis 中分组 [#I3EQWO](https://gitee.com/dotnetchina/Furion/issues/I3EQWO) + -  更新 远程请求在请求拦截次发起二次请求导致异常问题 [#I3ER71](https://gitee.com/dotnetchina/Furion/issues/I3ER71) + -  更新 多租户默认缓存改为分布式缓存 [#I3EXEU](https://gitee.com/dotnetchina/Furion/issues/I3EXEU) + +- **文档变化** + + -  更新 数据库操作文档 [#I3E84X](https://gitee.com/dotnetchina/Furion/issues/I3E84X) + +- **问答答疑** + + -  答疑 如何方便的获取 `IDynamicApiController` API 产生的 url 和 谓词 [#I3ED17](https://gitee.com/dotnetchina/Furion/issues/I3ED17) + -  答疑 Code First -执行命令 `Add-Migration` 遇到了问题 [#I3EHD0](https://gitee.com/dotnetchina/Furion/issues/I3EHD0) + -  答疑 tools v1.16.0 无法生成实体,一直提示 Missing required argument ``. [#I3ENZ8](https://gitee.com/dotnetchina/Furion/issues/I3ENZ8) + -  答疑 Authorize 的 Logout 按钮,无法实时请空 token[#I3EOF9](https://gitee.com/dotnetchina/Furion/issues/I3EOF9) + +--- + +## v1.17.0 (已发布) + +- **新特性** + + -  新增 动态 WebAPI 支持继承基类配置特性 [#I3D5PX](https://gitee.com/dotnetchina/Furion/issues/I3D5PX) + -  新增 远程请求支持 `multipart/form-data` 内容类型处理 [#I3D7KG](https://gitee.com/dotnetchina/Furion/issues/I3D7KG) + -  新增 字符串加密拓展 [#I3DHBW](https://gitee.com/dotnetchina/Furion/issues/I3DHBW) + -  新增 新增远程请求可直接下载返回值内容转为 string 类型 [#I3DIGR](https://gitee.com/dotnetchina/Furion/issues/I3DIGR) + -  新增 远程请求地址支持模板引擎 [#I3D5Y8](https://gitee.com/dotnetchina/Furion/issues/I3D5Y8) + -  新增 `[DataValidation]` 错误消息支持 `string.Format` 操作 [#I3E08W](https://gitee.com/dotnetchina/Furion/issues/I3E08W) + -  新增 远程请求 `HttpRequestMessage` 拓展方法 `AppendQueries()` 追加更多 `query` 参数拓展 [#I3E3DI](https://gitee.com/dotnetchina/Furion/issues/I3E3DI) + +- **突破性变化** + + -  调整 `IRepository.AsAsyncEnumerable()` 返回值 [#I3DIQ1](https://gitee.com/dotnetchina/Furion/issues/I3DIQ1),调整为:`rep.AsQueryable().ToListAsync()` + +- **问题修复** + + -  修复 数据验证失败后也打印了成功的字段 [#I3CVBS](https://gitee.com/dotnetchina/Furion/issues/I3CVBS) + -  修复 远程请求配置 `contentType` 为 `application/x-www-form-urlencoded` 无效问题[#I3CWBS](https://gitee.com/dotnetchina/Furion/issues/I3CWBS) + -  修复 远程请求无法打印完整的请求地址,比如配置了 HttpClient 之后 [#I3CY42](https://gitee.com/dotnetchina/Furion/issues/I3CY42) + -  修复 程序启动时排除默认配置文件算法不对,应该采用正则表达式匹配 [#I3D9E7](https://gitee.com/dotnetchina/Furion/issues/I3D9E7) + -  修复 远程请求成功请求拦截不生效 [#I3DOE4](https://gitee.com/dotnetchina/Furion/issues/I3DOE4) + -  修复 `Dapper` 拓展数据库切换为 oracle 时,系统找不到指定的文件 `Oracle.ManagedDataAccess.Core` [#I3DYM3](https://gitee.com/dotnetchina/Furion/issues/I3DYM3) + +- **其他更改** + + -  更新 获取 `JWT token` 信息支持配置 `Token` 前缀,如 `Bearer ` [#I3DJIV](https://gitee.com/dotnetchina/Furion/issues/I3DJIV) + -  更新 刷新 Token 黑名单存储方式,将内存缓存调整为分布式缓存 [#I3DPBR](https://gitee.com/dotnetchina/Furion/issues/I3DPBR) + +- **文档变化** + + -  调整 远程请求文档 [#I3CPJO](https://gitee.com/dotnetchina/Furion/issues/I3CPJO) + +- **问答答疑** + + -  答疑 `LinqExpression.And` 没有 2 个参数的方法 [#I3CXKZ](https://gitee.com/dotnetchina/Furion/issues/I3CXKZ) + -  答疑 异常信息 如何记录到数据库中:) [#I3DDGO](https://gitee.com/dotnetchina/Furion/issues/I3DDGO) + -  答疑 无键实体选用 `IEntityNotKey` [#I3DWRF](https://gitee.com/dotnetchina/Furion/issues/I3DWRF) + -  答疑 根据主键删除一条记录不成功,无错误信息 [#I3DWWF](https://gitee.com/dotnetchina/Furion/issues/I3DWWF) + -  答疑 如何自定义接口返回格式 [#I3DZN6](https://gitee.com/dotnetchina/Furion/issues/I3DZN6) + -  答疑 DynamicApiController 如何在运行时决定是否公开一个 Action [#I3D5UL](https://gitee.com/dotnetchina/Furion/issues/I3D5UL) + -  答疑 `Furion.DatabaseAccessor.DbHelpers` 方法:`ConvertToDbParameters` 是不是应该过滤掉贴 `NotMapped` 的特性 [#I3E2XS](https://gitee.com/dotnetchina/Furion/issues/I3E2XS) + +--- + +## v1.16.0 (已发布) + +- **新特性** + + -  新增 `IDGenerator` 雪花 ID 算法,感谢 [idgenerator](https://gitee.com/yitter/idgenerator) 作者提交 PR [#PR204](https://gitee.com/dotnetchina/Furion/pulls/204) [#I3B60S](https://gitee.com/dotnetchina/Furion/issues/I3B60S) + -  新增 `DbContext` 刷新多租户缓存拓展方法 [#I39N5U](https://gitee.com/dotnetchina/Furion/issues/I39N5U) + -  新增 自定义配置单个控制器名称规范,如小写路由 [#I3A5XL](https://gitee.com/dotnetchina/Furion/issues/I3A5XL) + -  新增 获取当前选择区域语言方法 [#I3BSDH](https://gitee.com/dotnetchina/Furion/issues/I3BSDH) + +- **突破性变化** + + -  升级 .NET 5 SDK 至 5.0.4 版本 [#I3ASTL](https://gitee.com/dotnetchina/Furion/issues/I3ASTL) + -  优化 远程请求所有功能 [#I2LB7M](https://gitee.com/dotnetchina/Furion/issues/I2LB7M) + -  优化 `JSON` 序列化功能,提供统一的抽象接口,方便自由替换 `JSON` 库 [#I39GT9](https://gitee.com/dotnetchina/Furion/issues/I39GT9) + -  优化 验证失败返回消息模型及规范化接口验证参数 [#I3AFQW](https://gitee.com/dotnetchina/Furion/issues/I3AFQW) + -  更新 插件式开发热插拔功能,实现动态加载卸载 [#PR200](https://gitee.com/dotnetchina/Furion/pulls/200), 感谢 [@SamWangCoder](https://gitee.com/samwangcoder) + -  移除 `JsonSerializerUtility` 静态类及移除属性大写序列化拓展配置 [#I3AFRJ](https://gitee.com/dotnetchina/Furion/issues/I3AFRJ) + +- **问题修复** + + -  修复 `MVC` 模式下不支持验证自定义验证逻辑 [#I39LM5](https://gitee.com/dotnetchina/Furion/issues/I39LM5) + -  修复 验证数值类型正则表达式不支持负数 bug [#I39YUV](https://gitee.com/dotnetchina/Furion/issues/I39YUV) + -  修复 框架启动时无法加载未被引用的程序集 bug [#I3A3Z4](https://gitee.com/dotnetchina/Furion/issues/I3A3Z4) + -  修复 `EFCoreRepository.IsAttached()` 方法判断错误 bug [#I3A824](https://gitee.com/dotnetchina/Furion/issues/I3A824) + -  修复 `动态API` 驼峰显示配置无效 bug [#I3AF32](https://gitee.com/dotnetchina/Furion/issues/I3AF32) + -  修复 `cli.ps1` 不支持新版本 `EFCore` bug [#I3APO9](https://gitee.com/dotnetchina/Furion/issues/I3APO9) + -  修复 `EFCore` 实体配置 `[Table]` 特性无效 bug [#I3BAYH](https://gitee.com/dotnetchina/Furion/issues/I3BAYH) + -  修复 动态 WebAPI `CheckIsSplitCamelCase` bug [#I3BLKX](https://gitee.com/dotnetchina/Furion/issues/I3BLKX) + -  修复 动态 WebAPI 配置保留 Action 的 Async 后缀无效问题 [#I3C3DA](https://gitee.com/dotnetchina/Furion/issues/I3C3DA) + -  修复 `JWT` Token 刷新后旧的刷新 Token 依旧可用 bug [#I3C8ZH](https://gitee.com/dotnetchina/Furion/issues/I3C8ZH) + -  修复 多语言 `Razor` 视图变量多语言乱码问题 [#I3CBMU](https://gitee.com/dotnetchina/Furion/issues/I3CBMU) + +- **其他更改** + + -  更新 默认序列化提供器 `System.Text.Json` 反序列化字符串时区分大小写问题 [#I3BSXV](https://gitee.com/dotnetchina/Furion/issues/I3BSXV) + -  更新 优化 `MessageCenter` 性能问题 [#I39PRR](https://gitee.com/dotnetchina/Furion/issues/I39PRR) + -  更新 数据库上下文池小性能优化 + +- **文档变化** + + -  新增 `Docker` 环境下自动化部署 [#PR209](https://gitee.com/dotnetchina/Furion/pulls/209) + -  新增 `JSON` 序列化 文档 [#I3B6D8](https://gitee.com/dotnetchina/Furion/issues/I3B6D8) + -  更新 跨域、安全授权、即时通信文档、多语言、规范化文档 + +- **问答答疑** + + -  答疑 `Furion.Extras.DatabaseAccessor.SqlSugar` 配置多个数据库打印 SQL 语句问题 [#I39PDC](https://gitee.com/dotnetchina/Furion/issues/I39PDC) + -  答疑 `ORACLE` 数据库多租户模式下返回值为指定类型时系统卡死 [#I39RNH](https://gitee.com/dotnetchina/Furion/issues/I39RNH) + -  答疑 假删除指向异常 [#I39XZA](https://gitee.com/dotnetchina/Furion/issues/I39XZA) + -  答疑 `Furion` 多语言配置节是放在 `AppSettings` 里面还是外面呢? [#I3A4SB](https://gitee.com/dotnetchina/Furion/issues/I3A4SB) + -  答疑 没找到数据库上下文 [#I3A5HS](https://gitee.com/dotnetchina/Furion/issues/I3A5HS) + -  答疑 有 `QQ` 交流群吗? [#I3AAM7](https://gitee.com/dotnetchina/Furion/issues/I3AAM7) + -  答疑 `Vue3` 环境下配置 `SignalR` 跨域出错 [#I3ALQ7](https://gitee.com/dotnetchina/Furion/issues/I3ALQ7) + -  答疑 设置 `Swagger` 参数非必填 [#I3AT02](https://gitee.com/dotnetchina/Furion/issues/I3AT02) + -  答疑 EFCore 调用 Insert 时报 `Unknown column 'Discriminator' in 'field list'` 异常 [#I3B2LC](https://gitee.com/dotnetchina/Furion/issues/I3B2LC) + -  答疑 逆向 `mysql` 数据库时 `cli` 出现错误 [#I3B64F](https://gitee.com/dotnetchina/Furion/issues/I3B64F) + -  答疑 Sql 高级代理使用过程中 DateTime 类型的参数序列化失败 [#I3AZXK](https://gitee.com/dotnetchina/Furion/issues/I3AZXK) + -  答疑 使用 Mysql 执行 Add-Migration 报错 [#I3B8EW](https://gitee.com/dotnetchina/Furion/issues/I3B8EW) + -  答疑 Saas 多租户模式-独立 Database 模式下无法获取 Tenant, 导致无法自动切换的问题[#I3AVXU](https://gitee.com/dotnetchina/Furion/issues/I3AVXU) + -  答疑 如何自定义 WebAPI 统一结果模型 [#I3BBYW](https://gitee.com/dotnetchina/Furion/issues/I3BBYW) [#I3BBYV](https://gitee.com/dotnetchina/Furion/issues/I3BBYV) + -  答疑 在 `Web.Entry` 项目新建了一个 `Controller`,多了未知方法 [#I3BKH5](https://gitee.com/dotnetchina/Furion/issues/I3BKH5) + -  答疑 `AOP` 拦截如何解析服务 [#I3BUM3](https://gitee.com/dotnetchina/Furion/issues/I3BUM3) + -  答疑 动态 WebAPI 返回参数被省略 [#I3C2XR](https://gitee.com/dotnetchina/Furion/issues/I3C2XR) + -  答疑 如何设置某一个接口响应数据不自动转小写,按原始字段名返回 [#I38L9B](https://gitee.com/dotnetchina/Furion/issues/I38L9B) + -  答疑 code first 如何配置自动迁移 [#I3CCR0](https://gitee.com/dotnetchina/Furion/issues/I3CCR0) + -  答疑 webapi 混合授权如何区分不同系统 [#I3CJCY](https://gitee.com/dotnetchina/Furion/issues/I3CJCY) + -  答疑 EFCore 不支持递归无限级遍历关系 [#I3CET9](https://gitee.com/dotnetchina/Furion/issues/I3CET9) + +--- + +## v1.15.0 (已发布) + +- **新特性** + + -  新增 跳过特定实体数据库操作监听特性 [#I386LB](https://gitee.com/dotnetchina/Furion/issues/I386LB) + -  新增 `IEntityChangedListener` 增加对 `OldEntity` 的支持 [#I385X2](https://gitee.com/dotnetchina/Furion/issues/I385X2) + -  新增 实时通信自动配置集线器拓展及特性 [#I387QX](https://gitee.com/dotnetchina/Furion/issues/I387QX) + -  新增 `Mapster` 拓展支持 `IMapper` 依赖注入方式 [#I38C7C](https://gitee.com/dotnetchina/Furion/issues/I38C7C) + -  新增 `[AppDbContext]` 特性默认构造函数 [#I38J97](https://gitee.com/dotnetchina/Furion/issues/I38J97) + -  新增 `UnifyContext.GetExceptionMetadata(context)` 返回错误码支持 [#I38ONX](https://gitee.com/dotnetchina/Furion/issues/I38ONX) + +- **突破性变化** + +- **问题修复** + + -  修复 多次循环中调用 `Db.GetNewDbContext()` 还是获取到同一个对象 [#I38NNP](https://gitee.com/dotnetchina/Furion/issues/I38NNP) + -  修复 `Swagger` 过滤掉 `object ` 类型属性问题 [#I38FHL](https://gitee.com/dotnetchina/Furion/issues/I38FHL) + -  修复 同一类不支持多继承 `IEntityChangedListener` 问题 [#I38UQJ](https://gitee.com/dotnetchina/Furion/issues/I38UQJ) + -  修复 自定义序列化属性名称导致验证失败属性不匹配问题 [#I38W8Z](https://gitee.com/dotnetchina/Furion/issues/I38W8Z) + +- **其他更改** + + -  更新 代码不规范命名导致开发者阅读代码时产生歧义 + +- **文档变化** + + -  新增 `FluentValidation` 集成文档 [#I38IOT](https://gitee.com/dotnetchina/Furion/issues/I38IOT) + +- **问答答疑** + + -  答疑 `Furion` 框架版本向下兼容问题 [#I38WMZ](https://gitee.com/dotnetchina/Furion/issues/I38WMZ) + +--- + +## v1.14.0(已发布) + +- **新特性** + + -  新增 `EFCore` 5.0 的 `Oracle` 数据库支持 [#I37Z8E](https://gitee.com/dotnetchina/Furion/issues/I37Z8E) + -  新增 控制是否在开发环境下显示数据库连接信息 [#I37YQ2](https://gitee.com/dotnetchina/Furion/issues/I37YQ2) + -  新增 `[NonUnify]` 支持在类中贴此特性 [#I359Q6](https://gitee.com/dotnetchina/Furion/issues/I359Q6) + -  新增 `网络请求` 字符串 `HttpClient` 拦截器 [#I35F3E](https://gitee.com/dotnetchina/Furion/issues/I35F3E) + -  新增 `HttpContext` 及 `HttpRequest` 获取远程地址拓展 [#I3688Z](https://gitee.com/dotnetchina/Furion/issues/I3688Z) + -  新增 `services.AddMvcFilter<>` 添加 `Mvc` 过滤器拓展 [#I368BH](https://gitee.com/dotnetchina/Furion/issues/I368BH) + +- **突破性变化** + + -  升级 框架依赖的 .NET 5 SDK 至最新版 5.0.3 [#I37YQQ](https://gitee.com/dotnetchina/Furion/issues/I37YQQ) + -  升级 `Swashbuckle.AspNetCore` 组件包到 `6.0.x` 版本 [#I37EZK](https://gitee.com/dotnetchina/Furion/issues/I37EZK) + -  移除 `Furion` 框架 `JWT` 拓展类,只在 `Furion.Extras.Authentication.JwtBearer` 中保留 [#I35D59](https://gitee.com/dotnetchina/Furion/issues/I35D59) + +- **问题修复** + + -  修复 传入错误 `JWT Token` 字符串导致自动刷新 `Token` 出现字符串边界值异常 bug [#I34ZE5](https://gitee.com/dotnetchina/Furion/issues/I34ZE5) + -  修复 瞬时作用域数据库上下文也会自动加入工作单元导致写日志时连锁异常 bug [#I37WTV](https://gitee.com/dotnetchina/Furion/issues/I37WTV) + +- **其他更改** + + -  更新 获取系统环境参数的性能 [#I36SR5](https://gitee.com/dotnetchina/Furion/issues/I36SR5) + -  更新 `Furion` 底层添加 `Mvc` 过滤器代码 [#I36SKA](https://gitee.com/dotnetchina/Furion/issues/I36SKA) + -  更新 添加默认 `Json` 序列化时间默认时间格式 [#I36SL0](https://gitee.com/dotnetchina/Furion/issues/I36SL0) + -  升级 `SqlSugar` 拓展包到 `5.0.2.6` 版本 [#I36SIG](https://gitee.com/dotnetchina/Furion/issues/I36SIG) + +- **文档变化** + + -  新增 数据库入门文档 [#I37Z8S](https://gitee.com/dotnetchina/Furion/issues/I37Z8S) + -  新增 更新日志文档 [#I36PI0](https://gitee.com/dotnetchina/Furion/issues/I36PI0) + -  新增 请求审计日志、执行 `Sql` 更新日志文档 [#I36PIK](https://gitee.com/dotnetchina/Furion/issues/I36PIK) + -  新增 前端使用 `axios` 跨域配置文档 [#I36PIT](https://gitee.com/dotnetchina/Furion/issues/I36PIT) + -  新增 `App` 静态类获取应用、环境更多信息数据 [#I36SOV](https://gitee.com/dotnetchina/Furion/issues/I36SOV) + -  新增 英文版 `README.md` 介绍 [#I37QHP](https://gitee.com/dotnetchina/Furion/issues/I37QHP) + +--- + +## v1.13.0(已发布) + +- **新特性** + + -  新增 多语言功能及拓展 [#I2DOCL](https://gitee.com/dotnetchina/Furion/issues/I2DOCL) + -  新增 事件总线功能及消息中心 [#I23BKN](https://gitee.com/dotnetchina/Furion/issues/I23BKN) + -  新增 `Swagger` 分组显示隐藏配置 [#I2AHH8](https://gitee.com/dotnetchina/Furion/issues/I2AHH8) + -  新增 `Furion.Extras.Logging.Serilog` 拓展插件 [#I2AAN8](https://gitee.com/dotnetchina/Furion/issues/I2AAN8) + -  新增 `cli.ps` 支持 `-Namespace` 命名空间指定 [#I2A175](https://gitee.com/dotnetchina/Furion/issues/I2A175) + -  新增 `Swagger` 规范化化文档授权失效后自动取消授权锁 [#I2AIWC](https://gitee.com/dotnetchina/Furion/issues/I2AIWC) + -  新增 `Request.Body` 支持重复读功能,主要解决微信 SDK 问题 [#I2AMG0](https://gitee.com/dotnetchina/Furion/issues/I2AMG0) + -  新增 网络请求功能及文档 [#I2APGJ](https://gitee.com/dotnetchina/Furion/issues/I2APGJ) + -  新增 `SqlSugar` 拓展包支持打印 `sql` 到 `MiniProfiler` 中 [#I2ASLS](https://gitee.com/dotnetchina/Furion/issues/I2ASLS) + -  新增 `Furion.Extras.DatabaseAccesssor.Dapper` 拓展插件 [#I2ASYA](https://gitee.com/dotnetchina/Furion/issues/I2ASYA) + -  新增 `Furion.Extras.DatabaseAccessor.PetaPoco` 拓展插件 [#I2AUGA](https://gitee.com/dotnetchina/Furion/issues/I2AUGA) + -  新增 网络请求字符串拓展方法 [#I2CPQ0](https://gitee.com/dotnetchina/Furion/issues/I2CPQ0) + -  新增 `SqlSugar` 拓展新增 `PagedList` 拓展 [#I2CW99](https://gitee.com/dotnetchina/Furion/issues/I2CW99) + -  新增 远程请求支持参数特性验证 [#I2CX5L](https://gitee.com/dotnetchina/Furion/issues/I2CX5L) + -  新增 `App.User` 获取当前授权用户信息便捷方法 [#I2CZLO](https://gitee.com/dotnetchina/Furion/issues/I2CZLO) + -  新增 规范化文档可配置功能,支持 `appsettings.json` 配置 [#I2D1K9](https://gitee.com/dotnetchina/Furion/issues/I2D1K9) + -  新增 远程请求拦截器添加方法和方法参数 [#I2D2CM](https://gitee.com/dotnetchina/Furion/issues/I2D2CM) + -  新增 远程请求出错返回默认值支持 [#I2D44M](https://gitee.com/dotnetchina/Furion/issues/I2D44M) + -  新增 远程请求 `body` 参数序列化支持设置 `PropertyNamingPolicy` [#I2D685](https://gitee.com/dotnetchina/Furion/issues/I2D685) + -  新增 远程服务接口客户端配置 [#I2D7PS](https://gitee.com/dotnetchina/Furion/issues/I2D7PS) + -  新增 `AddInject` 和 `UseInject` 允许自定义 `SecurityDefinitions` 和 `SwaggerUI` [#I2DIMG](https://gitee.com/dotnetchina/Furion/issues/I2DIMG) + -  新增 `[SecurityDefine]` 默认构造函数 [#I2DNXT](https://gitee.com/dotnetchina/Furion/issues/I2DNXT) + -  新增 `AspectDispatchProxy` 动态代理类 [#I2DO6I](https://gitee.com/dotnetchina/Furion/issues/I2DO6I) + -  新增 `[QueryParameters]` 特性,支持一键将 `Action` 参数添加 `[FromQuery]` 特性 [#I2G8TF](https://gitee.com/dotnetchina/Furion/issues/I2G8TF) + -  新增 动态日志配置及拓展方法 [#I2GDGD](https://gitee.com/dotnetchina/Furion/issues/I2GDGD) + -  新增 `WebApi` 请求谓词默认规则配置功能 [#I2M70X](https://gitee.com/dotnetchina/Furion/issues/I2M70X) + +- **突破性变化** + + -  升级 `.NET 5` SDK 到 `.NET 5.0.2` 版本 [#I2D0PZ](https://gitee.com/dotnetchina/Furion/issues/I2D0PZ) + -  调整 框架内所有拓展类命名空间,全部迁移到 `Furion.模块.Extensions` 下 [#I2AH54](https://gitee.com/dotnetchina/Furion/issues/I2AH54) + -  调整 `Swagger` 记住授权存储方式,替换 `Session` 存储方式为 `LocalStorage` 方式 [#I2AKUA](https://gitee.com/dotnetchina/Furion/issues/I2AKUA) + -  调整 `Furion` 框架包描述文件,减少框架体积 [#I2APAU](https://gitee.com/dotnetchina/Furion/issues/I2APAU) + -  调整 `App.CanBeScanTypes` 为 `App.EffectiveTypes` [#I2B0ZR](https://gitee.com/dotnetchina/Furion/issues/I2B0ZR) + -  调整 `App.ServiceProvider` 属性并移除 `App.GetDuplicateXXX` 方法 [#I2CYZE](https://gitee.com/dotnetchina/Furion/issues/I2CYZE) + -  调整 `Db.GetDuplicateDbContext` 为 `Db.GetNewDbContext` [#I2CZ04](https://gitee.com/dotnetchina/Furion/issues/I2CZ04) + -  调整 `Db.GetSqlDispatchProxy` 为 `Db.GetSqlProxy` [#I2DO9T](https://gitee.com/dotnetchina/Furion/issues/I2DO9T) + -  优化 `Aop` 服务拦截器,支持异步、同步两种方式 [#I2B9HQ](https://gitee.com/dotnetchina/Furion/issues/I2B9HQ) + -  优化 网络请求所有功能 [#I2BMR7](https://gitee.com/dotnetchina/Furion/issues/I2BMR7) + +- **问题修复** + + -  修复 `Swagger` 规范化化结果不一致 bug [#I2ACF3](https://gitee.com/dotnetchina/Furion/issues/I2ACF3) + -  修复 数据库新增或更新忽略空值操作方法报空异常 [#I2AB6C](https://gitee.com/dotnetchina/Furion/issues/I2AB6C) + -  修复 `Startup.cs` Aop 全局拦截无效 [#I2A7T2](https://gitee.com/dotnetchina/Furion/issues/I2A7T2) + -  修复 `Token` 过期后自动刷新 `Token` 无法获取最新的用户信息 bug [#I2AWQI](https://gitee.com/dotnetchina/Furion/issues/I2AWQI) + -  修复 `[ApiDescriptionSettings(Tag="xx")]` 导致 `swagger.json` 报错 bug [#I2B47R](https://gitee.com/dotnetchina/Furion/issues/I2B47R) + -  修复 `Mysql` sql 数据库查询结果 `tinyint` 类型转换出错 bug [#I2BEBM](https://gitee.com/dotnetchina/Furion/issues/I2BEBM) + -  修复 规范化结果多次包裹类型 bug [#I2BHHZ](https://gitee.com/dotnetchina/Furion/issues/I2BHHZ) + -  修复 动态 Api 基元类型数组问题 [#I2BMS5](https://gitee.com/dotnetchina/Furion/issues/I2BMS5) + -  修复 `sql` 查询枚举类型转换异常 bug [#I2BS2Y](https://gitee.com/dotnetchina/Furion/issues/I2BS2Y) + -  修复 `string.SqlQuerizeAsync()` 拓展返回错误 bug [#I2BSTS](https://gitee.com/dotnetchina/Furion/issues/I2BSTS) + -  修复 动态 Api 子类重写父类方法并取别名后 `Swagger` 异常 bug [#I2C9VP](https://gitee.com/dotnetchina/Furion/issues/I2C9VP) + -  修复 网络请求 `application/json` 序列化大小写问题 [#I2CRJC](https://gitee.com/dotnetchina/Furion/issues/I2CRJC) + -  修复 多数据库定位器实体嵌套关联 bug [#I2CVN0](https://gitee.com/dotnetchina/Furion/issues/I2CVN0) + -  修复 跨域响应头设置无效 bug [#I2CW5T](https://gitee.com/dotnetchina/Furion/issues/I2CW5T) + -  修复 远程网络请求代理打印到 `MiniProfiler` bug [#I2CZBC](https://gitee.com/dotnetchina/Furion/issues/I2CZBC) + -  修复 远程请求响应拦截器 bug [#I2D4DG](https://gitee.com/dotnetchina/Furion/issues/I2D4DG) + -  修复 `SqlSugar` 框架 `AsQueryable()` 一直追加参数 [#I2DH1D](https://gitee.com/dotnetchina/Furion/issues/I2DH1D) + -  修复 自动刷新 `Token` 空异常 bug [#I2DO29](https://gitee.com/dotnetchina/Furion/issues/I2DO29) + -  修复 生成 `JWT Token` 不传过期时间出现验证 401 bug [#I2DO8L](https://gitee.com/dotnetchina/Furion/issues/I2DO8L) + -  修复 `AppStartup` 排序无效 [#I2DVD2](https://gitee.com/dotnetchina/Furion/issues/I2DVD2) + -  修复 未启用多语言服务时友好异常和验证出现空异常 [#I2ECUJ](https://gitee.com/dotnetchina/Furion/issues/I2ECUJ) + -  修复 数据校验字母和数字组合无法匹配 bug [#I2EF2Q](https://gitee.com/dotnetchina/Furion/issues/I2EF2Q) + -  修复 数据校验手机或固话无效 bug [#I2M5IZ](https://gitee.com/dotnetchina/Furion/issues/I2M5IZ) + -  修复 `Dapper` 拓展解析 `SqlConnection` 异常 bug [#I2M5P2](https://gitee.com/dotnetchina/Furion/issues/I2M5P2) + -  修复 开启多语言后,`EF` 迁移异常 bug [#I2M7DT](https://gitee.com/dotnetchina/Furion/issues/I2M7DT) + -  修复 `IEntityTypeBuilder` 不支持多重继承 bug [#I2PAOD](https://gitee.com/dotnetchina/Furion/issues/I2PAOD) + -  修复 `JwtHandler` 设置自动刷新后,匿名访问无法通过 bug [#I2SDOX](https://gitee.com/dotnetchina/Furion/issues/I2SDOX) + -  修复 `Dapper` 拓展中 `SqlServer` 数据库获取连接对象类型 bug [#PR159](https://gitee.com/dotnetchina/Furion/pulls/159) + +- **其他更改** + + -  移除 `Sql` 查询结果映射检查 `[NotMapper]` 特性机制 [#I34XD0](https://gitee.com/dotnetchina/Furion/issues/I34XD0) + -  更新 依赖注入时排除 `IDynamicApiController` 接口 [#I2ECTG](https://gitee.com/dotnetchina/Furion/issues/I2ECTG) + -  更新 `MD5` 加密性能 [#PR158](https://gitee.com/dotnetchina/Furion/pulls/158) + +- **文档变化** + + -  优化 文档首页 [#I34XBR](https://gitee.com/dotnetchina/Furion/issues/I34XBR) + -  新增 网络请求文档 [#I2APGJ](https://gitee.com/dotnetchina/Furion/issues/I2APGJ) + -  新增 多语言文档 [#I2DOCL](https://gitee.com/dotnetchina/Furion/issues/I2DOCL) + -  新增 文档全文搜索引擎 [#I34XAW](https://gitee.com/dotnetchina/Furion/issues/I34XAW) + -  新增 全局静态类类型 [#I34XB4](https://gitee.com/dotnetchina/Furion/issues/I34XB4) + -  新增 框架可配置选项文档 [#I34XB9](https://gitee.com/dotnetchina/Furion/issues/I34XB9) + -  新增 事件总线文档 [#I34XBI](https://gitee.com/dotnetchina/Furion/issues/I34XBI) + -  新增 数据加解密文档 [#I34XC0](https://gitee.com/dotnetchina/Furion/issues/I34XC0) + -  新增 贡献指南文档 [#I34XC8](https://gitee.com/dotnetchina/Furion/issues/I34XC8) + -  新增 `HttpContext` 及 `文件上传下载` 博客文章 [#I34XCB](https://gitee.com/dotnetchina/Furion/issues/I34XCB) + -  优化 文档小调整,小优化 + +- **问答答疑** + + -  答疑 跨域设置无效 [#I2ASNJ](https://gitee.com/dotnetchina/Furion/issues/I2ASNJ) + -  答疑 `MVC` 视图无效,原因是 `.cshtml` 文件没有设置为 `内容` [#I2AXUU](https://gitee.com/dotnetchina/Furion/issues/I2AXUU) + -  答疑 `Sql` 操作可以实现事务吗?[#I2B0NX](https://gitee.com/dotnetchina/Furion/issues/I2B0NX) + -  答疑 `IRepository` 操作数据库会打开多次数据库连接 [#I2BB7B](https://gitee.com/dotnetchina/Furion/issues/I2BB7B) + -  答疑 如何进入自定义 `AppAuthorizeHandler` 断点 [#I2BGXY](https://gitee.com/dotnetchina/Furion/issues/I2BGXY) + -  答疑 `SqlSugar` 注入问题 [#I2C2AQ](https://gitee.com/dotnetchina/Furion/issues/I2C2AQ) + -  答疑 建议增加 API 签名验证,时效验证 [#I2C6ET](https://gitee.com/dotnetchina/Furion/issues/I2C6ET) + -  答疑 多数据库多租户同时使用 `Add-Migration` 报错 [#I2CEHS](https://gitee.com/dotnetchina/Furion/issues/I2CEHS) + -  答疑 `ISqlSugarRepository` 没有 `Getxxx` 方法 [#I2CJLZ](https://gitee.com/dotnetchina/Furion/issues/I2CJLZ) + -  答疑 `cli.ps1` 如何将 `sql` 里的表导出成 `model` 类 [#I2CSUL](https://gitee.com/dotnetchina/Furion/issues/I2CSUL) + -  答疑 手动修改 `Swagger` 终结点路径无效 [#I2D608](https://gitee.com/dotnetchina/Furion/issues/I2D608) + -  答疑 `DefaultDbContext` 不能识别 [#I2DCZX](https://gitee.com/dotnetchina/Furion/issues/I2DCZX) + -  答疑 各分层项目 `Startup.cs` 支持 `Configuration` [#I2DDUP](https://gitee.com/dotnetchina/Furion/issues/I2DDUP) + -  答疑 `Aop` 无法拦截,无效 [#I2DEY8](https://gitee.com/dotnetchina/Furion/issues/I2DEY8) + -  答疑 `mysql` 执行 `Add-Migration` 报错 [#I2DSB8](https://gitee.com/dotnetchina/Furion/issues/I2DSB8)\ + -  答疑 `Entity` 创建时间和是否删除添加默认值 [#I2E04H](https://gitee.com/dotnetchina/Furion/issues/I2E04H) + -  答疑 `swagger` 中多个 `servers` 设置 [#I2E0IF](https://gitee.com/dotnetchina/Furion/issues/I2E0IF) + -  答疑 全局筛选器 没有执行 [#I2E5R4](https://gitee.com/dotnetchina/Furion/issues/I2E5R4) + -  答疑 多数据库定位器疑问 [#I2E77T](https://gitee.com/dotnetchina/Furion/issues/I2E77T) + -  答疑 `cli.ps` 逆向工程 `Mysql` 数据库报错 [#I2E7I5](https://gitee.com/dotnetchina/Furion/issues/I2E7I5) + -  答疑 `Swagger` 开发环境 `applicationsettings.json` 中文乱码 [#I2EAG1](https://gitee.com/dotnetchina/Furion/issues/I2EAG1) + -  答疑 增加指定路径程序集映射 [#I2EEO2](https://gitee.com/dotnetchina/Furion/issues/I2EEO2) + -  答疑 动态编译 `cs` 脚本文件 [#I2EH66](https://gitee.com/dotnetchina/Furion/issues/I2EH66) + -  答疑 自定义中间件,返回的错误没有规范化结果 [#I2NV8S](https://gitee.com/dotnetchina/Furion/issues/I2NV8S) + -  答疑 `Swagger` 循环引用设置生成文档层级无效 [#I2PLQQ](https://gitee.com/dotnetchina/Furion/issues/I2PLQQ) + -  答疑 配置文件支持 `yaml` 文件吗? [#I2TJ3N](https://gitee.com/dotnetchina/Furion/issues/I2TJ3N) + -  答疑 修改数据库未 `mysql` 执行 `Add-Migration` 报错 [#I2VR64](https://gitee.com/dotnetchina/Furion/issues/I2VR64) + -  答疑 多数据库使用定位器时报错 [#I2VR8F](https://gitee.com/dotnetchina/Furion/issues/I2VR8F) + -  答疑 `Migration To Oracle` 异常 [#I2WBYQ](https://gitee.com/dotnetchina/Furion/issues/I2WBYQ) + -  答疑 开发时显示 `Swagger`,上线时关闭 `Swagger`,这需要怎么配置 [#I2WOYV](https://gitee.com/dotnetchina/Furion/issues/I2WOYV) + -  答疑 兼容 Mvc 复杂验证没有试验成功 [#I2X3GV](https://gitee.com/dotnetchina/Furion/issues/I2X3GV) + -  答疑 `Aop` 能不能支持无接口的类 [#I2X8AS](https://gitee.com/dotnetchina/Furion/issues/I2X8AS) + -  答疑 关于 `JWT Token` 自动刷新问题 [#I2YD4K](https://gitee.com/dotnetchina/Furion/issues/I2YD4K) + -  答疑 能否增加一个拓展的 `Entity`,增加一些拓展的属性 [#I2YDKT](https://gitee.com/dotnetchina/Furion/issues/I2YDKT) + -  答疑 `Furion` 无法还原包,使用`NuGet` 下载和通过最新的的脚手架下载都提示这个问题 [#I30446](https://gitee.com/dotnetchina/Furion/issues/I30446) + -  答疑 复杂校验与特性验证不能并行 [#I3046U](https://gitee.com/dotnetchina/Furion/issues/I3046U) + +--- + +## v1.7.0(已发布) + +- **新特性** + + -  新增 `Furion.Extras.ObjectMapper.Mapster` 拓展包 [#I29LSJ](https://gitee.com/dotnetchina/Furion/issues/I29LSJ) + -  新增 `Furion.Extras.Logging.Serilog` 拓展包 [#I2AAN8](https://gitee.com/dotnetchina/Furion/issues/I2AAN8) + -  新增 `Furion.Extras.Web.HttpContext` 拓展包 [#I29LSM](https://gitee.com/dotnetchina/Furion/issues/I29LSM) + -  新增 内置 `Token` 刷新机制支持 [#I29K57](https://gitee.com/dotnetchina/Furion/issues/I29K57) + -  新增 动态数据库上下文,支持运行时执行 `OnModelCreating` [#I28UDT](https://gitee.com/dotnetchina/Furion/issues/I28UDT) + -  新增 支持依赖注入排除指定接口 [#I29693](https://gitee.com/dotnetchina/Furion/issues/I29693) + -  新增 规范化结果返回时间戳字段 [#I29697](https://gitee.com/dotnetchina/Furion/issues/I29697) + -  新增 基础 `CURD` 父类操作例子 [#I296SR](https://gitee.com/dotnetchina/Furion/issues/I296SR) + -  新增 `sql.Change("定位器完整类型名称")` 支持 [#I29LAB](https://gitee.com/dotnetchina/Furion/issues/I29LAB) + -  新增 `UpdateInclude` 和 `UpdateExclude` 忽略空参数支持 [#I29VUG](https://gitee.com/dotnetchina/Furion/issues/I29VUG) + -  新增 数据库上下文内置假删除查询过滤器支持 [#I29Y2R](https://gitee.com/dotnetchina/Furion/issues/I29Y2R) + -  新增 忽略空值排除默认时间格式 [#I29VUV](https://gitee.com/dotnetchina/Furion/issues/I29VUV) + -  升级 `MiniProfiler` 组件 [#I297R9](https://gitee.com/dotnetchina/Furion/issues/I297R9) + +- **突破性变化** + + -  调整 `AppAuthorizeHandler` 授权管道为异步处理 [#I29MD9](https://gitee.com/dotnetchina/Furion/issues/I29MD9) + -  调整 `Swagger` 默认启用 `JWT` 授权支持 [#I29LI4](https://gitee.com/dotnetchina/Furion/issues/I29LI4) + -  调整 `HttpContextUtilities` 名称改为 `HttpContextLocal` [#I29KQE](https://gitee.com/dotnetchina/Furion/issues/I29KQE) + -  调整 `UnifyResultContext` 名称改为 `UnifyContext` [#I29LLZ](https://gitee.com/dotnetchina/Furion/issues/I29LLZ) + -  调整 只有执行迁移命令才扫描种子数据 [#I29E6P](https://gitee.com/dotnetchina/Furion/issues/I29E6P) + -  调整 规范化结果 `Successed` 属性名为 `Succeeded` [#I29NMV](https://gitee.com/dotnetchina/Furion/issues/I29NMV) + -  移除 `Mapster` 对象组件,采用提供拓展方式 [#I29D2M](https://gitee.com/dotnetchina/Furion/issues/I29D2M) + -  移除 `CacheManager` 拓展类 [#I29LU1](https://gitee.com/dotnetchina/Furion/issues/I29LU1) + -  优化 `SaveChanges` 拦截器 [#I292LO](https://gitee.com/dotnetchina/Furion/issues/I292LO) + +- **问题修复** + + -  修复 未注册的数据库上下文也被引用全局查询拦截器 bug [#I29ZXJ](https://gitee.com/dotnetchina/Furion/issues/I29ZXJ) + -  修复 手动返回 `BadObjectResult` 或 `ValidationProblemDetails` 结果类型时规范化结果失效 bug [#I29ZU9](https://gitee.com/dotnetchina/Furion/issues/I29ZU9) + -  修复 动态 WebApi `KeepName`,`KeepVerb`、`SplitCamelCase` 无效 bug [#I29X90](https://gitee.com/dotnetchina/Furion/issues/I29X90) + -  修复 `Sql代理` 返回 `元组` 类型出错 bug [#I29SMV](https://gitee.com/dotnetchina/Furion/issues/I29SMV) + -  修复 `401,403` 状态码规范化返回值属性变大写 bug [#I29M8Y](https://gitee.com/dotnetchina/Furion/issues/I29M8Y) + -  修复 `HttpContext` 空异常 bug [#I29LU4](https://gitee.com/dotnetchina/Furion/issues/I29LU4) + -  修复 接口无返回值没有应用规范化结果 bug [#I29GT7](https://gitee.com/dotnetchina/Furion/issues/I29GT7) + -  修复 前端 `Less` 配置文件导致主机启动失败 bug [#I29E7P](https://gitee.com/dotnetchina/Furion/issues/I29E7P) + -  修复 执行 `sql` 结果转泛型后属性重复赋值 bug [#I29BUO](https://gitee.com/dotnetchina/Furion/issues/I29BUO) + -  修复 `Swagger` 关闭 `MiniProfiler` 之后 `组中组` 失效 [#I29789](https://gitee.com/dotnetchina/Furion/issues/I29789) + -  修复 未启用规范化结果时异常返回 `System.Object` 字符 [#I2969A](https://gitee.com/dotnetchina/Furion/issues/I2969A) + -  修复 正数数据验证 0 也验证通过 bug [#I2955T](https://gitee.com/dotnetchina/Furion/issues/I2955T) + -  修复 非泛型类集成泛型接口依赖注入 bug [#I294YT](https://gitee.com/dotnetchina/Furion/issues/I294YT) + -  修复 `Swagger` 不支持 `new` 覆盖父类的 bug [#I28Z1A](https://gitee.com/dotnetchina/Furion/issues/I28Z1A) + -  修复 `JsonSerializerUtility` 没有公开 bug [#I28WMI](https://gitee.com/dotnetchina/Furion/issues/I28WMI) + -  修复 `SqlSugar` 拓展查询泛型类型注册异常 bug [#I28VMT](https://gitee.com/dotnetchina/Furion/issues/I28VMT) + -  修复 `Furion Tools` 不支持生成不同命名空间的实体 bug [#I2A175](https://gitee.com/dotnetchina/Furion/issues/I2A175) + -  修复 全局拦截器无效 bug [#I2A7T2](https://gitee.com/dotnetchina/Furion/issues/I2A7T2) + -  修复 新增或更新忽略空值空异常 bug [#I2AB6C](https://gitee.com/dotnetchina/Furion/issues/I2AB6C) + +- **其他更改** + + -  更新 `Token` 生成加密算法 [#I29KIH](https://gitee.com/dotnetchina/Furion/issues/I29KIH) + +- **文档变化** + + -  新增 日志文档 [#I28Y9D](https://gitee.com/dotnetchina/Furion/issues/I28Y9D) + -  调整 数据库上下文、实体拦截器、配置、一分钟入门等等文档 + +- **问答答疑** + + -  答疑 `Swagger` 如何实现授权访问 [#I294F2](https://gitee.com/dotnetchina/Furion/issues/I294F2) + -  答疑 如何实现多个数据库多对多实体配置 [#I29G6S](https://gitee.com/dotnetchina/Furion/issues/I29G6S) + -  答疑 动态 WebApi 支持文件上传吗 [#I29R5E](https://gitee.com/dotnetchina/Furion/issues/I29R5E) + -  答疑 多个数据库上下文无法生成迁移代码 [#I2A6II](https://gitee.com/dotnetchina/Furion/issues/I2A6II) + +--- + +## v1.4.0(已发布) + +- **新特性** + + -  新增 `Furion` 支持二级虚拟目录部署功能 [#I28B77](https://gitee.com/dotnetchina/Furion/issues/I28B77) + -  新增 `Furion.Template.RazorWithWebApi` 脚手架 [#I28QGI](https://gitee.com/dotnetchina/Furion/issues/I28QGI) + -  新增 `Furion.Template.BlazorWithWebApi` 脚手架 [#I27Z3O](https://gitee.com/dotnetchina/Furion/issues/I27Z3O) + -  新增 `EFCore` 时态查询拓展 [#I28AJ](https://gitee.com/dotnetchina/Furion/issues/I28AJ6) + -  新增 `[AppDbContext(连接字符串,数据库类型)]` 配置支持 [#I28QTB](https://gitee.com/dotnetchina/Furion/issues/I28QTB) + -  新增 `DateTimeOffset` 转 `DateTime` 拓展方法 [#I27MQA](https://gitee.com/dotnetchina/Furion/issues/I27MQA) + -  新增 `ValidationTypes` 验证正则表达式智能提示 [#I2801V](https://gitee.com/dotnetchina/Furion/issues/I2801V) + -  新增 `ValiationTypes.WordWithNumber` 验证 [#I2805](https://gitee.com/dotnetchina/Furion/issues/I2805A) + -  新增 获取客户端和服务端 IP 地址 [#I28QV9](https://gitee.com/dotnetchina/Furion/issues/I28QV9) + +- **突破性变化** + + -  升级 .NET 5.0 版本至 .NET 5.0.1 版本 [#I28QU](https://gitee.com/dotnetchina/Furion/issues/I28QU1) + -  优化 视图引擎功能,优化不规范命名和新增字符串模板编译 [#I28G0S](https://gitee.com/dotnetchina/Furion/issues/I28G0S) + -  优化 数据库实体查找算法,并优化性能 [#I28QUQ](https://gitee.com/dotnetchina/Furion/issues/I28QUQ) + -  更新 应用启动初始化性能和数据库第一次自动配置 `DbSet` 性能 + +- **问题修复** + + -  修复 多数据库上下文配置定位器后实体无法正确生成 bug [#I2888L](https://gitee.com/dotnetchina/Furion/issues/I2888L) + -  修复 多租户数据库上下文实体生成 bug [#I2891G](https://gitee.com/dotnetchina/Furion/issues/I2891G) + -  修复 对象验证失败提示消息没有应用 `JSON` 大小写配置 bug [#I27UTX](https://gitee.com/dotnetchina/Furion/issues/I27UTX) + -  修复 仓储 `Insert` 或 `Update` 方法指定 `ignoreNullValues` 无效 bug [#I27UN6](https://gitee.com/dotnetchina/Furion/issues/I27UN6) + -  修复 `Controller` 派生类如果贴了 `[Route]` 特性后出现在 `Swagger` 中 bug [#I27TN7](https://gitee.com/dotnetchina/Furion/issues/I27TN7) + -  修复 `SqlScalar` 执行 `sql` 返回 `Nullable` 类型出现转换失败 bug [#I27S2N](https://gitee.com/dotnetchina/Furion/issues/I27S2N) + -  修复 `[UnitOfWork]` 特性异常 bug [#I27MLM](https://gitee.com/dotnetchina/Furion/issues/I27MLM) + -  修复 `sql` 静态执行方式和 `sql` 高级代理无法监听数据库连接状态 bug [#I27M4F](https://gitee.com/dotnetchina/Furion/issues/I27M4F) + -  修复 更换 Json 序列化库无效 bug,如替换为 `Microsoft.AspNetCore.Mvc.NewtonsoftJson` [#I27M43](https://gitee.com/dotnetchina/Furion/issues/I27M43) + -  修复 `Furion Tools` 工具生成模型 bug [#I27XI5](https://gitee.com/dotnetchina/Furion/issues/I27XI5) + -  修复 软删除没有生效 bug [#I2804I](https://gitee.com/dotnetchina/Furion/issues/I2804I) + -  修复 `Furion Tools` 识别带多个 `\\` 的连接字符串识别 bug [#I280TS](https://gitee.com/dotnetchina/Furion/issues/I280TS),[#PR91](https://gitee.com/dotnetchina/Furion/pulls/91) + -  修复 `Furion Tools` 无法取消生成 bug [#I2816M](https://gitee.com/dotnetchina/Furion/issues/I2816M) + -  修复 `DateTimeOffset` 转本地时间差 8 小时 bug [#I28BA9](https://gitee.com/dotnetchina/Furion/issues/I28BA9) + -  修复 启用 `bundle js&css` 压缩后启动异常 bug [#I28KR](https://gitee.com/dotnetchina/Furion/issues/I28KRP) + -  修复 `ValidationTypes.Required` 无效 bug [#PR98](https://gitee.com/dotnetchina/Furion/pulls/98) + -  修复 规范化结果`OnValidateFailed` 参数名拼写错误 bug [#PR93](https://gitee.com/dotnetchina/Furion/pulls/93),[#PR92](https://gitee.com/dotnetchina/Furion/pulls/92) + -  修复 授权管道验证失败还显示结果 bug [#PR89](https://gitee.com/dotnetchina/Furion/pulls/89) + +- **其他更改** + + -  更新 README.md 友情连接地址 [#PR88](https://gitee.com/dotnetchina/Furion/pulls/88) + -  更新 模板脚手架源码,添加 `EFCore Tools` 库 [#PR87](https://gitee.com/dotnetchina/Furion/pulls/87) + -  更新 README.md NuGet 图标 [#PR85](https://gitee.com/dotnetchina/Furion/pulls/85) + -  移除 将 `List` 转 `DateTable` [#PR97](https://gitee.com/dotnetchina/Furion/pulls/97) + +- **文档变化** + + -  新增 视图引擎模板文档 [#I27ZVA](https://gitee.com/dotnetchina/Furion/issues/I27ZVA) + -  新增 `EFCore` 时态查询文档 [#I28AJ](https://gitee.com/dotnetchina/Furion/issues/I28AJ6), [DOC](http://furion.baiqian.ltd/docs/dbcontext-hight-query/#91111-%E6%97%B6%E6%80%81%E6%9F%A5%E8%AF%A2) + -  更新 仓储文档书写纰漏 bug [#PR90](https://gitee.com/dotnetchina/Furion/pulls/90) + -  更新 选项文档错误 bug [#PR86](https://gitee.com/dotnetchina/Furion/pulls/86) + -  更新 `实体数据监听器` 文档书写错误 bug [#PR83](https://gitee.com/dotnetchina/Furion/pulls/83/files) + -  更新 数据库上下文、多数据库、脚手架等文档 + +- **问答答疑** + + -  答疑 希望 `api` 返回的值自动将 null 转为 `''` 或 `[]` [#I286IJ](https://gitee.com/dotnetchina/Furion/issues/I286IJ) + -  答疑 添加网关功能 [#I27TP7](https://gitee.com/dotnetchina/Furion/issues/I27TP7),【已关闭】 + -  答疑 `SqlQuery` 获取单条记录方法 [#I28M1V](https://gitee.com/dotnetchina/Furion/issues/I28M1V) + -  答疑 希望可以提供集成 `Serilog` 例子 [#I282J4](https://gitee.com/dotnetchina/Furion/issues/I282J4) + -  答疑 如何通过特性配置唯一约束 [#I2891L](https://gitee.com/dotnetchina/Furion/issues/I2891L) + -  答疑 怎么读取 `appsettings.json` 数组 [#I27WU](https://gitee.com/dotnetchina/Furion/issues/I27WUR) + -  答疑 `IRepository` 出现空异常 [#I281IE](https://gitee.com/dotnetchina/Furion/issues/I281IE) + -  答疑 规范化接口问题问题 [#I28NMZ](https://gitee.com/dotnetchina/Furion/issues/I28NMZ) + -  答疑 统一返回值模型中 OnResponseStatusCodes 未执行 [#I28NNL](https://gitee.com/dotnetchina/Furion/issues/I28NNL) + +--- + +## v1.2.0(已发布) + +- **新特性** + + -  新增 雪花算法 [#I26OXG](https://gitee.com/dotnetchina/Furion/issues/I26OXG), [#PR78](https://gitee.com/dotnetchina/Furion/pulls/78) + -  新增 `[AppDbContext]` 配置数据库提供器支持 [#I27G3T](https://gitee.com/dotnetchina/Furion/issues/I27G3T) + -  新增 实体表数据更改监听接口 `IEntityDataChangedListener` [#I278DD](https://gitee.com/dotnetchina/Furion/issues/I278DD), [#I278LQ](https://gitee.com/dotnetchina/Furion/issues/I278LQ) + -  新增 全局服务接口 AOP 拦截功能 [#I278CP](https://gitee.com/dotnetchina/Furion/issues/I278CP) + -  新增 定位器仓储 `IDbRepository` [#I276Q3](https://gitee.com/dotnetchina/Furion/issues/I276Q3) + -  新增 数据库操作 `InsertOrUpdate` 支持排除空字符串功能 [#I272OG](https://gitee.com/dotnetchina/Furion/issues/I272OG) + -  新增 数据库操作 `UpdateInclude` 和 `UpdateExclude` 匿名对象支持 [#I271X0](https://gitee.com/dotnetchina/Furion/issues/I271X0) + -  新增 数据验证传入空对象跳过验证支持 [#I273R4](https://gitee.com/dotnetchina/Furion/issues/I273R4) + -  新增 应用启动时支持排除特定配置文件自动加载 [#I26U0A](https://gitee.com/dotnetchina/Furion/issues/I26U0A) + -  新增 单个实体表名前缀支持 [#I26LX0](https://gitee.com/dotnetchina/Furion/issues/I26LX0) + -  新增 `MySql` 数据库自动配置默认版本号 [#I26XQ6](https://gitee.com/dotnetchina/Furion/issues/I26XQ6) + -  更新 授权处理程序代码 + +- **突破性变化** + + -  新增 实体表数据更改监听接口 `IEntityDataChangedListener` [#I278DD](https://gitee.com/dotnetchina/Furion/issues/I278DD), [#I278LQ](https://gitee.com/dotnetchina/Furion/issues/I278LQ) + -  新增 全局服务接口 AOP 拦截功能 [#I278CP](https://gitee.com/dotnetchina/Furion/issues/I278CP) + -  新增 雪花算法 [#I26OXG](https://gitee.com/dotnetchina/Furion/issues/I26OXG), [#PR78](https://gitee.com/dotnetchina/Furion/pulls/78) + +- **问题修复** + + -  修复 视图引擎加载外部程序集出错 bug + -  修复 依赖注入代理接口报空对象异常 bug + -  修复 `EFCore` 取消附加实体出错 bug + -  修复 数据库仓储在非 Web 请求下出现空异常 bug + -  修复 多个授权策略共存问题出现无效 bug + -  修复 友好异常 `Oop.Oh` 不支持普通方法 bug + -  修复 获取多租户对象时数据库上下文出现作用域验证失败 bug + -  修复 工作单元不支持 `Sql代理` 拦截 bug [#I27GST](https://gitee.com/dotnetchina/Furion/issues/I27GST) + +- **文档变化** + + -  新增 [实体数据监听器](http://furion.baiqian.ltd/docs/dbcontext-entitytrigger) 文档 + -  更新 一分钟入门、应用启动、官方脚手架、数据库操作指南、对象映射、规范化文档、异常处理、鉴权授权文档 + +--- + +## v1.1.0(已发布) + +- **新特性** + + -  新增 `Db.GetDbContext()` 获取默认数据库上下文方法 + -  新增 `HttpContextUtility.GetCurrentHttpContext()` 获取全局 `HttpContext` 上下文 + -  新增 `App.GetRequiredService<>` 解析服务方法 + -  新增 `object.GetService<>` 对象拓展方法 + -  新增 策略授权 `PolicyPipeline` 基类方法,支持多重判断授权 + -  新增 `JWTEncryption.ValidateJwtBearerToken` 手动验证静态方法 + -  新增 全局数据库上下文 `InsertOrUpdateIgnoreNullValues` 和 `EnabledEntityStateTracked` 全局配置 + -  新增 `Swagger Jwt授权` 全局授权参数 [#I26GLR](https://gitee.com/dotnetchina/Furion/issues/I26GLR) + -  新增 `InsertOrUpdate` 支持自定义判断条件功能 [#I269Q1](https://gitee.com/dotnetchina/Furion/issues/I269Q1) + -  新增 字符串字段小写命名支持 [#I2695D](https://gitee.com/dotnetchina/Furion/issues/I2695D) + -  新增 字符串文本对比功能 [#I268LE](https://gitee.com/dotnetchina/Furion/issues/I268LE) + -  新增 全局异常特性消息功能 [#I2662O](https://gitee.com/dotnetchina/Furion/issues/I2662O) + -  新增 `Insert` 或 `Update` 数据库忽略空值功能 [#I264Q4](https://gitee.com/dotnetchina/Furion/issues/I264Q4) + +- **突破性变化** + + -  调整 `Fur` 项目名为 `Furion` + -  调整 `Db.GetRequestDbContext<>()` 命名为 `Db.GetDbContext<>()` + -  调整 `Db.GetDbContext<>()` 命名为 `Db.GetDuplicateDbContext<>()` + -  优化 `App.GetService<>` 解析服务的底层逻辑,大大提高了解析服务的性能 + -  优化 授权核心代码,保持和微软一致的授权规范 [#I26DCB](https://gitee.com/dotnetchina/Furion/issues/I26DCB) + -  移除 `App.GetRequestService<>` 方法 + -  移除 `ValidateJwtBearer` Jwt 授权方法,无需手动判断了 + +- **问题修复** + + -  修复 Furion 官方脚手架生成后编译异常 bug + -  修复 `Tenant` 内置属性不是 `virtual` 修饰 bug + -  修复 `dockerfile` 新命名构建失败 bug + -  修复 自定义角色授权和多个授权共存出现 403 bug [#I26H1L](https://gitee.com/dotnetchina/Furion/issues/I26H1L) + -  修复 `httpContext.GetEndpoint()` 空异常 bug [#PR73](https://gitee.com/dotnetchina/Furion/pulls/73) + -  修复 `Oops.Oh` 空异常和不支持服务抛异常 bug [#I26EFU](https://gitee.com/dotnetchina/Furion/issues/I26EFU),[#I26GM4](https://gitee.com/dotnetchina/Furion/issues/I26GM4) + -  修复 `cli.ps` 生成文件编码乱码 bug [#I26DVT](https://gitee.com/dotnetchina/Furion/issues/I26DVT) + -  修复 `Swagger` 文件上传按钮不显示 [#I26B6U](https://gitee.com/dotnetchina/Furion/issues/I26B6U) + -  修复 规范化结果授权状态码序列化大小写不一致问题 [#I26B26](https://gitee.com/dotnetchina/Furion/issues/I26B26) + -  修复 未启用规范化结果时中文乱码 bug [#I268T5](https://gitee.com/dotnetchina/Furion/issues/I268T5) + -  修复 `MySql` 异步异常捕获不到 bug [#I265SO](https://gitee.com/dotnetchina/Furion/issues/I265SO) + -  修复 `cli.ps1` 提示找不到数据库连接字符串 bug [#I2647U](https://gitee.com/dotnetchina/Furion/issues/I2647U) + +- **其他更改** + + -  优化 代码性能小优化和小调整 + +- **文档变化** + + -  更新 一分钟入门、安全鉴权、数据库等文档 + +--- + +## v1.0.3(已发布) + +- **新特性** + + -  新增 Mvc 模板脚手架:`Fur.Template.Mvc` + -  新增 WebApi 模板:`Fur.Template.Api` + -  新增 Mvc/WebApi 模板:`Fur.Template.App` + -  新增 Razar Pages 模板:`Fur.Template.Razor` + -  新增 Blazor 模板:`Fur.Template.Blazor` + +- **突破性变化** + + -  调整 `PagedList` 到 `System.Collections.Generic` 命名空间下 + -  更新 解析服务性能问题,底层代码大量优化 + +- **问题修复** + + -  修复 `ApiSears.ControllerEnd`不起作用 bug [#I25KH6](https://gitee.com/dotnetchina/Furion/issues/I25KH6) + -  修复 `RemoteRequest` 请求完成结果序列化属性大小写问题 [#I25I8R](https://gitee.com/dotnetchina/Furion/issues/I25I8R) + -  修复 `HttpContext.GetEndpoinet()` 空异常 bug [#PR73](https://gitee.com/dotnetchina/Furion/pulls/73) + +- **文档变化** + + -  更新 入门文档、数据库上下文文档、多数据库操作文档 + +--- + +## v1.0.2(已发布) + +- **新特性** + + -  新增 `Pomelo.EntityFrameworkCore.MySql` 最新 .NET 5 包配置 [#I24ZQK](https://gitee.com/dotnetchina/Furion/issues/I24ZQK) + -  新增 `.AddDateTimeJsonConverter(format)` 时间格式序列化配置 + -  新增 `DateTime` 和 `DateTimeOffset` 类型序列化格式配置 [#I253FI](https://gitee.com/dotnetchina/Furion/issues/I253FI) + +- **突破性变化** + + -  更新 `Mapster` 包至 `7.0.0` 版 + -  调整 `App.Services` 名为 `App.ServiceProvider` + -  移除 `App.ApplicationServices` 和 `App.GetRequestService<>()` + -  移除 非 Web 主机注入拓展 + +- **问题修复** + + -  修复 `services.AddFriendlyException()` 缺少配置注入 bug + -  修复 数据库上下文池被释放和高并发下内存溢出 bug [#I2524K](https://gitee.com/dotnetchina/Furion/issues/I2524K),[#I24UMN](https://gitee.com/dotnetchina/Furion/issues/I24UMN) + -  修复 `Sql代理` 返回空数据时异常 bug [#I24TCK](https://gitee.com/dotnetchina/Furion/issues/I24TCK) + -  修复 工作单元 `[UnitOfWork]` 多数据库被释放 bug [#I24Q6W](https://gitee.com/dotnetchina/Furion/issues/I24Q6W) + +- **其他更改** + + -  调整 `EntityBase` 和 `Entity` 所有属性为 `vitural` 修饰 + -  更新 `Jwt` 读取和解析性能 + -  更新 优化代码支持 C# 9.0 最新语法 + -  更新 `MD5` 加密性能 [#PR71](https://gitee.com/dotnetchina/Furion/pulls/71) + -  移除 无用或未使用代码 + +- **文档** + + -  更新 数据库上下文、多数据库、一分钟入门文档 + +--- + +## v1.0.0(已发布,.NET5) + +- **新特性** + + -  新增 网络请求 `RemoteRequest` 组件 [#I1YYWD](https://gitee.com/dotnetchina/Furion/issues/I1YYWD) + -  新增 `.AddInjectBase()` 注入,只包含基础服务注入 + -  新增 所有服务都支持 `IServiceCollection` 和 `IMvcBuilder` 注入 + -  新增 抛异常状态码设置功能 `StatusCode` + -  新增 `Swagger` 序列化支持 `Pascal` 属性命名方式 + +- **突破性变化** + + -  更新 **所有的包为 `.NET 5` 正式版** + +- **问题修复** + + -  修复 `SqlProxy` 代理异步处理 bug + -  修复 数据库类型 `Datetime` 转 `DateTimeOffset` bug + -  修复 属性首字母大小写序列化不匹配出现 `null` bug + -  修复 对象序列化中文出现乱码 bug + -  修复 默认序列化配置无效 bug + -  修复 数据库非依赖注入方式提交无效 bug + -  修复 应用程序池提交所有 `DbContext` 空异常 bug + -  修复 `Saas` 多租户 `Tenant` 类型字符串属性在 `MySql` 数据库下出现 `longtext` 类型 bug + -  修复 `Mvc` 自动验证字符串空值 bug [#I24M2T](https://gitee.com/dotnetchina/Furion/issues/I24M2T) + -  修复 枚举注释被覆盖 bug [#I24N6J](https://gitee.com/dotnetchina/Furion/issues/I24N6J) + -  修复 忽略规范化结果无效 bug [#I24B8P](https://gitee.com/dotnetchina/Furion/issues/I24B8P) + -  修复 `Swagger` 默认 `ContentType` 不是 `applicaiton/json` bug [#I24F3U](https://gitee.com/dotnetchina/Furion/issues/I24F3U) + -  修复 内置 `System.Text.Json` 和 `Newtonsoft.Json` 冲突 bug [#I24F3U](https://gitee.com/dotnetchina/Furion/issues/I24F3U) + +- **其他更改** + + -  调整 `Fur` 框架域名为:[http://furion.baiqian.ltd](http://furion.baiqian.ltd) + -  调整 仓储 `FromSqlRaw` 和 `FromSqlInterpolated` 接口位置 + -  更新 数据加解密性能,[#PR70](https://gitee.com/dotnetchina/Furion/pulls/70) + +- **文档** + + -  更新 README.md、框架介绍、数据库上下文、配置选项、多租户、跨域文档 diff --git a/handbook/docs/view-engine.mdx b/handbook/docs/view-engine.mdx new file mode 100644 index 0000000000000000000000000000000000000000..db90095429ddcae42448d9a500c4da4d5d719fdb --- /dev/null +++ b/handbook/docs/view-engine.mdx @@ -0,0 +1,485 @@ +--- +id: view-engine +title: 17. 视图/模板引擎 +sidebar_label: 17. 视图/模板引擎 +--- + +import Tag from "@site/src/components/Tag.js"; + +
+ 📝 模块更新日志 +
+
+ +- **新特性** + + -  新增 `TP.WrapperRectangle` 绘制矩形日志模板 4.8.8.25 ⏱️2023.06.14 [60ffd76](https://gitee.com/dotnetchina/Furion/commit/60ffd76783633ac4fc7baaf845e14cb59518b795) + -  新增 视图引擎支持无命名空间的强类型 4.8.4.16 ⏱️2023.01.15 [#I6ABN3](https://gitee.com/dotnetchina/Furion/issues/I6ABN3) [#I6A7SI](https://gitee.com/dotnetchina/Furion/issues/I6A7SI) [076bb17](https://gitee.com/dotnetchina/Furion/commit/076bb1781ab1a31b5b6a42a56910909b3528d25e) + -  新增 视图引擎支持匿名类型模型带集合类型属性 `@foreach` 遍历 4.8.4.15 ⏱️2023.01.13 [#I6A7SI](https://gitee.com/dotnetchina/Furion/issues/I6A7SI) + +- **问题修复** + + -  修复 模板引擎高并发读取缓存模板出现线程占用问题 4.8.8.43 ⏱️2023.09.14 [#I80ZKB](https://gitee.com/dotnetchina/Furion/issues/I80ZKB) + -  修复 `TP.Wrapper` 静态类不能准确识别多行内容问题 4.8.7.40 ⏱️2023.04.10 [#I6UAC8](https://gitee.com/dotnetchina/Furion/issues/I6UAC8) + -  修复 视图引擎模型为匿名泛型集合类型时出现类型转换异常 4.8.7.38 ⏱️2023.04.07 [!773](https://gitee.com/dotnetchina/Furion/pulls/773) + -  修复 视图引擎不支持强制转换的 `(object)model` 类型 4.8.7.16 ⏱️2023.03.19 [#I6O3BD](https://gitee.com/dotnetchina/Furion/issues/I6O3BD) + +- **其他更改** + + -  调整 视图引擎默认程序集,追加 `System.Collections` 程序集 4.8.7.16 ⏱️2023.03.18 [#I6O3BD](https://gitee.com/dotnetchina/Furion/issues/I6O3BD) + +
+
+
+ +## 17.1 关于视图引擎 + +视图引擎负责根据视图模板创建 HTML。视图通常是 HTML 和编程语言的某种混合。支持变量定义、方法调用及逻辑编写。 + +在 `Furion` 框架中,底层集成了微软提供的 `Razor` 视图引擎组件并提供更加灵活方便的语法糖。 + +## 17.2 视图引擎作用 + +- **支持 `ASP.NET Core` 完整的 `Razor` 语法** +- 根据不同的数据编译模板产生不同的输出 +- 实现强大的插件化机制 +- 实现全站页面静态化 +- 可以用作邮件模板、短信模板、优惠券信息模板等 + +## 17.3 基础使用 + +### 17.3.1 注册服务 + +使用之前需在 `Startup.cs` 中注册 `视图引擎服务`: + +```cs showLineNumbers {3} +public void ConfigureServices(IServiceCollection services) +{ + services.AddViewEngine(); +} +``` + +### 17.3.2 使用方式 + +- 构造函数注入 `IViewEngine` + +```cs showLineNumbers {2,9,12} +using Furion.DynamicApiController; +using Furion.ViewEngine; + +namespace Furion.Application +{ + public class ViewEngineService : IDynamicApiController + { + private readonly IViewEngine _viewEngine; + public ViewEngineService(IViewEngine viewEngine) + { + _viewEngine = viewEngine; + var result = _viewEngine.RunCompile("Hello @Model.Name", new { Name = "Furion" }); + } + } +} +``` + +- 字符串方式 + +```cs showLineNumbers +var result = "Hello @Model.Name".RunCompile(new { Name = "Furion" }); +``` + +### 17.3.3 弱类型模板 + +```cs showLineNumbers +var result = _viewEngine.RunCompile("Hello @Model.Name", new { Name = "Furion" }); +``` + +结果: + +```html showLineNumbers +Hello Furion +``` + +支持异步 `RunCompileAsync` + +### 17.3.4 强类型模板 + +- **类型定义** + +```cs showLineNumbers {1} +namespace YourProject; // Furion 4.8.4.16+ 支持无命名空间写法 + +public class TestModel +{ + public string Name { get; set; } + public int[] Items { get; set; } +} +``` + +- **使用强类型** + +```cs showLineNumbers {1,7} +var result = _viewEngine.RunCompile(@" +Hello @Model.Name +@foreach(var item in Model.Items) +{ +

@item

+} +", new TestModel // Furion 4.8.4.16+ 支持匿名类型 +{ + Name = "Furion", + Items = new[] { 3, 1, 2 } +}); +``` + +结果: + +```html showLineNumbers +Hello Furion +

3

+

1

+

2

+``` + +支持异步 `RunCompileAsync` + +### 17.3.5 高性能模板缓存 🥇 + +由于模板编译需要消耗大量的性能,所以建议使用带 `FromCached` 结尾的 `RunCompileFromCached` 替代。调用该方法后会自动将模板编译成 `.dll` 以便下次使用。减少第二次之后使用模板的性能损耗。 + +如,强类型模板: + +```cs showLineNumbers {1} +var result = _viewEngine.RunCompileFromCached(@" +Hello @Model.Name +@foreach(var item in Model.Items) +{ +

@item

+} +", new TestModel // Furion 4.8.4.16+ 支持匿名类型 +{ + Name = "Furion", + Items = new[] { 3, 1, 2 } +}); +``` + +结果: + +```html showLineNumbers +Hello Furion +

3

+

1

+

2

+``` + +**调用 `RunCompileFromCached` 方法之后将会使用 `MD5` 加密模板并生成 `MD5`字符串的 `.dll` 存放在网站根目录下的 `templates` 目录中。只要模板内容不变,数据发生改变也不会重新编译模板。这样大大的提高了首次之后的性能。** + +如,传入新的数据: + +```cs showLineNumbers {1,10} +var result = _viewEngine.RunCompileFromCached(@" +Hello @Model.Name +@foreach(var item in Model.Items) +{ +

@item

+} +", new TestModel // Furion 4.8.4.16+ 支持匿名类型 +{ + Name = "Furion", + Items = new[] { 5,6,7,8 } +}); +``` + +结果: + +```html showLineNumbers +Hello Furion +

5

+

6

+

7

+

8

+``` + +模板不再重新编译,只是重新替换数据。 + +## 17.4 高级用法 + +高级用法支持将特定程序集、特定命名空间、特定类型引入到模板中使用。 + +### 17.4.1 添加程序集 + +比如这里添加 `System.IO` 程序集: + +```cs showLineNumbers {3} +var result = _viewEngine.RunCompileFromCached(@"
@System.IO.Path.Combine(""Furion"", ""ViewEngine"")
", builderAction: builder => + { + builder.AddAssemblyReferenceByName("System.IO"); + }); +``` + +结果: + +```html showLineNumbers +
Furion\\ViewEngine
+``` + +另外,`Furion` 提供多种方式加载程序集: + +```cs showLineNumbers +builder.AddAssemblyReferenceByName("System.Security"); // 通过名称 +builder.AddAssemblyReference(typeof(System.IO.File)); // 通过类型 +builder.AddAssemblyReference(Assembly.Load("source")); // 通过元数据引用 +``` + +### 17.4.2 添加命名空间 + +```cs showLineNumbers {3-4} +var result = _viewEngine.RunCompileFromCached(@"
@Path.Combine(""Furion"", ""ViewEngine"")
", builderAction: builder => + { + builder.AddUsing("System.IO"); + builder.AddAssemblyReferenceByName("System.IO"); + }); +``` + +结果: + +```html showLineNumbers +
Furion\\ViewEngine
+``` + +也支持加入多个 `using`: + +``` +builder.AddUsing("System.IO"); +builder.AddUsing("Furion"); +``` + +### 17.4.3 定义模板方法 + +```cs showLineNumbers +var result = _viewEngine.RunCompileFromCached(@" + + @{ RecursionTest(3); } + + +@{ + void RecursionTest(int level) + { + if (level <= 0) + { + return; + } + +
LEVEL: @level
+ @{ RecursionTest(level - 1); } + } +} +"); +``` + +结果: + +```html showLineNumbers + +
LEVEL: 3
+
LEVEL: 2
+
LEVEL: 1
+ +``` + +### 17.4.4 调用类方法 + +定义 `CustomModel` 类并继承 `ViewEngineModel` 基类 + +```cs showLineNumbers {1,6} +public class CustomModel : ViewEngineModel +{ + public int A { get; set; } + public string B { get; set; } + + public string Decorator(object value) + { + return "-=" + value + "=-"; + } +} +``` + +在模板中调用 `Decorator(value)` 方法: + +```cs showLineNumbers {1,3,5} +var content = @"Hello @A, @B, @Decorator(123)"; + +var template = _viewEngine.Compile(content); + +var result = template.Run(instance => +{ + instance.A = 10; + instance.B = "Alex"; +}); +``` + +结果: + +```html showLineNumbers +Hello 10, Alex, -=123=- +``` + +## 17.5 特殊字符处理 + +如有特殊符号如:`<`,`<>`,`&` 等符号,可通过 `@("<")` 或 `@('<')` 进行原样输出,如: + +```cs showLineNumbers +var str = "bool value = 1 @('<') 2"; // 源字符串为:bool value = 1 < 2; +``` + +## 17.6 字符串模板替换引擎 + +`Furion` 除了内置视图引擎之外,还支持以下几种模板替换,如: + +```cs showLineNumbers {2-4,7} +// 提供数据模板方式 +var str = "我叫{name}".Render(new Dictionary{ {"name", "Furion"} }); +var str = "我叫{Name}".Render(new { Name = "Furion" }); +var str = "我叫{Detail.Name}".Render(new { Detail = new { Name = "Furoin" } }); + +// 从配置读取方式 +var str = "我叫#(Furion:Address)".Render(); +``` + +```json showLineNumbers +{ + "Furion": { + "Address": "http://furion.baiqian.ltd" + } +} +``` + +## 17.7 格式化规范模板 + +在 `Furion v3.5.3+` 新增了 `TP.Wrapper(...)` 规范模板,使用如下: + +```cs showLineNumbers {2} +// 生成模板字符串 +var template = TP.Wrapper("Furion 框架", "让 .NET 开发更简单,更通用,更流行。", + "##作者## 百小僧", + "##当前版本## v3.5.3", + "##文档地址## http://furion.baiqian.ltd", + "##Copyright## 百小僧, 百签科技(广东)有限公司"); + +Console.WriteLine(template); +``` + +日志打印模板如下: + +```bash showLineNumbers +┏━━━━━━━━━━━ Furion 框架 ━━━━━━━━━━━ +┣ 让 .NET 开发更简单,更通用,更流行。 +┣ +┣ 作者: 百小僧 +┣ 当前版本: v3.5.3 +┣ 文档地址: http://furion.baiqian.ltd +┣ Copyright: 百小僧, 百签科技(广东)有限公司 +┗━━━━━━━━━━━ Furion 框架 ━━━━━━━━━━━ +``` + +:::tip 关于属性生成 + +如果列表项以 `##属性名##` 开头,自动生成 `属性名:` 作为行首且自动等宽对齐。 + +`Furion 3.9.1` 之前版本使用 `[属性名]` 开头。 + +::: + +## 17.8 生成矩形日志模板 + +:::important 版本说明 + +以下内容仅限 `Furion 4.8.8.25 +` 版本使用。 + +::: + +```cs showLineNumbers {1} +var template = TP.WrapperRectangle(new[] { + "百小僧", + "让 .NET 开发更简单,更通用,更流行。", + "一个应用程序框架,您可以将它集成到任何 .NET/C# 应用程序中。" +}); + +Console.WriteLine(template); +``` + +日志打印模板如下: + +```bash showLineNumbers ++-----------------------------------------------------------------------------+ +| 百小僧 | +| 让 .NET 开发更简单,更通用,更流行。 | +| 一个应用程序框架,您可以将它集成到任何 .NET/C# 应用程序中。 | ++-----------------------------------------------------------------------------+ +``` + +还可以配置左对齐,居中对齐,右对齐: + +```cs showLineNumbers {6,13,20} +// 左对齐 +var template = TP.WrapperRectangle(new[] { + "百小僧", + "让 .NET 开发更简单,更通用,更流行。", + "一个应用程序框架,您可以将它集成到任何 .NET/C# 应用程序中。" +}, -1); // -1 表示左对齐 + +// 居中对齐 +var template = TP.WrapperRectangle(new[] { + "百小僧", + "让 .NET 开发更简单,更通用,更流行。", + "一个应用程序框架,您可以将它集成到任何 .NET/C# 应用程序中。" +}, 0); // 0 表示居中对齐 + +// 右对齐 +var template = TP.WrapperRectangle(new[] { + "百小僧", + "让 .NET 开发更简单,更通用,更流行。", + "一个应用程序框架,您可以将它集成到任何 .NET/C# 应用程序中。" +}, 1); // 1 表示右对齐 +``` + +输出如下: + +```bash showLineNumbers ++-----------------------------------------------------------------------------+ +| 百小僧 | +| 让 .NET 开发更简单,更通用,更流行。 | +| 一个应用程序框架,您可以将它集成到任何 .NET/C# 应用程序中。 | ++-----------------------------------------------------------------------------+ + ++-----------------------------------------------------------------------------+ +| 百小僧 | +| 让 .NET 开发更简单,更通用,更流行。 | +| 一个应用程序框架,您可以将它集成到任何 .NET/C# 应用程序中。 | ++-----------------------------------------------------------------------------+ + ++-----------------------------------------------------------------------------+ +| 百小僧 | +| 让 .NET 开发更简单,更通用,更流行。 | +| 一个应用程序框架,您可以将它集成到任何 .NET/C# 应用程序中。 | ++-----------------------------------------------------------------------------+ +``` + +另外还可以配置矩形最长字符串的追加长度。 + +```cs +// 右对齐 +var template = TP.WrapperRectangle(new[] { + "百小僧", + "让 .NET 开发更简单,更通用,更流行。", + "一个应用程序框架,您可以将它集成到任何 .NET/C# 应用程序中。" +}, 1, 20); // 20 表示最长字符串长度 + 20 +``` + + +## 17.9 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/virtual-deploy.mdx b/handbook/docs/virtual-deploy.mdx new file mode 100644 index 0000000000000000000000000000000000000000..3ed28fc0a727cd827d069fb755779e840ad5ee4e --- /dev/null +++ b/handbook/docs/virtual-deploy.mdx @@ -0,0 +1,115 @@ +--- +id: virtual-deploy +title: 34.4 二级虚拟目录部署 +sidebar_label: 34.4 二级虚拟目录部署 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +:::tip 视频教程 + +[https://www.bilibili.com/video/BV1Rv4y1P7ZB](https://www.bilibili.com/video/BV1Rv4y1P7ZB) + +::: + +## 34.4.1 关于二级虚拟目录 + +通常我们的站点都是部署在网站的根目录下的,但是有些时候,我们可能将网站根目录下的目录作为二级站点或子站点,这个时候,就会出现 `404` 错误了。 + +这个时候就需要进行一些特殊配置了。 + +## 34.4.2 针对在 `IIS` 网站下添加 `Application` 情况 + +:::warning 版本说明 + +以下内容仅限 `Furion 3.2.0 +` 版本使用。 + +::: + +如果网站是在 `Site` 下面添加 `Application` 下部署,则遵循以下步骤: + +1. 添加 `Application Pool` 应用程序池,并设置为非托管模式 + + + +2. 添加 `Application` 并选择刚刚创建的应用程序池 + + + +
+ + + +
+ + + +3. 添加应用配置: + +```json showLineNumbers {2,3} +{ + "SpecificationDocumentSettings": { + "ServerDir": "IIS中应用程序名(Applicaiton)" + } +} +``` + +**该配置主要是解决 `Swagger` 出现 `404` 问题。** + +:::tip 个别情况 + +如果配置之后还 `Swagger` 还出现不能加载 `swagger.json` 文件问题,那么需修改启动注册代码: + +```cs showLineNumbers +app.UseInject(string.Empty); // 确保参数是 strng.Empty +``` + +::: + +## 34.4.3 针对非 `IIS` 下部署情况 + +:::caution 注意事项 + +这里是针对 `非 IIS` 部署使用的!!! + +::: + +### 34.4.3.1 配置 `AppSettings` + +我们只需要配置 `AppSettings` 即可: + +```json showLineNumbers {2,3} +{ + "AppSettings": { + "VirtualPath": "/虚拟目录" + } +} +``` + +### 34.4.3.2 `.NET6 WebApplication 模式下虚拟目录配置` + +:::important 版本说明 + +以下内容仅限 `Furion 3.2.0 +` 版本使用。 + +::: + +由于在 `.NET6` 的 `WebApplication` 模式下微软底层发生了改变,**所以需要使用 `app.UseVirtualPath()` 包裹 `app.UseInject()` 和 `app.MapRouteControllers()`**: + +```cs showLineNumbers {1,4} +app.UseVirtualPath(app => +{ + app.UseInject(String.Empty); // 注意 String.Empty 只是例子,可以不填或填其他的,见一分钟入门 + app.MapRouteControllers(); +}); +``` + +**注意,`app.MapRouteControllers()` 是替换 `app.MapControllers()` 的!** + +## 34.4.4 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/vsfast.mdx b/handbook/docs/vsfast.mdx new file mode 100644 index 0000000000000000000000000000000000000000..4d2cfa0af39faf7f3049782432be78704d8848ce --- /dev/null +++ b/handbook/docs/vsfast.mdx @@ -0,0 +1,70 @@ +--- +id: vsfast +title: 2.14 Visual Studio 高效率 +sidebar_label: 2.14 Visual Studio 高效率 +description: 工欲善其事必先利其器 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +## 工欲善其事必先利其器! + +Visual Studio 提供了非常多代码辅助功能,启用这些功能将大大提高开发效率,这里列举一些常见的配置,后续不断完善。 + +## 2.14.1 开启内联参数提示 + + + + + +## 2.14.2 开启全局智能提示 + + + + + +## 2.14.3 实时显示诊断错误 + +在过去,我们需要写完代码编译才能知道具体的错误,最新版的 `Visual Studio` 支持 **内联诊断错误**,开启如下: + + + + + +## 2.14.4 中文智能提示 + +打开网站 [https://dotnet.microsoft.com/zh-cn/download/intellisense](https://dotnet.microsoft.com/zh-cn/download/intellisense) 下载对应的语言版本。 + +:::tip 配置教程 + +如果配置了不能显示中文,可以查看此篇教程 [https://blog.csdn.net/sD7O95O/article/details/103776077](https://blog.csdn.net/sD7O95O/article/details/103776077) + +::: + +:::tip 关于 `NET6` 的中文智能提示 + +因为官方不再提供本地化包了,详情可查看相关 Issue [https://github.com/dotnet/docs/issues/27283](https://github.com/dotnet/docs/issues/27283) + +可以使用博客园网友 `@internalnet` 制作的本地化包 [https://www.cnblogs.com/internalnet/p/16185298.html](https://www.cnblogs.com/internalnet/p/16185298.html) + +::: + + + + + +## 2.14.5 代码搜索 + +`Visual Studio 2022` 提供了非常强大的 **代码搜索和功能搜索**,只需要快捷键 `Ctrl + Q` 或 `Ctrl + T` 呼出,如: + + + +退出搜索只需快捷键 `ESC` 即可。 + +## 2.14.6 反馈与建议 + +:::note 与我们交流 + +给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。 + +::: diff --git a/handbook/docs/whyfurion.mdx b/handbook/docs/whyfurion.mdx new file mode 100644 index 0000000000000000000000000000000000000000..940ea9058bb86e270ae74c00ecb4eb812ef58d78 --- /dev/null +++ b/handbook/docs/whyfurion.mdx @@ -0,0 +1,31 @@ +--- +id: whyfurion +title: 1.10 为什么是 Furion? +sidebar_label: 1.10 为什么是 Furion? +description: 百小僧全天候待命,只要确认 Bug,当天修复,当天发版 +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; + +## 选择 `Furion` 的十大理由 + +1. **大多数企业招聘的 `.NET` 架构师/程序员绝大概率没有 [百小僧](https://gitee.com/monksoul) 的水平。** +2. **[百小僧](https://gitee.com/monksoul) 全天候待命,只要确认 `Bug`,当天修复,当天发版。** +3. `NuGet` 总下载量超 [1160 万](https://www.nuget.org/profiles/monk.soul),**坑洼前人早就躺过,你不是一个人在战斗**。 +4. 超 `340万` 字的保姆级使用手册,”**保温杯里泡枸杞**“ 的养生法(**护发**)你不需要。 +5. **程序员上岗无需培训,直接丢文档了事,实习生来了都可以扛大旗。** +6. [MIT](https://gitee.com/dotnetchina/Furion/blob/v4/LICENSE) 开源协议,**商用无猫腻**,超高质量的源码,**可做企业内部框架**。 +7. 底层核心无第三方依赖,应用层仅依赖了两个第三方包,**功能自主可控**。 +8. 框架 **持续创新迭代,精益求精**,为软件系统开发维护保驾护航。 +9. **超 `250位` 开发者贡献代码和文档,国内 `.NET` 开源项目仅此一份。** +10. ~~选择 `Furion` 还需要理由吗?~~ + +--- + +`Furion` 已经持续迭代跨了 4 个年头([2020,2021,2022,2023](http://furion.baiqian.ltd/docs/course)),**发布的版本超 800 个**:[查看日志](http://furion.baiqian.ltd/docs/upgrade) + +您的痛点,Furion 已阅已历;Furion 的惊喜,您且慢慢享受。 还是那句话:只有中国人才懂中国人,选来选去还是 Furion 好用。 **时间不等人,市场不等人,与其选择新的框架,不如选择成熟稳定且持续迭代的框架。** + +--- + +**成熟,稳定,高效是企业及开发者的首要选择,`Furion` 已具备这个条件。👍** diff --git a/handbook/docusaurus.config.js b/handbook/docusaurus.config.js new file mode 100644 index 0000000000000000000000000000000000000000..d56461cd1d225b359866da725d701604e47e1191 --- /dev/null +++ b/handbook/docusaurus.config.js @@ -0,0 +1,219 @@ +module.exports = { + title: "Furion", + tagline: "让 .NET 开发更简单,更通用,更流行。", + url: "http://furion.baiqian.ltd", + baseUrl: "/", + onBrokenLinks: "throw", + onBrokenMarkdownLinks: "warn", + favicon: "img/favicon.ico", + organizationName: "百签科技(广东)有限公司", + projectName: "Furion", + scripts: [], + themeConfig: { + zoom: { + selector: + ".markdown :not(em) > img,.markdown > img, article img[loading]", + background: { + light: "rgb(255, 255, 255)", + dark: "rgb(50, 50, 50)", + }, + // options you can specify via https://github.com/francoischalifour/medium-zoom#usage + config: {}, + }, + announcementBar: { + id: "vip", + content: + "⭐️ 开通 VIP 服务仅需 499 元/年,尊享 365 天项目无忧 立即开通⭐️", + backgroundColor: "#4623d9", + textColor: "yellow", + isCloseable: false, + }, + docs: { + sidebar: { + hideable: true, + autoCollapseCategories: true, + }, + }, + prism: { + additionalLanguages: ["powershell", "csharp", "sql"], + // theme: require("prism-react-renderer/themes/github"), + // darkTheme: require("prism-react-renderer/themes/dracula"), + }, + navbar: { + title: "Furion", + logo: { + alt: "Furion Logo", + src: "img/furionlogo.png", + }, + hideOnScroll: true, + items: [ + // { + // type: "docsVersionDropdown", + // position: "left", + // }, + { + to: "docs/category/appendix", + activeBasePath: "docs", + label: "文档", + position: "left", + }, + { + to: "docs/global/app", + activeBasePath: "docs/global", + label: "静态类", + position: "left", + }, + { + to: "docs/settings/appsettings", + activeBasePath: "docs/settings", + label: "配置", + position: "left", + }, + { to: "blog", label: "博客", position: "left" }, + { + label: "更新日志", + position: "left", + items: [ + { + label: "📝 查看日志(v4.9.1.7)", + href: "/docs/upgrade", + }, + { + label: "🚀 路线图", + href: "/docs/target", + }, + ], + }, + { + label: "API", + position: "left", + href: "http://furion.baiqian.ltd/api/api", + }, + // { + // to: "docs/net6-to-net7", + // activeBasePath: "docs/net6-to-net7", + // label: ".NET7🚀", + // position: "left", + // }, + { + label: "仓库", + position: "right", + items: [ + { + label: "Gitee(主库)", + href: "https://gitee.com/dotnetchina/Furion", + }, + { + label: "GitHub", + href: "https://github.com/MonkSoul/Furion", + }, + { + label: "NuGet", + href: "https://www.nuget.org/profiles/monk.soul", + }, + ], + }, + { + label: "社区", + position: "right", + href: "https://gitee.com/dotnetchina", + }, + { + label: "案例", + position: "right", + to: "docs/case", + activeBasePath: "docs/case", + }, + { + label: "赞助", + position: "right", + to: "docs/donate", + activeBasePath: "docs/donate", + }, + ], + }, + footer: { + style: "dark", + links: [ + { + title: "文档", + items: [ + { + label: "入门", + to: "docs/category/getstart", + }, + { + label: "手册", + to: "docs/category/appendix", + }, + ], + }, + { + title: "社区", + items: [ + { + label: "讨论", + href: "https://gitee.com/dotnetchina/Furion/issues", + }, + { + label: "看板", + href: "https://gitee.com/dotnetchina/Furion/board", + }, + ], + }, + { + title: "更多", + items: [ + { + label: "博客", + to: "blog", + }, + { + label: "仓库", + href: "https://gitee.com/dotnetchina/Furion", + }, + ], + }, + ], + copyright: `版权 © 2020-present 百小僧, 百签科技(广东)有限公司`, + logo: { + src: "img/chinadotnet.png", + href: "https://gitee.com/dotnetchina", + }, + }, + }, + presets: [ + [ + "@docusaurus/preset-classic", + { + docs: { + sidebarPath: require.resolve("./sidebars.js"), + editUrl: "https://gitee.com/dotnetchina/Furion/tree/v4/handbook/", + showLastUpdateTime: true, + showLastUpdateAuthor: true, + sidebarCollapsible: true, + sidebarCollapsed: true, + }, + blog: { + showReadingTime: true, + editUrl: "https://gitee.com/dotnetchina/Furion/tree/v4/handbook/", + }, + theme: { + customCss: require.resolve("./src/css/custom.css"), + }, + }, + ], + ], + plugins: [require.resolve("docusaurus-plugin-image-zoom")], + themes: [ + [ + "@easyops-cn/docusaurus-search-local", + { + hashed: true, + language: ["en", "zh"], + highlightSearchTermsOnTargetPage: true, + explicitSearchResultPath: true, + }, + ], + ], +}; diff --git a/handbook/iconfont.json b/handbook/iconfont.json new file mode 100644 index 0000000000000000000000000000000000000000..4e53040c173541199340824ee373e96a710a0bec --- /dev/null +++ b/handbook/iconfont.json @@ -0,0 +1,8 @@ +{ + "symbol_url": "//at.alicdn.com/t/c/font_3276321_js2bwtaq9jc.js", + "use_typescript": false, + "save_dir": "./src/components/iconfonts", + "trim_icon_prefix": "icon", + "unit": "px", + "default_icon_size": 18 +} diff --git a/handbook/package.json b/handbook/package.json new file mode 100644 index 0000000000000000000000000000000000000000..8036a8a13f460e9cd69b0c815b6fd42ff6cd3c4a --- /dev/null +++ b/handbook/package.json @@ -0,0 +1,55 @@ +{ + "name": "furion", + "version": "v4.9.1.7", + "private": true, + "scripts": { + "docusaurus": "docusaurus", + "start": "docusaurus start", + "build": "docusaurus build", + "swizzle": "docusaurus swizzle", + "deploy": "docusaurus deploy", + "clear": "docusaurus clear", + "serve": "docusaurus serve", + "write-translations": "docusaurus write-translations", + "write-heading-ids": "docusaurus write-heading-ids" + }, + "dependencies": { + "@docusaurus/core": "3.0.0", + "@docusaurus/preset-classic": "3.0.0", + "@easyops-cn/docusaurus-search-local": "^0.37.4", + "@mdx-js/react": "^3.0.0", + "@svgr/webpack": "^8.1.0", + "@uiw/react-drawer": "^4.22.2", + "@uiw/react-modal": "^4.22.2", + "@uiw/react-notify": "^4.22.2", + "@uiw/react-popover": "^4.22.2", + "@uiw/react-tooltip": "^4.22.2", + "clsx": "^2.0.0", + "docusaurus-plugin-image-zoom": "^1.0.1", + "file-loader": "^6.2.0", + "prism-react-renderer": "^2.2.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "url-loader": "^4.1.1" + }, + "browserslist": { + "production": [ + ">0.5%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@docusaurus/module-type-aliases": "3.0.0", + "@docusaurus/types": "3.0.0", + "react-iconfont-cli": "^2.0.2" + }, + "engines": { + "node": ">=18.0" + } +} diff --git a/handbook/sidebars.js b/handbook/sidebars.js new file mode 100644 index 0000000000000000000000000000000000000000..5a66e41b4d9b5300c9b8bb55797a32f69415415b --- /dev/null +++ b/handbook/sidebars.js @@ -0,0 +1,409 @@ +module.exports = { + docs: [ + { + type: "category", + label: "1. 附录", + link: { + type: "generated-index", + slug: "/category/appendix", + }, + items: [ + "introduce", + "author", + "source", + "case", + "donate", + "upgrade", + "course", + "target", + "position", + "whyfurion", + "subscribe", + ], + }, + { + type: "category", + label: "2. 入门指南", + link: { + type: "generated-index", + slug: "/category/getstart", + }, + items: [ + "serverun", + "get-start-net5", + "get-start-net6", + "get-start-net7", + "get-start-net8", + "template", + "reference", + "inject", + "net5-to-net6", + "net6-to-net7", + "net7-to-net8", + "globalusing", + "jsonschema", + "vsfast", + "nuget-local", + ], + }, + { + type: "category", + label: "3. 应用启动", + link: { + type: "generated-index", + slug: "/category/appstart", + }, + items: ["appstartup", "component"], + }, + { + type: "category", + link: { + type: "generated-index", + slug: "/category/configuration", + }, + label: "4. 配置与选项", + items: ["configuration", "options"], + }, + { + type: "category", + label: "5. Web 应用开发", + link: { + type: "generated-index", + slug: "/category/web", + }, + items: [ + "dynamic-api-controller", + "httpcontext", + "filter", + "audit", + "middleware", + "clientapi", + ], + }, + { + type: "doc", + id: "specification-document", + }, + { + type: "doc", + id: "friendly-exception", + }, + { + type: "doc", + id: "data-validation", + }, + { + type: "category", + label: "9. 数据库操作指南(EFCore)", + link: { + type: "generated-index", + slug: "/category/efcore", + }, + items: [ + "dbcontext-start", + "dbcontext", + "dbcontext-locator", + "entity", + "dbcontext-repository", + "dbcontext-add", + "dbcontext-update", + "dbcontext-add-or-update", + "dbcontext-delete", + "dbcontext-batch", + "dbcontext-query", + "dbcontext-hight-query", + "dbcontext-view", + "dbcontext-proc", + "dbcontext-function", + "dbcontext-sql", + "dbcontext-sql-template", + "dbcontext-sql-proxy", + "dbcontext-multi-database", + "dbcontext-db-first", + "dbcontext-code-first", + "dbcontext-seed-data", + "dbcontext-audit", + "dbcontext-filter", + "dbcontext-Interceptor", + "dbcontext-entitytrigger", + "tran", + "dbcontext-read-write", + "split-db", + "efcore-recommend", + ], + }, + { + type: "category", + label: "10. SqlSugar 或其他 ORM", + link: { + type: "generated-index", + slug: "/category/orm", + }, + items: ["sqlsugar", "dapper", "mongodb"], + }, + { + type: "doc", + id: "saas", + }, + { + type: "doc", + id: "dependency-injection", + }, + { + type: "doc", + id: "object-mapper", + }, + { + type: "doc", + id: "cache", + }, + { + type: "doc", + id: "auth-control", + }, + { + type: "doc", + id: "cors", + }, + { + type: "doc", + id: "view-engine", + }, + { + type: "doc", + id: "logging", + }, + { + type: "doc", + id: "http", + }, + { + type: "doc", + id: "encryption", + }, + { + type: "doc", + id: "local-language", + }, + { + type: "doc", + id: "event-bus", + }, + { + type: "doc", + id: "json-serialization", + }, + { + type: "doc", + id: "signalr", + }, + { + type: "doc", + id: "process-service", + }, + { + type: "category", + label: "26. 定时任务 (Schedule)", + link: { + type: "generated-index", + slug: "/category/job", + }, + items: ["job", "cron", "task-queue"], + }, + { + type: "doc", + id: "idgenerator", + }, + { + type: "doc", + id: "module-dev", + }, + { + type: "doc", + id: "clayobj", + }, + { + type: "doc", + id: "sensitive-detection", + }, + { + type: "doc", + id: "file-provider", + }, + { + type: "doc", + id: "sesssion-state", + }, + { + type: "doc", + id: "ipc", + }, + { + type: "category", + label: "34. 托管/部署/发布", + link: { + type: "generated-index", + slug: "/category/deploy", + }, + items: [ + "deploy-iis", + "deploy-docker", + "deploy-nginx", + "virtual-deploy", + "singlefile", + "pm2", + "bs-to-cs", + ], + }, + { + type: "category", + link: { + type: "generated-index", + slug: "/category/devops", + }, + label: "35. 持续部署集成", + items: ["deploy-docker-auto", "devops"], + }, + { + type: "category", + label: "36. 测试指南", + link: { + type: "generated-index", + slug: "/category/test", + }, + items: ["unittest", "performance", "benchmark", "bingfa"], + }, + { + type: "doc", + id: "dotnet-tools", + }, + { + type: "doc", + id: "contribute", + }, + { + type: "doc", + id: "bug-report", + }, + ], + settings: [ + { + type: "doc", + id: "settings/appsettings", + }, + { + type: "doc", + id: "settings/corsaccessorsettings", + }, + { + type: "doc", + id: "settings/validationTypemessagesettings", + }, + { + type: "doc", + id: "settings/dependencyinjectionsettings", + }, + { + type: "doc", + id: "settings/dynamicapicontrollersettings", + }, + { + type: "doc", + id: "settings/friendlyexceptionsettings", + }, + { + type: "doc", + id: "settings/specificationdocumentsettings", + }, + { + type: "doc", + id: "settings/localizationsettings", + }, + { + type: "doc", + id: "settings/jwtsettings", + }, + { + type: "doc", + id: "settings/unifyresultsettings", + }, + ], + global: [ + { + type: "doc", + id: "global/app", + }, + { + type: "doc", + id: "global/db", + }, + { + type: "doc", + id: "global/datavalidator", + }, + { + type: "doc", + id: "global/oops", + }, + { + type: "doc", + id: "global/linqexpression", + }, + { + type: "doc", + id: "global/shttp", + }, + { + type: "doc", + id: "global/jsonserializer", + }, + { + type: "doc", + id: "global/l", + }, + { + type: "doc", + id: "global/messagecenter", + }, + { + type: "doc", + id: "global/json", + }, + { + type: "doc", + id: "global/scoped", + }, + { + type: "doc", + id: "global/sparetime", + }, + { + type: "doc", + id: "global/fs", + }, + { + type: "doc", + id: "global/jwt", + }, + { + type: "doc", + id: "global/tp", + }, + { + type: "doc", + id: "global/log", + }, + { + type: "doc", + id: "global/schedular", + }, + { + type: "doc", + id: "global/taskqueued", + }, + { + type: "doc", + id: "global/native", + }, + ], +}; diff --git a/handbook/src/components/Assistance.js b/handbook/src/components/Assistance.js new file mode 100644 index 0000000000000000000000000000000000000000..a8e39ac5817cb3bf0fd98e0386de3c78e36ae93f --- /dev/null +++ b/handbook/src/components/Assistance.js @@ -0,0 +1,35 @@ +import Link from "@docusaurus/Link"; +import useBaseUrl from "@docusaurus/useBaseUrl"; +import Tooltip from "@uiw/react-tooltip"; +import React from "react"; +import classes from "./Assistance.module.css"; + +export default function Assistance({ style = {}, onClick }) { + const count = 344; + const tip = "已有 " + count + " 位用户开通 VIP 服务"; + + return ( + + +
开通 VIP 服务尊享一对一技术指导
+
+
{count}
+
+
+
+
1000
+
+ +
+ ); +} diff --git a/handbook/src/components/Assistance.module.css b/handbook/src/components/Assistance.module.css new file mode 100644 index 0000000000000000000000000000000000000000..dcdd6ef4764bcd3bf443f377b8f225ead59d2fad --- /dev/null +++ b/handbook/src/components/Assistance.module.css @@ -0,0 +1,79 @@ +.ass { + display: block; + user-select: none; + margin: 0.5em 0.5em 0 0.5em; + box-sizing: border-box; + padding: 10px; + /* height: 80px; */ + border-radius: 5px; + background-color: #4623d9; + position: relative; + transition: all 0.4s cubic-bezier(0.08, 0.82, 0.17, 1) 0s; + + animation: progress-bar-stripes 2s linear infinite; + background-image: linear-gradient( + 45deg, + hsla(0, 0%, 100%, 0.15) 25%, + transparent 0, + transparent 50%, + hsla(0, 0%, 100%, 0.15) 0, + hsla(0, 0%, 100%, 0.15) 75%, + transparent 0, + transparent + ); + background-size: 12px 12px; + text-decoration: none; +} + +.ass:hover { + text-decoration: none; + background-color: #0958d9; +} + +.title { + font-size: 16; + text-align: center; + margin-bottom: 5px; + color: #e6f4ff; +} + +.number { + font-size: 12px; + color: #48576a; + color: #e6f4ff; + opacity: 0.8; +} + +.progress { + display: flex; + flex-direction: row; + align-items: center; +} + +.percent { + flex: 1; + height: 6px; + background-color: #e5e9f2; + border-radius: 6px; + overflow: hidden; + position: relative; + margin: 0 5px; +} + +.current { + height: 6px; + background-color: #1677ff; + display: inline-block; + position: absolute; + left: 0; + top: 0; +} + +@keyframes progress-bar-stripes { + from { + background-position: 0 0; + } + to { + background-position: 24px 0; + } +} diff --git a/handbook/src/components/Donate.js b/handbook/src/components/Donate.js new file mode 100644 index 0000000000000000000000000000000000000000..eb93c0186db7a358d9ece0a1cfb63dc172041034 --- /dev/null +++ b/handbook/src/components/Donate.js @@ -0,0 +1,97 @@ +import useBaseUrl from "@docusaurus/useBaseUrl"; +import React, { useContext } from "react"; +import classes from "./Donate.module.css"; +import GlobalContext from "./GlobalContext"; + +export default function Donate({ style }) { + const { setDonate } = useContext(GlobalContext); + + return ( + <> +
setDonate(true)} + > +
+ 赞助 Furion + + 查看大图 + +
+
+

+ 谢谢您对 Furion 的认可! +

+
+ 微信:ibaiqian + + star + +
+
+
+ + ); +} diff --git a/handbook/src/components/Donate.module.css b/handbook/src/components/Donate.module.css new file mode 100644 index 0000000000000000000000000000000000000000..55fb787a181859f64204e721beecef03acd4a11f --- /dev/null +++ b/handbook/src/components/Donate.module.css @@ -0,0 +1,15 @@ +.donate { + display: flex; + padding: 5px; + position: relative; + box-sizing: border-box; + background-color: #f4f8fa; + height: 80px; + border-radius: 5px; + cursor: pointer; + position: relative; +} + +.donate:hover { + opacity: 0.85; +} diff --git a/handbook/src/components/FloatBar.js b/handbook/src/components/FloatBar.js new file mode 100644 index 0000000000000000000000000000000000000000..435137d264a8586f127e57616ee3094339dc6549 --- /dev/null +++ b/handbook/src/components/FloatBar.js @@ -0,0 +1,61 @@ +import Link from "@docusaurus/Link"; +import useBaseUrl from "@docusaurus/useBaseUrl"; +import React from "react"; +import styles from "./FloatBar.module.css"; + +export default function FloatBar() { + return ( +
+
+ +
❤️ 关注 Furion 微信公众号有惊喜哦!
+
+
+
🫠 遇到问题了
+
+ +
+
⭐️ VIP 服务 ⭐️
+
+ 仅需 499 元/年,尊享 365 天项目无忧 +
+
+
+ + + window.open( + "https://gitee.com/dotnetchina/Furion/issues", + "_blank" + ) + } + /> +
+
+
+ ); +} + +function Item({ title, description, onClick }) { + return ( +
+
+
{title}
+
{description}
+
+
+
+ ); +} diff --git a/handbook/src/components/FloatBar.module.css b/handbook/src/components/FloatBar.module.css new file mode 100644 index 0000000000000000000000000000000000000000..09772bbc8c15a02faf788f50908c32a242c170da --- /dev/null +++ b/handbook/src/components/FloatBar.module.css @@ -0,0 +1,141 @@ +.floatbar { + position: fixed; + z-index: 10000; + right: 0; + bottom: 220px; + text-align: center; + cursor: default; + color: #fff; + display: flex; + flex-direction: column; + font-size: 14px; + align-items: flex-start; + animation: shake 5s; +} + +.title { + width: 44px; + padding: 10px 0; + writing-mode: vertical-lr; + display: flex; + align-items: center; + letter-spacing: 4px; + margin-left: 10px; + box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.1); + border-radius: 4px; + font-weight: 500; + background: #8759ff; + font-size: 15px; + position: relative; +} + +.extend { + display: none; + box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.1); + background: #8759ff; + font-size: 15px; + border-radius: 4px; + width: 180px; + overflow: hidden; +} + +.qrcode { + display: none; + box-shadow: 0 0 20px 0 #8759ff; + margin-bottom: 10px; + margin-right: 10px; + color: #666; + font-size: 12px; + padding-bottom: 10px; + background-color: #fff; +} + +.qrcode img { + width: 233px; +} + +.floatbar:hover { + animation: none; +} + +.floatbar:hover .extend, +.floatbar:hover .qrcode { + display: block; +} + +.item { + padding: 10px 15px; + text-align: left; + display: flex; + flex-direction: row; + cursor: pointer; + align-items: flex-start; + text-decoration: none; + color: #fff; +} + +.itemDesc { + font-size: 12px; + color: #e9e9e9; +} + +.item:hover { + background-color: #723cff; + text-decoration: none; + color: #fff; +} + +.item:hover .itemTitle { + font-weight: 500; +} + +.jiantou { + width: 7px; + height: 7px; + border-top: 1px solid #f1f1f1; + border-right: 1px solid #f1f1f1; + transform: rotate(45deg); + margin-top: 8px; +} + +@keyframes shake { + 0% { + transform: rotate(0deg); + } + 2% { + transform: rotate(10deg); + } + 4% { + transform: rotate(-10deg); + } + 6% { + transform: rotate(10deg); + } + 8% { + transform: rotate(-10deg); + } + 10% { + transform: rotate(10deg); + } + 12% { + transform: rotate(-10deg); + } + 14% { + transform: rotate(10deg); + } + 16% { + transform: rotate(-10deg); + } + 18% { + transform: rotate(10deg); + } + 20% { + transform: rotate(-10deg); + } + 22% { + transform: rotate(0deg); + } + 100% { + transform: rotate(0deg); + } +} diff --git a/handbook/src/components/GlobalContext.js b/handbook/src/components/GlobalContext.js new file mode 100644 index 0000000000000000000000000000000000000000..729a236f58644543e4fcba11bb9b7566baf8d51b --- /dev/null +++ b/handbook/src/components/GlobalContext.js @@ -0,0 +1,5 @@ +import { createContext } from "react"; + +const GlobalContext = createContext(null); + +export default GlobalContext; \ No newline at end of file diff --git a/handbook/src/components/PayContent.js b/handbook/src/components/PayContent.js new file mode 100644 index 0000000000000000000000000000000000000000..b85f960ebc69fdc9c0da6b3dc902d22205c2c548 --- /dev/null +++ b/handbook/src/components/PayContent.js @@ -0,0 +1,24 @@ +import useBaseUrl from "@docusaurus/useBaseUrl"; +import React from "react"; + +export function PayContent() { + return ( +
+
+ 开通 VIP 服务仅需 499 元/年 +
+ +
+ ); +} diff --git a/handbook/src/components/SpecDonate.js b/handbook/src/components/SpecDonate.js new file mode 100644 index 0000000000000000000000000000000000000000..b746b24ce2893057d3561ecad41ba703c579dd3b --- /dev/null +++ b/handbook/src/components/SpecDonate.js @@ -0,0 +1,81 @@ +import useBaseUrl from "@docusaurus/useBaseUrl"; +import React, { useContext } from "react"; +import GlobalContext from "./GlobalContext"; + +const sponsorTagStyle = { + position: "absolute", + display: "block", + right: 0, + bottom: 0, + zIndex: 5, + fontSize: 12, + backgroundColor: "rgba(0,0,0,0.8)", + padding: "0 5px", +}; + +export default function SpecDonate({ style }) { + const { setDonate } = useContext(GlobalContext); + + return ( + // setDonate(true)} + // > + //

+ // 特别赞助(虚席以待) + //

+ //
+ // 如果 Furion 对您有所帮助,并且您希望 Furion 能够继续发展下去,请考虑 + // ⌈赞助⌋ 我们。 + //
+ //
+ + + 特别赞助 + + ); +} diff --git a/handbook/src/components/Sponsor.js b/handbook/src/components/Sponsor.js new file mode 100644 index 0000000000000000000000000000000000000000..ba0e8bbd54bd002e856229b0e3c95f1d8008f917 --- /dev/null +++ b/handbook/src/components/Sponsor.js @@ -0,0 +1,142 @@ +import useBaseUrl from "@docusaurus/useBaseUrl"; +import React from "react"; +import sponsors from "../data/sponsor"; +import Donate from "./Donate"; + +export function Sponsor() { + var tops = sponsors.filter((u) => u.top); + var unTops = sponsors.filter((u) => !u.top); + + return ( +
+ {tops.map((item) => ( + + ))} + + {unTops.map(({ picture, url, title, tag }, i) => ( + + ))} + +
+ ); +} + +const sponsorItemStyle = { + display: "block", + position: "relative", + alignItems: "center", + boxSizing: "border-box", + backgroundColor: "#fff", +}; + +const sponsorTagStyle = { + position: "absolute", + display: "block", + right: 0, + bottom: 0, + zIndex: 5, + fontSize: 12, + backgroundColor: "rgba(0,0,0,0.8)", + padding: "0 5px", + color: "#25c2a0", +}; + +export function SponsorItem({ picture, url, last, title, top, tag, style }) { + return ( + + + {top && ( + + 👑 + + )} + {tag} + + ); +} + +const sponsorSmartStyle = { + display: "inline-block", + position: "relative", + width: "48.5%", + position: "relative", + boxSizing: "border-box", + backgroundColor: "#fff", +}; + +export function SponsorItemSmart({ picture, url, title, tag, i }) { + return ( + + + + ); +} + +export const closeStyle = { + margin: "0 auto", + display: "inline-block", + position: "relative", + top: 5, + marginTop: -28, + cursor: "pointer", + borderRadius: "50%", + width: 28, + height: 28, + minWidth: 28, + minHeight: 28, + display: "flex", + alignItems: "center", + justifyContent: "center", + boxSizing: "border-box", + userSelect: "none", + fontSize: 12, + backgroundColor: "#3fbbfe", + color: "#fff", + fontWeight: "bold", +}; diff --git a/handbook/src/components/SponsorToc.js b/handbook/src/components/SponsorToc.js new file mode 100644 index 0000000000000000000000000000000000000000..3c40078041b73a785c53b8749c221dd2413d8551 --- /dev/null +++ b/handbook/src/components/SponsorToc.js @@ -0,0 +1,127 @@ +import useBaseUrl from "@docusaurus/useBaseUrl"; +import React from "react"; +import sponsors from "../data/sponsor"; + +export function SponsorToc() { + var tops = sponsors.filter((u) => u.id === 1); + + return ( +
+ {tops.map((item) => ( + + ))} +
+ ); +} + +const sponsorItemStyle = { + display: "block", + position: "relative", + alignItems: "center", + boxSizing: "border-box", +}; + +const sponsorTagStyle = { + position: "absolute", + display: "block", + right: 0, + bottom: 0, + zIndex: 5, + fontSize: 12, + backgroundColor: "rgba(0,0,0,0.8)", + padding: "0 5px", +}; + +export function SponsorItem({ picture, url, last, title, top, tag, style }) { + return ( + + + {top && ( + + 👑 + + )} + {tag} + + ); +} + +const sponsorSmartStyle = { + display: "inline-block", + position: "relative", + width: "48.5%", + position: "relative", + boxSizing: "border-box", +}; + +export function SponsorItemSmart({ picture, url, title, tag, i }) { + return ( + + + + ); +} + +export const closeStyle = { + margin: "0 auto", + display: "inline-block", + position: "relative", + top: 5, + marginTop: -28, + cursor: "pointer", + borderRadius: "50%", + width: 28, + height: 28, + minWidth: 28, + minHeight: 28, + display: "flex", + alignItems: "center", + justifyContent: "center", + boxSizing: "border-box", + userSelect: "none", + fontSize: 12, + backgroundColor: "#3fbbfe", + color: "#fff", + fontWeight: "bold", +}; diff --git a/handbook/src/components/Tag.js b/handbook/src/components/Tag.js new file mode 100644 index 0000000000000000000000000000000000000000..dceea3467be259f3a90f2cd5860762fb0f807a12 --- /dev/null +++ b/handbook/src/components/Tag.js @@ -0,0 +1,60 @@ +import React from "react"; +import IconFont from "./iconfonts"; +import classes from "./Tag.module.css"; + +export default function (props) { + const { children } = props; + const operates = { + 新增: { + icon: "xinzeng", + bgColor: "#39b54a", + }, + 修复: { + icon: "bug", + bgColor: "#9c26b0", + }, + 文档: { + icon: "wendang", + bgColor: "rgb(79, 147, 255)", + }, + 更新: { + icon: "gengxin", + bgColor: "#0081ff", + }, + 调整: { + icon: "tiaozheng", + bgColor: "#333", + }, + 升级: { + icon: "shengji", + bgColor: "#e03997", + }, + 移除: { + icon: "shanchu", + bgColor: "#666", + }, + 答疑: { + icon: "dayi", + bgColor: "#bbb", + }, + 优化: { + icon: "youhua", + bgColor: "#38e550", + }, + }; + return ( + + ); +} diff --git a/handbook/src/components/Tag.module.css b/handbook/src/components/Tag.module.css new file mode 100644 index 0000000000000000000000000000000000000000..8565ff2e37b3fa78f39744039a336306dfed064c --- /dev/null +++ b/handbook/src/components/Tag.module.css @@ -0,0 +1,17 @@ +.label { + display: inline-flex; + align-items: center; + color: #fff; + padding: 4px 6px; + font-size: 12px; + color: #fff; + border-radius: 3px; + line-height: normal; + margin-left: -3px; + vertical-align: middle; + margin-right: 6px; +} + +.icon { + margin-right: 4px; +} \ No newline at end of file diff --git a/handbook/src/components/TopBanner.js b/handbook/src/components/TopBanner.js new file mode 100644 index 0000000000000000000000000000000000000000..ad3193202292988c77111a61ad0dcd7ca01d9204 --- /dev/null +++ b/handbook/src/components/TopBanner.js @@ -0,0 +1,12 @@ +import classes from "./TopBanner.module.css"; +import VipImageList from "./VipImageList"; + +export default function TopBanner() { + return ( +
+
+ +
+
+ ); +} diff --git a/handbook/src/components/TopBanner.module.css b/handbook/src/components/TopBanner.module.css new file mode 100644 index 0000000000000000000000000000000000000000..6b08917f673fdec06f4f3ad5bc45d4283aee576f --- /dev/null +++ b/handbook/src/components/TopBanner.module.css @@ -0,0 +1,36 @@ +.container { + animation: scaleIn 0.8s forwards; + transform-origin: center; + box-sizing: border-box; + top: 0; + left: 50%; + transform: translate(-50%, 0); + position: relative; + background: rgb(70, 35, 217); + background-size: cover; + color: #fff; + overflow: hidden; +} + +.container u { + text-decoration: none !important; +} + +@keyframes scaleIn { + 0% { + width: 0; + height: 0; + opacity: 0; + } + 100% { + width: 100%; + height: auto; + opacity: 1; + } +} + +@media screen and (max-width: 1024px) { + .container { + display: none; + } +} diff --git a/handbook/src/components/Vip.js b/handbook/src/components/Vip.js new file mode 100644 index 0000000000000000000000000000000000000000..9969943381f41dafa19990a6f03e53b18d85ea64 --- /dev/null +++ b/handbook/src/components/Vip.js @@ -0,0 +1,61 @@ +import Link from "@docusaurus/Link"; +import useBaseUrl from "@docusaurus/useBaseUrl"; +import { useContext } from "react"; +import GlobalContext from "./GlobalContext"; + +export default function Vip({ style, closable = true }) { + const { setVip, setRightVip } = useContext(GlobalContext); + + return ( +
+ {closable && ( +
{ + setVip(false); + setRightVip(true); + }} + > + X +
+ )} + + + +
+ ); +} diff --git a/handbook/src/components/VipDesc.mdx b/handbook/src/components/VipDesc.mdx new file mode 100644 index 0000000000000000000000000000000000000000..88008445302fc6af14acca501db62afeee265e62 --- /dev/null +++ b/handbook/src/components/VipDesc.mdx @@ -0,0 +1,7 @@ +**在保持 Furion 初衷不变的前提下**,我们努力尝试和探索 Furion 开源商业化的方式,以确保既满足原有开源的初衷,又能为企业或项目提供强有力的契约保证。 + +**我们的目标是建立长期的合作关系,并共同实现成功!** + +**有了这笔资金,我们团队将能够加大研发投入,研发出一款能够显著提升企业开发效率并降低成本的优秀框架。 🤜🤛** + +**成功需要建立在契约上的朋友,Furion 与您做朋友。 🤝** diff --git a/handbook/src/components/VipImageList.js b/handbook/src/components/VipImageList.js new file mode 100644 index 0000000000000000000000000000000000000000..6f4e132ce5b86657a2fd3b4513e685302a2133b8 --- /dev/null +++ b/handbook/src/components/VipImageList.js @@ -0,0 +1,51 @@ +import Link from "@docusaurus/Link"; +import useBaseUrl from "@docusaurus/useBaseUrl"; +import { useContext } from "react"; +import GlobalContext from "./GlobalContext"; + +export default function VipImageList({ padding = 5 }) { + const { setDonate } = useContext(GlobalContext); + + return ( + setDonate(false)} + > + {Array.from({ length: 10 }, (_, i) => ( + + ))} + + ); +} + +function Image({ index, padding }) { + return ( +
+ +
+ ); +} diff --git a/handbook/src/components/iconfonts/IconBug.d.ts b/handbook/src/components/iconfonts/IconBug.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..741f1e6758604602050618370555dc1f6afaff60 --- /dev/null +++ b/handbook/src/components/iconfonts/IconBug.d.ts @@ -0,0 +1,12 @@ +/* eslint-disable */ + +import { SVGAttributes, FunctionComponent } from 'react'; + +interface Props extends Omit, 'color'> { + size?: number; + color?: string | string[]; +} + +declare const IconBug: FunctionComponent; + +export default IconBug; diff --git a/handbook/src/components/iconfonts/IconBug.js b/handbook/src/components/iconfonts/IconBug.js new file mode 100644 index 0000000000000000000000000000000000000000..b5654cc43352082713dd690b697bb9337ca68461 --- /dev/null +++ b/handbook/src/components/iconfonts/IconBug.js @@ -0,0 +1,31 @@ +/* eslint-disable */ + +import React from 'react'; +import { getIconColor } from './helper'; + +const DEFAULT_STYLE = { + display: 'block', +}; + +const IconBug = ({ size, color, style: _style, ...rest }) => { + const style = _style ? { ...DEFAULT_STYLE, ..._style } : DEFAULT_STYLE; + + return ( + + + + + ); +}; + +IconBug.defaultProps = { + size: 18, +}; + +export default IconBug; diff --git a/handbook/src/components/iconfonts/IconDayi.d.ts b/handbook/src/components/iconfonts/IconDayi.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..aecc11461607819aa8489c8674fccfb59d3f1677 --- /dev/null +++ b/handbook/src/components/iconfonts/IconDayi.d.ts @@ -0,0 +1,12 @@ +/* eslint-disable */ + +import { SVGAttributes, FunctionComponent } from 'react'; + +interface Props extends Omit, 'color'> { + size?: number; + color?: string | string[]; +} + +declare const IconDayi: FunctionComponent; + +export default IconDayi; diff --git a/handbook/src/components/iconfonts/IconDayi.js b/handbook/src/components/iconfonts/IconDayi.js new file mode 100644 index 0000000000000000000000000000000000000000..fd7f6347562a4f0c46ca3b20051cb2dcfba20ca9 --- /dev/null +++ b/handbook/src/components/iconfonts/IconDayi.js @@ -0,0 +1,31 @@ +/* eslint-disable */ + +import React from 'react'; +import { getIconColor } from './helper'; + +const DEFAULT_STYLE = { + display: 'block', +}; + +const IconDayi = ({ size, color, style: _style, ...rest }) => { + const style = _style ? { ...DEFAULT_STYLE, ..._style } : DEFAULT_STYLE; + + return ( + + + + + ); +}; + +IconDayi.defaultProps = { + size: 18, +}; + +export default IconDayi; diff --git a/handbook/src/components/iconfonts/IconDown.d.ts b/handbook/src/components/iconfonts/IconDown.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..4d06f276ca1017cacac08b19a93d3a62172fe508 --- /dev/null +++ b/handbook/src/components/iconfonts/IconDown.d.ts @@ -0,0 +1,12 @@ +/* eslint-disable */ + +import { SVGAttributes, FunctionComponent } from 'react'; + +interface Props extends Omit, 'color'> { + size?: number; + color?: string | string[]; +} + +declare const IconDown: FunctionComponent; + +export default IconDown; diff --git a/handbook/src/components/iconfonts/IconDown.js b/handbook/src/components/iconfonts/IconDown.js new file mode 100644 index 0000000000000000000000000000000000000000..e25ecf6b46da8aebac520eb1b074b45d66ac9aeb --- /dev/null +++ b/handbook/src/components/iconfonts/IconDown.js @@ -0,0 +1,27 @@ +/* eslint-disable */ + +import React from 'react'; +import { getIconColor } from './helper'; + +const DEFAULT_STYLE = { + display: 'block', +}; + +const IconDown = ({ size, color, style: _style, ...rest }) => { + const style = _style ? { ...DEFAULT_STYLE, ..._style } : DEFAULT_STYLE; + + return ( + + + + ); +}; + +IconDown.defaultProps = { + size: 18, +}; + +export default IconDown; diff --git a/handbook/src/components/iconfonts/IconFuwu.d.ts b/handbook/src/components/iconfonts/IconFuwu.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..44e8e3d814f2c1d5eb6a69ee59ce7f246e3a8f12 --- /dev/null +++ b/handbook/src/components/iconfonts/IconFuwu.d.ts @@ -0,0 +1,12 @@ +/* eslint-disable */ + +import { SVGAttributes, FunctionComponent } from 'react'; + +interface Props extends Omit, 'color'> { + size?: number; + color?: string | string[]; +} + +declare const IconFuwu: FunctionComponent; + +export default IconFuwu; diff --git a/handbook/src/components/iconfonts/IconFuwu.js b/handbook/src/components/iconfonts/IconFuwu.js new file mode 100644 index 0000000000000000000000000000000000000000..a1770ed73719f1837bdad4d5f8bb967fad978017 --- /dev/null +++ b/handbook/src/components/iconfonts/IconFuwu.js @@ -0,0 +1,27 @@ +/* eslint-disable */ + +import React from 'react'; +import { getIconColor } from './helper'; + +const DEFAULT_STYLE = { + display: 'block', +}; + +const IconFuwu = ({ size, color, style: _style, ...rest }) => { + const style = _style ? { ...DEFAULT_STYLE, ..._style } : DEFAULT_STYLE; + + return ( + + + + ); +}; + +IconFuwu.defaultProps = { + size: 18, +}; + +export default IconFuwu; diff --git a/handbook/src/components/iconfonts/IconGengxin.d.ts b/handbook/src/components/iconfonts/IconGengxin.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..6d0f8e89a3d0513ebd20f3e71992afda0d89224c --- /dev/null +++ b/handbook/src/components/iconfonts/IconGengxin.d.ts @@ -0,0 +1,12 @@ +/* eslint-disable */ + +import { SVGAttributes, FunctionComponent } from 'react'; + +interface Props extends Omit, 'color'> { + size?: number; + color?: string | string[]; +} + +declare const IconGengxin: FunctionComponent; + +export default IconGengxin; diff --git a/handbook/src/components/iconfonts/IconGengxin.js b/handbook/src/components/iconfonts/IconGengxin.js new file mode 100644 index 0000000000000000000000000000000000000000..e58ce4953da32f9a1c62e336d84af7013746ca4e --- /dev/null +++ b/handbook/src/components/iconfonts/IconGengxin.js @@ -0,0 +1,27 @@ +/* eslint-disable */ + +import React from 'react'; +import { getIconColor } from './helper'; + +const DEFAULT_STYLE = { + display: 'block', +}; + +const IconGengxin = ({ size, color, style: _style, ...rest }) => { + const style = _style ? { ...DEFAULT_STYLE, ..._style } : DEFAULT_STYLE; + + return ( + + + + ); +}; + +IconGengxin.defaultProps = { + size: 18, +}; + +export default IconGengxin; diff --git a/handbook/src/components/iconfonts/IconShanchu.d.ts b/handbook/src/components/iconfonts/IconShanchu.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..bc478b775b3abaa136fdab0b12ca8abc5f27c335 --- /dev/null +++ b/handbook/src/components/iconfonts/IconShanchu.d.ts @@ -0,0 +1,12 @@ +/* eslint-disable */ + +import { SVGAttributes, FunctionComponent } from 'react'; + +interface Props extends Omit, 'color'> { + size?: number; + color?: string | string[]; +} + +declare const IconShanchu: FunctionComponent; + +export default IconShanchu; diff --git a/handbook/src/components/iconfonts/IconShanchu.js b/handbook/src/components/iconfonts/IconShanchu.js new file mode 100644 index 0000000000000000000000000000000000000000..6b40a32470f3b86bc8d813817c21b86f08a23b37 --- /dev/null +++ b/handbook/src/components/iconfonts/IconShanchu.js @@ -0,0 +1,31 @@ +/* eslint-disable */ + +import React from 'react'; +import { getIconColor } from './helper'; + +const DEFAULT_STYLE = { + display: 'block', +}; + +const IconShanchu = ({ size, color, style: _style, ...rest }) => { + const style = _style ? { ...DEFAULT_STYLE, ..._style } : DEFAULT_STYLE; + + return ( + + + + + ); +}; + +IconShanchu.defaultProps = { + size: 18, +}; + +export default IconShanchu; diff --git a/handbook/src/components/iconfonts/IconShengji.d.ts b/handbook/src/components/iconfonts/IconShengji.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..b56a539adb637d3cb7ac0c78e737d83d4d0590ca --- /dev/null +++ b/handbook/src/components/iconfonts/IconShengji.d.ts @@ -0,0 +1,12 @@ +/* eslint-disable */ + +import { SVGAttributes, FunctionComponent } from 'react'; + +interface Props extends Omit, 'color'> { + size?: number; + color?: string | string[]; +} + +declare const IconShengji: FunctionComponent; + +export default IconShengji; diff --git a/handbook/src/components/iconfonts/IconShengji.js b/handbook/src/components/iconfonts/IconShengji.js new file mode 100644 index 0000000000000000000000000000000000000000..a338addab3ee5302b1c5f6bc9b4cab28f9501886 --- /dev/null +++ b/handbook/src/components/iconfonts/IconShengji.js @@ -0,0 +1,35 @@ +/* eslint-disable */ + +import React from 'react'; +import { getIconColor } from './helper'; + +const DEFAULT_STYLE = { + display: 'block', +}; + +const IconShengji = ({ size, color, style: _style, ...rest }) => { + const style = _style ? { ...DEFAULT_STYLE, ..._style } : DEFAULT_STYLE; + + return ( + + + + + + ); +}; + +IconShengji.defaultProps = { + size: 18, +}; + +export default IconShengji; diff --git a/handbook/src/components/iconfonts/IconTiaozheng.d.ts b/handbook/src/components/iconfonts/IconTiaozheng.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..bce4bfa8ea9afe522985f86338f4a571ade94b59 --- /dev/null +++ b/handbook/src/components/iconfonts/IconTiaozheng.d.ts @@ -0,0 +1,12 @@ +/* eslint-disable */ + +import { SVGAttributes, FunctionComponent } from 'react'; + +interface Props extends Omit, 'color'> { + size?: number; + color?: string | string[]; +} + +declare const IconTiaozheng: FunctionComponent; + +export default IconTiaozheng; diff --git a/handbook/src/components/iconfonts/IconTiaozheng.js b/handbook/src/components/iconfonts/IconTiaozheng.js new file mode 100644 index 0000000000000000000000000000000000000000..32230b2c6ac7b6788f3218c7bf71d68aeb101ee8 --- /dev/null +++ b/handbook/src/components/iconfonts/IconTiaozheng.js @@ -0,0 +1,27 @@ +/* eslint-disable */ + +import React from 'react'; +import { getIconColor } from './helper'; + +const DEFAULT_STYLE = { + display: 'block', +}; + +const IconTiaozheng = ({ size, color, style: _style, ...rest }) => { + const style = _style ? { ...DEFAULT_STYLE, ..._style } : DEFAULT_STYLE; + + return ( + + + + ); +}; + +IconTiaozheng.defaultProps = { + size: 18, +}; + +export default IconTiaozheng; diff --git a/handbook/src/components/iconfonts/IconUp.d.ts b/handbook/src/components/iconfonts/IconUp.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..e80e34c5ddc752c0e7072688aaa9f8c36d3d6f53 --- /dev/null +++ b/handbook/src/components/iconfonts/IconUp.d.ts @@ -0,0 +1,12 @@ +/* eslint-disable */ + +import { SVGAttributes, FunctionComponent } from 'react'; + +interface Props extends Omit, 'color'> { + size?: number; + color?: string | string[]; +} + +declare const IconUp: FunctionComponent; + +export default IconUp; diff --git a/handbook/src/components/iconfonts/IconUp.js b/handbook/src/components/iconfonts/IconUp.js new file mode 100644 index 0000000000000000000000000000000000000000..0f13d7561723f39261401c12fbd1daa76f5a1c57 --- /dev/null +++ b/handbook/src/components/iconfonts/IconUp.js @@ -0,0 +1,27 @@ +/* eslint-disable */ + +import React from 'react'; +import { getIconColor } from './helper'; + +const DEFAULT_STYLE = { + display: 'block', +}; + +const IconUp = ({ size, color, style: _style, ...rest }) => { + const style = _style ? { ...DEFAULT_STYLE, ..._style } : DEFAULT_STYLE; + + return ( + + + + ); +}; + +IconUp.defaultProps = { + size: 18, +}; + +export default IconUp; diff --git a/handbook/src/components/iconfonts/IconWendang.d.ts b/handbook/src/components/iconfonts/IconWendang.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..9a94d9de356faf49465d5a4768caea8a5510edba --- /dev/null +++ b/handbook/src/components/iconfonts/IconWendang.d.ts @@ -0,0 +1,12 @@ +/* eslint-disable */ + +import { SVGAttributes, FunctionComponent } from 'react'; + +interface Props extends Omit, 'color'> { + size?: number; + color?: string | string[]; +} + +declare const IconWendang: FunctionComponent; + +export default IconWendang; diff --git a/handbook/src/components/iconfonts/IconWendang.js b/handbook/src/components/iconfonts/IconWendang.js new file mode 100644 index 0000000000000000000000000000000000000000..318206f94b9b168a4a7b727068263ee24ada2d85 --- /dev/null +++ b/handbook/src/components/iconfonts/IconWendang.js @@ -0,0 +1,35 @@ +/* eslint-disable */ + +import React from 'react'; +import { getIconColor } from './helper'; + +const DEFAULT_STYLE = { + display: 'block', +}; + +const IconWendang = ({ size, color, style: _style, ...rest }) => { + const style = _style ? { ...DEFAULT_STYLE, ..._style } : DEFAULT_STYLE; + + return ( + + + + + + ); +}; + +IconWendang.defaultProps = { + size: 18, +}; + +export default IconWendang; diff --git a/handbook/src/components/iconfonts/IconXinzeng.d.ts b/handbook/src/components/iconfonts/IconXinzeng.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..5a0360afdb81fb00f14fcdea6c527a489f0547ca --- /dev/null +++ b/handbook/src/components/iconfonts/IconXinzeng.d.ts @@ -0,0 +1,12 @@ +/* eslint-disable */ + +import { SVGAttributes, FunctionComponent } from 'react'; + +interface Props extends Omit, 'color'> { + size?: number; + color?: string | string[]; +} + +declare const IconXinzeng: FunctionComponent; + +export default IconXinzeng; diff --git a/handbook/src/components/iconfonts/IconXinzeng.js b/handbook/src/components/iconfonts/IconXinzeng.js new file mode 100644 index 0000000000000000000000000000000000000000..808dcbf0989084c47dac21c2e856fb825f0ca3b4 --- /dev/null +++ b/handbook/src/components/iconfonts/IconXinzeng.js @@ -0,0 +1,31 @@ +/* eslint-disable */ + +import React from 'react'; +import { getIconColor } from './helper'; + +const DEFAULT_STYLE = { + display: 'block', +}; + +const IconXinzeng = ({ size, color, style: _style, ...rest }) => { + const style = _style ? { ...DEFAULT_STYLE, ..._style } : DEFAULT_STYLE; + + return ( + + + + + ); +}; + +IconXinzeng.defaultProps = { + size: 18, +}; + +export default IconXinzeng; diff --git a/handbook/src/components/iconfonts/IconYouhua.d.ts b/handbook/src/components/iconfonts/IconYouhua.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..ff2448879f1135f12c2ba808846797dd7d0e9d0b --- /dev/null +++ b/handbook/src/components/iconfonts/IconYouhua.d.ts @@ -0,0 +1,12 @@ +/* eslint-disable */ + +import { SVGAttributes, FunctionComponent } from 'react'; + +interface Props extends Omit, 'color'> { + size?: number; + color?: string | string[]; +} + +declare const IconYouhua: FunctionComponent; + +export default IconYouhua; diff --git a/handbook/src/components/iconfonts/IconYouhua.js b/handbook/src/components/iconfonts/IconYouhua.js new file mode 100644 index 0000000000000000000000000000000000000000..5af3063a926a22196bf95c7a5694affc3cb6caf7 --- /dev/null +++ b/handbook/src/components/iconfonts/IconYouhua.js @@ -0,0 +1,27 @@ +/* eslint-disable */ + +import React from 'react'; +import { getIconColor } from './helper'; + +const DEFAULT_STYLE = { + display: 'block', +}; + +const IconYouhua = ({ size, color, style: _style, ...rest }) => { + const style = _style ? { ...DEFAULT_STYLE, ..._style } : DEFAULT_STYLE; + + return ( + + + + ); +}; + +IconYouhua.defaultProps = { + size: 18, +}; + +export default IconYouhua; diff --git a/handbook/src/components/iconfonts/helper.d.ts b/handbook/src/components/iconfonts/helper.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..7d22b9b98af9ce45d3d7706a10aca3c80ad41ec7 --- /dev/null +++ b/handbook/src/components/iconfonts/helper.d.ts @@ -0,0 +1,3 @@ +/* eslint-disable */ + +export declare const getIconColor: (color: string | string[] | undefined, index: number, defaultColor: string) => string; diff --git a/handbook/src/components/iconfonts/helper.js b/handbook/src/components/iconfonts/helper.js new file mode 100644 index 0000000000000000000000000000000000000000..b566c4cc70bb2534fbe8937882d5e3522db34ffc --- /dev/null +++ b/handbook/src/components/iconfonts/helper.js @@ -0,0 +1,17 @@ +/* eslint-disable */ + +/** + * @param {string | string[] | undefined} color + * @param {number} index + * @param {string} defaultColor + * @return {string} + */ +export const getIconColor = (color, index, defaultColor) => { + return color + ? ( + typeof color === 'string' + ? color + : color[index] || defaultColor + ) + : defaultColor; +}; diff --git a/handbook/src/components/iconfonts/index.d.ts b/handbook/src/components/iconfonts/index.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..928ddc5d1498285a0e7063490eafe146e7061e79 --- /dev/null +++ b/handbook/src/components/iconfonts/index.d.ts @@ -0,0 +1,25 @@ +/* eslint-disable */ + +import { SVGAttributes, FunctionComponent } from 'react'; +export { default as IconYouhua } from './IconYouhua'; +export { default as IconDayi } from './IconDayi'; +export { default as IconShengji } from './IconShengji'; +export { default as IconTiaozheng } from './IconTiaozheng'; +export { default as IconGengxin } from './IconGengxin'; +export { default as IconWendang } from './IconWendang'; +export { default as IconShanchu } from './IconShanchu'; +export { default as IconBug } from './IconBug'; +export { default as IconXinzeng } from './IconXinzeng'; +export { default as IconFuwu } from './IconFuwu'; +export { default as IconDown } from './IconDown'; +export { default as IconUp } from './IconUp'; + +interface Props extends Omit, 'color'> { + name: 'youhua' | 'dayi' | 'shengji' | 'tiaozheng' | 'gengxin' | 'wendang' | 'shanchu' | 'bug' | 'xinzeng' | 'fuwu' | 'down' | 'up'; + size?: number; + color?: string | string[]; +} + +declare const IconFont: FunctionComponent; + +export default IconFont; diff --git a/handbook/src/components/iconfonts/index.js b/handbook/src/components/iconfonts/index.js new file mode 100644 index 0000000000000000000000000000000000000000..9e798086831d7509a71682829ee7ccdfd7111a77 --- /dev/null +++ b/handbook/src/components/iconfonts/index.js @@ -0,0 +1,61 @@ +/* eslint-disable */ + +import React from 'react'; +import IconYouhua from './IconYouhua'; +import IconDayi from './IconDayi'; +import IconShengji from './IconShengji'; +import IconTiaozheng from './IconTiaozheng'; +import IconGengxin from './IconGengxin'; +import IconWendang from './IconWendang'; +import IconShanchu from './IconShanchu'; +import IconBug from './IconBug'; +import IconXinzeng from './IconXinzeng'; +import IconFuwu from './IconFuwu'; +import IconDown from './IconDown'; +import IconUp from './IconUp'; +export { default as IconYouhua } from './IconYouhua'; +export { default as IconDayi } from './IconDayi'; +export { default as IconShengji } from './IconShengji'; +export { default as IconTiaozheng } from './IconTiaozheng'; +export { default as IconGengxin } from './IconGengxin'; +export { default as IconWendang } from './IconWendang'; +export { default as IconShanchu } from './IconShanchu'; +export { default as IconBug } from './IconBug'; +export { default as IconXinzeng } from './IconXinzeng'; +export { default as IconFuwu } from './IconFuwu'; +export { default as IconDown } from './IconDown'; +export { default as IconUp } from './IconUp'; + +const IconFont = ({ name, ...rest }) => { + switch (name) { + case 'youhua': + return ; + case 'dayi': + return ; + case 'shengji': + return ; + case 'tiaozheng': + return ; + case 'gengxin': + return ; + case 'wendang': + return ; + case 'shanchu': + return ; + case 'bug': + return ; + case 'xinzeng': + return ; + case 'fuwu': + return ; + case 'down': + return ; + case 'up': + return ; + + } + + return null; +}; + +export default IconFont; diff --git a/handbook/src/css/custom.css b/handbook/src/css/custom.css new file mode 100644 index 0000000000000000000000000000000000000000..74ba0f27f39d54932268e3e9cd8a8deed83b15e4 --- /dev/null +++ b/handbook/src/css/custom.css @@ -0,0 +1,25 @@ +/* stylelint-disable docusaurus/copyright-header */ +/** + * Any CSS included here will be global. The classic template + * bundles Infima by default. Infima is a CSS framework designed to + * work well for content-centric websites. + */ + +/* You can override the default Infima variables here. */ +:root { + --ifm-color-primary: #25c2a0; + --ifm-color-primary-dark: rgb(33, 175, 144); + --ifm-color-primary-darker: rgb(31, 165, 136); + --ifm-color-primary-darkest: rgb(26, 136, 112); + --ifm-color-primary-light: rgb(70, 203, 174); + --ifm-color-primary-lighter: rgb(102, 212, 189); + --ifm-color-primary-lightest: rgb(146, 224, 208); + --ifm-code-font-size: 95%; +} + +.docusaurus-highlight-code-line { + background-color: rgb(72, 77, 91); + display: block; + margin: 0 calc(-1 * var(--ifm-pre-padding)); + padding: 0 var(--ifm-pre-padding); +} diff --git a/handbook/src/data/sponsor.js b/handbook/src/data/sponsor.js new file mode 100644 index 0000000000000000000000000000000000000000..70dded8946c130ec49d1a44f34c564abe2d28ff6 --- /dev/null +++ b/handbook/src/data/sponsor.js @@ -0,0 +1,90 @@ +const sponsors = [ + { + id: 1, + title: "CRMEB 专注开源电商系统研发", + picture: "img/crmeb.jpg", + url: "http://github.crmeb.net/u/furion", + top: false, + tag: "铂金", + }, + { + id: 2, + title: "流之云 - 信息化、数字化服务提供商", + picture: "img/tpflow.png", + url: "https://www.gadmin8.com?from=furion", + top: false, + tag: "", + }, + { + id: 3, + title: "CoreShop 移动端/小程序商城系统", + picture: "img/coreshop.gif", + url: "https://www.coreshop.cn?from=furion", + top: false, + tag: "", + }, + // { + // title: "FirstUI 跨平台移动端组件库", + // picture: "img/firstui.jpeg", + // url: "https://www.firstui.cn?from=furion", + // top: false, + // }, + // { + // title: "JNPF 基于代码生成器的 .NET 框架", + // picture: "img/jnpfsoft.png", + // url: "https://dotnet.jnpfsoft.com/login?from=furion", + // top: false, + // }, + // { + // id: 4, + // title: "Layui-Vue 开源前端 UI 框架", + // picture: "img/layui.png", + // url: "http://www.layui-vue.com?from=furion", + // top: false, + // tag: "", + // }, + // { + // id: 5, + // title: "工作服定制T恤 一件起订来图定做", + // picture: "img/weishen.jpg", + // url: "https://eshan.tmall.com?from=furion", + // top: false, + // tag: "", + // }, + // { + // title: "ProcessOn:在线流程图思维导图工具", + // picture: "img/processon1.png", + // url: "https://www.processon.com?from=furion", + // }, + // { + // title: "Rust 语言圣经", + // picture: "img/rust.png", + // url: "https://course.rs/", + // }, + { + id: 6, + title: "DIY 可视化 UniApp 代码生成器", + picture: "img/lk.jpg", + url: "https://www.diygw.com?from=furion", + top: false, + tag: "", + }, + { + id: 7, + title: "MaxKey - 业界领先的单点登录产品", + picture: "img/maxkey.png", + url: "https://gitee.com/dromara/MaxKey?from=furion", + top: true, + tag: "铂金", + }, + // { + // id: 100, + // title: "赞助 Furion 快速提升企业品牌知名度", + // picture: "img/xxyd.jpeg", + // url: "http://furion.baiqian.ltd/docs/donate", + // top: true, + // tag: "铂金", + // }, +]; + +export default sponsors; diff --git a/handbook/src/data/urls.js b/handbook/src/data/urls.js new file mode 100644 index 0000000000000000000000000000000000000000..9a53f0d015353f4084f0ad22dc4a76e356fb8685 --- /dev/null +++ b/handbook/src/data/urls.js @@ -0,0 +1,189 @@ +const urls = [ + { + url: "https://www.oschina.net/", + text: "开源中国", + title: "", + }, + { + url: "https://gitee.com/", + text: "Gitee", + title: "", + }, + { + url: "https://gitee.com/dotnetchina", + text: "dotNET China", + title: "", + }, + { + url: "https://github.com/sunkaixuan/SqlSugar", + text: "SqlSugar", + title: "", + }, + { + url: "https://gitee.com/dotnetchina/SmartSQL/", + text: "SmartSQL", + title: "", + }, + { + url: "http://www.thinkphp.cn/", + text: "ThinkPHP", + title: "", + }, + { + url: "https://hutool.cn/", + text: "Hutool", + title: "", + }, + { + url: "https://www.tiocloud.com/2/index.html", + text: "t-io", + title: "", + }, + { + url: "https://gitee.com/monksoul/LayX", + text: "Layx", + title: "", + }, + { + url: "https://gitee.com/dotnetchina/IoTSharp", + text: "IoTSharp", + title: "", + }, + { + url: "https://www.eova.cn/", + text: "Eova", + title: "", + }, + { + url: "http://www.pearadmin.com/", + text: "PearAdmin", + title: "", + }, + { + url: "https://github.com/mengshukeji/Luckysheet", + text: "Luckysheet", + title: "", + }, + { + url: "https://blog.lindexi.com/", + text: "林德熙博客", + title: "", + }, + { + url: "http://www.easyson.com.cn", + text: "易胜科技", + title: "", + }, + { + url: "https://gitee.com/pig0224/ExamKing", + text: "考试君", + title: "", + }, + { + url: "https://gitee.com/veal98/Echo", + text: "Echo", + title: "", + }, + { + url: "https://gitee.com/opencc/ccflow", + text: "驰骋工作流", + title: "", + }, + { + url: "https://gitee.com/dotnetchina/weaving-socket", + text: "weaving-socket", + title: "", + }, + { + url: "https://gitee.com/dotnetchina/SiMayRemoteMonitorOS", + text: "SiMayRemoteMonitorOS", + title: "", + }, + { + url: "https://gitee.com/zuohuaijun/Admin.NET", + text: "Admin.NET", + title: "", + }, + { + url: "https://gitee.com/dotnetchina/TouchSocket", + text: "TouchSocket", + title: "", + }, + { + url: "https://gitee.com/dotnetchina/anno.core", + text: "Anno.Core", + title: "", + }, + { + url: "https://gitee.com/dotnetchina/DBCHM", + text: "DBCHM", + title: "", + }, + { + url: "https://gitee.com/dotnetchina/OpenAuth.Net", + text: "OpenAuth.Net", + title: "", + }, + { + url: "https://dotnet9.com/", + text: "Dotnet9", + title: "", + }, + { + url: "https://gitee.com/yhuse/SunnyUI", + text: "SunnyUI", + title: "", + }, + { + url: "https://gitee.com/dotnetchina/MiniExcel", + text: "MiniExcel", + title: "", + }, + { + url: "https://gitee.com/handyorg/HandyControl", + text: "HandyControl", + title: "", + }, + { + url: "https://shopxo.net/", + text: "ShopXO 开源商城", + title: "ShopXO 企业级免费开源商城", + }, + { + url: "https://gitee.com/ntdgg/tpflow", + text: "tpflow", + title: "", + }, + { + url: "https://gitee.com/CoreUnion/CoreShop", + text: "CoreShop", + title: "", + }, + { + url: "https://gitee.com/dromara/TLog", + text: "TLog", + title: "", + }, + { + url: "https://gitee.com/dromara/liteFlow", + text: "LiteFlow", + title: "", + }, + { + url: "http://www.layui-vue.com", + text: "Layui Vue", + title: "Layui - Vue 开源前端 UI 框架", + }, + { + url: "https://www.gadmin8.com/", + text: "流之云", + title: "流之云 - 信息化、数字化服务提供商", + }, + { + url: "https://www.diygw.com", + text: "UniApp 可视化源码", + title: "DIY 可视化 UniApp 代码生成器", + }, +]; + +export default urls; diff --git a/handbook/src/pages/docker.svg b/handbook/src/pages/docker.svg new file mode 100644 index 0000000000000000000000000000000000000000..9a33c47505a15bd2530ad6c12bf42e00b811446f --- /dev/null +++ b/handbook/src/pages/docker.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/handbook/src/pages/index.css b/handbook/src/pages/index.css new file mode 100644 index 0000000000000000000000000000000000000000..71a0a99ffc4e2ea65b70000e3a886725de28e436 --- /dev/null +++ b/handbook/src/pages/index.css @@ -0,0 +1,535 @@ +.furion-banner { + padding: 4rem 2rem; + align-items: center; + /* background-color: #4623d9; */ + background: #4623d9 url("/img/csharp-certification.png"); + background-size: cover; + color: #fff; + overflow: hidden; +} + +.furioin-index-gif { + position: absolute; + /* top: -20px; + left: 280px; */ + top: 135px; + left: 120px; + height: 60px; + z-index: 3; +} + +.furion-banner-container { + display: flex; + justify-content: space-between; + max-width: 1140px; + margin: 0 auto; + position: relative; +} + +.furion-banner-item { + flex: 1; + position: relative; +} + +.furion-banner-project { + font-size: 1.5em; + font-weight: 700; +} + +.furion-banner-description { + margin: 24px 0; + font-size: 2.5em; + font-weight: 700; + line-height: 1.25; + background-image: linear-gradient(81deg, #8759ff, #3fc4fe, #42ffac); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; +} + +.furion-banner-spec { + padding: 0; + opacity: 0.7; + font-family: Muli; + font-size: 1em; + font-weight: 500; + line-height: 1.33; +} + +.furion-banner-spec li { + list-style: none; + position: relative; + padding-left: 1em; + margin-bottom: 1em; +} + +.furion-banner-spec li::before { + content: ""; + position: absolute; + top: 0.5em; + left: 0; + width: 4px; + height: 4px; + background-color: rgb(135, 89, 255); +} + +.furion-support-platform { + font-size: 0.85em; + line-height: 2; + margin-top: 3em; + font-weight: 500; + opacity: 0.6; + color: white; + font-family: Muli; +} + +.furion-support-icons { + display: flex; + margin-top: 12px; +} + +.furion-support-icons span { + margin-right: 20px; +} + +.furion-get-start, +.furion-try-demo { + margin-top: 4em; + border-radius: 2em; + min-width: 145px; + color: #fff; + background: #8759ff; + position: relative; + line-height: 1.5; + text-align: center; + padding: 8px 32px; + text-decoration: none; + display: inline-block; + white-space: nowrap; + z-index: 2; +} + +.furion-try-demo { + background-color: rgb(199, 29, 36); + margin-left: 20px; +} + +.furion-get-start:hover { + background: rgba(135, 89, 255, 0.9); +} + +.furion-try-demo:hover { + opacity: 0.9; +} + +.furion-banner-item .system-window { + width: 34rem; +} + +.furion-get-start:hover, +.furion-try-demo:hover { + color: #fff; + text-decoration: none; +} + +.system-top-bar { + background-image: linear-gradient( + to right, + rgba(136, 89, 255, 0.2), + rgba(63, 196, 254, 0.2) 90%, + rgba(66, 255, 172, 0.2) + ); + padding: 0.25em 1em; +} + +.system-top-bar-circle { + display: inline-block; + width: 0.5em; + height: 0.5em; + margin-left: 0.3em; + border-radius: 50%; + filter: brightness(100%); +} + +.system-window { + --ifm-leading: 0; + width: 95%; + padding: 0; + border-radius: 1em; + overflow: hidden; + background: rgb(33, 27, 80); +} + +.system-window iframe { + border-radius: unset; +} + +.system-window pre { + margin-bottom: 0 !important; +} + +.blue-accent { + --uni-border-color: #3fbbfe; + --uni-box-shadow-color: rgba(63, 187, 254, 0.1); + --ifm-menu-color-active: #3fbbfe; +} + +.preview-border { + box-shadow: 0 6px 58px 0 rgba(63, 187, 254, 0.1); + border: solid 1px #3fbbfe; +} + +.furion-content { + margin-top: 4em; + margin-bottom: 4em; + text-align: center; +} + +.furion-small-title { + color: #4623d9; + font-family: Muli; + font-size: 1em; + font-weight: 600; + letter-spacing: 1px; + opacity: 0.6; +} + +.furion-small-title.dark { + color: #f5f6f7; +} + +.furion-big-title { + color: #4623d9; + font-family: Poppins; + font-size: 2em; + font-weight: 700; + line-height: 1.31; + margin-bottom: 2em; +} + +.furion-big-title.dark { + color: #f5f6f7; +} + +.furion-gitee-log { + display: flex; + justify-content: center; + flex-wrap: nowrap; +} + +.furion-log-item { + width: 260px; + height: 173px; + box-sizing: border-box; + margin-right: 65px; + position: relative; +} + +.furion-log-jiao { + width: 100px; + height: 100px; + background: #fff; + position: absolute; + top: 0; + right: 0; + top: -6px; + right: -6px; + border-top: 1px dashed #a795e8; + border-right: 1px dashed #a795e8; +} + +.furion-log-jiao.dark { + background: rgb(27, 27, 29); +} + +.furion-log-item:last-child { + margin-right: 0; +} + +.furion-log-number { + position: relative; + z-index: 2; + height: 100%; + display: flex; + width: 100%; + justify-content: center; + align-items: center; + flex-direction: column; +} + +.furion-log-number div { + font-size: 3em; + font-weight: 700; +} + +.furion-log-number span { + font-family: Poppins, sans-serif; + font-stretch: normal; + font-style: normal; + letter-spacing: normal; + line-height: normal; + color: #1c1e21; +} + +.furion-log-number span.dark { + color: #f5f6f7; +} + +.furion-whouse { + align-items: center; + background-color: #4623d9 !important; + color: #fff; + display: flex; + padding: 5rem 0; + overflow: hidden; +} + +.furion-who-custom { + background-color: #fff; + min-height: 500px; + width: 60%; + box-sizing: border-box; + padding: 6rem; + justify-content: center; + color: #723cff; + text-align: center; + display: flex; + flex-wrap: wrap; + flex-direction: column; +} + +.furion-who-des-text { + max-width: 350px; +} + +.furion-donate-wrap { + box-sizing: border-box; + justify-content: center; + align-items: center; + text-align: center; + display: flex; + flex-wrap: wrap; + padding: 25px 0; + margin-bottom: 20px; +} + +.furion-custom-img { + text-decoration: none; + color: transparent; + margin-left: 3em; +} + +.furion-who-des { + padding: 0 5rem; + box-sizing: border-box; +} + +.furion-who-des p { + color: #fff; + font-family: Muli; + font-size: 1em; + line-height: 1.75; + margin-bottom: 0.8em; + opacity: 0.8; +} + +.furion-links.furion-fuchi-wrap { + margin: 4em; +} + +.furion-fuchi { + flex-direction: row; +} + +.furion-fuchi-content { + padding: 0 5rem; +} + +.footer { + background-color: #4623d9 !important; +} + +.furion-links { + margin: 4em; + text-align: center; +} + +.furion-links-content a { + display: inline-block; + margin: 0 1em; + font-size: 20px; + font-weight: 600; +} + +.furion-proccesson { + margin: 4em 0; + text-align: center; +} + +#dotnet-china { + height: 100px; +} + +.furion-get-start-btn { + position: relative; + display: flex; +} + +.furion-version { + position: absolute; + z-index: 10; + right: 0; + top: -10px; + color: yellow; + font-size: 16px; +} + +@media screen and (max-width: 1024px) { + .furion-banner-container { + justify-content: unset; + flex-direction: column; + } + + .furion-get-start-btn { + text-align: center; + } + + #dotnet-china { + height: 45px; + } + + .furion-banner-item .system-window { + width: 100%; + margin-top: 3rem; + } + + .furion-gitee-log { + justify-content: center; + flex-wrap: unset; + flex-direction: column; + align-items: center; + padding: 20px; + } + + .furion-log-item { + width: 100%; + height: 173px; + margin-right: 0; + margin-top: 25px; + } + + .furion-big-title { + margin-bottom: 1em; + } + + .furion-whouse { + flex-direction: column; + padding-bottom: 1em; + } + + .furion-who-des { + padding-top: 1em; + padding-bottom: 2em; + } + + .furion-custom-img { + margin-left: 0; + margin-bottom: 2em; + } + + .furion-custom-img img { + max-width: unset; + } + + .furion-who-custom { + justify-content: center; + align-items: center; + width: 100%; + text-align: center; + padding: 2rem; + } + + .furion-links { + margin: 4em 2em; + text-align: center; + } + + .furion-links.furion-fuchi-wrap { + margin: 4em 2em 0 2em; + } + + .furion-fuchi { + flex-direction: column-reverse; + } + + .furion-fuchi-content { + padding: 0; + margin-top: 3em; + } + + .w-overlay-container:before { + position: absolute; + left: 0; + right: 0; + bottom: 0; + top: 0; + } + + .furion-who-des-text { + padding: 2em 0; + max-width: 100%; + } +} + +.furion-bifa { + background: linear-gradient(rgba(33, 27, 80, 0.8), rgba(65, 42, 148, 0.8)), + url(@site/static/img/bgs.jpg); + background-position: center center; + background-repeat: no-repeat; + background-size: cover; + color: #fff; + display: flex; + flex-direction: row-reverse; + justify-content: center; + padding: 100px 0; + overflow-x: hidden; +} + +.furion-wzi { + border-left: 1px solid rgba(137, 147, 180, 0.5); + writing-mode: vertical-lr; + font-size: 20px; + font-family: Arial, Helvetica, sans-serif; + letter-spacing: 10px; + padding: 25px; + height: 320px; +} + +.furion-wzi span { + color: rgb(68, 188, 254); + font-weight: 500; +} + +.furion-wzi-title { + width: 84px; + height: 320px; + writing-mode: vertical-lr; + background: rgb(68, 114, 196); + background-image: linear-gradient(rgb(91, 128, 212) 1px, transparent 0), + linear-gradient(90deg, rgb(91, 128, 212) 1px, transparent 0); + background-size: 8px 8px; + margin-left: 40px; + font-size: 22px; + font-weight: 500; + letter-spacing: 12px; + display: flex; + align-items: center; +} + +.furion-wzi-title b { + font-size: 20px; + writing-mode: horizontal-tb; + font-family: Arial, Helvetica, sans-serif; + margin: 20px 0; + letter-spacing: 2px; +} diff --git a/handbook/src/pages/index.js b/handbook/src/pages/index.js new file mode 100644 index 0000000000000000000000000000000000000000..026b3e93688bb73288fa99e64b88a0fcd8d06c6e --- /dev/null +++ b/handbook/src/pages/index.js @@ -0,0 +1,523 @@ +import Link from "@docusaurus/Link"; +import { useColorMode } from "@docusaurus/theme-common"; +import useBaseUrl from "@docusaurus/useBaseUrl"; +import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; +import Layout from "@theme/Layout"; +import components from "@theme/MDXComponents"; +import Tooltip from "@uiw/react-tooltip"; +import React, { useContext } from "react"; +import Assistance from "../components/Assistance"; +import Donate from "../components/Donate"; +import GlobalContext from "../components/GlobalContext"; +import SpecDonate from "../components/SpecDonate"; +import urls from "../data/urls"; +import DockerIcon from "./docker.svg"; +import "./index.css"; +import "./index.own.css"; +import KubernetesIcon from "./kubernetes.svg"; +import LinuxIcon from "./linux.svg"; +import MacOSIcon from "./macos.svg"; +import NetConf from "./netconf-bot.svg"; +import WindowIcon from "./windows.svg"; + +const count = "已有 344 位用户开通 VIP 服务"; + +function Home() { + const context = useDocusaurusContext(); + const { siteConfig = {} } = context; + + return ( + + + + + + + + + + ); +} + +function Banner() { + return ( +
+
+ +
+ +
+ Furion{" "} + + [ˈfjʊəriən] {" "} + | [ˈfjʊriən]{" "} + + +
+
+ 您的痛点,Furion 已阅已历;Furion 的惊喜,您且慢慢享受。 +
+
+ 让 .NET 开发更简单,更通用,更流行。 +
+
+
    +
  • MIT 宽松开源协议,商用项目首选
  • +
  • 支持 .NET5/6/7/8+,没有历史包袱
  • +
  • 极少依赖,只依赖两个第三方包
  • +
  • 代码无侵入性,兼容原生写法
  • +
  • + + 了解 「选择 Furion 的十大理由」 + +
  • +
+
+
受支持平台:
+
+ + + + + + + + + + + + + + + +
+ +
+ + 入门指南 + v4.9.1.7 + + + + VIP 服务 + 499元/年 + + +
+
+
+ + _userRepository; + // highlight-next-line + public FurionAppService(IRepository userRepository) + { + _userRepository = userRepository; + } + + // highlight-next-line + [IfException(1000, ErrorMessage = "用户ID: {0} 不存在")] + public async Task GetUser([Range(1, int.MaxValue)] int userId) + { + var user = await _userRepository.FindOrDefaultAsync(userId); + // highlight-next-line + _ = user ?? throw Oops.Oh(1000, userId); + return user.Adapt(); + } + + public async Task GetRemote(string id) + { + // highlight-next-line + var data = await $"http://furion.baiqian.ltd/data?id={id}".GetAsAsync(); + return data; + } +} +`} + /> + +
+
+
+ ); +} + +function Gitee() { + const { colorMode, setLightTheme, setDarkTheme } = useColorMode(); + const isDarkTheme = colorMode === "dark"; + + return ( +
+

+ MIT 宽松开源协议/商用项目首选 +

+

+ ⭐️ MIT 开源协议,代码在 Gitee/GitHub 平台托管 ⭐️ +

+
+
+
+
+
12,000 +
+ Stars +
+
+
+
+
+
4,200 +
+ Forks +
+
+
+
+
+
11,648,355
+ Downloads +
+
+
+
+ ); +} + +function WhoUse() { + const { setDonate } = useContext(GlobalContext); + + return ( +
+
+

特别赞助

+
+
+ +
+
+

铂金赞助

+
+
+ + + +
+
+ + + +
+
+

金牌赞助

+
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+
+
+
+

请考虑赞助 Furion

+

+ Furion 是一个 MIT 许可的开源项目,在 NuGet 平台获得超 1160 + 万次下载。从小型企业到企业的解决方案及知名企业,他们在简单软件和复杂管理系统的开发方面都信任我们。 +
+
+ 如果 Furion 对您有所帮助,并且您希望 Furion 能够继续发展下去,请考虑 + ⌈赞助⌋ 我们。 +

+
+ +
+ + setDonate(true)} + > + 成为赞助商 + +
+
+
+ ); +} + +function Links() { + const { colorMode, setLightTheme, setDarkTheme } = useColorMode(); + const isDarkTheme = colorMode === "dark"; + return ( +
+

+ 友情链接 +

+

+ 更多优秀的项目/网站 +

+
+ {urls.map((item, i) => ( + + {item.text} + + ))} +
+
+ ); +} + +function ProccessOn() { + const { colorMode, setLightTheme, setDarkTheme } = useColorMode(); + const isDarkTheme = colorMode === "dark"; + return ( +
+

+ 功能模块 +

+

+ 麻雀虽小五脏俱全 +

+
+ +
+
+ ); +} + +function CodeSection(props) { + let { language, replace, section, source } = props; + + source = source.replace(/\/\/ <.*?\n/g, ""); + + if (replace) { + for (const [pattern, value] of Object.entries(replace)) { + source = source.replace(new RegExp(pattern, "gs"), value); + } + } + + source = source.trim(); + if (!source.includes("\n")) { + source += "\n"; + } + + return ( + + + + ); +} + +function SystemWindow(systemWindowProps) { + const { children, className, ...props } = systemWindowProps; + return ( +
+
+ + + +
+ {children} +
+ ); +} + +function Bifa() { + return ( +
+
+ Furion + 历经三年打磨 +
+ 网友笔伐过 + 用户捧杀过 + + 内心反复放弃过 + + + 最终化茧成蝶 + + + 为祖国信创添砖加瓦 + +
+ ); +} + +function Wzi(props) { + return
{props.children}
; +} + +function FuChi() { + const { colorMode, setLightTheme, setDarkTheme } = useColorMode(); + const isDarkTheme = colorMode === "dark"; + return ( +
+

+ 开发者扶持计划 +

+

+ 如果 C# 不能成就我们,那我们就成就 C#。 +

+
+
+ 为了回馈那些为 Furion + 提供问题解决、拉取请求以及开源案例的开发者,Furion 将推出全新的{" "} + "开发者扶持计划" + 。该计划将使用从 VIP 服务中获得的一部分资金来资助这些开发者。 +
+
+ 我们由衷感谢各位的支持。我们团队愿意将 VIP + 服务收到的一部分资金用来赞助那些为 Furion 框架贡献的人。 +
+
+ + + 即将到来... + +
+
+ +
+
+
+ ); +} + +export default Home; diff --git a/handbook/src/pages/index.own.css b/handbook/src/pages/index.own.css new file mode 100644 index 0000000000000000000000000000000000000000..27b2436c20468728001e07fa0a6adca3e1d1d0ff --- /dev/null +++ b/handbook/src/pages/index.own.css @@ -0,0 +1,38 @@ +.navbar { + background-color: #4623d9; +} + +.navbar__brand { + color: #fff; +} + +.navbar__link { + color: #fff; +} + +.navbar__link:hover, +.navbar__link--active { + color: yellow; +} + +.navbar__items { + color: #fff; +} + +.menu__list-item .navbar__link--active, +.menu__list-item .navbar__link:hover { + color: #743dff; +} + +.footer__logo { + background-color: #fff !important; +} + +#announcementBar-2 { + text-align: center; + padding: 0.6em; + background-color: #182c61; + font-size: 14px; + color: yellow; + position: relative; +} \ No newline at end of file diff --git a/handbook/src/pages/kubernetes.svg b/handbook/src/pages/kubernetes.svg new file mode 100644 index 0000000000000000000000000000000000000000..224bc3596336c8281531ec0f901f715b29f484bb --- /dev/null +++ b/handbook/src/pages/kubernetes.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/handbook/src/pages/linux.svg b/handbook/src/pages/linux.svg new file mode 100644 index 0000000000000000000000000000000000000000..fdf3b4cbe2825e306bbd1089a1d12de9b6197195 --- /dev/null +++ b/handbook/src/pages/linux.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/handbook/src/pages/macos.svg b/handbook/src/pages/macos.svg new file mode 100644 index 0000000000000000000000000000000000000000..684071c2b1ecc05eca680dee94781acec0f6797d --- /dev/null +++ b/handbook/src/pages/macos.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/handbook/src/pages/netconf-bot.svg b/handbook/src/pages/netconf-bot.svg new file mode 100644 index 0000000000000000000000000000000000000000..961055a7511c58d43161ab89645e01293b24a017 --- /dev/null +++ b/handbook/src/pages/netconf-bot.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/handbook/src/pages/windows.svg b/handbook/src/pages/windows.svg new file mode 100644 index 0000000000000000000000000000000000000000..c67da7c3c3e76c2f2a08d685eadacac43c5adfa1 --- /dev/null +++ b/handbook/src/pages/windows.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/handbook/src/theme/DocItem/Content/index.js b/handbook/src/theme/DocItem/Content/index.js new file mode 100644 index 0000000000000000000000000000000000000000..bfefcc005617f71110c6f59f5c3c32b81fd304db --- /dev/null +++ b/handbook/src/theme/DocItem/Content/index.js @@ -0,0 +1,49 @@ +import { ThemeClassNames } from "@docusaurus/theme-common"; +import { useDoc } from "@docusaurus/theme-common/internal"; +import Heading from "@theme/Heading"; +import MDXContent from "@theme/MDXContent"; +import clsx from "clsx"; +import React, { useContext } from "react"; +import GlobalContext from "../../../components/GlobalContext"; +import SpecDonate from "../../../components/SpecDonate"; + +/** + Title can be declared inside md content or declared through + front matter and added manually. To make both cases consistent, + the added title is added under the same div.markdown block + See https://github.com/facebook/docusaurus/pull/4882#issuecomment-853021120 + + We render a "synthetic title" if: + - user doesn't ask to hide it with front matter + - the markdown content does not already contain a top-level h1 heading +*/ +function useSyntheticTitle() { + const { metadata, frontMatter, contentTitle } = useDoc(); + const shouldRender = + !frontMatter.hide_title && typeof contentTitle === "undefined"; + if (!shouldRender) { + return null; + } + return metadata.title; +} +export default function DocItemContent({ children }) { + const syntheticTitle = useSyntheticTitle(); + const { adv } = useContext(GlobalContext); + + return ( +
+ {adv && ( + <> + + + )} + {syntheticTitle && ( +
+ {syntheticTitle} +
+ )} + + {children} +
+ ); +} diff --git a/handbook/src/theme/DocItem/Footer/index.js b/handbook/src/theme/DocItem/Footer/index.js new file mode 100644 index 0000000000000000000000000000000000000000..db5fb563070ef9e2d599f66265554fa2c683986d --- /dev/null +++ b/handbook/src/theme/DocItem/Footer/index.js @@ -0,0 +1,137 @@ +import { ThemeClassNames } from "@docusaurus/theme-common"; +import { useDoc } from "@docusaurus/theme-common/internal"; +import EditThisPage from "@theme/EditThisPage"; +import LastUpdated from "@theme/LastUpdated"; +import TagsListInline from "@theme/TagsListInline"; +import clsx from "clsx"; +import React, { useContext } from "react"; +import Assistance from "../../../components/Assistance"; +import Donate from "../../../components/Donate"; +import GlobalContext from "../../../components/GlobalContext"; +import SpecDonate from "../../../components/SpecDonate"; +import VipImageList from "../../../components/VipImageList"; +import styles from "./styles.module.css"; + +function TagsRow(props) { + return ( +
+
+ +
+
+ ); +} +function EditMetaRow({ + editUrl, + lastUpdatedAt, + lastUpdatedBy, + formattedLastUpdatedAt, +}) { + const { adv } = useContext(GlobalContext); + + return ( +
+
+ {editUrl && ( + <> + {adv && ( + + )} + + + )} +
+ +
+ {(lastUpdatedAt || lastUpdatedBy) && ( + <> + {adv && ( + + )} + + + )} +
+
+ ); +} +export default function DocItemFooter() { + const { metadata } = useDoc(); + const { + editUrl, + lastUpdatedAt, + formattedLastUpdatedAt, + lastUpdatedBy, + tags, + } = metadata; + const canDisplayTagsRow = tags.length > 0; + const canDisplayEditMetaRow = !!(editUrl || lastUpdatedAt || lastUpdatedBy); + const canDisplayFooter = canDisplayTagsRow || canDisplayEditMetaRow; + const { adv } = useContext(GlobalContext); + + if (!canDisplayFooter) { + return null; + } + return ( +
+ {adv && ( + <> + + + )} + {canDisplayTagsRow && } + {canDisplayEditMetaRow && ( + <> + + + {adv && ( + + //
+ // ⭐️{" "} + // + // 开通 VIP 服务仅需 499 元/年,尊享 365 天项目无忧 + // {" "} + // ⭐️ + //
+ )} + + )} +
+ ); +} diff --git a/handbook/src/theme/DocItem/Footer/styles.module.css b/handbook/src/theme/DocItem/Footer/styles.module.css new file mode 100644 index 0000000000000000000000000000000000000000..7c1e964419179b723b8d64cf67d996d34e6026c8 --- /dev/null +++ b/handbook/src/theme/DocItem/Footer/styles.module.css @@ -0,0 +1,11 @@ +.lastUpdated { + margin-top: 0.2rem; + font-style: italic; + font-size: smaller; +} + +@media (min-width: 997px) { + .lastUpdated { + text-align: right; + } +} diff --git a/handbook/src/theme/DocRoot/Layout/Main/index.js b/handbook/src/theme/DocRoot/Layout/Main/index.js new file mode 100644 index 0000000000000000000000000000000000000000..d8e89af4de5a86454739ebbaabde11ab3dbadec7 --- /dev/null +++ b/handbook/src/theme/DocRoot/Layout/Main/index.js @@ -0,0 +1,93 @@ +import Link from "@docusaurus/Link"; +import { useColorMode } from "@docusaurus/theme-common"; +import { useDocsSidebar } from "@docusaurus/theme-common/internal"; +import useBaseUrl from "@docusaurus/useBaseUrl"; +import Popover from "@uiw/react-popover"; +import clsx from "clsx"; +import React from "react"; +import { PayContent } from "../../../../components/PayContent"; +import styles from "./styles.module.css"; + +export default function DocRootLayoutMain({ + hiddenSidebarContainer, + children, +}) { + const sidebar = useDocsSidebar(); + return ( +
+ {/* */} + +
+ {children} +
+
+ ); +} + +function Cases({}) { + const { colorMode, setLightTheme, setDarkTheme } = useColorMode(); + const isDarkTheme = colorMode === "dark"; + + return ( +
+ + +
+ ); +} + +function Item({ url, title, logoUrl, desc }) { + return ( + + {logoUrl && } + {title} + + ); +} + +function Notice() { + const { colorMode, setLightTheme, setDarkTheme } = useColorMode(); + const isDarkTheme = colorMode === "dark"; + + return ( +
+
+ + 🚀 Furion v4.9.1.7 版本已发布。 + +
+
+ ⭐️ 开通 VIP 服务仅需 499 元/年,尊享 365 天项目无忧{" "} + + } + autoAdjustOverflow + > + + 立即开通 + + + {" "} + ⭐️ +
+
+ ); +} diff --git a/handbook/src/theme/DocRoot/Layout/Main/styles.module.css b/handbook/src/theme/DocRoot/Layout/Main/styles.module.css new file mode 100644 index 0000000000000000000000000000000000000000..ad97b7143652250ac699c85a2bfd239b7886536e --- /dev/null +++ b/handbook/src/theme/DocRoot/Layout/Main/styles.module.css @@ -0,0 +1,112 @@ +.docMainContainer { + display: flex; + width: 100%; +} + +.cases { + padding: 5px 15px 0px 15px; + border-bottom: 1px solid #dadde1; + box-sizing: border-box; + display: flex; + flex-wrap: wrap; + justify-content: flex-start; + align-items: flex-start; + position: sticky; + top: 0; + background-color: #fff; + color: #525860; + z-index: 10; +} + +.caseItem { + display: inline-flex; + text-decoration: none; + font-weight: 600; + color: inherit; + font-size: 14px; + padding: 5px; + align-items: center; + justify-content: center; + margin-right: 5px; + margin-bottom: 5px; +} + +.caseItem:hover { + text-decoration: none; + background-color: rgb(239, 239, 239); +} + +.caseItem img { + display: block; + height: 26px; + margin-right: 5px; +} + +.cases.caseDark { + background-color: rgb(27, 27, 29); + color: #e3e3e3; + border-bottom: 1px solid #444950; +} + +.caseDark .caseItem:hover { + background-color: rgb(239, 239, 239, 0.1); +} + +.notice { + padding: 10px 15px; + border-bottom: 1px solid #dadde1; + box-sizing: border-box; + font-size: 14px; + position: sticky; + top: 0; + background-color: #fff; + color: #525860; + z-index: 10; +} + +.notice.noticeDark { + background-color: rgb(27, 27, 29); + color: #e3e3e3; + border-bottom: 1px solid #444950; +} + +.tip { + background-color: rgb(199, 29, 36); + color: #ffffff; + border-radius: 3px; + text-decoration: none; + display: inline-block; + margin: 0 5px; + font-size: 12px; + cursor: pointer; + margin-left: 10px; + display: inline-block; + position: relative; +} + +.tip span { + padding: 1px 10px; + box-sizing: border-box; +} + +.tip:hover { + text-decoration: none; + color: #fff; +} + +@media (min-width: 997px) { + .docMainContainer { + flex-grow: 1; + max-width: calc(100% - var(--doc-sidebar-width)); + } + + .docMainContainerEnhanced { + max-width: calc(100% - var(--doc-sidebar-hidden-width)); + } + + .docItemWrapperEnhanced { + max-width: calc( + var(--ifm-container-width) + var(--doc-sidebar-width) + ) !important; + } +} diff --git a/handbook/src/theme/DocSidebar/Desktop/index.js b/handbook/src/theme/DocSidebar/Desktop/index.js new file mode 100644 index 0000000000000000000000000000000000000000..50542ca8698c944a33a3515cb88c2082cd556015 --- /dev/null +++ b/handbook/src/theme/DocSidebar/Desktop/index.js @@ -0,0 +1,68 @@ +import { useThemeConfig } from "@docusaurus/theme-common"; +import CollapseButton from "@theme/DocSidebar/Desktop/CollapseButton"; +import Content from "@theme/DocSidebar/Desktop/Content"; +import Logo from "@theme/Logo"; +import Tooltip from "@uiw/react-tooltip"; +import clsx from "clsx"; +import React, { useContext } from "react"; +import Assistance from "../../../components/Assistance"; +import Donate from "../../../components/Donate"; +import GlobalContext from "../../../components/GlobalContext"; +import { Sponsor, SponsorItem, closeStyle } from "../../../components/Sponsor"; +import sponsors from "../../../data/sponsor"; +import styles from "./styles.module.css"; + +function DocSidebarDesktop({ path, sidebar, onCollapse, isHidden }) { + const { + navbar: { hideOnScroll }, + docs: { + sidebar: { hideable }, + }, + } = useThemeConfig(); + const { adv, setAdv } = useContext(GlobalContext); + const sponsor = sponsors.find((u) => u.id == 100); + + return ( +
+ {hideOnScroll && } + + {adv ? ( + <> + + + setAdv((s) => !s)}> + 收 + + + + ) : ( + + )} + + + {adv && sponsor && ( +
+ +
+ )} + {hideable && } +
+ ); +} + +export default React.memo(DocSidebarDesktop); diff --git a/handbook/src/theme/DocSidebar/Desktop/styles.module.css b/handbook/src/theme/DocSidebar/Desktop/styles.module.css new file mode 100644 index 0000000000000000000000000000000000000000..dc6828843af8d5ef0c9b48c062fa5af129268072 --- /dev/null +++ b/handbook/src/theme/DocSidebar/Desktop/styles.module.css @@ -0,0 +1,43 @@ +@media (min-width: 997px) { + .sidebar { + display: flex; + flex-direction: column; + max-height: 100vh; + height: 100%; + position: sticky; + top: 0; + padding-top: var(--ifm-navbar-height); + width: var(--doc-sidebar-width); + transition: opacity 50ms ease; + } + + .sidebarWithHideableNavbar { + padding-top: 0; + } + + .sidebarHidden { + opacity: 0; + height: 0; + overflow: hidden; + visibility: hidden; + } + + .sidebarLogo { + display: flex !important; + align-items: center; + margin: 0 var(--ifm-navbar-padding-horizontal); + min-height: var(--ifm-navbar-height); + max-height: var(--ifm-navbar-height); + color: inherit !important; + text-decoration: none !important; + } + + .sidebarLogo img { + margin-right: 0.5rem; + height: 2rem; + } +} + +.sidebarLogo { + display: none; +} diff --git a/handbook/src/theme/DocSidebar/Mobile/index.js b/handbook/src/theme/DocSidebar/Mobile/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f287cc5d47bb92fe0c4f074cf84e79d0ab34919c --- /dev/null +++ b/handbook/src/theme/DocSidebar/Mobile/index.js @@ -0,0 +1,76 @@ +import { + NavbarSecondaryMenuFiller, + ThemeClassNames, +} from "@docusaurus/theme-common"; +import { useNavbarMobileSidebar } from "@docusaurus/theme-common/internal"; +import DocSidebarItems from "@theme/DocSidebarItems"; +import clsx from "clsx"; +import React, { useContext } from "react"; +import Assistance from "../../../components/Assistance"; +import Donate from "../../../components/Donate"; +import GlobalContext from "../../../components/GlobalContext"; +import { Sponsor, SponsorItem, closeStyle } from "../../../components/Sponsor"; +import sponsors from "../../../data/sponsor"; + +// eslint-disable-next-line react/function-component-definition +const DocSidebarMobileSecondaryMenu = ({ sidebar, path }) => { + const mobileSidebar = useNavbarMobileSidebar(); + const { adv, setAdv } = useContext(GlobalContext); + const sponsor = sponsors.find((u) => u.id == 100); + + return ( + <> + + {adv ? ( + <> + + setAdv((s) => !s)}> + 收 + + + ) : ( + + )} +
    + { + // Mobile sidebar should only be closed if the category has a link + if (item.type === "category" && item.href) { + mobileSidebar.toggle(); + } + if (item.type === "link") { + mobileSidebar.toggle(); + } + }} + level={1} + /> +
+ {adv && sponsor && ( +
+ +
+ )} + + ); +}; +function DocSidebarMobile(props) { + return ( + + ); +} + +export default React.memo(DocSidebarMobile); diff --git a/handbook/src/theme/Root.js b/handbook/src/theme/Root.js new file mode 100644 index 0000000000000000000000000000000000000000..d6958349ed901c1befa3738ac23385975c4b5735 --- /dev/null +++ b/handbook/src/theme/Root.js @@ -0,0 +1,208 @@ +import BrowserOnly from "@docusaurus/BrowserOnly"; +import Link from "@docusaurus/Link"; +import useBaseUrl from "@docusaurus/useBaseUrl"; +import Modal from "@uiw/react-modal"; +import React, { useContext, useEffect, useState } from "react"; +import Assistance from "../components/Assistance"; +import FloatBar from "../components/FloatBar"; +import GlobalContext from "../components/GlobalContext"; +import Vip from "../components/Vip"; +import VipDesc from "../components/VipDesc.mdx"; +import VipImageList from "../components/VipImageList"; + +function Root({ children }) { + const [donate, setDonate] = useState(false); + const [showVip, setVip] = useState(false); + const [adv, setAdv] = useState(true); + const [drawer, showDrawer] = useState(true); // 弹窗 + const [rightVip, setRightVip] = useState(false); + const [topVip, setTopVip] = useState(true); + + const onClosed = () => { + setDonate(false); + }; + + useEffect(() => { + if (!drawer) { + setTimeout(() => { + setTopVip(false); + }, 5000); + } + }, [drawer]); + + return ( + + {showVip && } + + {/* {!drawer && topVip && } */} + {children} + + + 如果 Furion 对您有所帮助,并且您希望 Furion 能够继续发展下去,请考虑{" "} + setDonate(false)} + > + ⌈赞助⌋ + {" "} + 我们。 +
+
+

个人微信扫码赞助

+ +
+
+

品牌商友情赞助

+
+
+

特别赞助

+
    +
  • 15,000/年 10,000/半年
  • +
  • + Gitee/Github 仓库 README.md 展示 +
  • +
  • + 文档页顶部和底部 ⌈大横幅⌋ 展示 +
  • +
  • 官网首页 ⌈特别赞助⌋ 展示
  • +
  • 文档页目录导航顶部 ⌈大图⌋ 展示
  • +
+
+
+

铂金赞助

+
    +
  • 7,500/年 5,000/半年
  • +
  • 官网首页 ⌈铂金赞助⌋ 展示
  • +
  • 文档页目录导航顶部 ⌈大图⌋ 展示
  • +
+
+
+

金牌赞助

+
    +
  • 5,000/年
  • +
  • 官网首页 ⌈金牌赞助⌋ 展示
  • +
  • 文档页目录导航顶部 ⌈小图⌋ 展示
  • +
+
+
+
+
+ 作者微信:ibaiqian +
+
+
+
+
+ + {/*
+ ⭐️{" "} + setDonate(false)} + > + 开通 VIP 服务仅需 499 元/年,尊享 365 天项目无忧 + {" "} + ⭐️ +
*/} +
+ + } /> +
+ ); +} + +function VipShow() { + const { drawer, showDrawer, setVip, setTopVip } = useContext(GlobalContext); + + return ( + { + showDrawer(false); + setVip(true); + }} + style={{ + color: "#1c1e21", + }} + bodyStyle={{ fontSize: 15, color: "#1c1e21" }} + // isCloseButtonShown={false} + // maskClosable={false} + > +
+ +
+
+ + { + setTopVip(false); + showDrawer(false); + }} + /> +
+ ); +} + +export default Root; diff --git a/handbook/src/theme/TOC/index.js b/handbook/src/theme/TOC/index.js new file mode 100644 index 0000000000000000000000000000000000000000..0a2e34334f7eb83dd422785ca50c29d6c31851a5 --- /dev/null +++ b/handbook/src/theme/TOC/index.js @@ -0,0 +1,68 @@ +import useBaseUrl from "@docusaurus/useBaseUrl"; +import TOCItems from "@theme/TOCItems"; +import clsx from "clsx"; +import React, { useContext } from "react"; +import GlobalContext from "../../components/GlobalContext"; +import { SponsorToc } from "../../components/SponsorToc"; +import Vip from "../../components/Vip"; +import VipImageList from "../../components/VipImageList"; +import styles from "./styles.module.css"; + +// Using a custom className +// This prevents TOCInline/TOCCollapsible getting highlighted by mistake +const LINK_CLASS_NAME = "table-of-contents__link toc-highlight"; +const LINK_ACTIVE_CLASS_NAME = "table-of-contents__link--active"; +export default function TOC({ className, ...props }) { + const { adv, rightVip } = useContext(GlobalContext); + + return ( +
+
+ +
+ {adv && } + + {rightVip && ( +
+ +
+ )} +
+ ); +} + +function DotNETChina() { + return ( + <> + + + + + + ); +} diff --git a/handbook/src/theme/TOC/styles.module.css b/handbook/src/theme/TOC/styles.module.css new file mode 100644 index 0000000000000000000000000000000000000000..858b029dd9fb29d75413728f46880db7dfd7c3ab --- /dev/null +++ b/handbook/src/theme/TOC/styles.module.css @@ -0,0 +1,16 @@ +.tableOfContents { + max-height: calc(100vh - (var(--ifm-navbar-height) + 2rem)); + overflow-y: auto; + position: sticky; + top: calc(var(--ifm-navbar-height) + 1rem); +} + +@media (max-width: 996px) { + .tableOfContents { + display: none; + } + + .docItemContainer { + padding: 0 0.3rem; + } +} \ No newline at end of file diff --git a/handbook/static/.nojekyll b/handbook/static/.nojekyll new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/handbook/static/img/01.png b/handbook/static/img/01.png new file mode 100644 index 0000000000000000000000000000000000000000..f03ec280f0cf6cc81d4b84fec079c3c7f10b3f91 Binary files /dev/null and b/handbook/static/img/01.png differ diff --git a/handbook/static/img/02.png b/handbook/static/img/02.png new file mode 100644 index 0000000000000000000000000000000000000000..96a1846d805a654a69e10f1d1639e059dad65400 Binary files /dev/null and b/handbook/static/img/02.png differ diff --git a/handbook/static/img/03.png b/handbook/static/img/03.png new file mode 100644 index 0000000000000000000000000000000000000000..da3f056c3533df3e7acfa3516a240e00d85c2b61 Binary files /dev/null and b/handbook/static/img/03.png differ diff --git a/handbook/static/img/04.png b/handbook/static/img/04.png new file mode 100644 index 0000000000000000000000000000000000000000..40514842c800a5ea43016c22d80c424dee66c835 Binary files /dev/null and b/handbook/static/img/04.png differ diff --git a/handbook/static/img/05.png b/handbook/static/img/05.png new file mode 100644 index 0000000000000000000000000000000000000000..54a333caa4cb37d4bb30a2891b36534585da82e5 Binary files /dev/null and b/handbook/static/img/05.png differ diff --git a/handbook/static/img/06.png b/handbook/static/img/06.png new file mode 100644 index 0000000000000000000000000000000000000000..2f476636b3d07492fe5d668f1eb5ff89a4251d65 Binary files /dev/null and b/handbook/static/img/06.png differ diff --git a/handbook/static/img/07.png b/handbook/static/img/07.png new file mode 100644 index 0000000000000000000000000000000000000000..52514d993ed95dd18d932ba4b392f6f2685b2201 Binary files /dev/null and b/handbook/static/img/07.png differ diff --git a/handbook/static/img/08.png b/handbook/static/img/08.png new file mode 100644 index 0000000000000000000000000000000000000000..b8eef901483ea08053350264ddd9b03582d16586 Binary files /dev/null and b/handbook/static/img/08.png differ diff --git a/handbook/static/img/09.png b/handbook/static/img/09.png new file mode 100644 index 0000000000000000000000000000000000000000..c1337f9796155ec88438ca0765cfdf3d933a7f86 Binary files /dev/null and b/handbook/static/img/09.png differ diff --git a/handbook/static/img/Admin.NET.png b/handbook/static/img/Admin.NET.png new file mode 100644 index 0000000000000000000000000000000000000000..760a14a37e7688c4c2d653cde6b7f2bd57c9912a Binary files /dev/null and b/handbook/static/img/Admin.NET.png differ diff --git a/handbook/static/img/Anno.Core.png b/handbook/static/img/Anno.Core.png new file mode 100644 index 0000000000000000000000000000000000000000..3f7652099d5ec46738cd05da4e8a9810a71d647f Binary files /dev/null and b/handbook/static/img/Anno.Core.png differ diff --git a/handbook/static/img/BootstrapBlazor.png b/handbook/static/img/BootstrapBlazor.png new file mode 100644 index 0000000000000000000000000000000000000000..7cdc21717d497d1608acfb1be365ca40729be2d1 Binary files /dev/null and b/handbook/static/img/BootstrapBlazor.png differ diff --git a/handbook/static/img/CCFlow.png b/handbook/static/img/CCFlow.png new file mode 100644 index 0000000000000000000000000000000000000000..a598574f68731397db56407939af841a09cbeda8 Binary files /dev/null and b/handbook/static/img/CCFlow.png differ diff --git a/handbook/static/img/CoreShop.png b/handbook/static/img/CoreShop.png new file mode 100644 index 0000000000000000000000000000000000000000..ee65ab238f5db11fcac5f17c781da142ad163ad7 Binary files /dev/null and b/handbook/static/img/CoreShop.png differ diff --git a/handbook/static/img/FastTunnel.png b/handbook/static/img/FastTunnel.png new file mode 100644 index 0000000000000000000000000000000000000000..c226200ea212dc266b25aadcf535dc10bf5e2517 Binary files /dev/null and b/handbook/static/img/FastTunnel.png differ diff --git a/handbook/static/img/Furion_Share.png b/handbook/static/img/Furion_Share.png new file mode 100644 index 0000000000000000000000000000000000000000..06ff26e96d2cfbcc80e7b726f04ebfad3569cb01 Binary files /dev/null and b/handbook/static/img/Furion_Share.png differ diff --git a/handbook/static/img/HandyControl.png b/handbook/static/img/HandyControl.png new file mode 100644 index 0000000000000000000000000000000000000000..cdf5dd088812ab5590a6ac19b36b3d41ec8cdf63 Binary files /dev/null and b/handbook/static/img/HandyControl.png differ diff --git a/handbook/static/img/IoTSharp.png b/handbook/static/img/IoTSharp.png new file mode 100644 index 0000000000000000000000000000000000000000..942eda53d9795e41512bb6adc6517c024cab5872 Binary files /dev/null and b/handbook/static/img/IoTSharp.png differ diff --git a/handbook/static/img/MiniExcel.png b/handbook/static/img/MiniExcel.png new file mode 100644 index 0000000000000000000000000000000000000000..082dfec3761d581e2a982bd128471da7ef906ef7 Binary files /dev/null and b/handbook/static/img/MiniExcel.png differ diff --git a/handbook/static/img/OpenAuth.NET.png b/handbook/static/img/OpenAuth.NET.png new file mode 100644 index 0000000000000000000000000000000000000000..c5a5405b4d5e522d754e2be636210441ebb08f84 Binary files /dev/null and b/handbook/static/img/OpenAuth.NET.png differ diff --git a/handbook/static/img/SqlSugar.png b/handbook/static/img/SqlSugar.png new file mode 100644 index 0000000000000000000000000000000000000000..616816794c7c28c1dbd8842755738f91cd31736d Binary files /dev/null and b/handbook/static/img/SqlSugar.png differ diff --git a/handbook/static/img/SunnyUI.png b/handbook/static/img/SunnyUI.png new file mode 100644 index 0000000000000000000000000000000000000000..7b3c882731b762ed693c59a8851cbe037678bfb4 Binary files /dev/null and b/handbook/static/img/SunnyUI.png differ diff --git a/handbook/static/img/allrl.png b/handbook/static/img/allrl.png new file mode 100644 index 0000000000000000000000000000000000000000..286510bc0d095bdab5c7d9f48d83ac7287aa8652 Binary files /dev/null and b/handbook/static/img/allrl.png differ diff --git a/handbook/static/img/bgs.jpg b/handbook/static/img/bgs.jpg new file mode 100644 index 0000000000000000000000000000000000000000..047207736a55bb883bbebfc850fc264963bd2862 Binary files /dev/null and b/handbook/static/img/bgs.jpg differ diff --git a/handbook/static/img/bm1.png b/handbook/static/img/bm1.png new file mode 100644 index 0000000000000000000000000000000000000000..21ce36d9314787dba8f2c630fd08909b03895adc Binary files /dev/null and b/handbook/static/img/bm1.png differ diff --git a/handbook/static/img/c10.jpg b/handbook/static/img/c10.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d6ab0cb140176c5b8d35d081cfd1d8577cdd483b Binary files /dev/null and b/handbook/static/img/c10.jpg differ diff --git a/handbook/static/img/cd2223.png b/handbook/static/img/cd2223.png new file mode 100644 index 0000000000000000000000000000000000000000..4d55bda574282cdd2071da61abc92dd040bbc3da Binary files /dev/null and b/handbook/static/img/cd2223.png differ diff --git a/handbook/static/img/cdr22.png b/handbook/static/img/cdr22.png new file mode 100644 index 0000000000000000000000000000000000000000..e84d8bcf5c01a3d6e8c3a4c2551abc6035f4cb3a Binary files /dev/null and b/handbook/static/img/cdr22.png differ diff --git a/handbook/static/img/chinadotnet.png b/handbook/static/img/chinadotnet.png new file mode 100644 index 0000000000000000000000000000000000000000..cbe49d197cf8c5e08dac0f8f88ac0d7697babc9a Binary files /dev/null and b/handbook/static/img/chinadotnet.png differ diff --git a/handbook/static/img/cli1.png b/handbook/static/img/cli1.png new file mode 100644 index 0000000000000000000000000000000000000000..2c58bd586071301d43501e34a88568f2b8421843 Binary files /dev/null and b/handbook/static/img/cli1.png differ diff --git a/handbook/static/img/cmp-donate.jpg b/handbook/static/img/cmp-donate.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0bd0536fe02c6ea642d55728df4181c28823715d Binary files /dev/null and b/handbook/static/img/cmp-donate.jpg differ diff --git a/handbook/static/img/cmp-qqgroup.jpg b/handbook/static/img/cmp-qqgroup.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f85cf050ebc03f72cc1eed9604462de63c719b05 Binary files /dev/null and b/handbook/static/img/cmp-qqgroup.jpg differ diff --git a/handbook/static/img/cmp-support.jpg b/handbook/static/img/cmp-support.jpg new file mode 100644 index 0000000000000000000000000000000000000000..572368c05e5378ce532287c97bfd02f5ad5c95fe Binary files /dev/null and b/handbook/static/img/cmp-support.jpg differ diff --git a/handbook/static/img/cmp-vip.jpeg b/handbook/static/img/cmp-vip.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..8ab1f592150642cdbd914218de314351ddfae539 Binary files /dev/null and b/handbook/static/img/cmp-vip.jpeg differ diff --git a/handbook/static/img/codefirst1.png b/handbook/static/img/codefirst1.png new file mode 100644 index 0000000000000000000000000000000000000000..0e0d8dbf806087970e67d96e30df11c1a75eb91f Binary files /dev/null and b/handbook/static/img/codefirst1.png differ diff --git a/handbook/static/img/codefirst2.png b/handbook/static/img/codefirst2.png new file mode 100644 index 0000000000000000000000000000000000000000..5d3edd4941a193a3d0b642e6541146d446516983 Binary files /dev/null and b/handbook/static/img/codefirst2.png differ diff --git a/handbook/static/img/codefirst3.png b/handbook/static/img/codefirst3.png new file mode 100644 index 0000000000000000000000000000000000000000..ee1f054dec489648b562e3bd7aa1f33bdb817a81 Binary files /dev/null and b/handbook/static/img/codefirst3.png differ diff --git a/handbook/static/img/coreshop.gif b/handbook/static/img/coreshop.gif new file mode 100644 index 0000000000000000000000000000000000000000..834ca6defb37feaf86b4827b19cbaf519e472ecf Binary files /dev/null and b/handbook/static/img/coreshop.gif differ diff --git a/handbook/static/img/coreshop_ad.png b/handbook/static/img/coreshop_ad.png new file mode 100644 index 0000000000000000000000000000000000000000..8ba92271ff0a17eb1b419fbd49bd6c4f7dc9ce80 Binary files /dev/null and b/handbook/static/img/coreshop_ad.png differ diff --git a/handbook/static/img/crmeb-right.jpg b/handbook/static/img/crmeb-right.jpg new file mode 100644 index 0000000000000000000000000000000000000000..da650f14b4db3caa01c4b3ffa922371a61b0e917 Binary files /dev/null and b/handbook/static/img/crmeb-right.jpg differ diff --git a/handbook/static/img/crmeb-spec.jpg b/handbook/static/img/crmeb-spec.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b15a0cca1df3cbc8de6d2a97294683299709c1bd Binary files /dev/null and b/handbook/static/img/crmeb-spec.jpg differ diff --git a/handbook/static/img/crmeb.jpg b/handbook/static/img/crmeb.jpg new file mode 100644 index 0000000000000000000000000000000000000000..962f375bf15d549b9a3fa993409cde582670becd Binary files /dev/null and b/handbook/static/img/crmeb.jpg differ diff --git a/handbook/static/img/cron.png b/handbook/static/img/cron.png new file mode 100644 index 0000000000000000000000000000000000000000..f3b8d7e313346efdf41c15963fc81b212ebe2525 Binary files /dev/null and b/handbook/static/img/cron.png differ diff --git a/handbook/static/img/csharp-certification.png b/handbook/static/img/csharp-certification.png new file mode 100644 index 0000000000000000000000000000000000000000..c88b4927e011eafe5cc63c29dfcbd72059502542 Binary files /dev/null and b/handbook/static/img/csharp-certification.png differ diff --git a/handbook/static/img/cswz.png b/handbook/static/img/cswz.png new file mode 100644 index 0000000000000000000000000000000000000000..8b7c43f5bdf0ef49dd42077b3ef80188247883a4 Binary files /dev/null and b/handbook/static/img/cswz.png differ diff --git a/handbook/static/img/custom1.png b/handbook/static/img/custom1.png new file mode 100644 index 0000000000000000000000000000000000000000..c7bd3e2eee91dcd8385509e5cc3ad8e1dbe1eab5 Binary files /dev/null and b/handbook/static/img/custom1.png differ diff --git a/handbook/static/img/custom10.png b/handbook/static/img/custom10.png new file mode 100644 index 0000000000000000000000000000000000000000..a1d27034ee5e70bc8e25c4c08b7e79e48db3fdf4 Binary files /dev/null and b/handbook/static/img/custom10.png differ diff --git a/handbook/static/img/custom11.png b/handbook/static/img/custom11.png new file mode 100644 index 0000000000000000000000000000000000000000..e8b5f16dffc6b2547403dc317df45d4f5f89838a Binary files /dev/null and b/handbook/static/img/custom11.png differ diff --git a/handbook/static/img/custom2.png b/handbook/static/img/custom2.png new file mode 100644 index 0000000000000000000000000000000000000000..badb16c2e0d058d8da3bc97d15a73630306573ef Binary files /dev/null and b/handbook/static/img/custom2.png differ diff --git a/handbook/static/img/custom3.png b/handbook/static/img/custom3.png new file mode 100644 index 0000000000000000000000000000000000000000..065526127bd8f59eadc7b5118c551fc0aebc8168 Binary files /dev/null and b/handbook/static/img/custom3.png differ diff --git a/handbook/static/img/custom4.jpg b/handbook/static/img/custom4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..78d2647b00bd5c41ee9c9b93f52fe7d3772311ad Binary files /dev/null and b/handbook/static/img/custom4.jpg differ diff --git a/handbook/static/img/custom5.png b/handbook/static/img/custom5.png new file mode 100644 index 0000000000000000000000000000000000000000..7aa9ff30acb4c4044d8b85246ef3ed171fc336db Binary files /dev/null and b/handbook/static/img/custom5.png differ diff --git a/handbook/static/img/custom6.png b/handbook/static/img/custom6.png new file mode 100644 index 0000000000000000000000000000000000000000..91235e611a9fca34f0f41e5135f14dc54012dce4 Binary files /dev/null and b/handbook/static/img/custom6.png differ diff --git a/handbook/static/img/dbcopy1.png b/handbook/static/img/dbcopy1.png new file mode 100644 index 0000000000000000000000000000000000000000..762f989529c2c3f781b8f689b9c2560f287752e0 Binary files /dev/null and b/handbook/static/img/dbcopy1.png differ diff --git a/handbook/static/img/dbcopy10.png b/handbook/static/img/dbcopy10.png new file mode 100644 index 0000000000000000000000000000000000000000..878ef97a75e5ed684e9193054a3ac5f54d21b59c Binary files /dev/null and b/handbook/static/img/dbcopy10.png differ diff --git a/handbook/static/img/dbcopy11.png b/handbook/static/img/dbcopy11.png new file mode 100644 index 0000000000000000000000000000000000000000..5ef5edcc70d81bda871588182c0a87b97daa6d5c Binary files /dev/null and b/handbook/static/img/dbcopy11.png differ diff --git a/handbook/static/img/dbcopy12.png b/handbook/static/img/dbcopy12.png new file mode 100644 index 0000000000000000000000000000000000000000..6b11a863016e422f06926a74f8722bee242a6f3b Binary files /dev/null and b/handbook/static/img/dbcopy12.png differ diff --git a/handbook/static/img/dbcopy2.png b/handbook/static/img/dbcopy2.png new file mode 100644 index 0000000000000000000000000000000000000000..e3b4301bfe51ccbc6b21014aa7e31351b746145d Binary files /dev/null and b/handbook/static/img/dbcopy2.png differ diff --git a/handbook/static/img/dbcopy3.png b/handbook/static/img/dbcopy3.png new file mode 100644 index 0000000000000000000000000000000000000000..33dfb67fbbc0427fda4514cc1a485e147accd3a0 Binary files /dev/null and b/handbook/static/img/dbcopy3.png differ diff --git a/handbook/static/img/dbcopy4.png b/handbook/static/img/dbcopy4.png new file mode 100644 index 0000000000000000000000000000000000000000..b24208840536bf7bcf1895b1ebbdc4f23cf4e274 Binary files /dev/null and b/handbook/static/img/dbcopy4.png differ diff --git a/handbook/static/img/dbcopy5.png b/handbook/static/img/dbcopy5.png new file mode 100644 index 0000000000000000000000000000000000000000..7834f52a7a83609b0a52b97eb7ab0b913d93e232 Binary files /dev/null and b/handbook/static/img/dbcopy5.png differ diff --git a/handbook/static/img/dbcopy6.png b/handbook/static/img/dbcopy6.png new file mode 100644 index 0000000000000000000000000000000000000000..ce9e11bf5d58a4318a216c6b74b8c945406b7654 Binary files /dev/null and b/handbook/static/img/dbcopy6.png differ diff --git a/handbook/static/img/dbcopy7.png b/handbook/static/img/dbcopy7.png new file mode 100644 index 0000000000000000000000000000000000000000..118da3e726d92396de1c4e35abd1a4e0b5aa6e63 Binary files /dev/null and b/handbook/static/img/dbcopy7.png differ diff --git a/handbook/static/img/dbcopy8.png b/handbook/static/img/dbcopy8.png new file mode 100644 index 0000000000000000000000000000000000000000..1e5cb904b85a28a6f9731274659d0f8cf061ef11 Binary files /dev/null and b/handbook/static/img/dbcopy8.png differ diff --git a/handbook/static/img/dbcopy9.png b/handbook/static/img/dbcopy9.png new file mode 100644 index 0000000000000000000000000000000000000000..278a8d0df5a0d0769b53e64f3e157b4c38211bf7 Binary files /dev/null and b/handbook/static/img/dbcopy9.png differ diff --git a/handbook/static/img/dbfirst1.png b/handbook/static/img/dbfirst1.png new file mode 100644 index 0000000000000000000000000000000000000000..f24f93890111402491fc3ef14eb71b078ffe2f61 Binary files /dev/null and b/handbook/static/img/dbfirst1.png differ diff --git a/handbook/static/img/dbfirst2.png b/handbook/static/img/dbfirst2.png new file mode 100644 index 0000000000000000000000000000000000000000..8c2638911f127440a380b0d401243584ba4464fe Binary files /dev/null and b/handbook/static/img/dbfirst2.png differ diff --git a/handbook/static/img/dbfirst3.png b/handbook/static/img/dbfirst3.png new file mode 100644 index 0000000000000000000000000000000000000000..08fcdf3e31c33dd3bb29fcc500c566f2c00e1050 Binary files /dev/null and b/handbook/static/img/dbfirst3.png differ diff --git a/handbook/static/img/dbfirst4.png b/handbook/static/img/dbfirst4.png new file mode 100644 index 0000000000000000000000000000000000000000..01ddf84c1206427c09a29efcf6030b49ed8ddcbf Binary files /dev/null and b/handbook/static/img/dbfirst4.png differ diff --git a/handbook/static/img/dbfirst5.png b/handbook/static/img/dbfirst5.png new file mode 100644 index 0000000000000000000000000000000000000000..7ffff7178ba394c1b559bf60b1103eb86a5d713c Binary files /dev/null and b/handbook/static/img/dbfirst5.png differ diff --git a/handbook/static/img/dbfirst6.png b/handbook/static/img/dbfirst6.png new file mode 100644 index 0000000000000000000000000000000000000000..2d98ca1e4aaa1945926239f4a36e541039e39d9f Binary files /dev/null and b/handbook/static/img/dbfirst6.png differ diff --git a/handbook/static/img/dbfirst7.png b/handbook/static/img/dbfirst7.png new file mode 100644 index 0000000000000000000000000000000000000000..9aafa25e02b636a4803152f6fc9a0b3fa5523884 Binary files /dev/null and b/handbook/static/img/dbfirst7.png differ diff --git a/handbook/static/img/dbfirst8.png b/handbook/static/img/dbfirst8.png new file mode 100644 index 0000000000000000000000000000000000000000..0435a5d2b54e124d78025c5d0b58b99697665267 Binary files /dev/null and b/handbook/static/img/dbfirst8.png differ diff --git a/handbook/static/img/dbfirst9.png b/handbook/static/img/dbfirst9.png new file mode 100644 index 0000000000000000000000000000000000000000..9a7a979819404c1bc7018cde2927b847d25297fd Binary files /dev/null and b/handbook/static/img/dbfirst9.png differ diff --git a/handbook/static/img/dbrece1.png b/handbook/static/img/dbrece1.png new file mode 100644 index 0000000000000000000000000000000000000000..ac94417a3a207018c9d8f278189601f382486b46 Binary files /dev/null and b/handbook/static/img/dbrece1.png differ diff --git a/handbook/static/img/dbrece10.png b/handbook/static/img/dbrece10.png new file mode 100644 index 0000000000000000000000000000000000000000..9bca161744cb2351a07a19ff5657123db4c6b673 Binary files /dev/null and b/handbook/static/img/dbrece10.png differ diff --git a/handbook/static/img/dbrece11.png b/handbook/static/img/dbrece11.png new file mode 100644 index 0000000000000000000000000000000000000000..687c9b1bb8872da5c71e9008eba6c9788087be07 Binary files /dev/null and b/handbook/static/img/dbrece11.png differ diff --git a/handbook/static/img/dbrece2.png b/handbook/static/img/dbrece2.png new file mode 100644 index 0000000000000000000000000000000000000000..e4448a70d4aff8ca4c41d3d1c2f67c94aa9acffa Binary files /dev/null and b/handbook/static/img/dbrece2.png differ diff --git a/handbook/static/img/dbrece3.png b/handbook/static/img/dbrece3.png new file mode 100644 index 0000000000000000000000000000000000000000..6950292ffa397901073830fbc81ff4187cf2605c Binary files /dev/null and b/handbook/static/img/dbrece3.png differ diff --git a/handbook/static/img/dbrece4.png b/handbook/static/img/dbrece4.png new file mode 100644 index 0000000000000000000000000000000000000000..a1c20a7d211a7b4e09cc8a0dea248454ce2ebb4e Binary files /dev/null and b/handbook/static/img/dbrece4.png differ diff --git a/handbook/static/img/dbrece5.png b/handbook/static/img/dbrece5.png new file mode 100644 index 0000000000000000000000000000000000000000..52ee3f91f39ab81cf514b53d115eabeb9699d943 Binary files /dev/null and b/handbook/static/img/dbrece5.png differ diff --git a/handbook/static/img/dbrece6.png b/handbook/static/img/dbrece6.png new file mode 100644 index 0000000000000000000000000000000000000000..ad906781b7b788a918574a82bd2c8ff07af26cec Binary files /dev/null and b/handbook/static/img/dbrece6.png differ diff --git a/handbook/static/img/dbrece7.png b/handbook/static/img/dbrece7.png new file mode 100644 index 0000000000000000000000000000000000000000..ddf1280f3786e67601cba7f7d3d0a026d8116a99 Binary files /dev/null and b/handbook/static/img/dbrece7.png differ diff --git a/handbook/static/img/dbrece8.png b/handbook/static/img/dbrece8.png new file mode 100644 index 0000000000000000000000000000000000000000..ba83d61c7fd2583ee9310e80b93f65ed0bd86036 Binary files /dev/null and b/handbook/static/img/dbrece8.png differ diff --git a/handbook/static/img/dbrece9.png b/handbook/static/img/dbrece9.png new file mode 100644 index 0000000000000000000000000000000000000000..3a454d5ea8c9387a20271420237c5d2bc2405ed8 Binary files /dev/null and b/handbook/static/img/dbrece9.png differ diff --git a/handbook/static/img/dbsplit1.jpg b/handbook/static/img/dbsplit1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..20c321a5a21f8f3adb7c7ab5eb1f1f44b9178e2f Binary files /dev/null and b/handbook/static/img/dbsplit1.jpg differ diff --git a/handbook/static/img/dbsplit2.jpg b/handbook/static/img/dbsplit2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e3eb2596c40d73a92b31697507a7f930ead5d263 Binary files /dev/null and b/handbook/static/img/dbsplit2.jpg differ diff --git a/handbook/static/img/dbsplit3.jpg b/handbook/static/img/dbsplit3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b63900c4e511f5308b9938c29e666db42cec0393 Binary files /dev/null and b/handbook/static/img/dbsplit3.jpg differ diff --git a/handbook/static/img/dbsplit4.jpg b/handbook/static/img/dbsplit4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..69ccfbbb9b8ea45d7d6542def841ec73e621434d Binary files /dev/null and b/handbook/static/img/dbsplit4.jpg differ diff --git a/handbook/static/img/dbsplit5.jpg b/handbook/static/img/dbsplit5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..21df07259b4d87f4d33bced3be8fcf426be2586c Binary files /dev/null and b/handbook/static/img/dbsplit5.jpg differ diff --git a/handbook/static/img/dbsplit6.jpg b/handbook/static/img/dbsplit6.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c58846b9f1049ff8e64998b9f0f646d08a7e76e4 Binary files /dev/null and b/handbook/static/img/dbsplit6.jpg differ diff --git a/handbook/static/img/dcsff.gif b/handbook/static/img/dcsff.gif new file mode 100644 index 0000000000000000000000000000000000000000..374213aa5458a8c6677a88c3034b3b5b32d0cd13 Binary files /dev/null and b/handbook/static/img/dcsff.gif differ diff --git a/handbook/static/img/demo.gif b/handbook/static/img/demo.gif new file mode 100644 index 0000000000000000000000000000000000000000..e1c8ec164d98118732d012859ee52b50c1c4ddd4 Binary files /dev/null and b/handbook/static/img/demo.gif differ diff --git a/handbook/static/img/df10.png b/handbook/static/img/df10.png new file mode 100644 index 0000000000000000000000000000000000000000..05b4c18a99f7be3c692e55337d9416821de6c2ee Binary files /dev/null and b/handbook/static/img/df10.png differ diff --git a/handbook/static/img/dfz.gif b/handbook/static/img/dfz.gif new file mode 100644 index 0000000000000000000000000000000000000000..897be33d0d65c716a7d7c5bc8e8e24060b891e12 Binary files /dev/null and b/handbook/static/img/dfz.gif differ diff --git a/handbook/static/img/dgqqwc.png b/handbook/static/img/dgqqwc.png new file mode 100644 index 0000000000000000000000000000000000000000..2206d4d94d24c94a058cd3c2a394c1af54bdd7d3 Binary files /dev/null and b/handbook/static/img/dgqqwc.png differ diff --git a/handbook/static/img/dgwc.png b/handbook/static/img/dgwc.png new file mode 100644 index 0000000000000000000000000000000000000000..8d5eb056fde9ef53311d25439e896914f10907de Binary files /dev/null and b/handbook/static/img/dgwc.png differ diff --git a/handbook/static/img/di1.gif b/handbook/static/img/di1.gif new file mode 100644 index 0000000000000000000000000000000000000000..a1add7bb0ef35ab87e549702e228484d6f1cfb01 Binary files /dev/null and b/handbook/static/img/di1.gif differ diff --git a/handbook/static/img/di2.gif b/handbook/static/img/di2.gif new file mode 100644 index 0000000000000000000000000000000000000000..20875d63ff6cbf964cdaefabad8824d46f899420 Binary files /dev/null and b/handbook/static/img/di2.gif differ diff --git a/handbook/static/img/dlrzh.gif b/handbook/static/img/dlrzh.gif new file mode 100644 index 0000000000000000000000000000000000000000..e418ec8bf2c7000a5dec87b1d9def9864a38bee4 Binary files /dev/null and b/handbook/static/img/dlrzh.gif differ diff --git a/handbook/static/img/domyself.png b/handbook/static/img/domyself.png new file mode 100644 index 0000000000000000000000000000000000000000..739c02fe10620c5df53b3db92555bd6cfbf21bc2 Binary files /dev/null and b/handbook/static/img/domyself.png differ diff --git a/handbook/static/img/donateme.png b/handbook/static/img/donateme.png new file mode 100644 index 0000000000000000000000000000000000000000..a5c4c42f22bfe52fb98e8a05da3c2f5b85f0f980 Binary files /dev/null and b/handbook/static/img/donateme.png differ diff --git a/handbook/static/img/dotnetchina.jpg b/handbook/static/img/dotnetchina.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6ce5c0f9c51a77bbacefa7b916286d6e81d922b2 Binary files /dev/null and b/handbook/static/img/dotnetchina.jpg differ diff --git a/handbook/static/img/dotnetchina2.jpg b/handbook/static/img/dotnetchina2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5161deddd8d495feb4550dd8b254916d56863b33 Binary files /dev/null and b/handbook/static/img/dotnetchina2.jpg differ diff --git a/handbook/static/img/dp1.png b/handbook/static/img/dp1.png new file mode 100644 index 0000000000000000000000000000000000000000..9c536419f00b7d695c6c790f56e6759a1e055138 Binary files /dev/null and b/handbook/static/img/dp1.png differ diff --git a/handbook/static/img/dp3.png b/handbook/static/img/dp3.png new file mode 100644 index 0000000000000000000000000000000000000000..aa168eb4da10b2a82c0c4401697a021139db799a Binary files /dev/null and b/handbook/static/img/dp3.png differ diff --git a/handbook/static/img/dp4.png b/handbook/static/img/dp4.png new file mode 100644 index 0000000000000000000000000000000000000000..bd5721bb4f714a5059f0ed66684a1ebd2a55e3ef Binary files /dev/null and b/handbook/static/img/dp4.png differ diff --git a/handbook/static/img/dr1.png b/handbook/static/img/dr1.png new file mode 100644 index 0000000000000000000000000000000000000000..e0446eaa5511ec239f092b2526d073d716fe1bb5 Binary files /dev/null and b/handbook/static/img/dr1.png differ diff --git a/handbook/static/img/dr2.png b/handbook/static/img/dr2.png new file mode 100644 index 0000000000000000000000000000000000000000..308ef4b92a8733d7d1c707da834155a0e5e00612 Binary files /dev/null and b/handbook/static/img/dr2.png differ diff --git a/handbook/static/img/ds1.png b/handbook/static/img/ds1.png new file mode 100644 index 0000000000000000000000000000000000000000..5f3684b625486684de023b709aa2a49ab76e74a2 Binary files /dev/null and b/handbook/static/img/ds1.png differ diff --git a/handbook/static/img/ds2.png b/handbook/static/img/ds2.png new file mode 100644 index 0000000000000000000000000000000000000000..ade539f769882a490b1d0d1abc213d71b74c2e10 Binary files /dev/null and b/handbook/static/img/ds2.png differ diff --git a/handbook/static/img/ds3.png b/handbook/static/img/ds3.png new file mode 100644 index 0000000000000000000000000000000000000000..1076c95ed4201bcd05d964b26081321b00507141 Binary files /dev/null and b/handbook/static/img/ds3.png differ diff --git a/handbook/static/img/ds4.png b/handbook/static/img/ds4.png new file mode 100644 index 0000000000000000000000000000000000000000..601710306fbb5fc2518dfce54031587c5482f636 Binary files /dev/null and b/handbook/static/img/ds4.png differ diff --git a/handbook/static/img/dy10.png b/handbook/static/img/dy10.png new file mode 100644 index 0000000000000000000000000000000000000000..80bc0eb572e32db375cc38c38f860b1f646c3b2d Binary files /dev/null and b/handbook/static/img/dy10.png differ diff --git a/handbook/static/img/dy2.png b/handbook/static/img/dy2.png new file mode 100644 index 0000000000000000000000000000000000000000..ce418fe7f9cc269550b63b396cc5fc172e069b60 Binary files /dev/null and b/handbook/static/img/dy2.png differ diff --git a/handbook/static/img/dyglz.gif b/handbook/static/img/dyglz.gif new file mode 100644 index 0000000000000000000000000000000000000000..93062451684d3597a7e5c954141630c0f344c43d Binary files /dev/null and b/handbook/static/img/dyglz.gif differ diff --git a/handbook/static/img/dyy1.png b/handbook/static/img/dyy1.png new file mode 100644 index 0000000000000000000000000000000000000000..13ab444f9e9fae4a1adf512412dec3cfe11525ae Binary files /dev/null and b/handbook/static/img/dyy1.png differ diff --git a/handbook/static/img/dyy2.png b/handbook/static/img/dyy2.png new file mode 100644 index 0000000000000000000000000000000000000000..88482cb8069abfaa4ef560c054023dcaa12d2ac7 Binary files /dev/null and b/handbook/static/img/dyy2.png differ diff --git a/handbook/static/img/dzffbb.png b/handbook/static/img/dzffbb.png new file mode 100644 index 0000000000000000000000000000000000000000..ef70ffbd493a3e74ce017b6404d14e7d59bb20a6 Binary files /dev/null and b/handbook/static/img/dzffbb.png differ diff --git a/handbook/static/img/dzffrl.png b/handbook/static/img/dzffrl.png new file mode 100644 index 0000000000000000000000000000000000000000..2cc00768ad1b2d5d2bd14926dbed4e9b3fd72f0f Binary files /dev/null and b/handbook/static/img/dzffrl.png differ diff --git a/handbook/static/img/dzmc.png b/handbook/static/img/dzmc.png new file mode 100644 index 0000000000000000000000000000000000000000..77e929903e949cbc8f188e13c80775495fb42d68 Binary files /dev/null and b/handbook/static/img/dzmc.png differ diff --git a/handbook/static/img/ebs.png b/handbook/static/img/ebs.png new file mode 100644 index 0000000000000000000000000000000000000000..273126da92cf2df11827c4dc4a27e9f5dab93913 Binary files /dev/null and b/handbook/static/img/ebs.png differ diff --git a/handbook/static/img/ebus1.png b/handbook/static/img/ebus1.png new file mode 100644 index 0000000000000000000000000000000000000000..273126da92cf2df11827c4dc4a27e9f5dab93913 Binary files /dev/null and b/handbook/static/img/ebus1.png differ diff --git a/handbook/static/img/enablepz.png b/handbook/static/img/enablepz.png new file mode 100644 index 0000000000000000000000000000000000000000..dff90f987c3ac8f03d52588d30299c2c8e9deca9 Binary files /dev/null and b/handbook/static/img/enablepz.png differ diff --git a/handbook/static/img/er1.png b/handbook/static/img/er1.png new file mode 100644 index 0000000000000000000000000000000000000000..75be6e6e2791033fe1d1af37aedb6d40fb5caae5 Binary files /dev/null and b/handbook/static/img/er1.png differ diff --git a/handbook/static/img/erp.jpg b/handbook/static/img/erp.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f2508b359c9a4d9c6d82a3ed61122f793effb5d8 Binary files /dev/null and b/handbook/static/img/erp.jpg differ diff --git a/handbook/static/img/event1.png b/handbook/static/img/event1.png new file mode 100644 index 0000000000000000000000000000000000000000..ad192e11400225dadb05ae2c1abb575d47b4ec8e Binary files /dev/null and b/handbook/static/img/event1.png differ diff --git a/handbook/static/img/event2.png b/handbook/static/img/event2.png new file mode 100644 index 0000000000000000000000000000000000000000..d532dba92cc2452c7ae51feb126654c1752c09f3 Binary files /dev/null and b/handbook/static/img/event2.png differ diff --git a/handbook/static/img/evs1.png b/handbook/static/img/evs1.png new file mode 100644 index 0000000000000000000000000000000000000000..942d48bdc5b197f7a483dcdbddfa8981016e27ed Binary files /dev/null and b/handbook/static/img/evs1.png differ diff --git a/handbook/static/img/f1.png b/handbook/static/img/f1.png new file mode 100644 index 0000000000000000000000000000000000000000..e50715d29c08a39ec9a45d2ec63150c3863ce3ae Binary files /dev/null and b/handbook/static/img/f1.png differ diff --git a/handbook/static/img/favicon.ico b/handbook/static/img/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..407a025af4c7e3067bfc884da54c8c1098ddc008 Binary files /dev/null and b/handbook/static/img/favicon.ico differ diff --git a/handbook/static/img/fgl.png b/handbook/static/img/fgl.png new file mode 100644 index 0000000000000000000000000000000000000000..4c3b6720529d0c412f75b1858745782f2880dd7f Binary files /dev/null and b/handbook/static/img/fgl.png differ diff --git a/handbook/static/img/fhzlx.png b/handbook/static/img/fhzlx.png new file mode 100644 index 0000000000000000000000000000000000000000..ec31a03d914b5b6cdbb70aab630f238f82d1321c Binary files /dev/null and b/handbook/static/img/fhzlx.png differ diff --git a/handbook/static/img/firstui.jpeg b/handbook/static/img/firstui.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..cfa6db99c0b68ab1609208a56cb23e21590ef670 Binary files /dev/null and b/handbook/static/img/firstui.jpeg differ diff --git a/handbook/static/img/fn1.png b/handbook/static/img/fn1.png new file mode 100644 index 0000000000000000000000000000000000000000..e1a3d3ec8e76d22d5245a3a2879325deff38dbe7 Binary files /dev/null and b/handbook/static/img/fn1.png differ diff --git a/handbook/static/img/functions.en.png b/handbook/static/img/functions.en.png new file mode 100644 index 0000000000000000000000000000000000000000..a5e27e42196a22506c46dac36d8f8ec68448d956 Binary files /dev/null and b/handbook/static/img/functions.en.png differ diff --git a/handbook/static/img/functions.png b/handbook/static/img/functions.png new file mode 100644 index 0000000000000000000000000000000000000000..ec4c38283e8b0c80d97579e17cc05a58a086534f Binary files /dev/null and b/handbook/static/img/functions.png differ diff --git a/handbook/static/img/furionlogo.png b/handbook/static/img/furionlogo.png new file mode 100644 index 0000000000000000000000000000000000000000..166b80180fb7e665c6617c39393aa57dd204046a Binary files /dev/null and b/handbook/static/img/furionlogo.png differ diff --git a/handbook/static/img/furionlogo_min.png b/handbook/static/img/furionlogo_min.png new file mode 100644 index 0000000000000000000000000000000000000000..82da36b64c9ed0357bddfa793cdf6219efc54a57 Binary files /dev/null and b/handbook/static/img/furionlogo_min.png differ diff --git a/handbook/static/img/getstart1.png b/handbook/static/img/getstart1.png new file mode 100644 index 0000000000000000000000000000000000000000..162a94dcf7fb936d1a8d924e97b17893483f000a Binary files /dev/null and b/handbook/static/img/getstart1.png differ diff --git a/handbook/static/img/getstart10.png b/handbook/static/img/getstart10.png new file mode 100644 index 0000000000000000000000000000000000000000..cb698a65cf47f399b536c4d19e204c3e3b20b2f6 Binary files /dev/null and b/handbook/static/img/getstart10.png differ diff --git a/handbook/static/img/getstart2.gif b/handbook/static/img/getstart2.gif new file mode 100644 index 0000000000000000000000000000000000000000..5dc7423899a9ebacb878dc1f084ea53811e33573 Binary files /dev/null and b/handbook/static/img/getstart2.gif differ diff --git a/handbook/static/img/getstart3.png b/handbook/static/img/getstart3.png new file mode 100644 index 0000000000000000000000000000000000000000..7aa49abe89dc69cc91ecae36dedffb25d2dfd234 Binary files /dev/null and b/handbook/static/img/getstart3.png differ diff --git a/handbook/static/img/getstart4.png b/handbook/static/img/getstart4.png new file mode 100644 index 0000000000000000000000000000000000000000..f753b6035b7112d2c95990e6a5c7f819e468f76e Binary files /dev/null and b/handbook/static/img/getstart4.png differ diff --git a/handbook/static/img/getstart5.png b/handbook/static/img/getstart5.png new file mode 100644 index 0000000000000000000000000000000000000000..0356d7be39a2fd0977f742369320994d6360311f Binary files /dev/null and b/handbook/static/img/getstart5.png differ diff --git a/handbook/static/img/getstart6.png b/handbook/static/img/getstart6.png new file mode 100644 index 0000000000000000000000000000000000000000..cb208395ee69ea78c6f23aa0295674006199e0c4 Binary files /dev/null and b/handbook/static/img/getstart6.png differ diff --git a/handbook/static/img/getstart7.png b/handbook/static/img/getstart7.png new file mode 100644 index 0000000000000000000000000000000000000000..a753214b32cbcdbc8472248f463341a79ae16c43 Binary files /dev/null and b/handbook/static/img/getstart7.png differ diff --git a/handbook/static/img/getstart8.gif b/handbook/static/img/getstart8.gif new file mode 100644 index 0000000000000000000000000000000000000000..70ad52871a47e76b2bcb07f8467d450bddca4dd0 Binary files /dev/null and b/handbook/static/img/getstart8.gif differ diff --git a/handbook/static/img/getstart9.png b/handbook/static/img/getstart9.png new file mode 100644 index 0000000000000000000000000000000000000000..1c8e314e8cf5bf10ea5f67eeb9790af876388100 Binary files /dev/null and b/handbook/static/img/getstart9.png differ diff --git a/handbook/static/img/getyycs.png b/handbook/static/img/getyycs.png new file mode 100644 index 0000000000000000000000000000000000000000..5294dc486db4e09fcc98aec377b94c386cbe1736 Binary files /dev/null and b/handbook/static/img/getyycs.png differ diff --git a/handbook/static/img/gvp.png b/handbook/static/img/gvp.png new file mode 100644 index 0000000000000000000000000000000000000000..4cb25d2f32f2f492d03d370b7666399955348acf Binary files /dev/null and b/handbook/static/img/gvp.png differ diff --git a/handbook/static/img/hl1.png b/handbook/static/img/hl1.png new file mode 100644 index 0000000000000000000000000000000000000000..bbfb1f2a8a5aac9c40c86eaa3ac4cacb017c811c Binary files /dev/null and b/handbook/static/img/hl1.png differ diff --git a/handbook/static/img/icon.png b/handbook/static/img/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..166b80180fb7e665c6617c39393aa57dd204046a Binary files /dev/null and b/handbook/static/img/icon.png differ diff --git a/handbook/static/img/iis2.png b/handbook/static/img/iis2.png new file mode 100644 index 0000000000000000000000000000000000000000..a04b025da8bf3a4f188125ef3f90c6c69bffba69 Binary files /dev/null and b/handbook/static/img/iis2.png differ diff --git a/handbook/static/img/iis3.png b/handbook/static/img/iis3.png new file mode 100644 index 0000000000000000000000000000000000000000..e4d9d02db5ba78dfc1e646cd6151b804e4af1205 Binary files /dev/null and b/handbook/static/img/iis3.png differ diff --git a/handbook/static/img/iis4.jpg b/handbook/static/img/iis4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3d87ea94fc2fda97a16ef4103a2e8b5d150ca12f Binary files /dev/null and b/handbook/static/img/iis4.jpg differ diff --git a/handbook/static/img/iis5.png b/handbook/static/img/iis5.png new file mode 100644 index 0000000000000000000000000000000000000000..b0d44d8c8ee7950d38f5f98d6725e864d11e8414 Binary files /dev/null and b/handbook/static/img/iis5.png differ diff --git a/handbook/static/img/iis6.png b/handbook/static/img/iis6.png new file mode 100644 index 0000000000000000000000000000000000000000..4880e4575d557639a3e54bf105b4720a0dba5c26 Binary files /dev/null and b/handbook/static/img/iis6.png differ diff --git a/handbook/static/img/iis7.png b/handbook/static/img/iis7.png new file mode 100644 index 0000000000000000000000000000000000000000..462cd9602467672cdb6a66a4da213823e3f33e46 Binary files /dev/null and b/handbook/static/img/iis7.png differ diff --git a/handbook/static/img/iis8.png b/handbook/static/img/iis8.png new file mode 100644 index 0000000000000000000000000000000000000000..9e19ea1a60f55fd91bbcd829f681344ebd0e0809 Binary files /dev/null and b/handbook/static/img/iis8.png differ diff --git a/handbook/static/img/iis9.png b/handbook/static/img/iis9.png new file mode 100644 index 0000000000000000000000000000000000000000..7bc9379a582ac5547d15df51b9e354972c20edcb Binary files /dev/null and b/handbook/static/img/iis9.png differ diff --git a/handbook/static/img/iishuishou.jpg b/handbook/static/img/iishuishou.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0887c865d90039ffe5614d2e37be69f1117f7f6b Binary files /dev/null and b/handbook/static/img/iishuishou.jpg differ diff --git a/handbook/static/img/jjd.png b/handbook/static/img/jjd.png new file mode 100644 index 0000000000000000000000000000000000000000..3af4be91a32f3af7e224a9e68133d8a142eaf443 Binary files /dev/null and b/handbook/static/img/jjd.png differ diff --git a/handbook/static/img/jnpfsoft.png b/handbook/static/img/jnpfsoft.png new file mode 100644 index 0000000000000000000000000000000000000000..1ab5ef35a7a18291b1398c61d51772872a135068 Binary files /dev/null and b/handbook/static/img/jnpfsoft.png differ diff --git a/handbook/static/img/job_dash.png b/handbook/static/img/job_dash.png new file mode 100644 index 0000000000000000000000000000000000000000..f69dc9b664ad3476c9792e62bd5117b68b1141a3 Binary files /dev/null and b/handbook/static/img/job_dash.png differ diff --git a/handbook/static/img/js1.png b/handbook/static/img/js1.png new file mode 100644 index 0000000000000000000000000000000000000000..49cdf5b89d7ac56fbf5803a024bdcfc6ab7a63be Binary files /dev/null and b/handbook/static/img/js1.png differ diff --git a/handbook/static/img/js10.png b/handbook/static/img/js10.png new file mode 100644 index 0000000000000000000000000000000000000000..46751b794a268fe3d587fd3075c74069b36e5d0b Binary files /dev/null and b/handbook/static/img/js10.png differ diff --git a/handbook/static/img/js11.png b/handbook/static/img/js11.png new file mode 100644 index 0000000000000000000000000000000000000000..47b73cb7593e52f53b918944e10b71c930f4426a Binary files /dev/null and b/handbook/static/img/js11.png differ diff --git a/handbook/static/img/js2.png b/handbook/static/img/js2.png new file mode 100644 index 0000000000000000000000000000000000000000..15689d1f5a2671d685c1d4cc23aee880024810ca Binary files /dev/null and b/handbook/static/img/js2.png differ diff --git a/handbook/static/img/js3.png b/handbook/static/img/js3.png new file mode 100644 index 0000000000000000000000000000000000000000..f3ef7ca872bac137b53750d2a0228fe99591ea4d Binary files /dev/null and b/handbook/static/img/js3.png differ diff --git a/handbook/static/img/js4.png b/handbook/static/img/js4.png new file mode 100644 index 0000000000000000000000000000000000000000..61762e9e0b092cc90ed7f235964f3db97fc1a9a4 Binary files /dev/null and b/handbook/static/img/js4.png differ diff --git a/handbook/static/img/js5.png b/handbook/static/img/js5.png new file mode 100644 index 0000000000000000000000000000000000000000..d556d42f87155b10ee857e3257dec8f60a4738de Binary files /dev/null and b/handbook/static/img/js5.png differ diff --git a/handbook/static/img/js6.png b/handbook/static/img/js6.png new file mode 100644 index 0000000000000000000000000000000000000000..da55ff51e64436033222c1c2ae15eb8efdc3c25e Binary files /dev/null and b/handbook/static/img/js6.png differ diff --git a/handbook/static/img/js7.png b/handbook/static/img/js7.png new file mode 100644 index 0000000000000000000000000000000000000000..a72bac34d16e0d3eb7ea2332e7723136d6f9ee76 Binary files /dev/null and b/handbook/static/img/js7.png differ diff --git a/handbook/static/img/js8.png b/handbook/static/img/js8.png new file mode 100644 index 0000000000000000000000000000000000000000..921a5d9544a5229463943dac199b87bab53f2f71 Binary files /dev/null and b/handbook/static/img/js8.png differ diff --git a/handbook/static/img/js9.png b/handbook/static/img/js9.png new file mode 100644 index 0000000000000000000000000000000000000000..4ffe16e8435231b9a6822e50dbd5f6755a8f8442 Binary files /dev/null and b/handbook/static/img/js9.png differ diff --git a/handbook/static/img/keepnamepz.png b/handbook/static/img/keepnamepz.png new file mode 100644 index 0000000000000000000000000000000000000000..17afcd3696bba6aace9fcddca231b25ee6986bef Binary files /dev/null and b/handbook/static/img/keepnamepz.png differ diff --git a/handbook/static/img/keepverbpz.png b/handbook/static/img/keepverbpz.png new file mode 100644 index 0000000000000000000000000000000000000000..9da97b4a4cfa738bc3b5b6f84c911e1c1d913869 Binary files /dev/null and b/handbook/static/img/keepverbpz.png differ diff --git a/handbook/static/img/kni.jpg b/handbook/static/img/kni.jpg new file mode 100644 index 0000000000000000000000000000000000000000..eee62d89af8984128342d0c28c676d1c23d0aa6f Binary files /dev/null and b/handbook/static/img/kni.jpg differ diff --git a/handbook/static/img/ksh.png b/handbook/static/img/ksh.png new file mode 100644 index 0000000000000000000000000000000000000000..619f27ec760bb0f895a652d1604df7ccf3690a5c Binary files /dev/null and b/handbook/static/img/ksh.png differ diff --git a/handbook/static/img/kzq.png b/handbook/static/img/kzq.png new file mode 100644 index 0000000000000000000000000000000000000000..72597dfb135745163e5aefd8445823e8fd47086e Binary files /dev/null and b/handbook/static/img/kzq.png differ diff --git a/handbook/static/img/kzqbb.png b/handbook/static/img/kzqbb.png new file mode 100644 index 0000000000000000000000000000000000000000..3a5d52dddf471b81ce86c1a6a4f3e17708280f61 Binary files /dev/null and b/handbook/static/img/kzqbb.png differ diff --git a/handbook/static/img/kzqrl.png b/handbook/static/img/kzqrl.png new file mode 100644 index 0000000000000000000000000000000000000000..d4ff155d44324be70204bf38f5135e5eb7287ad6 Binary files /dev/null and b/handbook/static/img/kzqrl.png differ diff --git a/handbook/static/img/lang1.png b/handbook/static/img/lang1.png new file mode 100644 index 0000000000000000000000000000000000000000..f3e12d1a5c774fc3425d9484ba05134a476b94dc Binary files /dev/null and b/handbook/static/img/lang1.png differ diff --git a/handbook/static/img/lang2.png b/handbook/static/img/lang2.png new file mode 100644 index 0000000000000000000000000000000000000000..87eb3cfaee08e0efbb72535fea586c75e70e666f Binary files /dev/null and b/handbook/static/img/lang2.png differ diff --git a/handbook/static/img/lang3.png b/handbook/static/img/lang3.png new file mode 100644 index 0000000000000000000000000000000000000000..617c3f6021c9014876577f321cb4e7717038ecbf Binary files /dev/null and b/handbook/static/img/lang3.png differ diff --git a/handbook/static/img/lang4.gif b/handbook/static/img/lang4.gif new file mode 100644 index 0000000000000000000000000000000000000000..2151042d9d7f4626c68275a35f19d5a95d1a0477 Binary files /dev/null and b/handbook/static/img/lang4.gif differ diff --git a/handbook/static/img/lang5.gif b/handbook/static/img/lang5.gif new file mode 100644 index 0000000000000000000000000000000000000000..34ee9e4bf6450882095786e6b0b6f86b81d2b055 Binary files /dev/null and b/handbook/static/img/lang5.gif differ diff --git a/handbook/static/img/lang6.png b/handbook/static/img/lang6.png new file mode 100644 index 0000000000000000000000000000000000000000..96eaf3eba1b69d0d810e5806a5b717c990dc93bc Binary files /dev/null and b/handbook/static/img/lang6.png differ diff --git a/handbook/static/img/layui.png b/handbook/static/img/layui.png new file mode 100644 index 0000000000000000000000000000000000000000..16b9e713f4159001a2c76f9fdaa9180f19a9ccb5 Binary files /dev/null and b/handbook/static/img/layui.png differ diff --git a/handbook/static/img/lk.jpg b/handbook/static/img/lk.jpg new file mode 100644 index 0000000000000000000000000000000000000000..717a50955de81b547751547f980b6399e3a641af Binary files /dev/null and b/handbook/static/img/lk.jpg differ diff --git a/handbook/static/img/lognone.png b/handbook/static/img/lognone.png new file mode 100644 index 0000000000000000000000000000000000000000..eb2ad565aa79b0bcf6d0cf4f2e5eb075d8600774 Binary files /dev/null and b/handbook/static/img/lognone.png differ diff --git a/handbook/static/img/logo.png b/handbook/static/img/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..166b80180fb7e665c6617c39393aa57dd204046a Binary files /dev/null and b/handbook/static/img/logo.png differ diff --git a/handbook/static/img/maxkey.png b/handbook/static/img/maxkey.png new file mode 100644 index 0000000000000000000000000000000000000000..60f2b71a372ec529bf14915582c67129f8631eee Binary files /dev/null and b/handbook/static/img/maxkey.png differ diff --git a/handbook/static/img/mdx.png b/handbook/static/img/mdx.png new file mode 100644 index 0000000000000000000000000000000000000000..ab1f1bb72f972eab7159c617958e29d61b2da15d Binary files /dev/null and b/handbook/static/img/mdx.png differ diff --git a/handbook/static/img/mdx2.png b/handbook/static/img/mdx2.png new file mode 100644 index 0000000000000000000000000000000000000000..26793ecf5de5caab7fe09d19a4bccb49531a99ff Binary files /dev/null and b/handbook/static/img/mdx2.png differ diff --git a/handbook/static/img/mip.png b/handbook/static/img/mip.png new file mode 100644 index 0000000000000000000000000000000000000000..a4cd1eab17d9b7e8940cbcdbd7a7125792474153 Binary files /dev/null and b/handbook/static/img/mip.png differ diff --git a/handbook/static/img/mipr.png b/handbook/static/img/mipr.png new file mode 100644 index 0000000000000000000000000000000000000000..93dda72d04ad7fb79d7f55c192c9d6c8c940dbb4 Binary files /dev/null and b/handbook/static/img/mipr.png differ diff --git a/handbook/static/img/modeltoquery.png b/handbook/static/img/modeltoquery.png new file mode 100644 index 0000000000000000000000000000000000000000..358a435e52de3e104cfae843e97cb52457f99734 Binary files /dev/null and b/handbook/static/img/modeltoquery.png differ diff --git a/handbook/static/img/modulepz.png b/handbook/static/img/modulepz.png new file mode 100644 index 0000000000000000000000000000000000000000..d2724ea9642ed9400274c0dacca63c17cae5e60e Binary files /dev/null and b/handbook/static/img/modulepz.png differ diff --git a/handbook/static/img/monksoul.jpg b/handbook/static/img/monksoul.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e6c65fc8fb8b0a36a0b0aaf758769e60f1442769 Binary files /dev/null and b/handbook/static/img/monksoul.jpg differ diff --git a/handbook/static/img/mulan.png b/handbook/static/img/mulan.png new file mode 100644 index 0000000000000000000000000000000000000000..79d55c2eb6b44636ed1b06f6f2558d4265a250d6 Binary files /dev/null and b/handbook/static/img/mulan.png differ diff --git a/handbook/static/img/n1.png b/handbook/static/img/n1.png new file mode 100644 index 0000000000000000000000000000000000000000..460a0a8a5353bba5f0145fc8ad2cd9706183201d Binary files /dev/null and b/handbook/static/img/n1.png differ diff --git a/handbook/static/img/n2.png b/handbook/static/img/n2.png new file mode 100644 index 0000000000000000000000000000000000000000..b5ed95bbd2046e52abe3fd28d176829574dc319e Binary files /dev/null and b/handbook/static/img/n2.png differ diff --git a/handbook/static/img/n3.png b/handbook/static/img/n3.png new file mode 100644 index 0000000000000000000000000000000000000000..231135838273716df6443c6a00cb311fa6c3c939 Binary files /dev/null and b/handbook/static/img/n3.png differ diff --git a/handbook/static/img/n4.png b/handbook/static/img/n4.png new file mode 100644 index 0000000000000000000000000000000000000000..894f0600ae8c53c77d0385d8bd987425010c7e27 Binary files /dev/null and b/handbook/static/img/n4.png differ diff --git a/handbook/static/img/n5.png b/handbook/static/img/n5.png new file mode 100644 index 0000000000000000000000000000000000000000..79f56a281be8649c3171d507cb112cb66a2f8411 Binary files /dev/null and b/handbook/static/img/n5.png differ diff --git a/handbook/static/img/namepz.png b/handbook/static/img/namepz.png new file mode 100644 index 0000000000000000000000000000000000000000..a26156b8c7568a6487d7bcfdf0e3ed8d91b847d5 Binary files /dev/null and b/handbook/static/img/namepz.png differ diff --git a/handbook/static/img/net71.png b/handbook/static/img/net71.png new file mode 100644 index 0000000000000000000000000000000000000000..c1ebd9d2e03c1792e412d4c28e3f624757faa5e3 Binary files /dev/null and b/handbook/static/img/net71.png differ diff --git a/handbook/static/img/net72.png b/handbook/static/img/net72.png new file mode 100644 index 0000000000000000000000000000000000000000..d6db76e300279be92a8e33de51d5347f31791a8a Binary files /dev/null and b/handbook/static/img/net72.png differ diff --git a/handbook/static/img/net73.png b/handbook/static/img/net73.png new file mode 100644 index 0000000000000000000000000000000000000000..fbdcb7beba15c778e9594e82662182e018ecfcdf Binary files /dev/null and b/handbook/static/img/net73.png differ diff --git a/handbook/static/img/net74.png b/handbook/static/img/net74.png new file mode 100644 index 0000000000000000000000000000000000000000..5d7e904c6aa957e8a67b1caf2bac84ae31aa0446 Binary files /dev/null and b/handbook/static/img/net74.png differ diff --git a/handbook/static/img/net75.png b/handbook/static/img/net75.png new file mode 100644 index 0000000000000000000000000000000000000000..e05d73da669c874dfa03148c1bc141c7e110cd84 Binary files /dev/null and b/handbook/static/img/net75.png differ diff --git a/handbook/static/img/net81.png b/handbook/static/img/net81.png new file mode 100644 index 0000000000000000000000000000000000000000..036a9363809277f2621e97ca46efac7af4510bf0 Binary files /dev/null and b/handbook/static/img/net81.png differ diff --git a/handbook/static/img/net82.png b/handbook/static/img/net82.png new file mode 100644 index 0000000000000000000000000000000000000000..bf5e6d5e5e83c43582557048b0b6115699aaed95 Binary files /dev/null and b/handbook/static/img/net82.png differ diff --git a/handbook/static/img/netconf-drone.gif b/handbook/static/img/netconf-drone.gif new file mode 100644 index 0000000000000000000000000000000000000000..6906721ce56f54237e644509c328fa3c2926b70f Binary files /dev/null and b/handbook/static/img/netconf-drone.gif differ diff --git a/handbook/static/img/ng1.png b/handbook/static/img/ng1.png new file mode 100644 index 0000000000000000000000000000000000000000..53a31ec4a2cba14935ea58254b19b398a07ed18a Binary files /dev/null and b/handbook/static/img/ng1.png differ diff --git a/handbook/static/img/ng2.png b/handbook/static/img/ng2.png new file mode 100644 index 0000000000000000000000000000000000000000..58c9219f34d5a3509b8885f1e66884ee28a05cd1 Binary files /dev/null and b/handbook/static/img/ng2.png differ diff --git a/handbook/static/img/ng3.png b/handbook/static/img/ng3.png new file mode 100644 index 0000000000000000000000000000000000000000..286754ddb86ba79c32c740c014650d97d08a0dd7 Binary files /dev/null and b/handbook/static/img/ng3.png differ diff --git a/handbook/static/img/ng4.png b/handbook/static/img/ng4.png new file mode 100644 index 0000000000000000000000000000000000000000..fa5c366c6ac7f7328b20bda26b6d07bdb4e5d0a5 Binary files /dev/null and b/handbook/static/img/ng4.png differ diff --git a/handbook/static/img/ng5.png b/handbook/static/img/ng5.png new file mode 100644 index 0000000000000000000000000000000000000000..4d98dd6db62cc7b94a691788cf219d7081200887 Binary files /dev/null and b/handbook/static/img/ng5.png differ diff --git a/handbook/static/img/ng6.png b/handbook/static/img/ng6.png new file mode 100644 index 0000000000000000000000000000000000000000..cedacfb7d246ba762d51f2effb1920a1ca6e4c9a Binary files /dev/null and b/handbook/static/img/ng6.png differ diff --git a/handbook/static/img/ng7.png b/handbook/static/img/ng7.png new file mode 100644 index 0000000000000000000000000000000000000000..2dd78588154e411e4e28301002e5c7f0b1000d0f Binary files /dev/null and b/handbook/static/img/ng7.png differ diff --git a/handbook/static/img/ng8.png b/handbook/static/img/ng8.png new file mode 100644 index 0000000000000000000000000000000000000000..d468c599e333ec9019e21983d65711d6ee6c123c Binary files /dev/null and b/handbook/static/img/ng8.png differ diff --git a/handbook/static/img/nrs.png b/handbook/static/img/nrs.png new file mode 100644 index 0000000000000000000000000000000000000000..d7b6f75c969dacce0c30d365f4a36cdb81288ba8 Binary files /dev/null and b/handbook/static/img/nrs.png differ diff --git a/handbook/static/img/pm2err.png b/handbook/static/img/pm2err.png new file mode 100644 index 0000000000000000000000000000000000000000..30fd3da1d7d714a115727f4fdebacab888669617 Binary files /dev/null and b/handbook/static/img/pm2err.png differ diff --git a/handbook/static/img/processon1.png b/handbook/static/img/processon1.png new file mode 100644 index 0000000000000000000000000000000000000000..488102ff7ec2095b999552b68ee7e2dee72d8ec8 Binary files /dev/null and b/handbook/static/img/processon1.png differ diff --git a/handbook/static/img/processon2.png b/handbook/static/img/processon2.png new file mode 100644 index 0000000000000000000000000000000000000000..cf851043bbfed1fd21299226d070e6237d2eb6a5 Binary files /dev/null and b/handbook/static/img/processon2.png differ diff --git a/handbook/static/img/readwrite.png b/handbook/static/img/readwrite.png new file mode 100644 index 0000000000000000000000000000000000000000..84e5d64901e71621aeb7cc947e259b2a6a052070 Binary files /dev/null and b/handbook/static/img/readwrite.png differ diff --git a/handbook/static/img/readwrite1.png b/handbook/static/img/readwrite1.png new file mode 100644 index 0000000000000000000000000000000000000000..045f7bc01604079749bb7029c4add6fcc44f95fd Binary files /dev/null and b/handbook/static/img/readwrite1.png differ diff --git a/handbook/static/img/readwrite2.png b/handbook/static/img/readwrite2.png new file mode 100644 index 0000000000000000000000000000000000000000..bf15bd12f84d13c78dd55302b967deebae58dd0c Binary files /dev/null and b/handbook/static/img/readwrite2.png differ diff --git a/handbook/static/img/rjs.jpg b/handbook/static/img/rjs.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9fa649d35cb508f63b5ad7d7389bc821614155a5 Binary files /dev/null and b/handbook/static/img/rjs.jpg differ diff --git a/handbook/static/img/rm1.png b/handbook/static/img/rm1.png new file mode 100644 index 0000000000000000000000000000000000000000..33011d5aeb77fc199d95aab2360a3b14b3e195e3 Binary files /dev/null and b/handbook/static/img/rm1.png differ diff --git a/handbook/static/img/rm2.png b/handbook/static/img/rm2.png new file mode 100644 index 0000000000000000000000000000000000000000..267dfc48c69ef25a559a5573fb8baba5697dbd87 Binary files /dev/null and b/handbook/static/img/rm2.png differ diff --git a/handbook/static/img/rm3.png b/handbook/static/img/rm3.png new file mode 100644 index 0000000000000000000000000000000000000000..0a9c4da36006dc2cd2229aed172c981dd861bc7b Binary files /dev/null and b/handbook/static/img/rm3.png differ diff --git a/handbook/static/img/rm4.png b/handbook/static/img/rm4.png new file mode 100644 index 0000000000000000000000000000000000000000..3b5f98ef015e15f3e888e57c1d20fbb9ad914d26 Binary files /dev/null and b/handbook/static/img/rm4.png differ diff --git a/handbook/static/img/rm5.png b/handbook/static/img/rm5.png new file mode 100644 index 0000000000000000000000000000000000000000..ad866292c229afc0e7223c33e548cf963f4472d2 Binary files /dev/null and b/handbook/static/img/rm5.png differ diff --git a/handbook/static/img/rm6.png b/handbook/static/img/rm6.png new file mode 100644 index 0000000000000000000000000000000000000000..206f3b0d2738c12e1b212b3b2e9370adcff939fa Binary files /dev/null and b/handbook/static/img/rm6.png differ diff --git a/handbook/static/img/rmn1.png b/handbook/static/img/rmn1.png new file mode 100644 index 0000000000000000000000000000000000000000..99b136c2f88fff3c1042fb297fb775f1261b06bf Binary files /dev/null and b/handbook/static/img/rmn1.png differ diff --git a/handbook/static/img/rmn2.png b/handbook/static/img/rmn2.png new file mode 100644 index 0000000000000000000000000000000000000000..6805b5102089ebc78b3363a5910888ec598691e2 Binary files /dev/null and b/handbook/static/img/rmn2.png differ diff --git a/handbook/static/img/rmn3.png b/handbook/static/img/rmn3.png new file mode 100644 index 0000000000000000000000000000000000000000..8d32cc9d0ed19e78bf60971911a9fac1a9a68487 Binary files /dev/null and b/handbook/static/img/rmn3.png differ diff --git a/handbook/static/img/rmn4.png b/handbook/static/img/rmn4.png new file mode 100644 index 0000000000000000000000000000000000000000..f07d5e68b283993e35d59b8bbcb6f16540a3dba6 Binary files /dev/null and b/handbook/static/img/rmn4.png differ diff --git a/handbook/static/img/rmn5.png b/handbook/static/img/rmn5.png new file mode 100644 index 0000000000000000000000000000000000000000..70da7d4713784421b08c4bdc379e318d52b1a1fe Binary files /dev/null and b/handbook/static/img/rmn5.png differ diff --git a/handbook/static/img/rmn6.png b/handbook/static/img/rmn6.png new file mode 100644 index 0000000000000000000000000000000000000000..818e5d0ec00af07c36f725aee738d104556e1a24 Binary files /dev/null and b/handbook/static/img/rmn6.png differ diff --git a/handbook/static/img/rust.png b/handbook/static/img/rust.png new file mode 100644 index 0000000000000000000000000000000000000000..bf2fdc4bfd7c2600f1e01ca8fc6aa584f31d551d Binary files /dev/null and b/handbook/static/img/rust.png differ diff --git a/handbook/static/img/saas1.png b/handbook/static/img/saas1.png new file mode 100644 index 0000000000000000000000000000000000000000..bdbebeb872feac313e5713a5c509586e3ec6373a Binary files /dev/null and b/handbook/static/img/saas1.png differ diff --git a/handbook/static/img/saas2.png b/handbook/static/img/saas2.png new file mode 100644 index 0000000000000000000000000000000000000000..8e33117db125be2a60d27218db507e35273a62cf Binary files /dev/null and b/handbook/static/img/saas2.png differ diff --git a/handbook/static/img/scdr.png b/handbook/static/img/scdr.png new file mode 100644 index 0000000000000000000000000000000000000000..75f90f901852573f107e6723067fdb5e278654b1 Binary files /dev/null and b/handbook/static/img/scdr.png differ diff --git a/handbook/static/img/sd20.png b/handbook/static/img/sd20.png new file mode 100644 index 0000000000000000000000000000000000000000..f7a373bb688ec5a2e803f59769dc6fecc9a1f88c Binary files /dev/null and b/handbook/static/img/sd20.png differ diff --git a/handbook/static/img/sf1.png b/handbook/static/img/sf1.png new file mode 100644 index 0000000000000000000000000000000000000000..4f2cebe58c2b6493b96be3cb49597692bd252d2f Binary files /dev/null and b/handbook/static/img/sf1.png differ diff --git a/handbook/static/img/sf2.png b/handbook/static/img/sf2.png new file mode 100644 index 0000000000000000000000000000000000000000..6b6db1be07be38da61c656e368e6ae858ceb067f Binary files /dev/null and b/handbook/static/img/sf2.png differ diff --git a/handbook/static/img/sf3.png b/handbook/static/img/sf3.png new file mode 100644 index 0000000000000000000000000000000000000000..cca0c88c628f204812a8ba2b469dda6b4b16a963 Binary files /dev/null and b/handbook/static/img/sf3.png differ diff --git a/handbook/static/img/sf30.png b/handbook/static/img/sf30.png new file mode 100644 index 0000000000000000000000000000000000000000..cd8180a463231d7b7cb54ebc301e1814264de405 Binary files /dev/null and b/handbook/static/img/sf30.png differ diff --git a/handbook/static/img/sf4.png b/handbook/static/img/sf4.png new file mode 100644 index 0000000000000000000000000000000000000000..e8d05ebba092e339dbe03c9a099c980584e29345 Binary files /dev/null and b/handbook/static/img/sf4.png differ diff --git a/handbook/static/img/sf5.png b/handbook/static/img/sf5.png new file mode 100644 index 0000000000000000000000000000000000000000..5e1c30ec0266b9cff4ca3cde1cf035508d1f0d4a Binary files /dev/null and b/handbook/static/img/sf5.png differ diff --git a/handbook/static/img/sf6.png b/handbook/static/img/sf6.png new file mode 100644 index 0000000000000000000000000000000000000000..a84a747190121bb2ef7fe1db875b3dae0e770973 Binary files /dev/null and b/handbook/static/img/sf6.png differ diff --git a/handbook/static/img/sg1.png b/handbook/static/img/sg1.png new file mode 100644 index 0000000000000000000000000000000000000000..e50214158eb000f781b81404ffcf4f1e515b0e8b Binary files /dev/null and b/handbook/static/img/sg1.png differ diff --git a/handbook/static/img/sg10.png b/handbook/static/img/sg10.png new file mode 100644 index 0000000000000000000000000000000000000000..b95b1721261a1d50f27322080b1e11106fea3c55 Binary files /dev/null and b/handbook/static/img/sg10.png differ diff --git a/handbook/static/img/sg11.png b/handbook/static/img/sg11.png new file mode 100644 index 0000000000000000000000000000000000000000..05e47a7a40dcf87a9e09f1580b98cfb881216e64 Binary files /dev/null and b/handbook/static/img/sg11.png differ diff --git a/handbook/static/img/sg12.png b/handbook/static/img/sg12.png new file mode 100644 index 0000000000000000000000000000000000000000..1891d03d1a935d99fd72befef60cde422c6df1ce Binary files /dev/null and b/handbook/static/img/sg12.png differ diff --git a/handbook/static/img/sg13.png b/handbook/static/img/sg13.png new file mode 100644 index 0000000000000000000000000000000000000000..071a2486ee8a638d6f116a2cead3ae266b2a160c Binary files /dev/null and b/handbook/static/img/sg13.png differ diff --git a/handbook/static/img/sg14.png b/handbook/static/img/sg14.png new file mode 100644 index 0000000000000000000000000000000000000000..4603679030f1c1dae4f8e8904691c4ab5d3aee92 Binary files /dev/null and b/handbook/static/img/sg14.png differ diff --git a/handbook/static/img/sg15.png b/handbook/static/img/sg15.png new file mode 100644 index 0000000000000000000000000000000000000000..8d18c2218b76bb90fceee2e21b47e0eca280de96 Binary files /dev/null and b/handbook/static/img/sg15.png differ diff --git a/handbook/static/img/sg16.png b/handbook/static/img/sg16.png new file mode 100644 index 0000000000000000000000000000000000000000..e3c04b29bb2ecdf285f4b580849d8b4d5c89065a Binary files /dev/null and b/handbook/static/img/sg16.png differ diff --git a/handbook/static/img/sg17.png b/handbook/static/img/sg17.png new file mode 100644 index 0000000000000000000000000000000000000000..6a9b4e370fbb3b89fe15002174d09bfa17887da5 Binary files /dev/null and b/handbook/static/img/sg17.png differ diff --git a/handbook/static/img/sg2.png b/handbook/static/img/sg2.png new file mode 100644 index 0000000000000000000000000000000000000000..28a3124da7a43f178d9c3230f0bdf47fb62e298d Binary files /dev/null and b/handbook/static/img/sg2.png differ diff --git a/handbook/static/img/sg20.png b/handbook/static/img/sg20.png new file mode 100644 index 0000000000000000000000000000000000000000..30d5a5907357b55a22afa44433c6802c90c15b60 Binary files /dev/null and b/handbook/static/img/sg20.png differ diff --git a/handbook/static/img/sg3.png b/handbook/static/img/sg3.png new file mode 100644 index 0000000000000000000000000000000000000000..39f9c486bda4c78e220af0e4ff9a2c73967d15dd Binary files /dev/null and b/handbook/static/img/sg3.png differ diff --git a/handbook/static/img/sg4.png b/handbook/static/img/sg4.png new file mode 100644 index 0000000000000000000000000000000000000000..08cf26253cae1c7310b5c57eeb20e84aad62c533 Binary files /dev/null and b/handbook/static/img/sg4.png differ diff --git a/handbook/static/img/sg5.png b/handbook/static/img/sg5.png new file mode 100644 index 0000000000000000000000000000000000000000..4242e9705311fcf782d337b3a6f1e489235a792f Binary files /dev/null and b/handbook/static/img/sg5.png differ diff --git a/handbook/static/img/sg6.png b/handbook/static/img/sg6.png new file mode 100644 index 0000000000000000000000000000000000000000..44f74d3483d337183b824db15e4216aa731bd353 Binary files /dev/null and b/handbook/static/img/sg6.png differ diff --git a/handbook/static/img/sg7.png b/handbook/static/img/sg7.png new file mode 100644 index 0000000000000000000000000000000000000000..97f100123ce0668362057734fc34c78892364b1e Binary files /dev/null and b/handbook/static/img/sg7.png differ diff --git a/handbook/static/img/sg8.png b/handbook/static/img/sg8.png new file mode 100644 index 0000000000000000000000000000000000000000..7079968d3969400f6f09edad3da2a72120a338e3 Binary files /dev/null and b/handbook/static/img/sg8.png differ diff --git a/handbook/static/img/sg9.png b/handbook/static/img/sg9.png new file mode 100644 index 0000000000000000000000000000000000000000..c38588ad8cd90709566b902c008c72d85eee9d18 Binary files /dev/null and b/handbook/static/img/sg9.png differ diff --git a/handbook/static/img/sjl1.png b/handbook/static/img/sjl1.png new file mode 100644 index 0000000000000000000000000000000000000000..daf68e0aeb9dabb14e60e587b999b8ec790e8fa8 Binary files /dev/null and b/handbook/static/img/sjl1.png differ diff --git a/handbook/static/img/sjl2.png b/handbook/static/img/sjl2.png new file mode 100644 index 0000000000000000000000000000000000000000..1aafa8e09294cce4bc81c45bd7fba4f1641e2dbb Binary files /dev/null and b/handbook/static/img/sjl2.png differ diff --git a/handbook/static/img/sjl3.png b/handbook/static/img/sjl3.png new file mode 100644 index 0000000000000000000000000000000000000000..89eab0e221639a94e081bfb71af3d4853f43a8f8 Binary files /dev/null and b/handbook/static/img/sjl3.png differ diff --git a/handbook/static/img/sjl4.png b/handbook/static/img/sjl4.png new file mode 100644 index 0000000000000000000000000000000000000000..5c850bd7442ca2bb52e88e0f4a896bb0a08cd8c5 Binary files /dev/null and b/handbook/static/img/sjl4.png differ diff --git a/handbook/static/img/sjl5.png b/handbook/static/img/sjl5.png new file mode 100644 index 0000000000000000000000000000000000000000..2789c2a34400ee135fca8d168f082d87fd24c373 Binary files /dev/null and b/handbook/static/img/sjl5.png differ diff --git a/handbook/static/img/sjl6.png b/handbook/static/img/sjl6.png new file mode 100644 index 0000000000000000000000000000000000000000..78383b108421bf75b8a1706995af5184505c20f1 Binary files /dev/null and b/handbook/static/img/sjl6.png differ diff --git a/handbook/static/img/sjyz1.gif b/handbook/static/img/sjyz1.gif new file mode 100644 index 0000000000000000000000000000000000000000..dc53cfd151a916f27b96cda451b8f8a07f781a62 Binary files /dev/null and b/handbook/static/img/sjyz1.gif differ diff --git a/handbook/static/img/sjyz2.gif b/handbook/static/img/sjyz2.gif new file mode 100644 index 0000000000000000000000000000000000000000..5b4df54c3d3f8e197e374a970ba5e586396d77f5 Binary files /dev/null and b/handbook/static/img/sjyz2.gif differ diff --git a/handbook/static/img/sjyz3.gif b/handbook/static/img/sjyz3.gif new file mode 100644 index 0000000000000000000000000000000000000000..ced5559c481be79021c409b98cfba3c987165676 Binary files /dev/null and b/handbook/static/img/sjyz3.gif differ diff --git a/handbook/static/img/sjyz4.png b/handbook/static/img/sjyz4.png new file mode 100644 index 0000000000000000000000000000000000000000..be638a6da706a8750dae708ccb01e4932283e0cb Binary files /dev/null and b/handbook/static/img/sjyz4.png differ diff --git a/handbook/static/img/sjyz5.png b/handbook/static/img/sjyz5.png new file mode 100644 index 0000000000000000000000000000000000000000..35ff9c16e31897561b67cd4f62eb5345670d5dda Binary files /dev/null and b/handbook/static/img/sjyz5.png differ diff --git a/handbook/static/img/sperr.png b/handbook/static/img/sperr.png new file mode 100644 index 0000000000000000000000000000000000000000..8107ee17aa78a133d8318484e0b4d6138043a550 Binary files /dev/null and b/handbook/static/img/sperr.png differ diff --git a/handbook/static/img/sperr2.png b/handbook/static/img/sperr2.png new file mode 100644 index 0000000000000000000000000000000000000000..7fdac9e82bd6dd37474d07e99123d4966d07d069 Binary files /dev/null and b/handbook/static/img/sperr2.png differ diff --git a/handbook/static/img/splitnamepz.png b/handbook/static/img/splitnamepz.png new file mode 100644 index 0000000000000000000000000000000000000000..a5f65e77224cedef008ae49ff46885ab3dbec8d8 Binary files /dev/null and b/handbook/static/img/splitnamepz.png differ diff --git a/handbook/static/img/support.png b/handbook/static/img/support.png new file mode 100644 index 0000000000000000000000000000000000000000..d93997a985526c95d471d29c6c424c5d4c481f08 Binary files /dev/null and b/handbook/static/img/support.png differ diff --git a/handbook/static/img/sw22.png b/handbook/static/img/sw22.png new file mode 100644 index 0000000000000000000000000000000000000000..3945eb15360b9ab14c5821fe1d3dd991d0199a47 Binary files /dev/null and b/handbook/static/img/sw22.png differ diff --git a/handbook/static/img/sw23.png b/handbook/static/img/sw23.png new file mode 100644 index 0000000000000000000000000000000000000000..58e45d701171c9d8472fd98301c571720302f3a2 Binary files /dev/null and b/handbook/static/img/sw23.png differ diff --git a/handbook/static/img/swagger1.png b/handbook/static/img/swagger1.png new file mode 100644 index 0000000000000000000000000000000000000000..89eada63f4d301f133bd49bc78debe3aa246bc34 Binary files /dev/null and b/handbook/static/img/swagger1.png differ diff --git a/handbook/static/img/swagger2.png b/handbook/static/img/swagger2.png new file mode 100644 index 0000000000000000000000000000000000000000..5cba3dcfbf877c14fec933b6bd3246d3fe78a720 Binary files /dev/null and b/handbook/static/img/swagger2.png differ diff --git a/handbook/static/img/swagger3.gif b/handbook/static/img/swagger3.gif new file mode 100644 index 0000000000000000000000000000000000000000..ee0c1ab2086a92568ed98d4acf115e543671d6aa Binary files /dev/null and b/handbook/static/img/swagger3.gif differ diff --git a/handbook/static/img/swagger4.png b/handbook/static/img/swagger4.png new file mode 100644 index 0000000000000000000000000000000000000000..2c225eb64e72f84d28d6df097b2f9164243020cf Binary files /dev/null and b/handbook/static/img/swagger4.png differ diff --git a/handbook/static/img/swagger5.png b/handbook/static/img/swagger5.png new file mode 100644 index 0000000000000000000000000000000000000000..2119bb7856e60d11af3a590958a9082c52ead22e Binary files /dev/null and b/handbook/static/img/swagger5.png differ diff --git a/handbook/static/img/swagger6.gif b/handbook/static/img/swagger6.gif new file mode 100644 index 0000000000000000000000000000000000000000..3222d9dc56ee7e0f6c7e12c9cd69bc6ae54f8923 Binary files /dev/null and b/handbook/static/img/swagger6.gif differ diff --git a/handbook/static/img/swagger7.png b/handbook/static/img/swagger7.png new file mode 100644 index 0000000000000000000000000000000000000000..19783331377d40ee816cd24c1ec4e9e23aa715b8 Binary files /dev/null and b/handbook/static/img/swagger7.png differ diff --git a/handbook/static/img/swagger8.gif b/handbook/static/img/swagger8.gif new file mode 100644 index 0000000000000000000000000000000000000000..e6ed6e5da2f7086c7ffa69c0faada645958c908d Binary files /dev/null and b/handbook/static/img/swagger8.gif differ diff --git a/handbook/static/img/swerror.png b/handbook/static/img/swerror.png new file mode 100644 index 0000000000000000000000000000000000000000..bd1ba094d7013a41ae8613b3b939195d404924f9 Binary files /dev/null and b/handbook/static/img/swerror.png differ diff --git a/handbook/static/img/swg1.png b/handbook/static/img/swg1.png new file mode 100644 index 0000000000000000000000000000000000000000..79204a5023275a0fa36dd3ecb8a303c90662cf4d Binary files /dev/null and b/handbook/static/img/swg1.png differ diff --git a/handbook/static/img/swg2.png b/handbook/static/img/swg2.png new file mode 100644 index 0000000000000000000000000000000000000000..2af82a30088d0fe658f312b6f4887b6e2f5b33ed Binary files /dev/null and b/handbook/static/img/swg2.png differ diff --git a/handbook/static/img/swgdl.png b/handbook/static/img/swgdl.png new file mode 100644 index 0000000000000000000000000000000000000000..2512fd9de4ee8f74c30308e9c10fe985fc8b68fc Binary files /dev/null and b/handbook/static/img/swgdl.png differ diff --git a/handbook/static/img/tag1.png b/handbook/static/img/tag1.png new file mode 100644 index 0000000000000000000000000000000000000000..963ffde16e21b6dc7d75f427b2f2284785ac6bde Binary files /dev/null and b/handbook/static/img/tag1.png differ diff --git a/handbook/static/img/tag2.png b/handbook/static/img/tag2.png new file mode 100644 index 0000000000000000000000000000000000000000..e89511797f3cb5e2501bcc43d0133d2c574412c4 Binary files /dev/null and b/handbook/static/img/tag2.png differ diff --git a/handbook/static/img/tip.png b/handbook/static/img/tip.png new file mode 100644 index 0000000000000000000000000000000000000000..2d4d9475b393728529e97d9043046948ecb2ec49 Binary files /dev/null and b/handbook/static/img/tip.png differ diff --git a/handbook/static/img/tm.png b/handbook/static/img/tm.png new file mode 100644 index 0000000000000000000000000000000000000000..54a8286e2594bcdd8a585e3c30c297c635095f02 Binary files /dev/null and b/handbook/static/img/tm.png differ diff --git a/handbook/static/img/tpflow.png b/handbook/static/img/tpflow.png new file mode 100644 index 0000000000000000000000000000000000000000..e7c9b6f95f939ecaaf80bc3fe5163d4cfaced9a6 Binary files /dev/null and b/handbook/static/img/tpflow.png differ diff --git a/handbook/static/img/ts1.png b/handbook/static/img/ts1.png new file mode 100644 index 0000000000000000000000000000000000000000..88519a65d36686bff804f1ddf204fa2ed1f668db Binary files /dev/null and b/handbook/static/img/ts1.png differ diff --git a/handbook/static/img/ts10.png b/handbook/static/img/ts10.png new file mode 100644 index 0000000000000000000000000000000000000000..f652096c52b650f98b0d8b0b04ccf4d76c368b2d Binary files /dev/null and b/handbook/static/img/ts10.png differ diff --git a/handbook/static/img/ts11.png b/handbook/static/img/ts11.png new file mode 100644 index 0000000000000000000000000000000000000000..6ba87979d8a7173f2ff1a75d33aa6b03a1bf6258 Binary files /dev/null and b/handbook/static/img/ts11.png differ diff --git a/handbook/static/img/ts12.png b/handbook/static/img/ts12.png new file mode 100644 index 0000000000000000000000000000000000000000..7b55dd191d78d63a899724cc8c90b2170c62c604 Binary files /dev/null and b/handbook/static/img/ts12.png differ diff --git a/handbook/static/img/ts13.png b/handbook/static/img/ts13.png new file mode 100644 index 0000000000000000000000000000000000000000..67e941764768ba63093906c9be8b186a26b30482 Binary files /dev/null and b/handbook/static/img/ts13.png differ diff --git a/handbook/static/img/ts14.png b/handbook/static/img/ts14.png new file mode 100644 index 0000000000000000000000000000000000000000..4b16c1accee0c9b73714ae4f129b7bdc20ca679d Binary files /dev/null and b/handbook/static/img/ts14.png differ diff --git a/handbook/static/img/ts2.png b/handbook/static/img/ts2.png new file mode 100644 index 0000000000000000000000000000000000000000..1694c5478401773e40882189e45cbb7644035b0d Binary files /dev/null and b/handbook/static/img/ts2.png differ diff --git a/handbook/static/img/ts3.png b/handbook/static/img/ts3.png new file mode 100644 index 0000000000000000000000000000000000000000..23418c11aeef845f372465d32fc6f380b03ffd03 Binary files /dev/null and b/handbook/static/img/ts3.png differ diff --git a/handbook/static/img/ts4.png b/handbook/static/img/ts4.png new file mode 100644 index 0000000000000000000000000000000000000000..5469ff71d5932c91bf217f614c16d89b3567e551 Binary files /dev/null and b/handbook/static/img/ts4.png differ diff --git a/handbook/static/img/ts5.png b/handbook/static/img/ts5.png new file mode 100644 index 0000000000000000000000000000000000000000..6e9c04730fde50d226274f0320f9172ad5a5a726 Binary files /dev/null and b/handbook/static/img/ts5.png differ diff --git a/handbook/static/img/ts6.png b/handbook/static/img/ts6.png new file mode 100644 index 0000000000000000000000000000000000000000..efa4533592d9066a01da9d1610c9460e42c554f1 Binary files /dev/null and b/handbook/static/img/ts6.png differ diff --git a/handbook/static/img/ts7.png b/handbook/static/img/ts7.png new file mode 100644 index 0000000000000000000000000000000000000000..767e7cc863dde50532ea56873392d564f3f25848 Binary files /dev/null and b/handbook/static/img/ts7.png differ diff --git a/handbook/static/img/ts8.png b/handbook/static/img/ts8.png new file mode 100644 index 0000000000000000000000000000000000000000..6a207b297d4be6e381ec4b5b3b7d0436459a2df1 Binary files /dev/null and b/handbook/static/img/ts8.png differ diff --git a/handbook/static/img/ts9.png b/handbook/static/img/ts9.png new file mode 100644 index 0000000000000000000000000000000000000000..3e84879584fd20d78588f6ab154ff5adf31464d1 Binary files /dev/null and b/handbook/static/img/ts9.png differ diff --git a/handbook/static/img/un1.png b/handbook/static/img/un1.png new file mode 100644 index 0000000000000000000000000000000000000000..2e43e2fa76c976eb3466bf91c8b383cb39f00f1a Binary files /dev/null and b/handbook/static/img/un1.png differ diff --git a/handbook/static/img/un3.png b/handbook/static/img/un3.png new file mode 100644 index 0000000000000000000000000000000000000000..8a7f3bafdfce5b68446bc9443d567ab7059c209b Binary files /dev/null and b/handbook/static/img/un3.png differ diff --git a/handbook/static/img/undraw_docusaurus_mountain.svg b/handbook/static/img/undraw_docusaurus_mountain.svg new file mode 100644 index 0000000000000000000000000000000000000000..431cef2f7fece86b135fe8fa5bae5b4ff168d9e9 --- /dev/null +++ b/handbook/static/img/undraw_docusaurus_mountain.svg @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/handbook/static/img/undraw_docusaurus_react.svg b/handbook/static/img/undraw_docusaurus_react.svg new file mode 100644 index 0000000000000000000000000000000000000000..e417050433381b4ca4d82ea00ce935167613e97f --- /dev/null +++ b/handbook/static/img/undraw_docusaurus_react.svg @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/handbook/static/img/undraw_docusaurus_tree.svg b/handbook/static/img/undraw_docusaurus_tree.svg new file mode 100644 index 0000000000000000000000000000000000000000..a05cc03dda90ff2852b222d01757cbb9f9cd1885 --- /dev/null +++ b/handbook/static/img/undraw_docusaurus_tree.svg @@ -0,0 +1 @@ +docu_tree \ No newline at end of file diff --git a/handbook/static/img/ut1.png b/handbook/static/img/ut1.png new file mode 100644 index 0000000000000000000000000000000000000000..6aa3ec3e9680213bd24a427599856d518711dd3d Binary files /dev/null and b/handbook/static/img/ut1.png differ diff --git a/handbook/static/img/ut10.png b/handbook/static/img/ut10.png new file mode 100644 index 0000000000000000000000000000000000000000..0780a541cb77d524f1f112c48813d0460fafc072 Binary files /dev/null and b/handbook/static/img/ut10.png differ diff --git a/handbook/static/img/ut11.png b/handbook/static/img/ut11.png new file mode 100644 index 0000000000000000000000000000000000000000..8e0437692c51d5e1a6d6cf499a4d0d1ad9387b01 Binary files /dev/null and b/handbook/static/img/ut11.png differ diff --git a/handbook/static/img/ut12.png b/handbook/static/img/ut12.png new file mode 100644 index 0000000000000000000000000000000000000000..fd6f39e29f1aebb9624c9150760a03999e401532 Binary files /dev/null and b/handbook/static/img/ut12.png differ diff --git a/handbook/static/img/ut13.png b/handbook/static/img/ut13.png new file mode 100644 index 0000000000000000000000000000000000000000..93edbed2244b3145987ad36f2edda173777487d2 Binary files /dev/null and b/handbook/static/img/ut13.png differ diff --git a/handbook/static/img/ut14.png b/handbook/static/img/ut14.png new file mode 100644 index 0000000000000000000000000000000000000000..85812a3718cd78a8663895b9d7f1d65aa49625ba Binary files /dev/null and b/handbook/static/img/ut14.png differ diff --git a/handbook/static/img/ut15.png b/handbook/static/img/ut15.png new file mode 100644 index 0000000000000000000000000000000000000000..978efdf4b9662ff1b979ecc4da86f00d8504bff4 Binary files /dev/null and b/handbook/static/img/ut15.png differ diff --git a/handbook/static/img/ut16.png b/handbook/static/img/ut16.png new file mode 100644 index 0000000000000000000000000000000000000000..c2e8d00d36e97ec7e8bda68d2f9532c4883dda1f Binary files /dev/null and b/handbook/static/img/ut16.png differ diff --git a/handbook/static/img/ut2.png b/handbook/static/img/ut2.png new file mode 100644 index 0000000000000000000000000000000000000000..95960522837920cc62e4e1fa11bb84024742c10f Binary files /dev/null and b/handbook/static/img/ut2.png differ diff --git a/handbook/static/img/ut3.png b/handbook/static/img/ut3.png new file mode 100644 index 0000000000000000000000000000000000000000..fdd6b49c1c01570acdbde679328e0dcc4f813bb8 Binary files /dev/null and b/handbook/static/img/ut3.png differ diff --git a/handbook/static/img/ut4.png b/handbook/static/img/ut4.png new file mode 100644 index 0000000000000000000000000000000000000000..78a343c0b9b5108df923f11a6cfe5d27a1794364 Binary files /dev/null and b/handbook/static/img/ut4.png differ diff --git a/handbook/static/img/ut5.png b/handbook/static/img/ut5.png new file mode 100644 index 0000000000000000000000000000000000000000..ece7282cea309d00bffb85d07b015fe482cf4d58 Binary files /dev/null and b/handbook/static/img/ut5.png differ diff --git a/handbook/static/img/ut6.png b/handbook/static/img/ut6.png new file mode 100644 index 0000000000000000000000000000000000000000..441d37ceeec88277e54fe20ba754fd3c7b7a1fc1 Binary files /dev/null and b/handbook/static/img/ut6.png differ diff --git a/handbook/static/img/ut7.png b/handbook/static/img/ut7.png new file mode 100644 index 0000000000000000000000000000000000000000..59272a901ea0d5d780cdc42c9d8026f4bd44fa65 Binary files /dev/null and b/handbook/static/img/ut7.png differ diff --git a/handbook/static/img/ut8.png b/handbook/static/img/ut8.png new file mode 100644 index 0000000000000000000000000000000000000000..a3b3e483376091414511bc276b7b06917e5cf621 Binary files /dev/null and b/handbook/static/img/ut8.png differ diff --git a/handbook/static/img/ut9.png b/handbook/static/img/ut9.png new file mode 100644 index 0000000000000000000000000000000000000000..f198535b6441a7b3f8eb8c1a1244b26048dcf7d2 Binary files /dev/null and b/handbook/static/img/ut9.png differ diff --git a/handbook/static/img/versionpz.png b/handbook/static/img/versionpz.png new file mode 100644 index 0000000000000000000000000000000000000000..ce4eb3fd791b999fc5f253ab3e8a59e194923383 Binary files /dev/null and b/handbook/static/img/versionpz.png differ diff --git a/handbook/static/img/vip1.jpeg b/handbook/static/img/vip1.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..b9d59ed21d2106747848461be86ff16d27cf1a1a Binary files /dev/null and b/handbook/static/img/vip1.jpeg differ diff --git a/handbook/static/img/vip10.jpeg b/handbook/static/img/vip10.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..c805475bccce6b9e7116841fd1b3a6f02d430249 Binary files /dev/null and b/handbook/static/img/vip10.jpeg differ diff --git a/handbook/static/img/vip2.jpeg b/handbook/static/img/vip2.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..20a061adb3c32768e95ef689e6e1a23195e66a1b Binary files /dev/null and b/handbook/static/img/vip2.jpeg differ diff --git a/handbook/static/img/vip3.jpeg b/handbook/static/img/vip3.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..cc6851a9d21e5a0b65f0aedae6371f1142bb3102 Binary files /dev/null and b/handbook/static/img/vip3.jpeg differ diff --git a/handbook/static/img/vip4.jpeg b/handbook/static/img/vip4.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..d9b2435b4c4097bac7dd68f428c7de0932423ba5 Binary files /dev/null and b/handbook/static/img/vip4.jpeg differ diff --git a/handbook/static/img/vip5.jpeg b/handbook/static/img/vip5.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..6ff6242f8d4324564ee28bd132744a62c3f08d03 Binary files /dev/null and b/handbook/static/img/vip5.jpeg differ diff --git a/handbook/static/img/vip6.jpeg b/handbook/static/img/vip6.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..20223d35872ddf64b1304cec23399c47b82f67a7 Binary files /dev/null and b/handbook/static/img/vip6.jpeg differ diff --git a/handbook/static/img/vip7.jpeg b/handbook/static/img/vip7.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..fcd8789e142e1040790d20a3b70142757c3bbaa9 Binary files /dev/null and b/handbook/static/img/vip7.jpeg differ diff --git a/handbook/static/img/vip8.jpeg b/handbook/static/img/vip8.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..aebb5f55fcbf5cc3fd2eb401d23cbaa74e49ffb8 Binary files /dev/null and b/handbook/static/img/vip8.jpeg differ diff --git a/handbook/static/img/vip9.jpeg b/handbook/static/img/vip9.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..7079571864a27da0dbb436f296a8b6a7a036f199 Binary files /dev/null and b/handbook/static/img/vip9.jpeg differ diff --git a/handbook/static/img/vr1.png b/handbook/static/img/vr1.png new file mode 100644 index 0000000000000000000000000000000000000000..b16cd9ef72159ceccb7d021a2bfbcd1c346c9e78 Binary files /dev/null and b/handbook/static/img/vr1.png differ diff --git a/handbook/static/img/vr2.png b/handbook/static/img/vr2.png new file mode 100644 index 0000000000000000000000000000000000000000..9ee87889dbbe95c4b4712eaf1cdf830b7c9cf5bd Binary files /dev/null and b/handbook/static/img/vr2.png differ diff --git a/handbook/static/img/vr3.png b/handbook/static/img/vr3.png new file mode 100644 index 0000000000000000000000000000000000000000..ca93ef13add576f9645bb267f0a1d757e638c5e3 Binary files /dev/null and b/handbook/static/img/vr3.png differ diff --git a/handbook/static/img/vr4.png b/handbook/static/img/vr4.png new file mode 100644 index 0000000000000000000000000000000000000000..dca318c63b8945f0f4d92d3e390042fd6ba21f26 Binary files /dev/null and b/handbook/static/img/vr4.png differ diff --git a/handbook/static/img/vs1.png b/handbook/static/img/vs1.png new file mode 100644 index 0000000000000000000000000000000000000000..b2afd2931d9fbdf99f88039cb5f8ab83e061b7d5 Binary files /dev/null and b/handbook/static/img/vs1.png differ diff --git a/handbook/static/img/vs10.png b/handbook/static/img/vs10.png new file mode 100644 index 0000000000000000000000000000000000000000..3aed57a4cc5ba39f71ccccaff929eee673a78085 Binary files /dev/null and b/handbook/static/img/vs10.png differ diff --git a/handbook/static/img/vs2.png b/handbook/static/img/vs2.png new file mode 100644 index 0000000000000000000000000000000000000000..6db85240993cf1ab545befee1fd48c3f0740cfb6 Binary files /dev/null and b/handbook/static/img/vs2.png differ diff --git a/handbook/static/img/vs3.png b/handbook/static/img/vs3.png new file mode 100644 index 0000000000000000000000000000000000000000..ab08a8e2745461edb26d524447f5c6ba178f77c5 Binary files /dev/null and b/handbook/static/img/vs3.png differ diff --git a/handbook/static/img/vs4.png b/handbook/static/img/vs4.png new file mode 100644 index 0000000000000000000000000000000000000000..b9503acbda7facc703e97e9588ad37610694e0e0 Binary files /dev/null and b/handbook/static/img/vs4.png differ diff --git a/handbook/static/img/vs5.png b/handbook/static/img/vs5.png new file mode 100644 index 0000000000000000000000000000000000000000..67d25f8d73b7ca90e441405b119252f5589646f8 Binary files /dev/null and b/handbook/static/img/vs5.png differ diff --git a/handbook/static/img/vs6.png b/handbook/static/img/vs6.png new file mode 100644 index 0000000000000000000000000000000000000000..59b10493103dd14874b85984995c36e7e0bca166 Binary files /dev/null and b/handbook/static/img/vs6.png differ diff --git a/handbook/static/img/vs7.png b/handbook/static/img/vs7.png new file mode 100644 index 0000000000000000000000000000000000000000..6697063d6489085455c3767b73cdfd3527a38961 Binary files /dev/null and b/handbook/static/img/vs7.png differ diff --git a/handbook/static/img/vs8.png b/handbook/static/img/vs8.png new file mode 100644 index 0000000000000000000000000000000000000000..9dce61f117bf47df389056a80f6c6b8bbdb4f3c1 Binary files /dev/null and b/handbook/static/img/vs8.png differ diff --git a/handbook/static/img/vs9.png b/handbook/static/img/vs9.png new file mode 100644 index 0000000000000000000000000000000000000000..c0046eb1d198e5ffedf36dcf21c7b72d6d17755f Binary files /dev/null and b/handbook/static/img/vs9.png differ diff --git a/handbook/static/img/wcrl.png b/handbook/static/img/wcrl.png new file mode 100644 index 0000000000000000000000000000000000000000..12c4dff8ba63629a18c88f7289fdcd9e19e25595 Binary files /dev/null and b/handbook/static/img/wcrl.png differ diff --git a/handbook/static/img/weishen.jpg b/handbook/static/img/weishen.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d3652e2b7c9fbea970fc1f21bbab71a7c392e0dd Binary files /dev/null and b/handbook/static/img/weishen.jpg differ diff --git a/handbook/static/img/weixin_qrcode.jpg b/handbook/static/img/weixin_qrcode.jpg new file mode 100644 index 0000000000000000000000000000000000000000..367ad96e8fcbe33f381b50abe18cba57a258e695 Binary files /dev/null and b/handbook/static/img/weixin_qrcode.jpg differ diff --git a/handbook/static/img/wk.png b/handbook/static/img/wk.png new file mode 100644 index 0000000000000000000000000000000000000000..fc2b16c8543ae686ba9321e94776b918c13ea916 Binary files /dev/null and b/handbook/static/img/wk.png differ diff --git a/handbook/static/img/xncs.png b/handbook/static/img/xncs.png new file mode 100644 index 0000000000000000000000000000000000000000..cc4ffad2456b13f8efcad8bcf8d80e00cc546b1b Binary files /dev/null and b/handbook/static/img/xncs.png differ diff --git a/handbook/static/img/xxyd.jpeg b/handbook/static/img/xxyd.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..38e76539c4333b4ac42eaca434657846de9c4a0a Binary files /dev/null and b/handbook/static/img/xxyd.jpeg differ diff --git a/handbook/static/img/xxyd2.jpeg b/handbook/static/img/xxyd2.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..78c3d65fbc0784a8f16a8648b2a7414c58b3b79d Binary files /dev/null and b/handbook/static/img/xxyd2.jpeg differ diff --git a/handbook/static/img/yhyc1.gif b/handbook/static/img/yhyc1.gif new file mode 100644 index 0000000000000000000000000000000000000000..7e2ce122ea57db8c604bc03a1bc475a794d7fe70 Binary files /dev/null and b/handbook/static/img/yhyc1.gif differ diff --git a/handbook/static/img/yhyc2.png b/handbook/static/img/yhyc2.png new file mode 100644 index 0000000000000000000000000000000000000000..b27b5f8784f35060c19223e50982931f946b9a76 Binary files /dev/null and b/handbook/static/img/yhyc2.png differ diff --git a/handbook/static/img/yhyc3.gif b/handbook/static/img/yhyc3.gif new file mode 100644 index 0000000000000000000000000000000000000000..e7d639334931f192a0c27a0923ed952462d878ca Binary files /dev/null and b/handbook/static/img/yhyc3.gif differ diff --git a/handbook/static/img/yhyc4.png b/handbook/static/img/yhyc4.png new file mode 100644 index 0000000000000000000000000000000000000000..c9d115da46240b8295fe0482d7d23bf58789d9a7 Binary files /dev/null and b/handbook/static/img/yhyc4.png differ diff --git a/handbook/static/img/yhyc5.png b/handbook/static/img/yhyc5.png new file mode 100644 index 0000000000000000000000000000000000000000..9bc44a53cc5fec5d51f5b21374ebe8d9d91cadc6 Binary files /dev/null and b/handbook/static/img/yhyc5.png differ diff --git a/handbook/static/img/zdywc.png b/handbook/static/img/zdywc.png new file mode 100644 index 0000000000000000000000000000000000000000..7e25c521b94319ed19bb5f7bf8c7ead99c67a225 Binary files /dev/null and b/handbook/static/img/zdywc.png differ diff --git a/handbook/static/img/zjj1.png b/handbook/static/img/zjj1.png new file mode 100644 index 0000000000000000000000000000000000000000..2ab6ecb203612016587d59cc04e5a90e5a15530f Binary files /dev/null and b/handbook/static/img/zjj1.png differ diff --git a/handbook/yarn.lock b/handbook/yarn.lock new file mode 100644 index 0000000000000000000000000000000000000000..e9ff82a3a136b04ab722ac0e9a78e184989d56b0 --- /dev/null +++ b/handbook/yarn.lock @@ -0,0 +1,9090 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@algolia/autocomplete-core@1.9.3": + version "1.9.3" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-core/-/autocomplete-core-1.9.3.tgz#1d56482a768c33aae0868c8533049e02e8961be7" + integrity sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw== + dependencies: + "@algolia/autocomplete-plugin-algolia-insights" "1.9.3" + "@algolia/autocomplete-shared" "1.9.3" + +"@algolia/autocomplete-plugin-algolia-insights@1.9.3": + version "1.9.3" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.9.3.tgz#9b7f8641052c8ead6d66c1623d444cbe19dde587" + integrity sha512-a/yTUkcO/Vyy+JffmAnTWbr4/90cLzw+CC3bRbhnULr/EM0fGNvM13oQQ14f2moLMcVDyAx/leczLlAOovhSZg== + dependencies: + "@algolia/autocomplete-shared" "1.9.3" + +"@algolia/autocomplete-preset-algolia@1.9.3": + version "1.9.3" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.9.3.tgz#64cca4a4304cfcad2cf730e83067e0c1b2f485da" + integrity sha512-d4qlt6YmrLMYy95n5TB52wtNDr6EgAIPH81dvvvW8UmuWRgxEtY0NJiPwl/h95JtG2vmRM804M0DSwMCNZlzRA== + dependencies: + "@algolia/autocomplete-shared" "1.9.3" + +"@algolia/autocomplete-shared@1.9.3": + version "1.9.3" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.9.3.tgz#2e22e830d36f0a9cf2c0ccd3c7f6d59435b77dfa" + integrity sha512-Wnm9E4Ye6Rl6sTTqjoymD+l8DjSTHsHboVRYrKgEt8Q7UHm9nYbqhN/i0fhUYA3OAEH7WA8x3jfpnmJm3rKvaQ== + +"@algolia/cache-browser-local-storage@4.20.0": + version "4.20.0" + resolved "https://registry.yarnpkg.com/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.20.0.tgz#357318242fc542ffce41d6eb5b4a9b402921b0bb" + integrity sha512-uujahcBt4DxduBTvYdwO3sBfHuJvJokiC3BP1+O70fglmE1ShkH8lpXqZBac1rrU3FnNYSUs4pL9lBdTKeRPOQ== + dependencies: + "@algolia/cache-common" "4.20.0" + +"@algolia/cache-common@4.20.0": + version "4.20.0" + resolved "https://registry.yarnpkg.com/@algolia/cache-common/-/cache-common-4.20.0.tgz#ec52230509fce891091ffd0d890618bcdc2fa20d" + integrity sha512-vCfxauaZutL3NImzB2G9LjLt36vKAckc6DhMp05An14kVo8F1Yofb6SIl6U3SaEz8pG2QOB9ptwM5c+zGevwIQ== + +"@algolia/cache-in-memory@4.20.0": + version "4.20.0" + resolved "https://registry.yarnpkg.com/@algolia/cache-in-memory/-/cache-in-memory-4.20.0.tgz#5f18d057bd6b3b075022df085c4f83bcca4e3e67" + integrity sha512-Wm9ak/IaacAZXS4mB3+qF/KCoVSBV6aLgIGFEtQtJwjv64g4ePMapORGmCyulCFwfePaRAtcaTbMcJF+voc/bg== + dependencies: + "@algolia/cache-common" "4.20.0" + +"@algolia/client-account@4.20.0": + version "4.20.0" + resolved "https://registry.yarnpkg.com/@algolia/client-account/-/client-account-4.20.0.tgz#23ce0b4cffd63100fb7c1aa1c67a4494de5bd645" + integrity sha512-GGToLQvrwo7am4zVkZTnKa72pheQeez/16sURDWm7Seyz+HUxKi3BM6fthVVPUEBhtJ0reyVtuK9ArmnaKl10Q== + dependencies: + "@algolia/client-common" "4.20.0" + "@algolia/client-search" "4.20.0" + "@algolia/transporter" "4.20.0" + +"@algolia/client-analytics@4.20.0": + version "4.20.0" + resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-4.20.0.tgz#0aa6bef35d3a41ac3991b3f46fcd0bf00d276fa9" + integrity sha512-EIr+PdFMOallRdBTHHdKI3CstslgLORQG7844Mq84ib5oVFRVASuuPmG4bXBgiDbcsMLUeOC6zRVJhv1KWI0ug== + dependencies: + "@algolia/client-common" "4.20.0" + "@algolia/client-search" "4.20.0" + "@algolia/requester-common" "4.20.0" + "@algolia/transporter" "4.20.0" + +"@algolia/client-common@4.20.0": + version "4.20.0" + resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-4.20.0.tgz#ca60f04466515548651c4371a742fbb8971790ef" + integrity sha512-P3WgMdEss915p+knMMSd/fwiHRHKvDu4DYRrCRaBrsfFw7EQHon+EbRSm4QisS9NYdxbS04kcvNoavVGthyfqQ== + dependencies: + "@algolia/requester-common" "4.20.0" + "@algolia/transporter" "4.20.0" + +"@algolia/client-personalization@4.20.0": + version "4.20.0" + resolved "https://registry.yarnpkg.com/@algolia/client-personalization/-/client-personalization-4.20.0.tgz#ca81308e8ad0db3b27458b78355f124f29657181" + integrity sha512-N9+zx0tWOQsLc3K4PVRDV8GUeOLAY0i445En79Pr3zWB+m67V+n/8w4Kw1C5LlbHDDJcyhMMIlqezh6BEk7xAQ== + dependencies: + "@algolia/client-common" "4.20.0" + "@algolia/requester-common" "4.20.0" + "@algolia/transporter" "4.20.0" + +"@algolia/client-search@4.20.0": + version "4.20.0" + resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-4.20.0.tgz#3bcce817ca6caedc835e0eaf6f580e02ee7c3e15" + integrity sha512-zgwqnMvhWLdpzKTpd3sGmMlr4c+iS7eyyLGiaO51zDZWGMkpgoNVmltkzdBwxOVXz0RsFMznIxB9zuarUv4TZg== + dependencies: + "@algolia/client-common" "4.20.0" + "@algolia/requester-common" "4.20.0" + "@algolia/transporter" "4.20.0" + +"@algolia/events@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@algolia/events/-/events-4.0.1.tgz#fd39e7477e7bc703d7f893b556f676c032af3950" + integrity sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ== + +"@algolia/logger-common@4.20.0": + version "4.20.0" + resolved "https://registry.yarnpkg.com/@algolia/logger-common/-/logger-common-4.20.0.tgz#f148ddf67e5d733a06213bebf7117cb8a651ab36" + integrity sha512-xouigCMB5WJYEwvoWW5XDv7Z9f0A8VoXJc3VKwlHJw/je+3p2RcDXfksLI4G4lIVncFUYMZx30tP/rsdlvvzHQ== + +"@algolia/logger-console@4.20.0": + version "4.20.0" + resolved "https://registry.yarnpkg.com/@algolia/logger-console/-/logger-console-4.20.0.tgz#ac443d27c4e94357f3063e675039cef0aa2de0a7" + integrity sha512-THlIGG1g/FS63z0StQqDhT6bprUczBI8wnLT3JWvfAQDZX5P6fCg7dG+pIrUBpDIHGszgkqYEqECaKKsdNKOUA== + dependencies: + "@algolia/logger-common" "4.20.0" + +"@algolia/requester-browser-xhr@4.20.0": + version "4.20.0" + resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.20.0.tgz#db16d0bdef018b93b51681d3f1e134aca4f64814" + integrity sha512-HbzoSjcjuUmYOkcHECkVTwAelmvTlgs48N6Owt4FnTOQdwn0b8pdht9eMgishvk8+F8bal354nhx/xOoTfwiAw== + dependencies: + "@algolia/requester-common" "4.20.0" + +"@algolia/requester-common@4.20.0": + version "4.20.0" + resolved "https://registry.yarnpkg.com/@algolia/requester-common/-/requester-common-4.20.0.tgz#65694b2263a8712b4360fef18680528ffd435b5c" + integrity sha512-9h6ye6RY/BkfmeJp7Z8gyyeMrmmWsMOCRBXQDs4mZKKsyVlfIVICpcSibbeYcuUdurLhIlrOUkH3rQEgZzonng== + +"@algolia/requester-node-http@4.20.0": + version "4.20.0" + resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-4.20.0.tgz#b52b182b52b0b16dec4070832267d484a6b1d5bb" + integrity sha512-ocJ66L60ABSSTRFnCHIEZpNHv6qTxsBwJEPfYaSBsLQodm0F9ptvalFkHMpvj5DfE22oZrcrLbOYM2bdPJRHng== + dependencies: + "@algolia/requester-common" "4.20.0" + +"@algolia/transporter@4.20.0": + version "4.20.0" + resolved "https://registry.yarnpkg.com/@algolia/transporter/-/transporter-4.20.0.tgz#7e5b24333d7cc9a926b2f6a249f87c2889b944a9" + integrity sha512-Lsii1pGWOAISbzeyuf+r/GPhvHMPHSPrTDWNcIzOE1SG1inlJHICaVe2ikuoRjcpgxZNU54Jl+if15SUCsaTUg== + dependencies: + "@algolia/cache-common" "4.20.0" + "@algolia/logger-common" "4.20.0" + "@algolia/requester-common" "4.20.0" + +"@ampproject/remapping@^2.2.0": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" + integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.22.13", "@babel/code-frame@^7.8.3": + version "7.22.13" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" + integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== + dependencies: + "@babel/highlight" "^7.22.13" + chalk "^2.4.2" + +"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.22.9", "@babel/compat-data@^7.23.2": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.2.tgz#6a12ced93455827037bfb5ed8492820d60fc32cc" + integrity sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ== + +"@babel/core@^7.19.6", "@babel/core@^7.21.3", "@babel/core@^7.22.9": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.2.tgz#ed10df0d580fff67c5f3ee70fd22e2e4c90a9f94" + integrity sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.22.13" + "@babel/generator" "^7.23.0" + "@babel/helper-compilation-targets" "^7.22.15" + "@babel/helper-module-transforms" "^7.23.0" + "@babel/helpers" "^7.23.2" + "@babel/parser" "^7.23.0" + "@babel/template" "^7.22.15" + "@babel/traverse" "^7.23.2" + "@babel/types" "^7.23.0" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.22.9", "@babel/generator@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420" + integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g== + dependencies: + "@babel/types" "^7.23.0" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + +"@babel/helper-annotate-as-pure@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882" + integrity sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-builder-binary-assignment-operator-visitor@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz#5426b109cf3ad47b91120f8328d8ab1be8b0b956" + integrity sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw== + dependencies: + "@babel/types" "^7.22.15" + +"@babel/helper-compilation-targets@^7.22.15", "@babel/helper-compilation-targets@^7.22.5", "@babel/helper-compilation-targets@^7.22.6": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz#0698fc44551a26cf29f18d4662d5bf545a6cfc52" + integrity sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw== + dependencies: + "@babel/compat-data" "^7.22.9" + "@babel/helper-validator-option" "^7.22.15" + browserslist "^4.21.9" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-create-class-features-plugin@^7.22.11", "@babel/helper-create-class-features-plugin@^7.22.15", "@babel/helper-create-class-features-plugin@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz#97a61b385e57fe458496fad19f8e63b63c867de4" + integrity sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-member-expression-to-functions" "^7.22.15" + "@babel/helper-optimise-call-expression" "^7.22.5" + "@babel/helper-replace-supers" "^7.22.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + semver "^6.3.1" + +"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz#5ee90093914ea09639b01c711db0d6775e558be1" + integrity sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + regexpu-core "^5.3.1" + semver "^6.3.1" + +"@babel/helper-define-polyfill-provider@^0.4.3": + version "0.4.3" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.3.tgz#a71c10f7146d809f4a256c373f462d9bba8cf6ba" + integrity sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug== + dependencies: + "@babel/helper-compilation-targets" "^7.22.6" + "@babel/helper-plugin-utils" "^7.22.5" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" + +"@babel/helper-environment-visitor@^7.22.20", "@babel/helper-environment-visitor@^7.22.5": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" + integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== + +"@babel/helper-function-name@^7.22.5", "@babel/helper-function-name@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" + integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== + dependencies: + "@babel/template" "^7.22.15" + "@babel/types" "^7.23.0" + +"@babel/helper-hoist-variables@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" + integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-member-expression-to-functions@^7.22.15": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz#9263e88cc5e41d39ec18c9a3e0eced59a3e7d366" + integrity sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA== + dependencies: + "@babel/types" "^7.23.0" + +"@babel/helper-module-imports@^7.22.15", "@babel/helper-module-imports@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0" + integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w== + dependencies: + "@babel/types" "^7.22.15" + +"@babel/helper-module-transforms@^7.22.5", "@babel/helper-module-transforms@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz#3ec246457f6c842c0aee62a01f60739906f7047e" + integrity sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-module-imports" "^7.22.15" + "@babel/helper-simple-access" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/helper-validator-identifier" "^7.22.20" + +"@babel/helper-optimise-call-expression@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz#f21531a9ccbff644fdd156b4077c16ff0c3f609e" + integrity sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" + integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== + +"@babel/helper-remap-async-to-generator@^7.22.20", "@babel/helper-remap-async-to-generator@^7.22.5": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz#7b68e1cb4fa964d2996fd063723fb48eca8498e0" + integrity sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-wrap-function" "^7.22.20" + +"@babel/helper-replace-supers@^7.22.5", "@babel/helper-replace-supers@^7.22.9": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz#e37d367123ca98fe455a9887734ed2e16eb7a793" + integrity sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-member-expression-to-functions" "^7.22.15" + "@babel/helper-optimise-call-expression" "^7.22.5" + +"@babel/helper-simple-access@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz#4938357dc7d782b80ed6dbb03a0fba3d22b1d5de" + integrity sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-skip-transparent-expression-wrappers@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz#007f15240b5751c537c40e77abb4e89eeaaa8847" + integrity sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-split-export-declaration@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" + integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-string-parser@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" + integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== + +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== + +"@babel/helper-validator-option@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz#694c30dfa1d09a6534cdfcafbe56789d36aba040" + integrity sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA== + +"@babel/helper-wrap-function@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz#15352b0b9bfb10fc9c76f79f6342c00e3411a569" + integrity sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw== + dependencies: + "@babel/helper-function-name" "^7.22.5" + "@babel/template" "^7.22.15" + "@babel/types" "^7.22.19" + +"@babel/helpers@^7.23.2": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.2.tgz#2832549a6e37d484286e15ba36a5330483cac767" + integrity sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ== + dependencies: + "@babel/template" "^7.22.15" + "@babel/traverse" "^7.23.2" + "@babel/types" "^7.23.0" + +"@babel/highlight@^7.22.13": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54" + integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + +"@babel/parser@^7.22.15", "@babel/parser@^7.22.7", "@babel/parser@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719" + integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw== + +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.15.tgz#02dc8a03f613ed5fdc29fb2f728397c78146c962" + integrity sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.15.tgz#2aeb91d337d4e1a1e7ce85b76a37f5301781200f" + integrity sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/plugin-transform-optional-chaining" "^7.22.15" + +"@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": + version "7.21.0-placeholder-for-preset-env.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" + integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-class-static-block@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" + integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-dynamic-import@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" + integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-export-namespace-from@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" + integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + +"@babel/plugin-syntax-import-assertions@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz#07d252e2aa0bc6125567f742cd58619cb14dce98" + integrity sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-syntax-import-attributes@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz#ab840248d834410b829f569f5262b9e517555ecb" + integrity sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-syntax-import-meta@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-jsx@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz#a6b68e84fb76e759fc3b93e901876ffabbe1d918" + integrity sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-private-property-in-object@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" + integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-top-level-await@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-typescript@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz#aac8d383b062c5072c647a31ef990c1d0af90272" + integrity sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-syntax-unicode-sets-regex@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" + integrity sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-arrow-functions@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz#e5ba566d0c58a5b2ba2a8b795450641950b71958" + integrity sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-async-generator-functions@^7.23.2": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.2.tgz#054afe290d64c6f576f371ccc321772c8ea87ebb" + integrity sha512-BBYVGxbDVHfoeXbOwcagAkOQAm9NxoTdMGfTqghu1GrvadSaw6iW3Je6IcL5PNOw8VwjxqBECXy50/iCQSY/lQ== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-remap-async-to-generator" "^7.22.20" + "@babel/plugin-syntax-async-generators" "^7.8.4" + +"@babel/plugin-transform-async-to-generator@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz#c7a85f44e46f8952f6d27fe57c2ed3cc084c3775" + integrity sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ== + dependencies: + "@babel/helper-module-imports" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-remap-async-to-generator" "^7.22.5" + +"@babel/plugin-transform-block-scoped-functions@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz#27978075bfaeb9fa586d3cb63a3d30c1de580024" + integrity sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-block-scoping@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.0.tgz#8744d02c6c264d82e1a4bc5d2d501fd8aff6f022" + integrity sha512-cOsrbmIOXmf+5YbL99/S49Y3j46k/T16b9ml8bm9lP6N9US5iQ2yBK7gpui1pg0V/WMcXdkfKbTb7HXq9u+v4g== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-class-properties@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz#97a56e31ad8c9dc06a0b3710ce7803d5a48cca77" + integrity sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-class-static-block@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz#dc8cc6e498f55692ac6b4b89e56d87cec766c974" + integrity sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.22.11" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + +"@babel/plugin-transform-classes@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.15.tgz#aaf4753aee262a232bbc95451b4bdf9599c65a0b" + integrity sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-compilation-targets" "^7.22.15" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-optimise-call-expression" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-replace-supers" "^7.22.9" + "@babel/helper-split-export-declaration" "^7.22.6" + globals "^11.1.0" + +"@babel/plugin-transform-computed-properties@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz#cd1e994bf9f316bd1c2dafcd02063ec261bb3869" + integrity sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/template" "^7.22.5" + +"@babel/plugin-transform-destructuring@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.0.tgz#6447aa686be48b32eaf65a73e0e2c0bd010a266c" + integrity sha512-vaMdgNXFkYrB+8lbgniSYWHsgqK5gjaMNcc84bMIOMRLH0L9AqYq3hwMdvnyqj1OPqea8UtjPEuS/DCenah1wg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-dotall-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz#dbb4f0e45766eb544e193fb00e65a1dd3b2a4165" + integrity sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-duplicate-keys@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz#b6e6428d9416f5f0bba19c70d1e6e7e0b88ab285" + integrity sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-dynamic-import@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz#2c7722d2a5c01839eaf31518c6ff96d408e447aa" + integrity sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + +"@babel/plugin-transform-exponentiation-operator@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz#402432ad544a1f9a480da865fda26be653e48f6a" + integrity sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g== + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-export-namespace-from@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz#b3c84c8f19880b6c7440108f8929caf6056db26c" + integrity sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + +"@babel/plugin-transform-for-of@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.15.tgz#f64b4ccc3a4f131a996388fae7680b472b306b29" + integrity sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-function-name@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz#935189af68b01898e0d6d99658db6b164205c143" + integrity sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg== + dependencies: + "@babel/helper-compilation-targets" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-json-strings@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz#689a34e1eed1928a40954e37f74509f48af67835" + integrity sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-json-strings" "^7.8.3" + +"@babel/plugin-transform-literals@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz#e9341f4b5a167952576e23db8d435849b1dd7920" + integrity sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-logical-assignment-operators@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz#24c522a61688bde045b7d9bc3c2597a4d948fc9c" + integrity sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + +"@babel/plugin-transform-member-expression-literals@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz#4fcc9050eded981a468347dd374539ed3e058def" + integrity sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-modules-amd@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.0.tgz#05b2bc43373faa6d30ca89214731f76f966f3b88" + integrity sha512-xWT5gefv2HGSm4QHtgc1sYPbseOyf+FFDo2JbpE25GWl5BqTGO9IMwTYJRoIdjsF85GE+VegHxSCUt5EvoYTAw== + dependencies: + "@babel/helper-module-transforms" "^7.23.0" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-modules-commonjs@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.0.tgz#b3dba4757133b2762c00f4f94590cf6d52602481" + integrity sha512-32Xzss14/UVc7k9g775yMIvkVK8xwKE0DPdP5JTapr3+Z9w4tzeOuLNY6BXDQR6BdnzIlXnCGAzsk/ICHBLVWQ== + dependencies: + "@babel/helper-module-transforms" "^7.23.0" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-simple-access" "^7.22.5" + +"@babel/plugin-transform-modules-systemjs@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.0.tgz#77591e126f3ff4132a40595a6cccd00a6b60d160" + integrity sha512-qBej6ctXZD2f+DhlOC9yO47yEYgUh5CZNz/aBoH4j/3NOlRfJXJbY7xDQCqQVf9KbrqGzIWER1f23doHGrIHFg== + dependencies: + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-module-transforms" "^7.23.0" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.20" + +"@babel/plugin-transform-modules-umd@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz#4694ae40a87b1745e3775b6a7fe96400315d4f98" + integrity sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ== + dependencies: + "@babel/helper-module-transforms" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz#67fe18ee8ce02d57c855185e27e3dc959b2e991f" + integrity sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-new-target@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz#1b248acea54ce44ea06dfd37247ba089fcf9758d" + integrity sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-nullish-coalescing-operator@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz#debef6c8ba795f5ac67cd861a81b744c5d38d9fc" + integrity sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + +"@babel/plugin-transform-numeric-separator@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz#498d77dc45a6c6db74bb829c02a01c1d719cbfbd" + integrity sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + +"@babel/plugin-transform-object-rest-spread@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.15.tgz#21a95db166be59b91cde48775310c0df6e1da56f" + integrity sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q== + dependencies: + "@babel/compat-data" "^7.22.9" + "@babel/helper-compilation-targets" "^7.22.15" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-transform-parameters" "^7.22.15" + +"@babel/plugin-transform-object-super@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz#794a8d2fcb5d0835af722173c1a9d704f44e218c" + integrity sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-replace-supers" "^7.22.5" + +"@babel/plugin-transform-optional-catch-binding@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz#461cc4f578a127bb055527b3e77404cad38c08e0" + integrity sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + +"@babel/plugin-transform-optional-chaining@^7.22.15", "@babel/plugin-transform-optional-chaining@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.0.tgz#73ff5fc1cf98f542f09f29c0631647d8ad0be158" + integrity sha512-sBBGXbLJjxTzLBF5rFWaikMnOGOk/BmK6vVByIdEggZ7Vn6CvWXZyRkkLFK6WE0IF8jSliyOkUN6SScFgzCM0g== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + +"@babel/plugin-transform-parameters@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.15.tgz#719ca82a01d177af358df64a514d64c2e3edb114" + integrity sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-private-methods@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz#21c8af791f76674420a147ae62e9935d790f8722" + integrity sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-private-property-in-object@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz#ad45c4fc440e9cb84c718ed0906d96cf40f9a4e1" + integrity sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.22.11" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + +"@babel/plugin-transform-property-literals@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz#b5ddabd73a4f7f26cd0e20f5db48290b88732766" + integrity sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-react-constant-elements@^7.18.12", "@babel/plugin-transform-react-constant-elements@^7.21.3": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.22.5.tgz#6dfa7c1c37f7d7279e417ceddf5a04abb8bb9c29" + integrity sha512-BF5SXoO+nX3h5OhlN78XbbDrBOffv+AxPP2ENaJOVqjWCgBDeOY3WcaUcddutGSfoap+5NEQ/q/4I3WZIvgkXA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-react-display-name@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.22.5.tgz#3c4326f9fce31c7968d6cb9debcaf32d9e279a2b" + integrity sha512-PVk3WPYudRF5z4GKMEYUrLjPl38fJSKNaEOkFuoprioowGuWN6w2RKznuFNSlJx7pzzXXStPUnNSOEO0jL5EVw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-react-jsx-development@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz#e716b6edbef972a92165cd69d92f1255f7e73e87" + integrity sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A== + dependencies: + "@babel/plugin-transform-react-jsx" "^7.22.5" + +"@babel/plugin-transform-react-jsx@^7.22.15", "@babel/plugin-transform-react-jsx@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.22.15.tgz#7e6266d88705d7c49f11c98db8b9464531289cd6" + integrity sha512-oKckg2eZFa8771O/5vi7XeTvmM6+O9cxZu+kanTU7tD4sin5nO/G8jGJhq8Hvt2Z0kUoEDRayuZLaUlYl8QuGA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-module-imports" "^7.22.15" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-jsx" "^7.22.5" + "@babel/types" "^7.22.15" + +"@babel/plugin-transform-react-pure-annotations@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.22.5.tgz#1f58363eef6626d6fa517b95ac66fe94685e32c0" + integrity sha512-gP4k85wx09q+brArVinTXhWiyzLl9UpmGva0+mWyKxk6JZequ05x3eUcIUE+FyttPKJFRRVtAvQaJ6YF9h1ZpA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-regenerator@^7.22.10": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz#8ceef3bd7375c4db7652878b0241b2be5d0c3cca" + integrity sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + regenerator-transform "^0.15.2" + +"@babel/plugin-transform-reserved-words@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz#832cd35b81c287c4bcd09ce03e22199641f964fb" + integrity sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-runtime@^7.22.9": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.2.tgz#c956a3f8d1aa50816ff6c30c6288d66635c12990" + integrity sha512-XOntj6icgzMS58jPVtQpiuF6ZFWxQiJavISGx5KGjRj+3gqZr8+N6Kx+N9BApWzgS+DOjIZfXXj0ZesenOWDyA== + dependencies: + "@babel/helper-module-imports" "^7.22.15" + "@babel/helper-plugin-utils" "^7.22.5" + babel-plugin-polyfill-corejs2 "^0.4.6" + babel-plugin-polyfill-corejs3 "^0.8.5" + babel-plugin-polyfill-regenerator "^0.5.3" + semver "^6.3.1" + +"@babel/plugin-transform-shorthand-properties@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz#6e277654be82b5559fc4b9f58088507c24f0c624" + integrity sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-spread@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz#6487fd29f229c95e284ba6c98d65eafb893fea6b" + integrity sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + +"@babel/plugin-transform-sticky-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz#295aba1595bfc8197abd02eae5fc288c0deb26aa" + integrity sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-template-literals@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz#8f38cf291e5f7a8e60e9f733193f0bcc10909bff" + integrity sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-typeof-symbol@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz#5e2ba478da4b603af8673ff7c54f75a97b716b34" + integrity sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-typescript@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.22.15.tgz#15adef906451d86349eb4b8764865c960eb54127" + integrity sha512-1uirS0TnijxvQLnlv5wQBwOX3E1wCFX7ITv+9pBV2wKEk4K+M5tqDaoNXnTH8tjEIYHLO98MwiTWO04Ggz4XuA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.22.15" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-typescript" "^7.22.5" + +"@babel/plugin-transform-unicode-escapes@^7.22.10": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz#c723f380f40a2b2f57a62df24c9005834c8616d9" + integrity sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-unicode-property-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz#098898f74d5c1e86660dc112057b2d11227f1c81" + integrity sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-unicode-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz#ce7e7bb3ef208c4ff67e02a22816656256d7a183" + integrity sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-unicode-sets-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz#77788060e511b708ffc7d42fdfbc5b37c3004e91" + integrity sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/preset-env@^7.19.4", "@babel/preset-env@^7.20.2", "@babel/preset-env@^7.22.9": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.23.2.tgz#1f22be0ff0e121113260337dbc3e58fafce8d059" + integrity sha512-BW3gsuDD+rvHL2VO2SjAUNTBe5YrjsTiDyqamPDWY723na3/yPQ65X5oQkFVJZ0o50/2d+svm1rkPoJeR1KxVQ== + dependencies: + "@babel/compat-data" "^7.23.2" + "@babel/helper-compilation-targets" "^7.22.15" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-validator-option" "^7.22.15" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.22.15" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.22.15" + "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-syntax-import-assertions" "^7.22.5" + "@babel/plugin-syntax-import-attributes" "^7.22.5" + "@babel/plugin-syntax-import-meta" "^7.10.4" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-syntax-top-level-await" "^7.14.5" + "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" + "@babel/plugin-transform-arrow-functions" "^7.22.5" + "@babel/plugin-transform-async-generator-functions" "^7.23.2" + "@babel/plugin-transform-async-to-generator" "^7.22.5" + "@babel/plugin-transform-block-scoped-functions" "^7.22.5" + "@babel/plugin-transform-block-scoping" "^7.23.0" + "@babel/plugin-transform-class-properties" "^7.22.5" + "@babel/plugin-transform-class-static-block" "^7.22.11" + "@babel/plugin-transform-classes" "^7.22.15" + "@babel/plugin-transform-computed-properties" "^7.22.5" + "@babel/plugin-transform-destructuring" "^7.23.0" + "@babel/plugin-transform-dotall-regex" "^7.22.5" + "@babel/plugin-transform-duplicate-keys" "^7.22.5" + "@babel/plugin-transform-dynamic-import" "^7.22.11" + "@babel/plugin-transform-exponentiation-operator" "^7.22.5" + "@babel/plugin-transform-export-namespace-from" "^7.22.11" + "@babel/plugin-transform-for-of" "^7.22.15" + "@babel/plugin-transform-function-name" "^7.22.5" + "@babel/plugin-transform-json-strings" "^7.22.11" + "@babel/plugin-transform-literals" "^7.22.5" + "@babel/plugin-transform-logical-assignment-operators" "^7.22.11" + "@babel/plugin-transform-member-expression-literals" "^7.22.5" + "@babel/plugin-transform-modules-amd" "^7.23.0" + "@babel/plugin-transform-modules-commonjs" "^7.23.0" + "@babel/plugin-transform-modules-systemjs" "^7.23.0" + "@babel/plugin-transform-modules-umd" "^7.22.5" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.22.5" + "@babel/plugin-transform-new-target" "^7.22.5" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.22.11" + "@babel/plugin-transform-numeric-separator" "^7.22.11" + "@babel/plugin-transform-object-rest-spread" "^7.22.15" + "@babel/plugin-transform-object-super" "^7.22.5" + "@babel/plugin-transform-optional-catch-binding" "^7.22.11" + "@babel/plugin-transform-optional-chaining" "^7.23.0" + "@babel/plugin-transform-parameters" "^7.22.15" + "@babel/plugin-transform-private-methods" "^7.22.5" + "@babel/plugin-transform-private-property-in-object" "^7.22.11" + "@babel/plugin-transform-property-literals" "^7.22.5" + "@babel/plugin-transform-regenerator" "^7.22.10" + "@babel/plugin-transform-reserved-words" "^7.22.5" + "@babel/plugin-transform-shorthand-properties" "^7.22.5" + "@babel/plugin-transform-spread" "^7.22.5" + "@babel/plugin-transform-sticky-regex" "^7.22.5" + "@babel/plugin-transform-template-literals" "^7.22.5" + "@babel/plugin-transform-typeof-symbol" "^7.22.5" + "@babel/plugin-transform-unicode-escapes" "^7.22.10" + "@babel/plugin-transform-unicode-property-regex" "^7.22.5" + "@babel/plugin-transform-unicode-regex" "^7.22.5" + "@babel/plugin-transform-unicode-sets-regex" "^7.22.5" + "@babel/preset-modules" "0.1.6-no-external-plugins" + "@babel/types" "^7.23.0" + babel-plugin-polyfill-corejs2 "^0.4.6" + babel-plugin-polyfill-corejs3 "^0.8.5" + babel-plugin-polyfill-regenerator "^0.5.3" + core-js-compat "^3.31.0" + semver "^6.3.1" + +"@babel/preset-modules@0.1.6-no-external-plugins": + version "0.1.6-no-external-plugins" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz#ccb88a2c49c817236861fee7826080573b8a923a" + integrity sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/types" "^7.4.4" + esutils "^2.0.2" + +"@babel/preset-react@^7.18.6", "@babel/preset-react@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.22.15.tgz#9a776892b648e13cc8ca2edf5ed1264eea6b6afc" + integrity sha512-Csy1IJ2uEh/PecCBXXoZGAZBeCATTuePzCSB7dLYWS0vOEj6CNpjxIhW4duWwZodBNueH7QO14WbGn8YyeuN9w== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-validator-option" "^7.22.15" + "@babel/plugin-transform-react-display-name" "^7.22.5" + "@babel/plugin-transform-react-jsx" "^7.22.15" + "@babel/plugin-transform-react-jsx-development" "^7.22.5" + "@babel/plugin-transform-react-pure-annotations" "^7.22.5" + +"@babel/preset-typescript@^7.18.6", "@babel/preset-typescript@^7.21.0", "@babel/preset-typescript@^7.22.5": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.23.2.tgz#c8de488130b7081f7e1482936ad3de5b018beef4" + integrity sha512-u4UJc1XsS1GhIGteM8rnGiIvf9rJpiVgMEeCnwlLA7WJPC+jcXWJAGxYmeqs5hOZD8BbAfnV5ezBOxQbb4OUxA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-validator-option" "^7.22.15" + "@babel/plugin-syntax-jsx" "^7.22.5" + "@babel/plugin-transform-modules-commonjs" "^7.23.0" + "@babel/plugin-transform-typescript" "^7.22.15" + +"@babel/regjsgen@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" + integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== + +"@babel/runtime-corejs3@^7.22.6": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.23.2.tgz#a5cd9d8b408fb946b2f074b21ea40c04e516795c" + integrity sha512-54cIh74Z1rp4oIjsHjqN+WM4fMyCBYe+LpZ9jWm51CZ1fbH3SkAzQD/3XLoNkjbJ7YEmjobLXyvQrFypRHOrXw== + dependencies: + core-js-pure "^3.30.2" + regenerator-runtime "^0.14.0" + +"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.22.6", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.2.tgz#062b0ac103261d68a966c4c7baf2ae3e62ec3885" + integrity sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg== + dependencies: + regenerator-runtime "^0.14.0" + +"@babel/template@^7.22.15", "@babel/template@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" + integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== + dependencies: + "@babel/code-frame" "^7.22.13" + "@babel/parser" "^7.22.15" + "@babel/types" "^7.22.15" + +"@babel/traverse@^7.22.8", "@babel/traverse@^7.23.2": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" + integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw== + dependencies: + "@babel/code-frame" "^7.22.13" + "@babel/generator" "^7.23.0" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.23.0" + "@babel/types" "^7.23.0" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/types@^7.20.0", "@babel/types@^7.21.3", "@babel/types@^7.22.15", "@babel/types@^7.22.19", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.4.4": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" + integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg== + dependencies: + "@babel/helper-string-parser" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + +"@discoveryjs/json-ext@0.5.7": + version "0.5.7" + resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" + integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== + +"@docsearch/css@3.5.2": + version "3.5.2" + resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-3.5.2.tgz#610f47b48814ca94041df969d9fcc47b91fc5aac" + integrity sha512-SPiDHaWKQZpwR2siD0KQUwlStvIAnEyK6tAE2h2Wuoq8ue9skzhlyVQ1ddzOxX6khULnAALDiR/isSF3bnuciA== + +"@docsearch/react@^3.5.2": + version "3.5.2" + resolved "https://registry.yarnpkg.com/@docsearch/react/-/react-3.5.2.tgz#2e6bbee00eb67333b64906352734da6aef1232b9" + integrity sha512-9Ahcrs5z2jq/DcAvYtvlqEBHImbm4YJI8M9y0x6Tqg598P40HTEkX7hsMcIuThI+hTFxRGZ9hll0Wygm2yEjng== + dependencies: + "@algolia/autocomplete-core" "1.9.3" + "@algolia/autocomplete-preset-algolia" "1.9.3" + "@docsearch/css" "3.5.2" + algoliasearch "^4.19.1" + +"@docusaurus/core@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-3.0.0.tgz#46bc9bf2bcd99ca98a1c8f10a70bf3afaaaf9dcb" + integrity sha512-bHWtY55tJTkd6pZhHrWz1MpWuwN4edZe0/UWgFF7PW/oJeDZvLSXKqwny3L91X1/LGGoypBGkeZn8EOuKeL4yQ== + dependencies: + "@babel/core" "^7.22.9" + "@babel/generator" "^7.22.9" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-transform-runtime" "^7.22.9" + "@babel/preset-env" "^7.22.9" + "@babel/preset-react" "^7.22.5" + "@babel/preset-typescript" "^7.22.5" + "@babel/runtime" "^7.22.6" + "@babel/runtime-corejs3" "^7.22.6" + "@babel/traverse" "^7.22.8" + "@docusaurus/cssnano-preset" "3.0.0" + "@docusaurus/logger" "3.0.0" + "@docusaurus/mdx-loader" "3.0.0" + "@docusaurus/react-loadable" "5.5.2" + "@docusaurus/utils" "3.0.0" + "@docusaurus/utils-common" "3.0.0" + "@docusaurus/utils-validation" "3.0.0" + "@slorber/static-site-generator-webpack-plugin" "^4.0.7" + "@svgr/webpack" "^6.5.1" + autoprefixer "^10.4.14" + babel-loader "^9.1.3" + babel-plugin-dynamic-import-node "^2.3.3" + boxen "^6.2.1" + chalk "^4.1.2" + chokidar "^3.5.3" + clean-css "^5.3.2" + cli-table3 "^0.6.3" + combine-promises "^1.1.0" + commander "^5.1.0" + copy-webpack-plugin "^11.0.0" + core-js "^3.31.1" + css-loader "^6.8.1" + css-minimizer-webpack-plugin "^4.2.2" + cssnano "^5.1.15" + del "^6.1.1" + detect-port "^1.5.1" + escape-html "^1.0.3" + eta "^2.2.0" + file-loader "^6.2.0" + fs-extra "^11.1.1" + html-minifier-terser "^7.2.0" + html-tags "^3.3.1" + html-webpack-plugin "^5.5.3" + leven "^3.1.0" + lodash "^4.17.21" + mini-css-extract-plugin "^2.7.6" + postcss "^8.4.26" + postcss-loader "^7.3.3" + prompts "^2.4.2" + react-dev-utils "^12.0.1" + react-helmet-async "^1.3.0" + react-loadable "npm:@docusaurus/react-loadable@5.5.2" + react-loadable-ssr-addon-v5-slorber "^1.0.1" + react-router "^5.3.4" + react-router-config "^5.1.1" + react-router-dom "^5.3.4" + rtl-detect "^1.0.4" + semver "^7.5.4" + serve-handler "^6.1.5" + shelljs "^0.8.5" + terser-webpack-plugin "^5.3.9" + tslib "^2.6.0" + update-notifier "^6.0.2" + url-loader "^4.1.1" + wait-on "^7.0.1" + webpack "^5.88.1" + webpack-bundle-analyzer "^4.9.0" + webpack-dev-server "^4.15.1" + webpack-merge "^5.9.0" + webpackbar "^5.0.2" + +"@docusaurus/cssnano-preset@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-3.0.0.tgz#87fbf9cbc7c383e207119b44c17fb1d05c73af7c" + integrity sha512-FHiRfwmVvIVdIGsHcijUOaX7hMn0mugVYB7m4GkpYI6Mi56zwQV4lH5p7DxcW5CUYNWMVxz2loWSCiWEm5ikwA== + dependencies: + cssnano-preset-advanced "^5.3.10" + postcss "^8.4.26" + postcss-sort-media-queries "^4.4.1" + tslib "^2.6.0" + +"@docusaurus/logger@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@docusaurus/logger/-/logger-3.0.0.tgz#02a4bfecec6aa3732c8bd9597ca9d5debab813a6" + integrity sha512-6eX0eOfioMQCk+qgCnHvbLLuyIAA+r2lSID6d6JusiLtDKmYMfNp3F4yyE8bnb0Abmzt2w68XwptEFYyALSAXw== + dependencies: + chalk "^4.1.2" + tslib "^2.6.0" + +"@docusaurus/mdx-loader@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-3.0.0.tgz#2593889e43dc4bbd8dfa074d86c8bb4206cf4171" + integrity sha512-JkGge6WYDrwjNgMxwkb6kNQHnpISt5L1tMaBWFDBKeDToFr5Kj29IL35MIQm0RfrnoOfr/29RjSH4aRtvlAR0A== + dependencies: + "@babel/parser" "^7.22.7" + "@babel/traverse" "^7.22.8" + "@docusaurus/logger" "3.0.0" + "@docusaurus/utils" "3.0.0" + "@docusaurus/utils-validation" "3.0.0" + "@mdx-js/mdx" "^3.0.0" + "@slorber/remark-comment" "^1.0.0" + escape-html "^1.0.3" + estree-util-value-to-estree "^3.0.1" + file-loader "^6.2.0" + fs-extra "^11.1.1" + image-size "^1.0.2" + mdast-util-mdx "^3.0.0" + mdast-util-to-string "^4.0.0" + rehype-raw "^7.0.0" + remark-directive "^3.0.0" + remark-emoji "^4.0.0" + remark-frontmatter "^5.0.0" + remark-gfm "^4.0.0" + stringify-object "^3.3.0" + tslib "^2.6.0" + unified "^11.0.3" + unist-util-visit "^5.0.0" + url-loader "^4.1.1" + vfile "^6.0.1" + webpack "^5.88.1" + +"@docusaurus/module-type-aliases@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@docusaurus/module-type-aliases/-/module-type-aliases-3.0.0.tgz#9a7dd323bb87ca666eb4b0b4b90d04425f2e05d6" + integrity sha512-CfC6CgN4u/ce+2+L1JdsHNyBd8yYjl4De2B2CBj2a9F7WuJ5RjV1ciuU7KDg8uyju+NRVllRgvJvxVUjCdkPiw== + dependencies: + "@docusaurus/react-loadable" "5.5.2" + "@docusaurus/types" "3.0.0" + "@types/history" "^4.7.11" + "@types/react" "*" + "@types/react-router-config" "*" + "@types/react-router-dom" "*" + react-helmet-async "*" + react-loadable "npm:@docusaurus/react-loadable@5.5.2" + +"@docusaurus/plugin-content-blog@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.0.0.tgz#5f3ede003b2b7103043918fbe3f436c116839ca8" + integrity sha512-iA8Wc3tIzVnROJxrbIsU/iSfixHW16YeW9RWsBw7hgEk4dyGsip9AsvEDXobnRq3lVv4mfdgoS545iGWf1Ip9w== + dependencies: + "@docusaurus/core" "3.0.0" + "@docusaurus/logger" "3.0.0" + "@docusaurus/mdx-loader" "3.0.0" + "@docusaurus/types" "3.0.0" + "@docusaurus/utils" "3.0.0" + "@docusaurus/utils-common" "3.0.0" + "@docusaurus/utils-validation" "3.0.0" + cheerio "^1.0.0-rc.12" + feed "^4.2.2" + fs-extra "^11.1.1" + lodash "^4.17.21" + reading-time "^1.5.0" + srcset "^4.0.0" + tslib "^2.6.0" + unist-util-visit "^5.0.0" + utility-types "^3.10.0" + webpack "^5.88.1" + +"@docusaurus/plugin-content-docs@3.0.0", "@docusaurus/plugin-content-docs@^2 || ^3": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.0.0.tgz#b579c65d7386905890043bdd4a8f9da3194e90fa" + integrity sha512-MFZsOSwmeJ6rvoZMLieXxPuJsA9M9vn7/mUZmfUzSUTeHAeq+fEqvLltFOxcj4DVVDTYlQhgWYd+PISIWgamKw== + dependencies: + "@docusaurus/core" "3.0.0" + "@docusaurus/logger" "3.0.0" + "@docusaurus/mdx-loader" "3.0.0" + "@docusaurus/module-type-aliases" "3.0.0" + "@docusaurus/types" "3.0.0" + "@docusaurus/utils" "3.0.0" + "@docusaurus/utils-validation" "3.0.0" + "@types/react-router-config" "^5.0.7" + combine-promises "^1.1.0" + fs-extra "^11.1.1" + js-yaml "^4.1.0" + lodash "^4.17.21" + tslib "^2.6.0" + utility-types "^3.10.0" + webpack "^5.88.1" + +"@docusaurus/plugin-content-pages@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.0.0.tgz#519a946a477a203989080db70dd787cb6db15fab" + integrity sha512-EXYHXK2Ea1B5BUmM0DgSwaOYt8EMSzWtYUToNo62Q/EoWxYOQFdWglYnw3n7ZEGyw5Kog4LHaRwlazAdmDomvQ== + dependencies: + "@docusaurus/core" "3.0.0" + "@docusaurus/mdx-loader" "3.0.0" + "@docusaurus/types" "3.0.0" + "@docusaurus/utils" "3.0.0" + "@docusaurus/utils-validation" "3.0.0" + fs-extra "^11.1.1" + tslib "^2.6.0" + webpack "^5.88.1" + +"@docusaurus/plugin-debug@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-3.0.0.tgz#9c6d4abfd5357dbebccf5b41f5aefc06116e03e3" + integrity sha512-gSV07HfQgnUboVEb3lucuVyv5pEoy33E7QXzzn++3kSc/NLEimkjXh3sSnTGOishkxCqlFV9BHfY/VMm5Lko5g== + dependencies: + "@docusaurus/core" "3.0.0" + "@docusaurus/types" "3.0.0" + "@docusaurus/utils" "3.0.0" + "@microlink/react-json-view" "^1.22.2" + fs-extra "^11.1.1" + tslib "^2.6.0" + +"@docusaurus/plugin-google-analytics@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.0.0.tgz#8a54f5e21b55c133b6be803ac51bf92d4a515cca" + integrity sha512-0zcLK8w+ohmSm1fjUQCqeRsjmQc0gflvXnaVA/QVVCtm2yCiBtkrSGQXqt4MdpD7Xq8mwo3qVd5nhIcvrcebqw== + dependencies: + "@docusaurus/core" "3.0.0" + "@docusaurus/types" "3.0.0" + "@docusaurus/utils-validation" "3.0.0" + tslib "^2.6.0" + +"@docusaurus/plugin-google-gtag@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.0.0.tgz#a4c407b80cb46773bea070816ebb547c5663f0b3" + integrity sha512-asEKavw8fczUqvXu/s9kG2m1epLnHJ19W6CCCRZEmpnkZUZKiM8rlkDiEmxApwIc2JDDbIMk+Y2TMkJI8mInbQ== + dependencies: + "@docusaurus/core" "3.0.0" + "@docusaurus/types" "3.0.0" + "@docusaurus/utils-validation" "3.0.0" + "@types/gtag.js" "^0.0.12" + tslib "^2.6.0" + +"@docusaurus/plugin-google-tag-manager@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.0.0.tgz#8befa315b4747618e9ea65add3f2f4e84df2c7ba" + integrity sha512-lytgu2eyn+7p4WklJkpMGRhwC29ezj4IjPPmVJ8vGzcSl6JkR1sADTHLG5xWOMuci420xZl9dGEiLTQ8FjCRyA== + dependencies: + "@docusaurus/core" "3.0.0" + "@docusaurus/types" "3.0.0" + "@docusaurus/utils-validation" "3.0.0" + tslib "^2.6.0" + +"@docusaurus/plugin-sitemap@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.0.0.tgz#91f300e500d476252ea2f40449ee828766b9b9d6" + integrity sha512-cfcONdWku56Oi7Hdus2uvUw/RKRRlIGMViiHLjvQ21CEsEqnQ297MRoIgjU28kL7/CXD/+OiANSq3T1ezAiMhA== + dependencies: + "@docusaurus/core" "3.0.0" + "@docusaurus/logger" "3.0.0" + "@docusaurus/types" "3.0.0" + "@docusaurus/utils" "3.0.0" + "@docusaurus/utils-common" "3.0.0" + "@docusaurus/utils-validation" "3.0.0" + fs-extra "^11.1.1" + sitemap "^7.1.1" + tslib "^2.6.0" + +"@docusaurus/preset-classic@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-3.0.0.tgz#b05c3960c4d0a731b2feb97e94e3757ab073c611" + integrity sha512-90aOKZGZdi0+GVQV+wt8xx4M4GiDrBRke8NO8nWwytMEXNrxrBxsQYFRD1YlISLJSCiHikKf3Z/MovMnQpnZyg== + dependencies: + "@docusaurus/core" "3.0.0" + "@docusaurus/plugin-content-blog" "3.0.0" + "@docusaurus/plugin-content-docs" "3.0.0" + "@docusaurus/plugin-content-pages" "3.0.0" + "@docusaurus/plugin-debug" "3.0.0" + "@docusaurus/plugin-google-analytics" "3.0.0" + "@docusaurus/plugin-google-gtag" "3.0.0" + "@docusaurus/plugin-google-tag-manager" "3.0.0" + "@docusaurus/plugin-sitemap" "3.0.0" + "@docusaurus/theme-classic" "3.0.0" + "@docusaurus/theme-common" "3.0.0" + "@docusaurus/theme-search-algolia" "3.0.0" + "@docusaurus/types" "3.0.0" + +"@docusaurus/react-loadable@5.5.2", "react-loadable@npm:@docusaurus/react-loadable@5.5.2": + version "5.5.2" + resolved "https://registry.yarnpkg.com/@docusaurus/react-loadable/-/react-loadable-5.5.2.tgz#81aae0db81ecafbdaee3651f12804580868fa6ce" + integrity sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ== + dependencies: + "@types/react" "*" + prop-types "^15.6.2" + +"@docusaurus/theme-classic@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-3.0.0.tgz#a47eda40747e1a6f79190e6bb786d3a7fc4e06b2" + integrity sha512-wWOHSrKMn7L4jTtXBsb5iEJ3xvTddBye5PjYBnWiCkTAlhle2yMdc4/qRXW35Ot+OV/VXu6YFG8XVUJEl99z0A== + dependencies: + "@docusaurus/core" "3.0.0" + "@docusaurus/mdx-loader" "3.0.0" + "@docusaurus/module-type-aliases" "3.0.0" + "@docusaurus/plugin-content-blog" "3.0.0" + "@docusaurus/plugin-content-docs" "3.0.0" + "@docusaurus/plugin-content-pages" "3.0.0" + "@docusaurus/theme-common" "3.0.0" + "@docusaurus/theme-translations" "3.0.0" + "@docusaurus/types" "3.0.0" + "@docusaurus/utils" "3.0.0" + "@docusaurus/utils-common" "3.0.0" + "@docusaurus/utils-validation" "3.0.0" + "@mdx-js/react" "^3.0.0" + clsx "^1.2.1" + copy-text-to-clipboard "^3.2.0" + infima "0.2.0-alpha.43" + lodash "^4.17.21" + nprogress "^0.2.0" + postcss "^8.4.26" + prism-react-renderer "^2.1.0" + prismjs "^1.29.0" + react-router-dom "^5.3.4" + rtlcss "^4.1.0" + tslib "^2.6.0" + utility-types "^3.10.0" + +"@docusaurus/theme-common@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-3.0.0.tgz#6dc8c39a7458dd39f95a2fa6eb1c6aaf32b7e103" + integrity sha512-PahRpCLRK5owCMEqcNtUeTMOkTUCzrJlKA+HLu7f+8osYOni617YurXvHASCsSTxurjXaLz/RqZMnASnqATxIA== + dependencies: + "@docusaurus/mdx-loader" "3.0.0" + "@docusaurus/module-type-aliases" "3.0.0" + "@docusaurus/plugin-content-blog" "3.0.0" + "@docusaurus/plugin-content-docs" "3.0.0" + "@docusaurus/plugin-content-pages" "3.0.0" + "@docusaurus/utils" "3.0.0" + "@docusaurus/utils-common" "3.0.0" + "@types/history" "^4.7.11" + "@types/react" "*" + "@types/react-router-config" "*" + clsx "^1.2.1" + parse-numeric-range "^1.3.0" + prism-react-renderer "^2.1.0" + tslib "^2.6.0" + utility-types "^3.10.0" + +"@docusaurus/theme-search-algolia@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.0.0.tgz#20701c2e7945a236df401365271b511a24ff3cad" + integrity sha512-PyMUNIS9yu0dx7XffB13ti4TG47pJq3G2KE/INvOFb6M0kWh+wwCnucPg4WAOysHOPh+SD9fjlXILoLQstgEIA== + dependencies: + "@docsearch/react" "^3.5.2" + "@docusaurus/core" "3.0.0" + "@docusaurus/logger" "3.0.0" + "@docusaurus/plugin-content-docs" "3.0.0" + "@docusaurus/theme-common" "3.0.0" + "@docusaurus/theme-translations" "3.0.0" + "@docusaurus/utils" "3.0.0" + "@docusaurus/utils-validation" "3.0.0" + algoliasearch "^4.18.0" + algoliasearch-helper "^3.13.3" + clsx "^1.2.1" + eta "^2.2.0" + fs-extra "^11.1.1" + lodash "^4.17.21" + tslib "^2.6.0" + utility-types "^3.10.0" + +"@docusaurus/theme-translations@3.0.0", "@docusaurus/theme-translations@^2 || ^3": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-translations/-/theme-translations-3.0.0.tgz#98590b80589f15b2064e0daa2acc3a82d126f53b" + integrity sha512-p/H3+5LdnDtbMU+csYukA6601U1ld2v9knqxGEEV96qV27HsHfP63J9Ta2RBZUrNhQAgrwFzIc9GdDO8P1Baag== + dependencies: + fs-extra "^11.1.1" + tslib "^2.6.0" + +"@docusaurus/types@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@docusaurus/types/-/types-3.0.0.tgz#3edabe43f70b45f81a48f3470d6a73a2eba41945" + integrity sha512-Qb+l/hmCOVemReuzvvcFdk84bUmUFyD0Zi81y651ie3VwMrXqC7C0E7yZLKMOsLj/vkqsxHbtkAuYMI89YzNzg== + dependencies: + "@types/history" "^4.7.11" + "@types/react" "*" + commander "^5.1.0" + joi "^17.9.2" + react-helmet-async "^1.3.0" + utility-types "^3.10.0" + webpack "^5.88.1" + webpack-merge "^5.9.0" + +"@docusaurus/utils-common@3.0.0", "@docusaurus/utils-common@^2 || ^3": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@docusaurus/utils-common/-/utils-common-3.0.0.tgz#fb019e5228b20852a5b98f50672a02843a03ba03" + integrity sha512-7iJWAtt4AHf4PFEPlEPXko9LZD/dbYnhLe0q8e3GRK1EXZyRASah2lznpMwB3lLmVjq/FR6ZAKF+E0wlmL5j0g== + dependencies: + tslib "^2.6.0" + +"@docusaurus/utils-validation@3.0.0", "@docusaurus/utils-validation@^2 || ^3": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-3.0.0.tgz#56f3ba89ceba9826989408a96827897c0b724612" + integrity sha512-MlIGUspB/HBW5CYgHvRhmkZbeMiUWKbyVoCQYvbGN8S19SSzVgzyy97KRpcjCOYYeEdkhmRCUwFBJBlLg3IoNQ== + dependencies: + "@docusaurus/logger" "3.0.0" + "@docusaurus/utils" "3.0.0" + joi "^17.9.2" + js-yaml "^4.1.0" + tslib "^2.6.0" + +"@docusaurus/utils@3.0.0", "@docusaurus/utils@^2 || ^3": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-3.0.0.tgz#2ef0c8e434036fe104dca4c694fd50022b2ba1ed" + integrity sha512-JwGjh5mtjG9XIAESyPxObL6CZ6LO/yU4OSTpq7Q0x+jN25zi/AMbvLjpSyZzWy+qm5uQiFiIhqFaOxvy+82Ekg== + dependencies: + "@docusaurus/logger" "3.0.0" + "@svgr/webpack" "^6.5.1" + escape-string-regexp "^4.0.0" + file-loader "^6.2.0" + fs-extra "^11.1.1" + github-slugger "^1.5.0" + globby "^11.1.0" + gray-matter "^4.0.3" + jiti "^1.20.0" + js-yaml "^4.1.0" + lodash "^4.17.21" + micromatch "^4.0.5" + resolve-pathname "^3.0.0" + shelljs "^0.8.5" + tslib "^2.6.0" + url-loader "^4.1.1" + webpack "^5.88.1" + +"@easyops-cn/autocomplete.js@^0.38.1": + version "0.38.1" + resolved "https://registry.yarnpkg.com/@easyops-cn/autocomplete.js/-/autocomplete.js-0.38.1.tgz#46dff5795a9a032fa9b9250fdf63ca6c61c07629" + integrity sha512-drg76jS6syilOUmVNkyo1c7ZEBPcPuK+aJA7AksM5ZIIbV57DMHCywiCr+uHyv8BE5jUTU98j/H7gVrkHrWW3Q== + dependencies: + cssesc "^3.0.0" + immediate "^3.2.3" + +"@easyops-cn/docusaurus-search-local@^0.37.4": + version "0.37.4" + resolved "https://registry.yarnpkg.com/@easyops-cn/docusaurus-search-local/-/docusaurus-search-local-0.37.4.tgz#b97a49532dd4bb02f7953c408979237595eaa1ca" + integrity sha512-OVGx0LPdhEt/UIybkwKVelx/6yXJn24fOskJMyzUAR6HcjzeEMZM7hGI3AuGglnWzpQXMhT5T3WbMW/63mwQVA== + dependencies: + "@docusaurus/plugin-content-docs" "^2 || ^3" + "@docusaurus/theme-translations" "^2 || ^3" + "@docusaurus/utils" "^2 || ^3" + "@docusaurus/utils-common" "^2 || ^3" + "@docusaurus/utils-validation" "^2 || ^3" + "@easyops-cn/autocomplete.js" "^0.38.1" + "@node-rs/jieba" "^1.6.0" + cheerio "^1.0.0-rc.3" + clsx "^1.1.1" + debug "^4.2.0" + fs-extra "^10.0.0" + klaw-sync "^6.0.0" + lunr "^2.3.9" + lunr-languages "^1.4.0" + mark.js "^8.11.1" + tslib "^2.4.0" + +"@hapi/hoek@^9.0.0": + version "9.3.0" + resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" + integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ== + +"@hapi/topo@^5.0.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.1.0.tgz#dc448e332c6c6e37a4dc02fd84ba8d44b9afb012" + integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg== + dependencies: + "@hapi/hoek" "^9.0.0" + +"@jest/schemas@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" + integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== + dependencies: + "@sinclair/typebox" "^0.27.8" + +"@jest/types@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59" + integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== + dependencies: + "@jest/schemas" "^29.6.3" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + +"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" + integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/source-map@^0.3.3": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.5.tgz#a3bb4d5c6825aab0d281268f47f6ad5853431e91" + integrity sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.20" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f" + integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@leichtgewicht/ip-codec@^2.0.1": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" + integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== + +"@mdx-js/mdx@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@mdx-js/mdx/-/mdx-3.0.0.tgz#37ef87685143fafedf1165f0a79e9fe95fbe5154" + integrity sha512-Icm0TBKBLYqroYbNW3BPnzMGn+7mwpQOK310aZ7+fkCtiU3aqv2cdcX+nd0Ydo3wI5Rx8bX2Z2QmGb/XcAClCw== + dependencies: + "@types/estree" "^1.0.0" + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/mdx" "^2.0.0" + collapse-white-space "^2.0.0" + devlop "^1.0.0" + estree-util-build-jsx "^3.0.0" + estree-util-is-identifier-name "^3.0.0" + estree-util-to-js "^2.0.0" + estree-walker "^3.0.0" + hast-util-to-estree "^3.0.0" + hast-util-to-jsx-runtime "^2.0.0" + markdown-extensions "^2.0.0" + periscopic "^3.0.0" + remark-mdx "^3.0.0" + remark-parse "^11.0.0" + remark-rehype "^11.0.0" + source-map "^0.7.0" + unified "^11.0.0" + unist-util-position-from-estree "^2.0.0" + unist-util-stringify-position "^4.0.0" + unist-util-visit "^5.0.0" + vfile "^6.0.0" + +"@mdx-js/react@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@mdx-js/react/-/react-3.0.0.tgz#eaccaa8d6a7736b19080aff5a70448a7ba692271" + integrity sha512-nDctevR9KyYFyV+m+/+S4cpzCWHqj+iHDHq3QrsWezcC+B17uZdIWgCguESUkwFhM3n/56KxWVE3V6EokrmONQ== + dependencies: + "@types/mdx" "^2.0.0" + +"@microlink/react-json-view@^1.22.2": + version "1.23.0" + resolved "https://registry.yarnpkg.com/@microlink/react-json-view/-/react-json-view-1.23.0.tgz#641c2483b1a0014818303d4e9cce634d5dacc7e9" + integrity sha512-HYJ1nsfO4/qn8afnAMhuk7+5a1vcjEaS8Gm5Vpr1SqdHDY0yLBJGpA+9DvKyxyVKaUkXzKXt3Mif9RcmFSdtYg== + dependencies: + flux "~4.0.1" + react-base16-styling "~0.6.0" + react-lifecycles-compat "~3.0.4" + react-textarea-autosize "~8.3.2" + +"@node-rs/jieba-android-arm-eabi@1.7.2": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@node-rs/jieba-android-arm-eabi/-/jieba-android-arm-eabi-1.7.2.tgz#06d10feef9a0718b4b3f07b2d7f161c193439b96" + integrity sha512-FyDHRNSRIHOQO7S6Q4RwuGffnnnuNwaXPH7K8WqSzifEY+zFIaSPcNqrZHrnqyeXc4JiYpBIHeP+0Mkf1kIGRA== + +"@node-rs/jieba-android-arm64@1.7.2": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@node-rs/jieba-android-arm64/-/jieba-android-arm64-1.7.2.tgz#10830a0d59e84faa1a52c0fea5618ef9b1d5ac03" + integrity sha512-z0UEZCGrAX/IiarhuDMsEIDZBS77UZv4SQyL/J48yrsbWKbb2lJ1vCrYxXIWqwp6auXHEu4r1O/pMriDAcEnPg== + +"@node-rs/jieba-darwin-arm64@1.7.2": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@node-rs/jieba-darwin-arm64/-/jieba-darwin-arm64-1.7.2.tgz#70e6b9f6d3167817b8a8259c49e0aa6693fd4871" + integrity sha512-M2cHIWRaaOmXGKy446SH2+Y2PzREaI2oYznPbg55wYEdioUp01YS/2WRG8CaoCKEj0aUocA7MFM2vVcoIAsbQw== + +"@node-rs/jieba-darwin-x64@1.7.2": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@node-rs/jieba-darwin-x64/-/jieba-darwin-x64-1.7.2.tgz#bf21be9875a30a5fad4030893b3088993831e54f" + integrity sha512-euDawBU2FxB0CGTR803BA6WABsiicIrqa61z2AFFDPkJCDrauEM0jbMg3GDKLAvbaLbZ1Etu3QNN5xyroqp4Qw== + +"@node-rs/jieba-freebsd-x64@1.7.2": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@node-rs/jieba-freebsd-x64/-/jieba-freebsd-x64-1.7.2.tgz#f173089e34135bca613ced7c816ad9fefc023c39" + integrity sha512-vXCaYxPb90d/xTBVG+ZZXrFLXsO2719pZSyiZCL2tey+UY28U7MOoK6394Wwmf0FCB/eRTQMCKjVIUDi+IRMUg== + +"@node-rs/jieba-linux-arm-gnueabihf@1.7.2": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@node-rs/jieba-linux-arm-gnueabihf/-/jieba-linux-arm-gnueabihf-1.7.2.tgz#307923401ba49bb4e28639ab2954c4be3362f807" + integrity sha512-HTep79XlJYO3KRYZ2kJChG9HnYr1DKSQTB+HEYWKLK0ifphqybcxGNLAdH0S4dViG2ciD0+iN/refgtqZEidpw== + +"@node-rs/jieba-linux-arm64-gnu@1.7.2": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@node-rs/jieba-linux-arm64-gnu/-/jieba-linux-arm64-gnu-1.7.2.tgz#3af77273d84b3e4d158ad3bfdaa23e523efb8fce" + integrity sha512-P8QJdQydOVewL1MIqYiRpI7LOfrRQag+p4/hwExe+YXH8C7DOrR8rWJD/7XNRTbpOimlHq1UN/e+ZzhxQF/cLw== + +"@node-rs/jieba-linux-arm64-musl@1.7.2": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@node-rs/jieba-linux-arm64-musl/-/jieba-linux-arm64-musl-1.7.2.tgz#c6cfe6eeb163683032e6265136b8713a9b28a793" + integrity sha512-WjnN0hmDvTXb2h3hMW5VnUGkK1xaqhs+WHfMMilau55+YN+YOYALKZ0TeBY4BapClLuBx54wqwmBX+B4hAXunQ== + +"@node-rs/jieba-linux-x64-gnu@1.7.2": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@node-rs/jieba-linux-x64-gnu/-/jieba-linux-x64-gnu-1.7.2.tgz#0f5ea2499a64d3f2e639b538d7ff78095fd0477a" + integrity sha512-gBXds/DwNSA6lNUxJjL6WIaNT6pnlM5juUgV/krLLkBJ8vXpOrQ07p0rrK1tnigz9b20xhsHaFRSwED1Y8zeXw== + +"@node-rs/jieba-linux-x64-musl@1.7.2": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@node-rs/jieba-linux-x64-musl/-/jieba-linux-x64-musl-1.7.2.tgz#c60ea4f3f4e7202208aa61b318dd1a34f82734b5" + integrity sha512-tNVD3SMuG5zAj7+bLS2Enio3zR7BPxi3PhQtpQ+Hv83jajIcN46QQ0EdoMFz/aB+hkQ9PlLAstu+VREFegs5EA== + +"@node-rs/jieba-win32-arm64-msvc@1.7.2": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@node-rs/jieba-win32-arm64-msvc/-/jieba-win32-arm64-msvc-1.7.2.tgz#3cc4a1781115d282d39d98f6e4039872c3c63c84" + integrity sha512-/e1iQ0Dh02lGPNCYTU/H3cfIsWydaGRzZ3TDj6GfWrxkWqXORL98x/VJ/C/uKLpc7GSLLd9ygyZG7SOAfKe2tA== + +"@node-rs/jieba-win32-ia32-msvc@1.7.2": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@node-rs/jieba-win32-ia32-msvc/-/jieba-win32-ia32-msvc-1.7.2.tgz#574695c602be2c38cc49708b4829b5cf0574e4b6" + integrity sha512-cYjA6YUiOwtuEzWErvwMMt/RETNWQDLcmAaiHA8ohsa6c0eB0kRJlQCc683tlaczZxqroY/7C9mxgJNGvoGRbw== + +"@node-rs/jieba-win32-x64-msvc@1.7.2": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@node-rs/jieba-win32-x64-msvc/-/jieba-win32-x64-msvc-1.7.2.tgz#847b23c6e2cb255e095d990c78b678fa73e518a6" + integrity sha512-2M+Um3woFF17sa8VBYQQ6E5PNMe9Kf9fdzmeDh/GzuNHXlxW4LyK9VTV8zchIv/bDNAR5Z85kfW4wASULUxvFQ== + +"@node-rs/jieba@^1.6.0": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@node-rs/jieba/-/jieba-1.7.2.tgz#2ddccfb1208c00e9a6733b2ba31fd18645e07677" + integrity sha512-zGto08NDU+KWm670qVHYGTb0YTEJ0A97dwH3WCnnhyRYMqTbOXKC6OwTc/cjzfSJP1UDBSar9Ug9BlmWmEThWg== + optionalDependencies: + "@node-rs/jieba-android-arm-eabi" "1.7.2" + "@node-rs/jieba-android-arm64" "1.7.2" + "@node-rs/jieba-darwin-arm64" "1.7.2" + "@node-rs/jieba-darwin-x64" "1.7.2" + "@node-rs/jieba-freebsd-x64" "1.7.2" + "@node-rs/jieba-linux-arm-gnueabihf" "1.7.2" + "@node-rs/jieba-linux-arm64-gnu" "1.7.2" + "@node-rs/jieba-linux-arm64-musl" "1.7.2" + "@node-rs/jieba-linux-x64-gnu" "1.7.2" + "@node-rs/jieba-linux-x64-musl" "1.7.2" + "@node-rs/jieba-win32-arm64-msvc" "1.7.2" + "@node-rs/jieba-win32-ia32-msvc" "1.7.2" + "@node-rs/jieba-win32-x64-msvc" "1.7.2" + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@pnpm/config.env-replace@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz#ab29da53df41e8948a00f2433f085f54de8b3a4c" + integrity sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w== + +"@pnpm/network.ca-file@^1.0.1": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz#2ab05e09c1af0cdf2fcf5035bea1484e222f7983" + integrity sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA== + dependencies: + graceful-fs "4.2.10" + +"@pnpm/npm-conf@^2.1.0": + version "2.2.2" + resolved "https://registry.yarnpkg.com/@pnpm/npm-conf/-/npm-conf-2.2.2.tgz#0058baf1c26cbb63a828f0193795401684ac86f0" + integrity sha512-UA91GwWPhFExt3IizW6bOeY/pQ0BkuNwKjk9iQW9KqxluGCrg4VenZ0/L+2Y0+ZOtme72EVvg6v0zo3AMQRCeA== + dependencies: + "@pnpm/config.env-replace" "^1.1.0" + "@pnpm/network.ca-file" "^1.0.1" + config-chain "^1.1.11" + +"@polka/url@^1.0.0-next.20": + version "1.0.0-next.23" + resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.23.tgz#498e41218ab3b6a1419c735e5c6ae2c5ed609b6c" + integrity sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg== + +"@sideway/address@^4.1.3": + version "4.1.4" + resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.4.tgz#03dccebc6ea47fdc226f7d3d1ad512955d4783f0" + integrity sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw== + dependencies: + "@hapi/hoek" "^9.0.0" + +"@sideway/formula@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" + integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== + +"@sideway/pinpoint@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" + integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== + +"@sinclair/typebox@^0.27.8": + version "0.27.8" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" + integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== + +"@sindresorhus/is@^3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-3.1.2.tgz#548650de521b344e3781fbdb0ece4aa6f729afb8" + integrity sha512-JiX9vxoKMmu8Y3Zr2RVathBL1Cdu4Nt4MuNWemt1Nc06A0RAin9c5FArkhGsyMBWfCu4zj+9b+GxtjAnE4qqLQ== + +"@sindresorhus/is@^5.2.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-5.6.0.tgz#41dd6093d34652cddb5d5bdeee04eafc33826668" + integrity sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g== + +"@slorber/remark-comment@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@slorber/remark-comment/-/remark-comment-1.0.0.tgz#2a020b3f4579c89dec0361673206c28d67e08f5a" + integrity sha512-RCE24n7jsOj1M0UPvIQCHTe7fI0sFL4S2nwKVWwHyVr/wI/H8GosgsJGyhnsZoGFnD/P2hLf1mSbrrgSLN93NA== + dependencies: + micromark-factory-space "^1.0.0" + micromark-util-character "^1.1.0" + micromark-util-symbol "^1.0.1" + +"@slorber/static-site-generator-webpack-plugin@^4.0.7": + version "4.0.7" + resolved "https://registry.yarnpkg.com/@slorber/static-site-generator-webpack-plugin/-/static-site-generator-webpack-plugin-4.0.7.tgz#fc1678bddefab014e2145cbe25b3ce4e1cfc36f3" + integrity sha512-Ug7x6z5lwrz0WqdnNFOMYrDQNTPAprvHLSh6+/fmml3qUiz6l5eq+2MzLKWtn/q5K5NpSiFsZTP/fck/3vjSxA== + dependencies: + eval "^0.1.8" + p-map "^4.0.0" + webpack-sources "^3.2.2" + +"@svgr/babel-plugin-add-jsx-attribute@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz#4001f5d5dd87fa13303e36ee106e3ff3a7eb8b22" + integrity sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g== + +"@svgr/babel-plugin-add-jsx-attribute@^6.5.1": + version "6.5.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-6.5.1.tgz#74a5d648bd0347bda99d82409d87b8ca80b9a1ba" + integrity sha512-9PYGcXrAxitycIjRmZB+Q0JaN07GZIWaTBIGQzfaZv+qr1n8X1XUEJ5rZ/vx6OVD9RRYlrNnXWExQXcmZeD/BQ== + +"@svgr/babel-plugin-remove-jsx-attribute@*", "@svgr/babel-plugin-remove-jsx-attribute@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz#69177f7937233caca3a1afb051906698f2f59186" + integrity sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA== + +"@svgr/babel-plugin-remove-jsx-empty-expression@*", "@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz#c2c48104cfd7dcd557f373b70a56e9e3bdae1d44" + integrity sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA== + +"@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz#8fbb6b2e91fa26ac5d4aa25c6b6e4f20f9c0ae27" + integrity sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ== + +"@svgr/babel-plugin-replace-jsx-attribute-value@^6.5.1": + version "6.5.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-6.5.1.tgz#fb9d22ea26d2bc5e0a44b763d4c46d5d3f596c60" + integrity sha512-8DPaVVE3fd5JKuIC29dqyMB54sA6mfgki2H2+swh+zNJoynC8pMPzOkidqHOSc6Wj032fhl8Z0TVn1GiPpAiJg== + +"@svgr/babel-plugin-svg-dynamic-title@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz#1d5ba1d281363fc0f2f29a60d6d936f9bbc657b0" + integrity sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og== + +"@svgr/babel-plugin-svg-dynamic-title@^6.5.1": + version "6.5.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-6.5.1.tgz#01b2024a2b53ffaa5efceaa0bf3e1d5a4c520ce4" + integrity sha512-FwOEi0Il72iAzlkaHrlemVurgSQRDFbk0OC8dSvD5fSBPHltNh7JtLsxmZUhjYBZo2PpcU/RJvvi6Q0l7O7ogw== + +"@svgr/babel-plugin-svg-em-dimensions@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz#35e08df300ea8b1d41cb8f62309c241b0369e501" + integrity sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g== + +"@svgr/babel-plugin-svg-em-dimensions@^6.5.1": + version "6.5.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-6.5.1.tgz#dd3fa9f5b24eb4f93bcf121c3d40ff5facecb217" + integrity sha512-gWGsiwjb4tw+ITOJ86ndY/DZZ6cuXMNE/SjcDRg+HLuCmwpcjOktwRF9WgAiycTqJD/QXqL2f8IzE2Rzh7aVXA== + +"@svgr/babel-plugin-transform-react-native-svg@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz#90a8b63998b688b284f255c6a5248abd5b28d754" + integrity sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q== + +"@svgr/babel-plugin-transform-react-native-svg@^6.5.1": + version "6.5.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-6.5.1.tgz#1d8e945a03df65b601551097d8f5e34351d3d305" + integrity sha512-2jT3nTayyYP7kI6aGutkyfJ7UMGtuguD72OjeGLwVNyfPRBD8zQthlvL+fAbAKk5n9ZNcvFkp/b1lZ7VsYqVJg== + +"@svgr/babel-plugin-transform-svg-component@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz#013b4bfca88779711f0ed2739f3f7efcefcf4f7e" + integrity sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw== + +"@svgr/babel-plugin-transform-svg-component@^6.5.1": + version "6.5.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-6.5.1.tgz#48620b9e590e25ff95a80f811544218d27f8a250" + integrity sha512-a1p6LF5Jt33O3rZoVRBqdxL350oge54iZWHNI6LJB5tQ7EelvD/Mb1mfBiZNAan0dt4i3VArkFRjA4iObuNykQ== + +"@svgr/babel-preset@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-preset/-/babel-preset-8.1.0.tgz#0e87119aecdf1c424840b9d4565b7137cabf9ece" + integrity sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug== + dependencies: + "@svgr/babel-plugin-add-jsx-attribute" "8.0.0" + "@svgr/babel-plugin-remove-jsx-attribute" "8.0.0" + "@svgr/babel-plugin-remove-jsx-empty-expression" "8.0.0" + "@svgr/babel-plugin-replace-jsx-attribute-value" "8.0.0" + "@svgr/babel-plugin-svg-dynamic-title" "8.0.0" + "@svgr/babel-plugin-svg-em-dimensions" "8.0.0" + "@svgr/babel-plugin-transform-react-native-svg" "8.1.0" + "@svgr/babel-plugin-transform-svg-component" "8.0.0" + +"@svgr/babel-preset@^6.5.1": + version "6.5.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-preset/-/babel-preset-6.5.1.tgz#b90de7979c8843c5c580c7e2ec71f024b49eb828" + integrity sha512-6127fvO/FF2oi5EzSQOAjo1LE3OtNVh11R+/8FXa+mHx1ptAaS4cknIjnUA7e6j6fwGGJ17NzaTJFUwOV2zwCw== + dependencies: + "@svgr/babel-plugin-add-jsx-attribute" "^6.5.1" + "@svgr/babel-plugin-remove-jsx-attribute" "*" + "@svgr/babel-plugin-remove-jsx-empty-expression" "*" + "@svgr/babel-plugin-replace-jsx-attribute-value" "^6.5.1" + "@svgr/babel-plugin-svg-dynamic-title" "^6.5.1" + "@svgr/babel-plugin-svg-em-dimensions" "^6.5.1" + "@svgr/babel-plugin-transform-react-native-svg" "^6.5.1" + "@svgr/babel-plugin-transform-svg-component" "^6.5.1" + +"@svgr/core@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/core/-/core-8.1.0.tgz#41146f9b40b1a10beaf5cc4f361a16a3c1885e88" + integrity sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA== + dependencies: + "@babel/core" "^7.21.3" + "@svgr/babel-preset" "8.1.0" + camelcase "^6.2.0" + cosmiconfig "^8.1.3" + snake-case "^3.0.4" + +"@svgr/core@^6.5.1": + version "6.5.1" + resolved "https://registry.yarnpkg.com/@svgr/core/-/core-6.5.1.tgz#d3e8aa9dbe3fbd747f9ee4282c1c77a27410488a" + integrity sha512-/xdLSWxK5QkqG524ONSjvg3V/FkNyCv538OIBdQqPNaAta3AsXj/Bd2FbvR87yMbXO2hFSWiAe/Q6IkVPDw+mw== + dependencies: + "@babel/core" "^7.19.6" + "@svgr/babel-preset" "^6.5.1" + "@svgr/plugin-jsx" "^6.5.1" + camelcase "^6.2.0" + cosmiconfig "^7.0.1" + +"@svgr/hast-util-to-babel-ast@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz#6952fd9ce0f470e1aded293b792a2705faf4ffd4" + integrity sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q== + dependencies: + "@babel/types" "^7.21.3" + entities "^4.4.0" + +"@svgr/hast-util-to-babel-ast@^6.5.1": + version "6.5.1" + resolved "https://registry.yarnpkg.com/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-6.5.1.tgz#81800bd09b5bcdb968bf6ee7c863d2288fdb80d2" + integrity sha512-1hnUxxjd83EAxbL4a0JDJoD3Dao3hmjvyvyEV8PzWmLK3B9m9NPlW7GKjFyoWE8nM7HnXzPcmmSyOW8yOddSXw== + dependencies: + "@babel/types" "^7.20.0" + entities "^4.4.0" + +"@svgr/plugin-jsx@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz#96969f04a24b58b174ee4cd974c60475acbd6928" + integrity sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA== + dependencies: + "@babel/core" "^7.21.3" + "@svgr/babel-preset" "8.1.0" + "@svgr/hast-util-to-babel-ast" "8.0.0" + svg-parser "^2.0.4" + +"@svgr/plugin-jsx@^6.5.1": + version "6.5.1" + resolved "https://registry.yarnpkg.com/@svgr/plugin-jsx/-/plugin-jsx-6.5.1.tgz#0e30d1878e771ca753c94e69581c7971542a7072" + integrity sha512-+UdQxI3jgtSjCykNSlEMuy1jSRQlGC7pqBCPvkG/2dATdWo082zHTTK3uhnAju2/6XpE6B5mZ3z4Z8Ns01S8Gw== + dependencies: + "@babel/core" "^7.19.6" + "@svgr/babel-preset" "^6.5.1" + "@svgr/hast-util-to-babel-ast" "^6.5.1" + svg-parser "^2.0.4" + +"@svgr/plugin-svgo@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz#b115b7b967b564f89ac58feae89b88c3decd0f00" + integrity sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA== + dependencies: + cosmiconfig "^8.1.3" + deepmerge "^4.3.1" + svgo "^3.0.2" + +"@svgr/plugin-svgo@^6.5.1": + version "6.5.1" + resolved "https://registry.yarnpkg.com/@svgr/plugin-svgo/-/plugin-svgo-6.5.1.tgz#0f91910e988fc0b842f88e0960c2862e022abe84" + integrity sha512-omvZKf8ixP9z6GWgwbtmP9qQMPX4ODXi+wzbVZgomNFsUIlHA1sf4fThdwTWSsZGgvGAG6yE+b/F5gWUkcZ/iQ== + dependencies: + cosmiconfig "^7.0.1" + deepmerge "^4.2.2" + svgo "^2.8.0" + +"@svgr/webpack@^6.5.1": + version "6.5.1" + resolved "https://registry.yarnpkg.com/@svgr/webpack/-/webpack-6.5.1.tgz#ecf027814fc1cb2decc29dc92f39c3cf691e40e8" + integrity sha512-cQ/AsnBkXPkEK8cLbv4Dm7JGXq2XrumKnL1dRpJD9rIO2fTIlJI9a1uCciYG1F2aUsox/hJQyNGbt3soDxSRkA== + dependencies: + "@babel/core" "^7.19.6" + "@babel/plugin-transform-react-constant-elements" "^7.18.12" + "@babel/preset-env" "^7.19.4" + "@babel/preset-react" "^7.18.6" + "@babel/preset-typescript" "^7.18.6" + "@svgr/core" "^6.5.1" + "@svgr/plugin-jsx" "^6.5.1" + "@svgr/plugin-svgo" "^6.5.1" + +"@svgr/webpack@^8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/webpack/-/webpack-8.1.0.tgz#16f1b5346f102f89fda6ec7338b96a701d8be0c2" + integrity sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA== + dependencies: + "@babel/core" "^7.21.3" + "@babel/plugin-transform-react-constant-elements" "^7.21.3" + "@babel/preset-env" "^7.20.2" + "@babel/preset-react" "^7.18.6" + "@babel/preset-typescript" "^7.21.0" + "@svgr/core" "8.1.0" + "@svgr/plugin-jsx" "8.1.0" + "@svgr/plugin-svgo" "8.1.0" + +"@szmarczak/http-timer@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-5.0.1.tgz#c7c1bf1141cdd4751b0399c8fc7b8b664cd5be3a" + integrity sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw== + dependencies: + defer-to-connect "^2.0.1" + +"@trysound/sax@0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" + integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== + +"@types/acorn@^4.0.0": + version "4.0.6" + resolved "https://registry.yarnpkg.com/@types/acorn/-/acorn-4.0.6.tgz#d61ca5480300ac41a7d973dd5b84d0a591154a22" + integrity sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ== + dependencies: + "@types/estree" "*" + +"@types/body-parser@*": + version "1.19.4" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.4.tgz#78ad68f1f79eb851aa3634db0c7f57f6f601b462" + integrity sha512-N7UDG0/xiPQa2D/XrVJXjkWbpqHCd2sBaB32ggRF2l83RhPfamgKGF8gwwqyksS95qUS5ZYF9aF+lLPRlwI2UA== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/bonjour@^3.5.9": + version "3.5.12" + resolved "https://registry.yarnpkg.com/@types/bonjour/-/bonjour-3.5.12.tgz#49badafb988e6c433ca675a5fd769b93b7649fc8" + integrity sha512-ky0kWSqXVxSqgqJvPIkgFkcn4C8MnRog308Ou8xBBIVo39OmUFy+jqNe0nPwLCDFxUpmT9EvT91YzOJgkDRcFg== + dependencies: + "@types/node" "*" + +"@types/connect-history-api-fallback@^1.3.5": + version "1.5.2" + resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.2.tgz#acf51e088b3bb6507f7b093bd2b0de20940179cc" + integrity sha512-gX2j9x+NzSh4zOhnRPSdPPmTepS4DfxES0AvIFv3jGv5QyeAJf6u6dY5/BAoAJU9Qq1uTvwOku8SSC2GnCRl6Q== + dependencies: + "@types/express-serve-static-core" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.37" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.37.tgz#c66a96689fd3127c8772eb3e9e5c6028ec1a9af5" + integrity sha512-zBUSRqkfZ59OcwXon4HVxhx5oWCJmc0OtBTK05M+p0dYjgN6iTwIL2T/WbsQZrEsdnwaF9cWQ+azOnpPvIqY3Q== + dependencies: + "@types/node" "*" + +"@types/debug@^4.0.0": + version "4.1.10" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.10.tgz#f23148a6eb771a34c466a4fc28379d8101e84494" + integrity sha512-tOSCru6s732pofZ+sMv9o4o3Zc+Sa8l3bxd/tweTQudFn06vAzb13ZX46Zi6m6EJ+RUbRTHvgQJ1gBtSgkaUYA== + dependencies: + "@types/ms" "*" + +"@types/eslint-scope@^3.7.3": + version "3.7.6" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.6.tgz#585578b368ed170e67de8aae7b93f54a1b2fdc26" + integrity sha512-zfM4ipmxVKWdxtDaJ3MP3pBurDXOCoyjvlpE3u6Qzrmw4BPbfm4/ambIeTk/r/J0iq/+2/xp0Fmt+gFvXJY2PQ== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*": + version "8.44.6" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.44.6.tgz#60e564551966dd255f4c01c459f0b4fb87068603" + integrity sha512-P6bY56TVmX8y9J87jHNgQh43h6VVU+6H7oN7hgvivV81K2XY8qJZ5vqPy/HdUoVIelii2kChYVzQanlswPWVFw== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree-jsx@^1.0.0": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@types/estree-jsx/-/estree-jsx-1.0.2.tgz#758bcb4f35f2a970362b2bd2b7021fe2ae6e8509" + integrity sha512-GNBWlGBMjiiiL5TSkvPtOteuXsiVitw5MYGY1UYlrAq0SKyczsls6sCD7TZ8fsjRsvCVxml7EbyjJezPb3DrSA== + dependencies: + "@types/estree" "*" + +"@types/estree@*", "@types/estree@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.3.tgz#2be19e759a3dd18c79f9f436bd7363556c1a73dd" + integrity sha512-CS2rOaoQ/eAgAfcTfq6amKG7bsN+EMcgGY4FAFQdvSj2y1ixvOZTUA9mOtCai7E1SYu283XNw7urKK30nP3wkQ== + +"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33": + version "4.17.39" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.39.tgz#2107afc0a4b035e6cb00accac3bdf2d76ae408c8" + integrity sha512-BiEUfAiGCOllomsRAZOiMFP7LAnrifHpt56pc4Z7l9K6ACyN06Ns1JLMBxwkfLOjJRlSf06NwWsT7yzfpaVpyQ== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@*", "@types/express@^4.17.13": + version "4.17.20" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.20.tgz#e7c9b40276d29e38a4e3564d7a3d65911e2aa433" + integrity sha512-rOaqlkgEvOW495xErXMsmyX3WKBInbhG5eqojXYi3cGUaLoRDlXa5d52fkfWZT963AZ3v2eZ4MbKE6WpDAGVsw== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/gtag.js@^0.0.12": + version "0.0.12" + resolved "https://registry.yarnpkg.com/@types/gtag.js/-/gtag.js-0.0.12.tgz#095122edca896689bdfcdd73b057e23064d23572" + integrity sha512-YQV9bUsemkzG81Ea295/nF/5GijnD2Af7QhEofh7xu+kvCN6RdodgNwwGWXB5GMI3NoyvQo0odNctoH/qLMIpg== + +"@types/hast@^3.0.0": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/hast/-/hast-3.0.2.tgz#e6c1126a33955cb9493a5074ddf1873fb48248c7" + integrity sha512-B5hZHgHsXvfCoO3xgNJvBnX7N8p86TqQeGKXcokW4XXi+qY4vxxPSFYofytvVmpFxzPv7oxDQzjg5Un5m2/xiw== + dependencies: + "@types/unist" "*" + +"@types/history@^4.7.11": + version "4.7.11" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64" + integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA== + +"@types/html-minifier-terser@^6.0.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35" + integrity sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg== + +"@types/http-cache-semantics@^4.0.2": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.3.tgz#a3ff232bf7d5c55f38e4e45693eda2ebb545794d" + integrity sha512-V46MYLFp08Wf2mmaBhvgjStM3tPa+2GAdy/iqoX+noX1//zje2x4XmrIU0cAwyClATsTmahbtoQ2EwP7I5WSiA== + +"@types/http-errors@*": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.3.tgz#c54e61f79b3947d040f150abd58f71efb422ff62" + integrity sha512-pP0P/9BnCj1OVvQR2lF41EkDG/lWWnDyA203b/4Fmi2eTyORnBtcDoKDwjWQthELrBvWkMOrvSOnZ8OVlW6tXA== + +"@types/http-proxy@^1.17.8": + version "1.17.13" + resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.13.tgz#dd3a4da550580eb0557d4c7128a2ff1d1a38d465" + integrity sha512-GkhdWcMNiR5QSQRYnJ+/oXzu0+7JJEPC8vkWXK351BkhjraZF+1W13CUYARUvX9+NqIU2n6YHA4iwywsc/M6Sw== + dependencies: + "@types/node" "*" + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#fdfdd69fa16d530047d9963635bd77c71a08c068" + integrity sha512-zONci81DZYCZjiLe0r6equvZut0b+dBRPBN5kBDjsONnutYNtJMoWQ9uR2RkL1gLG9NMTzvf+29e5RFfPbeKhQ== + +"@types/istanbul-lib-report@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.2.tgz#394798d5f727402eb5ec99eb9618ffcd2b7645a1" + integrity sha512-8toY6FgdltSdONav1XtUHl4LN1yTmLza+EuDazb/fEmRNCwjyqNVIQWs2IfC74IqjHkREs/nQ2FWq5kZU9IC0w== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^3.0.0": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.3.tgz#0313e2608e6d6955d195f55361ddeebd4b74c6e7" + integrity sha512-1nESsePMBlf0RPRffLZi5ujYh7IH1BWL4y9pr+Bn3cJBdxz+RTP8bUFljLz9HvzhhOSWKdyBZ4DIivdL6rvgZg== + dependencies: + "@types/istanbul-lib-report" "*" + +"@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": + version "7.0.14" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.14.tgz#74a97a5573980802f32c8e47b663530ab3b6b7d1" + integrity sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw== + +"@types/mdast@^4.0.0", "@types/mdast@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.2.tgz#4695661024ffbd9e52cf71e05c69a1f08c0792f6" + integrity sha512-tYR83EignvhYO9iU3kDg8V28M0jqyh9zzp5GV+EO+AYnyUl3P5ltkTeJuTiFZQFz670FSb3EwT/6LQdX+UdKfw== + dependencies: + "@types/unist" "*" + +"@types/mdx@^2.0.0": + version "2.0.9" + resolved "https://registry.yarnpkg.com/@types/mdx/-/mdx-2.0.9.tgz#80971e367bb884350ab5b2ce8fc06b34960170e7" + integrity sha512-OKMdj17y8Cs+k1r0XFyp59ChSOwf8ODGtMQ4mnpfz5eFDk1aO41yN3pSKGuvVzmWAkFp37seubY1tzOVpwfWwg== + +"@types/mime@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.3.tgz#886674659ce55fe7c6c06ec5ca7c0eb276a08f91" + integrity sha512-i8MBln35l856k5iOhKk2XJ4SeAWg75mLIpZB4v6imOagKL6twsukBZGDMNhdOVk7yRFTMPpfILocMos59Q1otQ== + +"@types/mime@^1": + version "1.3.4" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.4.tgz#a4ed836e069491414bab92c31fdea9e557aca0d9" + integrity sha512-1Gjee59G25MrQGk8bsNvC6fxNiRgUlGn2wlhGf95a59DrprnnHk80FIMMFG9XHMdrfsuA119ht06QPDXA1Z7tw== + +"@types/ms@*": + version "0.7.33" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.33.tgz#80bf1da64b15f21fd8c1dc387c31929317d99ee9" + integrity sha512-AuHIyzR5Hea7ij0P9q7vx7xu4z0C28ucwjAZC0ja7JhINyCnOw8/DnvAPQQ9TfOlCtZAmCERKQX9+o1mgQhuOQ== + +"@types/node-forge@^1.3.0": + version "1.3.8" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.8.tgz#044ad98354ff309a031a55a40ad122f3be1ac2bb" + integrity sha512-vGXshY9vim9CJjrpcS5raqSjEfKlJcWy2HNdgUasR66fAnVEYarrf1ULV4nfvpC1nZq/moA9qyqBcu83x+Jlrg== + dependencies: + "@types/node" "*" + +"@types/node@*": + version "20.8.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.9.tgz#646390b4fab269abce59c308fc286dcd818a2b08" + integrity sha512-UzykFsT3FhHb1h7yD4CA4YhBHq545JC0YnEz41xkipN88eKQtL6rSgocL5tbAP6Ola9Izm/Aw4Ora8He4x0BHg== + dependencies: + undici-types "~5.26.4" + +"@types/node@^17.0.5": + version "17.0.45" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190" + integrity sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw== + +"@types/parse-json@^4.0.0": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.1.tgz#27f7559836ad796cea31acb63163b203756a5b4e" + integrity sha512-3YmXzzPAdOTVljVMkTMBdBEvlOLg2cDQaDhnnhT3nT9uDbnJzjWhKlzb+desT12Y7tGqaN6d+AbozcKzyL36Ng== + +"@types/prismjs@^1.26.0": + version "1.26.2" + resolved "https://registry.yarnpkg.com/@types/prismjs/-/prismjs-1.26.2.tgz#f574066903a7f0d516353581185db74326100edc" + integrity sha512-/r7Cp7iUIk7gts26mHXD66geUC+2Fo26TZYjQK6Nr4LDfi6lmdRmMqM0oPwfiMhUwoBAOFe8GstKi2pf6hZvwA== + +"@types/prop-types@*": + version "15.7.9" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.9.tgz#b6f785caa7ea1fe4414d9df42ee0ab67f23d8a6d" + integrity sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g== + +"@types/qs@*": + version "6.9.9" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.9.tgz#66f7b26288f6799d279edf13da7ccd40d2fa9197" + integrity sha512-wYLxw35euwqGvTDx6zfY1vokBFnsK0HNrzc6xNHchxfO2hpuRg74GbkEW7e3sSmPvj0TjCDT1VCa6OtHXnubsg== + +"@types/range-parser@*": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.6.tgz#7cb33992049fd7340d5b10c0098e104184dfcd2a" + integrity sha512-+0autS93xyXizIYiyL02FCY8N+KkKPhILhcUSA276HxzreZ16kl+cmwvV2qAM/PuCCwPXzOXOWhiPcw20uSFcA== + +"@types/react-router-config@*", "@types/react-router-config@^5.0.7": + version "5.0.9" + resolved "https://registry.yarnpkg.com/@types/react-router-config/-/react-router-config-5.0.9.tgz#8dad16c6b887d08efaa6d9da83e40db629ac21b6" + integrity sha512-a7zOj9yVUtM3Ns5stoseQAAsmppNxZpXDv6tZiFV5qlRmV4W96u53on1vApBX1eRSc8mrFOiB54Hc0Pk1J8GFg== + dependencies: + "@types/history" "^4.7.11" + "@types/react" "*" + "@types/react-router" "^5.1.0" + +"@types/react-router-dom@*": + version "5.3.3" + resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83" + integrity sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw== + dependencies: + "@types/history" "^4.7.11" + "@types/react" "*" + "@types/react-router" "*" + +"@types/react-router@*", "@types/react-router@^5.1.0": + version "5.1.20" + resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.20.tgz#88eccaa122a82405ef3efbcaaa5dcdd9f021387c" + integrity sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q== + dependencies: + "@types/history" "^4.7.11" + "@types/react" "*" + +"@types/react@*": + version "18.2.33" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.33.tgz#055356243dc4350a9ee6c6a2c07c5cae12e38877" + integrity sha512-v+I7S+hu3PIBoVkKGpSYYpiBT1ijqEzWpzQD62/jm4K74hPpSP7FF9BnKG6+fg2+62weJYkkBWDJlZt5JO/9hg== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + +"@types/retry@0.12.0": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" + integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== + +"@types/sax@^1.2.1": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@types/sax/-/sax-1.2.6.tgz#6e563455034014db5ab9e24a929404cf9b43adc3" + integrity sha512-A1mpYCYu1aHFayy8XKN57ebXeAbh9oQIZ1wXcno6b1ESUAfMBDMx7mf/QGlYwcMRaFryh9YBuH03i/3FlPGDkQ== + dependencies: + "@types/node" "*" + +"@types/scheduler@*": + version "0.16.5" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.5.tgz#4751153abbf8d6199babb345a52e1eb4167d64af" + integrity sha512-s/FPdYRmZR8SjLWGMCuax7r3qCWQw9QKHzXVukAuuIJkXkDRwp+Pu5LMIVFi0Fxbav35WURicYr8u1QsoybnQw== + +"@types/send@*": + version "0.17.3" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.3.tgz#81b2ea5a3a18aad357405af2d643ccbe5a09020b" + integrity sha512-/7fKxvKUoETxjFUsuFlPB9YndePpxxRAOfGC/yJdc9kTjTeP5kRCTzfnE8kPUKCeyiyIZu0YQ76s50hCedI1ug== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-index@^1.9.1": + version "1.9.3" + resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.3.tgz#af9403916eb6fbf7d6ec6f47b2a4c46eb3222cc9" + integrity sha512-4KG+yMEuvDPRrYq5fyVm/I2uqAJSAwZK9VSa+Zf+zUq9/oxSSvy3kkIqyL+jjStv6UCVi8/Aho0NHtB1Fwosrg== + dependencies: + "@types/express" "*" + +"@types/serve-static@*", "@types/serve-static@^1.13.10": + version "1.15.4" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.4.tgz#44b5895a68ca637f06c229119e1c774ca88f81b2" + integrity sha512-aqqNfs1XTF0HDrFdlY//+SGUxmdSUbjeRXb5iaZc3x0/vMbYmdw9qvOgHWOyyLFxSSRnUuP5+724zBgfw8/WAw== + dependencies: + "@types/http-errors" "*" + "@types/mime" "*" + "@types/node" "*" + +"@types/sockjs@^0.3.33": + version "0.3.35" + resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.35.tgz#f4a568c73d2a8071944bd6ffdca0d4e66810cd21" + integrity sha512-tIF57KB+ZvOBpAQwSaACfEu7htponHXaFzP7RfKYgsOS0NoYnn+9+jzp7bbq4fWerizI3dTB4NfAZoyeQKWJLw== + dependencies: + "@types/node" "*" + +"@types/unist@*", "@types/unist@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.1.tgz#778652d02ddec1bfc9e5e938fec8d407b8e56cba" + integrity sha512-ue/hDUpPjC85m+PM9OQDMZr3LywT+CT6mPsQq8OJtCLiERkGRcQUFvu9XASF5XWqyZFXbf15lvb3JFJ4dRLWPg== + +"@types/unist@^2.0.0": + version "2.0.9" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.9.tgz#72e164381659a49557b0a078b28308f2c6a3e1ce" + integrity sha512-zC0iXxAv1C1ERURduJueYzkzZ2zaGyc+P2c95hgkikHPr3z8EdUZOlgEQ5X0DRmwDZn+hekycQnoeiiRVrmilQ== + +"@types/ws@^8.5.5": + version "8.5.8" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.8.tgz#13efec7bd439d0bdf2af93030804a94f163b1430" + integrity sha512-flUksGIQCnJd6sZ1l5dqCEG/ksaoAg/eUwiLAGTJQcfgvZJKF++Ta4bJA6A5aPSJmsr+xlseHn4KLgVlNnvPTg== + dependencies: + "@types/node" "*" + +"@types/yargs-parser@*": + version "21.0.2" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.2.tgz#7bd04c5da378496ef1695a1008bf8f71847a8b8b" + integrity sha512-5qcvofLPbfjmBfKaLfj/+f+Sbd6pN4zl7w7VSVI5uz7m9QZTuB2aZAa2uo1wHFBNN2x6g/SoTkXmd8mQnQF2Cw== + +"@types/yargs@^17.0.8": + version "17.0.29" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.29.tgz#06aabc72497b798c643c812a8b561537fea760cf" + integrity sha512-nacjqA3ee9zRF/++a3FUY1suHTFKZeHba2n8WeDw9cCVdmzmHpIxyzOJBcpHvvEmS8E9KqWlSnWHUkOrkhWcvA== + dependencies: + "@types/yargs-parser" "*" + +"@uiw/icons@~2.6.0": + version "2.6.10" + resolved "https://registry.yarnpkg.com/@uiw/icons/-/icons-2.6.10.tgz#d7fc9580f227032d100e2c7b7259d5c9b08a1560" + integrity sha512-+tDpgrbJWjy5dnR3CTqItnc7yQuB2spDU8OmQCUUTaMEJ/v9nTZfZ8rDro4P//SN+QYPQQ0lmJbTj8/KOWeV5Q== + +"@uiw/react-alert@^4.22.2": + version "4.22.2" + resolved "https://registry.yarnpkg.com/@uiw/react-alert/-/react-alert-4.22.2.tgz#a12805672f6970df27e4a056d593a785769e9611" + integrity sha512-wQyc29cXOR8ojXRFhnLo/mg0psdlcThCpmcB8maTZH/LF+q6XHrIqcqKy+Jzpm89FyTf+aknNwUADQEpWxIfBQ== + dependencies: + "@uiw/react-modal" "^4.22.2" + "@uiw/utils" "^4.22.2" + +"@uiw/react-button@^4.22.2": + version "4.22.2" + resolved "https://registry.yarnpkg.com/@uiw/react-button/-/react-button-4.22.2.tgz#6bc69ecf57b4af44cd3e40bff811d2724049bafa" + integrity sha512-qBDEwdmMBBJRWSI3qfm+bZltk2qYeC8J74cLgEL4J7HKBvvQaQGvvWcQ5h1UmGQCiSDUY58THSM2vzsoJq0Weg== + dependencies: + "@uiw/react-icon" "^4.22.2" + "@uiw/utils" "^4.22.2" + +"@uiw/react-drawer@^4.22.2": + version "4.22.2" + resolved "https://registry.yarnpkg.com/@uiw/react-drawer/-/react-drawer-4.22.2.tgz#31d0180306ae0a013c2fb2d5b0edcfc269903f9b" + integrity sha512-UGiDimo0F1YNiBCmwiKnmN/aADwX7sCp4mGz8OuwK752YSOkdGzjNb7ZvZG3YhbyQs8QmUhMvjC3A3IgwEDBoQ== + dependencies: + "@uiw/react-button" "^4.22.2" + "@uiw/react-icon" "^4.22.2" + "@uiw/react-overlay" "^4.22.2" + "@uiw/utils" "^4.22.2" + +"@uiw/react-icon@^4.22.2": + version "4.22.2" + resolved "https://registry.yarnpkg.com/@uiw/react-icon/-/react-icon-4.22.2.tgz#5dba79d195e72b598a3913cdd952f676a79cf09e" + integrity sha512-Bi+SLPbWw83YJWnEJ6US/kl/OBDxnugEzm+W7FSRH2nJ3W8RPy9gsydTZi6bB4Ily6sGGqLoymmKceeL0wRZTw== + dependencies: + "@uiw/icons" "~2.6.0" + +"@uiw/react-modal@^4.22.2": + version "4.22.2" + resolved "https://registry.yarnpkg.com/@uiw/react-modal/-/react-modal-4.22.2.tgz#fca0f0eb203df25d92bf973cf015d41dc111cf0e" + integrity sha512-UmU8uTrw2dA+626OvN80kQ+WmUJ9jd1hMpA86YME5HyuOCLJQ5bXozkv7KL3b6Lhqo/XZ9e8/EdZPr5GV4nQag== + dependencies: + "@uiw/react-button" "^4.22.2" + "@uiw/react-icon" "^4.22.2" + "@uiw/react-overlay" "^4.22.2" + "@uiw/utils" "^4.22.2" + +"@uiw/react-notify@^4.22.2": + version "4.22.2" + resolved "https://registry.yarnpkg.com/@uiw/react-notify/-/react-notify-4.22.2.tgz#32043e7d03246ea1e2df83fbdc73732637b1839e" + integrity sha512-ONjmC0rHr2idvgBwCCar8pd3IVwDL6s1+BukOpgmUYJ0xz6J9TYp8HLRykz6UJKq82kpDnX5NHRipiiIEpx5Yw== + dependencies: + "@uiw/react-alert" "^4.22.2" + "@uiw/react-button" "^4.22.2" + "@uiw/react-icon" "^4.22.2" + "@uiw/utils" "^4.22.2" + +"@uiw/react-overlay-trigger@^4.22.2": + version "4.22.2" + resolved "https://registry.yarnpkg.com/@uiw/react-overlay-trigger/-/react-overlay-trigger-4.22.2.tgz#48a57ca343ad5c93e3cfe18548d301d9ed64198f" + integrity sha512-EdHMi8w7VfiNId8j5xm1M0TDSL8wSvERUY44XpQKPk4h0WGZA2TPSmmQnFQipqM5TUEJ4KUk5oWDlsPoexsEwA== + dependencies: + "@uiw/react-overlay" "^4.22.2" + "@uiw/utils" "^4.22.2" + +"@uiw/react-overlay@^4.22.2": + version "4.22.2" + resolved "https://registry.yarnpkg.com/@uiw/react-overlay/-/react-overlay-4.22.2.tgz#5e71fb79de590c0c8c07adec65f21c9d3317a14e" + integrity sha512-xHxaUT1Jfhl5CobrZmUNaC6HwYGi1eNVFOCN8WB/p9/sVmytet0ivN7io8gTRkaOirs/B6apktBTxPrFau/ziQ== + dependencies: + "@uiw/react-portal" "^4.22.2" + "@uiw/utils" "^4.22.2" + react-transition-group "~4.4.2" + +"@uiw/react-popover@^4.22.2": + version "4.22.2" + resolved "https://registry.yarnpkg.com/@uiw/react-popover/-/react-popover-4.22.2.tgz#b656971282bce12178ea05d788c188870a6218b9" + integrity sha512-Md9q8kUK9oeubkZw1lrxU+kFfIk5vR8hbDmdqPQc0FVdKYbELVMjat+FRHNvd8nzbweiva1oDjAoaePkOdvWhA== + dependencies: + "@uiw/react-button" "^4.22.2" + "@uiw/react-icon" "^4.22.2" + "@uiw/react-overlay-trigger" "^4.22.2" + +"@uiw/react-portal@^4.22.2": + version "4.22.2" + resolved "https://registry.yarnpkg.com/@uiw/react-portal/-/react-portal-4.22.2.tgz#8b06bb0bb95b44839fe8b1fbbe28db39d2c6e1cd" + integrity sha512-qmLOIRYwRwmLjd1G9Aha2A6XdtMdyPrkNBBfGhCY9JqTa9IoYddFkcVpJYyGIpPcNeB5a+mvyx/qfnC3DmnxHg== + +"@uiw/react-tooltip@^4.22.2": + version "4.22.2" + resolved "https://registry.yarnpkg.com/@uiw/react-tooltip/-/react-tooltip-4.22.2.tgz#c3e30fbce902052a7410fbdf37431d84e574e283" + integrity sha512-YEDAK9g46QqpFZzUcBrRIoqVj61HzAZHg0MiIbAR4KudXlhgxh/VnfpUezEURA01QjxdvUYX3bRzmWcW1+IVwQ== + dependencies: + "@uiw/react-overlay-trigger" "^4.22.2" + "@uiw/utils" "^4.22.2" + +"@uiw/utils@^4.22.2": + version "4.22.2" + resolved "https://registry.yarnpkg.com/@uiw/utils/-/utils-4.22.2.tgz#579fe11d3674abb5209575869dc06d17f4bd2922" + integrity sha512-FwbJPvUKTMGOXZZcKvvYwNoGT/vql3T+TWgjIi21GqlkVEoAcmO/2zrfRzOQzs3SNwlMmv5B7b0ZrAfEDzjDcg== + +"@ungap/structured-clone@^1.0.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" + integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== + +"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" + integrity sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q== + dependencies: + "@webassemblyjs/helper-numbers" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + +"@webassemblyjs/floating-point-hex-parser@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" + integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== + +"@webassemblyjs/helper-api-error@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" + integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== + +"@webassemblyjs/helper-buffer@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz#b66d73c43e296fd5e88006f18524feb0f2c7c093" + integrity sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA== + +"@webassemblyjs/helper-numbers@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" + integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" + integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== + +"@webassemblyjs/helper-wasm-section@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz#ff97f3863c55ee7f580fd5c41a381e9def4aa577" + integrity sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + +"@webassemblyjs/ieee754@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" + integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" + integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" + integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== + +"@webassemblyjs/wasm-edit@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz#c72fa8220524c9b416249f3d94c2958dfe70ceab" + integrity sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/wasm-opt" "1.11.6" + "@webassemblyjs/wasm-parser" "1.11.6" + "@webassemblyjs/wast-printer" "1.11.6" + +"@webassemblyjs/wasm-gen@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz#fb5283e0e8b4551cc4e9c3c0d7184a65faf7c268" + integrity sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wasm-opt@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz#d9a22d651248422ca498b09aa3232a81041487c2" + integrity sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/wasm-parser" "1.11.6" + +"@webassemblyjs/wasm-parser@1.11.6", "@webassemblyjs/wasm-parser@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz#bb85378c527df824004812bbdb784eea539174a1" + integrity sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wast-printer@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz#a7bf8dd7e362aeb1668ff43f35cb849f188eff20" + integrity sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@xtuc/long" "4.2.2" + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-import-assertions@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" + integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== + +acorn-jsx@^5.0.0: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn-walk@^8.0.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.0.tgz#2097665af50fd0cf7a2dfccd2b9368964e66540f" + integrity sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA== + +acorn@^8.0.0, acorn@^8.0.4, acorn@^8.7.1, acorn@^8.8.2: + version "8.11.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b" + integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w== + +address@^1.0.1, address@^1.1.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/address/-/address-1.2.2.tgz#2b5248dac5485a6390532c6a517fda2e3faac89e" + integrity sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA== + +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + +ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + +ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv-keywords@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" + integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== + dependencies: + fast-deep-equal "^3.1.3" + +ajv@^6.12.2, ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^8.0.0, ajv@^8.9.0: + version "8.12.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" + integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +algoliasearch-helper@^3.13.3: + version "3.15.0" + resolved "https://registry.yarnpkg.com/algoliasearch-helper/-/algoliasearch-helper-3.15.0.tgz#d680783329920a3619a74504dccb97a4fb943443" + integrity sha512-DGUnK3TGtDQsaUE4ayF/LjSN0DGsuYThB8WBgnnDY0Wq04K6lNVruO3LfqJOgSfDiezp+Iyt8Tj4YKHi+/ivSA== + dependencies: + "@algolia/events" "^4.0.1" + +algoliasearch@^4.18.0, algoliasearch@^4.19.1: + version "4.20.0" + resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-4.20.0.tgz#700c2cb66e14f8a288460036c7b2a554d0d93cf4" + integrity sha512-y+UHEjnOItoNy0bYO+WWmLWBlPwDjKHW6mNHrPi0NkuhpQOOEbrkwQH/wgKFDLh7qlKjzoKeiRtlpewDPDG23g== + dependencies: + "@algolia/cache-browser-local-storage" "4.20.0" + "@algolia/cache-common" "4.20.0" + "@algolia/cache-in-memory" "4.20.0" + "@algolia/client-account" "4.20.0" + "@algolia/client-analytics" "4.20.0" + "@algolia/client-common" "4.20.0" + "@algolia/client-personalization" "4.20.0" + "@algolia/client-search" "4.20.0" + "@algolia/logger-common" "4.20.0" + "@algolia/logger-console" "4.20.0" + "@algolia/requester-browser-xhr" "4.20.0" + "@algolia/requester-common" "4.20.0" + "@algolia/requester-node-http" "4.20.0" + "@algolia/transporter" "4.20.0" + +ansi-align@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" + integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w== + dependencies: + string-width "^4.1.0" + +ansi-html-community@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" + integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-regex@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" + integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +arg@^5.0.0: + version "5.0.2" + resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" + integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +array-flatten@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" + integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +asap@~2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== + +astring@^1.8.0: + version "1.8.6" + resolved "https://registry.yarnpkg.com/astring/-/astring-1.8.6.tgz#2c9c157cf1739d67561c56ba896e6948f6b93731" + integrity sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + +autoprefixer@^10.4.12, autoprefixer@^10.4.14: + version "10.4.16" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.16.tgz#fad1411024d8670880bdece3970aa72e3572feb8" + integrity sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ== + dependencies: + browserslist "^4.21.10" + caniuse-lite "^1.0.30001538" + fraction.js "^4.3.6" + normalize-range "^0.1.2" + picocolors "^1.0.0" + postcss-value-parser "^4.2.0" + +axios@^0.19.0: + version "0.19.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27" + integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA== + dependencies: + follow-redirects "1.5.10" + +axios@^0.27.2: + version "0.27.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" + integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== + dependencies: + follow-redirects "^1.14.9" + form-data "^4.0.0" + +babel-loader@^9.1.3: + version "9.1.3" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.1.3.tgz#3d0e01b4e69760cc694ee306fe16d358aa1c6f9a" + integrity sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw== + dependencies: + find-cache-dir "^4.0.0" + schema-utils "^4.0.0" + +babel-plugin-dynamic-import-node@^2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3" + integrity sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ== + dependencies: + object.assign "^4.1.0" + +babel-plugin-polyfill-corejs2@^0.4.6: + version "0.4.6" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.6.tgz#b2df0251d8e99f229a8e60fc4efa9a68b41c8313" + integrity sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q== + dependencies: + "@babel/compat-data" "^7.22.6" + "@babel/helper-define-polyfill-provider" "^0.4.3" + semver "^6.3.1" + +babel-plugin-polyfill-corejs3@^0.8.5: + version "0.8.6" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.6.tgz#25c2d20002da91fe328ff89095c85a391d6856cf" + integrity sha512-leDIc4l4tUgU7str5BWLS2h8q2N4Nf6lGZP6UrNDxdtfF2g69eJ5L0H7S8A5Ln/arfFAfHor5InAdZuIOwZdgQ== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.4.3" + core-js-compat "^3.33.1" + +babel-plugin-polyfill-regenerator@^0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.3.tgz#d4c49e4b44614607c13fb769bcd85c72bb26a4a5" + integrity sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.4.3" + +bail@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/bail/-/bail-2.0.2.tgz#d26f5cd8fe5d6f832a31517b9f7c356040ba6d5d" + integrity sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base16@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/base16/-/base16-1.0.0.tgz#e297f60d7ec1014a7a971a39ebc8a98c0b681e70" + integrity sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ== + +batch@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" + integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== + +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +body-parser@1.20.1: + version "1.20.1" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" + integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" + +bonjour-service@^1.0.11: + version "1.1.1" + resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.1.1.tgz#960948fa0e0153f5d26743ab15baf8e33752c135" + integrity sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg== + dependencies: + array-flatten "^2.1.2" + dns-equal "^1.0.0" + fast-deep-equal "^3.1.3" + multicast-dns "^7.2.5" + +boolbase@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== + +boxen@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-6.2.1.tgz#b098a2278b2cd2845deef2dff2efc38d329b434d" + integrity sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw== + dependencies: + ansi-align "^3.0.1" + camelcase "^6.2.0" + chalk "^4.1.2" + cli-boxes "^3.0.0" + string-width "^5.0.1" + type-fest "^2.5.0" + widest-line "^4.0.1" + wrap-ansi "^8.0.1" + +boxen@^7.0.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-7.1.1.tgz#f9ba525413c2fec9cdb88987d835c4f7cad9c8f4" + integrity sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog== + dependencies: + ansi-align "^3.0.1" + camelcase "^7.0.1" + chalk "^5.2.0" + cli-boxes "^3.0.0" + string-width "^5.1.2" + type-fest "^2.13.0" + widest-line "^4.0.1" + wrap-ansi "^8.1.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.2, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.18.1, browserslist@^4.21.10, browserslist@^4.21.4, browserslist@^4.21.9, browserslist@^4.22.1: + version "4.22.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.1.tgz#ba91958d1a59b87dab6fed8dfbcb3da5e2e9c619" + integrity sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ== + dependencies: + caniuse-lite "^1.0.30001541" + electron-to-chromium "^1.4.535" + node-releases "^2.0.13" + update-browserslist-db "^1.0.13" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +cacheable-lookup@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz#3476a8215d046e5a3202a9209dd13fec1f933a27" + integrity sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w== + +cacheable-request@^10.2.8: + version "10.2.14" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-10.2.14.tgz#eb915b665fda41b79652782df3f553449c406b9d" + integrity sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ== + dependencies: + "@types/http-cache-semantics" "^4.0.2" + get-stream "^6.0.1" + http-cache-semantics "^4.1.1" + keyv "^4.5.3" + mimic-response "^4.0.0" + normalize-url "^8.0.0" + responselike "^3.0.0" + +call-bind@^1.0.0, call-bind@^1.0.2: + version "1.0.5" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.5.tgz#6fa2b7845ce0ea49bf4d8b9ef64727a2c2e2e513" + integrity sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ== + dependencies: + function-bind "^1.1.2" + get-intrinsic "^1.2.1" + set-function-length "^1.1.1" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camel-case@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a" + integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw== + dependencies: + pascal-case "^3.1.2" + tslib "^2.0.3" + +camelcase@^6.2.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +camelcase@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-7.0.1.tgz#f02e50af9fd7782bc8b88a3558c32fd3a388f048" + integrity sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw== + +caniuse-api@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" + integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw== + dependencies: + browserslist "^4.0.0" + caniuse-lite "^1.0.0" + lodash.memoize "^4.1.2" + lodash.uniq "^4.5.0" + +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001538, caniuse-lite@^1.0.30001541: + version "1.0.30001558" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001558.tgz#d2c6e21fdbfe83817f70feab902421a19b7983ee" + integrity sha512-/Et7DwLqpjS47JPEcz6VnxU9PwcIdVi0ciLXRWBQdj1XFye68pSQYpV0QtPTfUKWuOaEig+/Vez2l74eDc1tPQ== + +ccount@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" + integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== + +chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chalk@^5.0.1, chalk@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" + integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== + +char-regex@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" + integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== + +character-entities-html4@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b" + integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA== + +character-entities-legacy@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b" + integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ== + +character-entities@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22" + integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ== + +character-reference-invalid@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz#85c66b041e43b47210faf401278abf808ac45cb9" + integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw== + +cheerio-select@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4" + integrity sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g== + dependencies: + boolbase "^1.0.0" + css-select "^5.1.0" + css-what "^6.1.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.0.1" + +cheerio@^1.0.0-rc.12, cheerio@^1.0.0-rc.3: + version "1.0.0-rc.12" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.12.tgz#788bf7466506b1c6bf5fae51d24a2c4d62e47683" + integrity sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q== + dependencies: + cheerio-select "^2.1.0" + dom-serializer "^2.0.0" + domhandler "^5.0.3" + domutils "^3.0.1" + htmlparser2 "^8.0.1" + parse5 "^7.0.0" + parse5-htmlparser2-tree-adapter "^7.0.0" + +chokidar@^3.4.2, chokidar@^3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chrome-trace-event@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" + integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== + +ci-info@^3.2.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" + integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== + +clean-css@^5.2.2, clean-css@^5.3.2, clean-css@~5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.2.tgz#70ecc7d4d4114921f5d298349ff86a31a9975224" + integrity sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww== + dependencies: + source-map "~0.6.0" + +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + +cli-boxes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-3.0.0.tgz#71a10c716feeba005e4504f36329ef0b17cf3145" + integrity sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g== + +cli-table3@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.3.tgz#61ab765aac156b52f222954ffc607a6f01dbeeb2" + integrity sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg== + dependencies: + string-width "^4.2.0" + optionalDependencies: + "@colors/colors" "1.5.0" + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +clsx@^1.1.1, clsx@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" + integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== + +clsx@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.0.0.tgz#12658f3fd98fafe62075595a5c30e43d18f3d00b" + integrity sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q== + +collapse-white-space@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-2.1.0.tgz#640257174f9f42c740b40f3b55ee752924feefca" + integrity sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw== + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +colord@^2.9.1: + version "2.9.3" + resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43" + integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw== + +colorette@^2.0.10: + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + +colors@^1.3.3, colors@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" + integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== + +combine-promises@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/combine-promises/-/combine-promises-1.2.0.tgz#5f2e68451862acf85761ded4d9e2af7769c2ca6a" + integrity sha512-VcQB1ziGD0NXrhKxiwyNbCDmRzs/OShMs2GqW2DlU2A/Sd0nQxE1oWDAE5O0ygSx5mgQOn9eIFh7yKPgFRVkPQ== + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +comma-separated-tokens@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" + integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== + +commander@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commander@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" + integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== + +commander@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + +commander@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" + integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== + +common-path-prefix@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/common-path-prefix/-/common-path-prefix-3.0.0.tgz#7d007a7e07c58c4b4d5f433131a19141b29f11e0" + integrity sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w== + +compressible@~2.0.16: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== + dependencies: + accepts "~1.3.5" + bytes "3.0.0" + compressible "~2.0.16" + debug "2.6.9" + on-headers "~1.0.2" + safe-buffer "5.1.2" + vary "~1.1.2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +config-chain@^1.1.11: + version "1.1.13" + resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4" + integrity sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ== + dependencies: + ini "^1.3.4" + proto-list "~1.2.1" + +configstore@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-6.0.0.tgz#49eca2ebc80983f77e09394a1a56e0aca8235566" + integrity sha512-cD31W1v3GqUlQvbBCGcXmd2Nj9SvLDOP1oQ0YFuLETufzSPaKp11rYBsSOm7rCsW3OnIRAFM3OxRhceaXNYHkA== + dependencies: + dot-prop "^6.0.1" + graceful-fs "^4.2.6" + unique-string "^3.0.0" + write-file-atomic "^3.0.3" + xdg-basedir "^5.0.1" + +connect-history-api-fallback@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" + integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== + +consola@^2.15.3: + version "2.15.3" + resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550" + integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw== + +content-disposition@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" + integrity sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + +copy-text-to-clipboard@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/copy-text-to-clipboard/-/copy-text-to-clipboard-3.2.0.tgz#0202b2d9bdae30a49a53f898626dcc3b49ad960b" + integrity sha512-RnJFp1XR/LOBDckxTib5Qjr/PMfkatD0MUCQgdpqS8MdKiNUzBjAQBEN6oUy+jW7LI93BBG3DtMB2KOOKpGs2Q== + +copy-webpack-plugin@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz#96d4dbdb5f73d02dd72d0528d1958721ab72e04a" + integrity sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ== + dependencies: + fast-glob "^3.2.11" + glob-parent "^6.0.1" + globby "^13.1.1" + normalize-path "^3.0.0" + schema-utils "^4.0.0" + serialize-javascript "^6.0.0" + +core-js-compat@^3.31.0, core-js-compat@^3.33.1: + version "3.33.2" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.33.2.tgz#3ea4563bfd015ad4e4b52442865b02c62aba5085" + integrity sha512-axfo+wxFVxnqf8RvxTzoAlzW4gRoacrHeoFlc9n0x50+7BEyZL/Rt3hicaED1/CEd7I6tPCPVUYcJwCMO5XUYw== + dependencies: + browserslist "^4.22.1" + +core-js-pure@^3.30.2: + version "3.33.2" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.33.2.tgz#644830db2507ef84d068a70980ccd99c275f5fa6" + integrity sha512-a8zeCdyVk7uF2elKIGz67AjcXOxjRbwOLz8SbklEso1V+2DoW4OkAMZN9S9GBgvZIaqQi/OemFX4OiSoQEmg1Q== + +core-js@^3.31.1: + version "3.33.2" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.33.2.tgz#312bbf6996a3a517c04c99b9909cdd27138d1ceb" + integrity sha512-XeBzWI6QL3nJQiHmdzbAOiMYqjrb7hwU7A39Qhvd/POSa/t9E1AeZyEZx3fNvp/vtM8zXwhoL0FsiS0hD0pruQ== + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cosmiconfig@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" + integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.1.0" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.7.2" + +cosmiconfig@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" + integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + +cosmiconfig@^8.1.3, cosmiconfig@^8.2.0: + version "8.3.6" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3" + integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA== + dependencies: + import-fresh "^3.3.0" + js-yaml "^4.1.0" + parse-json "^5.2.0" + path-type "^4.0.0" + +cross-fetch@^3.1.5: + version "3.1.8" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" + integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg== + dependencies: + node-fetch "^2.6.12" + +cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +crypto-random-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-4.0.0.tgz#5a3cc53d7dd86183df5da0312816ceeeb5bb1fc2" + integrity sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA== + dependencies: + type-fest "^1.0.1" + +css-declaration-sorter@^6.3.1: + version "6.4.1" + resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz#28beac7c20bad7f1775be3a7129d7eae409a3a71" + integrity sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g== + +css-loader@^6.8.1: + version "6.8.1" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.8.1.tgz#0f8f52699f60f5e679eab4ec0fcd68b8e8a50a88" + integrity sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g== + dependencies: + icss-utils "^5.1.0" + postcss "^8.4.21" + postcss-modules-extract-imports "^3.0.0" + postcss-modules-local-by-default "^4.0.3" + postcss-modules-scope "^3.0.0" + postcss-modules-values "^4.0.0" + postcss-value-parser "^4.2.0" + semver "^7.3.8" + +css-minimizer-webpack-plugin@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-4.2.2.tgz#79f6199eb5adf1ff7ba57f105e3752d15211eb35" + integrity sha512-s3Of/4jKfw1Hj9CxEO1E5oXhQAxlayuHO2y/ML+C6I9sQ7FdzfEV6QgMLN3vI+qFsjJGIAFLKtQK7t8BOXAIyA== + dependencies: + cssnano "^5.1.8" + jest-worker "^29.1.2" + postcss "^8.4.17" + schema-utils "^4.0.0" + serialize-javascript "^6.0.0" + source-map "^0.6.1" + +css-select@^4.1.3: + version "4.3.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" + integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ== + dependencies: + boolbase "^1.0.0" + css-what "^6.0.1" + domhandler "^4.3.1" + domutils "^2.8.0" + nth-check "^2.0.1" + +css-select@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" + integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== + dependencies: + boolbase "^1.0.0" + css-what "^6.1.0" + domhandler "^5.0.2" + domutils "^3.0.1" + nth-check "^2.0.1" + +css-tree@^1.1.2, css-tree@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" + integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== + dependencies: + mdn-data "2.0.14" + source-map "^0.6.1" + +css-tree@^2.2.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.3.1.tgz#10264ce1e5442e8572fc82fbe490644ff54b5c20" + integrity sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw== + dependencies: + mdn-data "2.0.30" + source-map-js "^1.0.1" + +css-tree@~2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.2.1.tgz#36115d382d60afd271e377f9c5f67d02bd48c032" + integrity sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA== + dependencies: + mdn-data "2.0.28" + source-map-js "^1.0.1" + +css-what@^6.0.1, css-what@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== + +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + +cssnano-preset-advanced@^5.3.10: + version "5.3.10" + resolved "https://registry.yarnpkg.com/cssnano-preset-advanced/-/cssnano-preset-advanced-5.3.10.tgz#25558a1fbf3a871fb6429ce71e41be7f5aca6eef" + integrity sha512-fnYJyCS9jgMU+cmHO1rPSPf9axbQyD7iUhLO5Df6O4G+fKIOMps+ZbU0PdGFejFBBZ3Pftf18fn1eG7MAPUSWQ== + dependencies: + autoprefixer "^10.4.12" + cssnano-preset-default "^5.2.14" + postcss-discard-unused "^5.1.0" + postcss-merge-idents "^5.1.1" + postcss-reduce-idents "^5.2.0" + postcss-zindex "^5.1.0" + +cssnano-preset-default@^5.2.14: + version "5.2.14" + resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz#309def4f7b7e16d71ab2438052093330d9ab45d8" + integrity sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A== + dependencies: + css-declaration-sorter "^6.3.1" + cssnano-utils "^3.1.0" + postcss-calc "^8.2.3" + postcss-colormin "^5.3.1" + postcss-convert-values "^5.1.3" + postcss-discard-comments "^5.1.2" + postcss-discard-duplicates "^5.1.0" + postcss-discard-empty "^5.1.1" + postcss-discard-overridden "^5.1.0" + postcss-merge-longhand "^5.1.7" + postcss-merge-rules "^5.1.4" + postcss-minify-font-values "^5.1.0" + postcss-minify-gradients "^5.1.1" + postcss-minify-params "^5.1.4" + postcss-minify-selectors "^5.2.1" + postcss-normalize-charset "^5.1.0" + postcss-normalize-display-values "^5.1.0" + postcss-normalize-positions "^5.1.1" + postcss-normalize-repeat-style "^5.1.1" + postcss-normalize-string "^5.1.0" + postcss-normalize-timing-functions "^5.1.0" + postcss-normalize-unicode "^5.1.1" + postcss-normalize-url "^5.1.0" + postcss-normalize-whitespace "^5.1.1" + postcss-ordered-values "^5.1.3" + postcss-reduce-initial "^5.1.2" + postcss-reduce-transforms "^5.1.0" + postcss-svgo "^5.1.0" + postcss-unique-selectors "^5.1.1" + +cssnano-utils@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-3.1.0.tgz#95684d08c91511edfc70d2636338ca37ef3a6861" + integrity sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA== + +cssnano@^5.1.15, cssnano@^5.1.8: + version "5.1.15" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.1.15.tgz#ded66b5480d5127fcb44dac12ea5a983755136bf" + integrity sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw== + dependencies: + cssnano-preset-default "^5.2.14" + lilconfig "^2.0.3" + yaml "^1.10.2" + +csso@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" + integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== + dependencies: + css-tree "^1.1.2" + +csso@^5.0.5: + version "5.0.5" + resolved "https://registry.yarnpkg.com/csso/-/csso-5.0.5.tgz#f9b7fe6cc6ac0b7d90781bb16d5e9874303e2ca6" + integrity sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ== + dependencies: + css-tree "~2.2.0" + +csstype@^3.0.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" + integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== + +debug@2.6.9, debug@^2.6.0: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +debug@=3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + +decode-named-character-reference@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e" + integrity sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg== + dependencies: + character-entities "^2.0.0" + +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== + dependencies: + mimic-response "^3.1.0" + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +deepmerge@^4.2.2, deepmerge@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + +default-gateway@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-6.0.3.tgz#819494c888053bdb743edbf343d6cdf7f2943a71" + integrity sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg== + dependencies: + execa "^5.0.0" + +defer-to-connect@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" + integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== + +define-data-property@^1.0.1, define-data-property@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.1.tgz#c35f7cd0ab09883480d12ac5cb213715587800b3" + integrity sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ== + dependencies: + get-intrinsic "^1.2.1" + gopd "^1.0.1" + has-property-descriptors "^1.0.0" + +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + +define-properties@^1.1.4: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + +del@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/del/-/del-6.1.1.tgz#3b70314f1ec0aa325c6b14eb36b95786671edb7a" + integrity sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg== + dependencies: + globby "^11.0.1" + graceful-fs "^4.2.4" + is-glob "^4.0.1" + is-path-cwd "^2.2.0" + is-path-inside "^3.0.2" + p-map "^4.0.0" + rimraf "^3.0.2" + slash "^3.0.0" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + +dequal@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-node@^2.0.4: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" + integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== + +detect-port-alt@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/detect-port-alt/-/detect-port-alt-1.1.6.tgz#24707deabe932d4a3cf621302027c2b266568275" + integrity sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q== + dependencies: + address "^1.0.1" + debug "^2.6.0" + +detect-port@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/detect-port/-/detect-port-1.5.1.tgz#451ca9b6eaf20451acb0799b8ab40dff7718727b" + integrity sha512-aBzdj76lueB6uUst5iAs7+0H/oOjqI5D16XUWxlWMIMROhcM0rfsNVk93zTngq1dDNpoXRr++Sus7ETAExppAQ== + dependencies: + address "^1.0.1" + debug "4" + +devlop@^1.0.0, devlop@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018" + integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA== + dependencies: + dequal "^2.0.0" + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +dns-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" + integrity sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg== + +dns-packet@^5.2.2: + version "5.6.1" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.6.1.tgz#ae888ad425a9d1478a0674256ab866de1012cf2f" + integrity sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw== + dependencies: + "@leichtgewicht/ip-codec" "^2.0.1" + +docusaurus-plugin-image-zoom@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/docusaurus-plugin-image-zoom/-/docusaurus-plugin-image-zoom-1.0.1.tgz#17afec39f2e630cac50a4ed3a8bbdad8d0aa8b9d" + integrity sha512-96IpSKUx2RWy3db9aZ0s673OQo5DWgV9UVWouS+CPOSIVEdCWh6HKmWf6tB9rsoaiIF3oNn9keiyv6neEyKb1Q== + dependencies: + medium-zoom "^1.0.6" + validate-peer-dependencies "^2.2.0" + +dom-converter@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" + integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA== + dependencies: + utila "~0.4" + +dom-helpers@^5.0.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" + integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^3.0.2" + +dom-serializer@^1.0.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" + integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.2.0" + entities "^2.0.0" + +dom-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.2" + entities "^4.2.0" + +domelementtype@^2.0.1, domelementtype@^2.2.0, domelementtype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + +domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" + integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== + dependencies: + domelementtype "^2.2.0" + +domhandler@^5.0.2, domhandler@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== + dependencies: + domelementtype "^2.3.0" + +domutils@^2.5.2, domutils@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" + integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== + dependencies: + dom-serializer "^1.0.1" + domelementtype "^2.2.0" + domhandler "^4.2.0" + +domutils@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" + integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + +dot-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" + integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + +dot-prop@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-6.0.1.tgz#fc26b3cf142b9e59b74dbd39ed66ce620c681083" + integrity sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA== + dependencies: + is-obj "^2.0.0" + +duplexer@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" + integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +electron-to-chromium@^1.4.535: + version "1.4.570" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.570.tgz#5fb79061ead248a411bc8532da34d1dbf6ae23c1" + integrity sha512-5GxH0PLSIfXKOUMMHMCT4M0olwj1WwAxsQHzVW5Vh3kbsvGw8b4k7LHQmTLC2aRhsgFzrF57XJomca4XLc/WHA== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +emojilib@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/emojilib/-/emojilib-2.4.0.tgz#ac518a8bb0d5f76dda57289ccb2fdf9d39ae721e" + integrity sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw== + +emojis-list@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" + integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== + +emoticon@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/emoticon/-/emoticon-4.0.1.tgz#2d2bbbf231ce3a5909e185bbb64a9da703a1e749" + integrity sha512-dqx7eA9YaqyvYtUhJwT4rC1HIp82j5ybS1/vQ42ur+jBe17dJMwZE4+gvL1XadSFfxaPFFGt3Xsw+Y8akThDlw== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +enhanced-resolve@^5.15.0: + version "5.15.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" + integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + +entities@^4.2.0, entities@^4.4.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-module-lexer@^1.2.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.3.1.tgz#c1b0dd5ada807a3b3155315911f364dc4e909db1" + integrity sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q== + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-goat@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-4.0.0.tgz#9424820331b510b0666b98f7873fe11ac4aa8081" + integrity sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg== + +escape-html@^1.0.3, escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +escape-string-regexp@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" + integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== + +eslint-scope@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +estree-util-attach-comments@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz#344bde6a64c8a31d15231e5ee9e297566a691c2d" + integrity sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw== + dependencies: + "@types/estree" "^1.0.0" + +estree-util-build-jsx@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz#b6d0bced1dcc4f06f25cf0ceda2b2dcaf98168f1" + integrity sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ== + dependencies: + "@types/estree-jsx" "^1.0.0" + devlop "^1.0.0" + estree-util-is-identifier-name "^3.0.0" + estree-walker "^3.0.0" + +estree-util-is-identifier-name@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz#0b5ef4c4ff13508b34dcd01ecfa945f61fce5dbd" + integrity sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg== + +estree-util-to-js@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz#10a6fb924814e6abb62becf0d2bc4dea51d04f17" + integrity sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg== + dependencies: + "@types/estree-jsx" "^1.0.0" + astring "^1.8.0" + source-map "^0.7.0" + +estree-util-value-to-estree@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/estree-util-value-to-estree/-/estree-util-value-to-estree-3.0.1.tgz#0b7b5d6b6a4aaad5c60999ffbc265a985df98ac5" + integrity sha512-b2tdzTurEIbwRh+mKrEcaWfu1wgb8J1hVsgREg7FFiecWwK/PhO8X0kyc+0bIcKNtD4sqxIdNoRy6/p/TvECEA== + dependencies: + "@types/estree" "^1.0.0" + is-plain-obj "^4.0.0" + +estree-util-visit@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/estree-util-visit/-/estree-util-visit-2.0.0.tgz#13a9a9f40ff50ed0c022f831ddf4b58d05446feb" + integrity sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/unist" "^3.0.0" + +estree-walker@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d" + integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== + dependencies: + "@types/estree" "^1.0.0" + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +eta@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/eta/-/eta-2.2.0.tgz#eb8b5f8c4e8b6306561a455e62cd7492fe3a9b8a" + integrity sha512-UVQ72Rqjy/ZKQalzV5dCCJP80GrmPrMxh6NlNf+erV6ObL0ZFkhCstWRawS85z3smdr3d2wXPsZEY7rDPfGd2g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +eval@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/eval/-/eval-0.1.8.tgz#2b903473b8cc1d1989b83a1e7923f883eb357f85" + integrity sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw== + dependencies: + "@types/node" "*" + require-like ">= 0.1.1" + +eventemitter3@^4.0.0: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +events@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +express@^4.17.3: + version "4.18.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" + integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.1" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.5.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + integrity sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug== + dependencies: + is-extendable "^0.1.0" + +extend@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.2.11, fast-glob@^3.2.9, fast-glob@^3.3.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" + integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-url-parser@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d" + integrity sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ== + dependencies: + punycode "^1.3.2" + +fastq@^1.6.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" + integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw== + dependencies: + reusify "^1.0.4" + +fault@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fault/-/fault-2.0.1.tgz#d47ca9f37ca26e4bd38374a7c500b5a384755b6c" + integrity sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ== + dependencies: + format "^0.2.0" + +faye-websocket@^0.11.3: + version "0.11.4" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" + integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== + dependencies: + websocket-driver ">=0.5.1" + +fbemitter@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/fbemitter/-/fbemitter-3.0.0.tgz#00b2a1af5411254aab416cd75f9e6289bee4bff3" + integrity sha512-KWKaceCwKQU0+HPoop6gn4eOHk50bBv/VxjJtGMfwmJt3D29JpN4H4eisCtIPA+a8GVBam+ldMMpMjJUvpDyHw== + dependencies: + fbjs "^3.0.0" + +fbjs-css-vars@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz#216551136ae02fe255932c3ec8775f18e2c078b8" + integrity sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ== + +fbjs@^3.0.0, fbjs@^3.0.1: + version "3.0.5" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-3.0.5.tgz#aa0edb7d5caa6340011790bd9249dbef8a81128d" + integrity sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg== + dependencies: + cross-fetch "^3.1.5" + fbjs-css-vars "^1.0.0" + loose-envify "^1.0.0" + object-assign "^4.1.0" + promise "^7.1.1" + setimmediate "^1.0.5" + ua-parser-js "^1.0.35" + +feed@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/feed/-/feed-4.2.2.tgz#865783ef6ed12579e2c44bbef3c9113bc4956a7e" + integrity sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ== + dependencies: + xml-js "^1.6.11" + +file-loader@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d" + integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + +filesize@^8.0.6: + version "8.0.7" + resolved "https://registry.yarnpkg.com/filesize/-/filesize-8.0.7.tgz#695e70d80f4e47012c132d57a059e80c6b580bd8" + integrity sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ== + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +find-cache-dir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-4.0.0.tgz#a30ee0448f81a3990708f6453633c733e2f6eec2" + integrity sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg== + dependencies: + common-path-prefix "^3.0.0" + pkg-dir "^7.0.0" + +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +find-up@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-6.3.0.tgz#2abab3d3280b2dc7ac10199ef324c4e002c8c790" + integrity sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw== + dependencies: + locate-path "^7.1.0" + path-exists "^5.0.0" + +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +flux@~4.0.1: + version "4.0.4" + resolved "https://registry.yarnpkg.com/flux/-/flux-4.0.4.tgz#9661182ea81d161ee1a6a6af10d20485ef2ac572" + integrity sha512-NCj3XlayA2UsapRpM7va6wU1+9rE5FIL7qoMcmxWHRzbp0yujihMBm9BBHZ1MDIk5h5o2Bl6eGiCe8rYELAmYw== + dependencies: + fbemitter "^3.0.0" + fbjs "^3.0.1" + +follow-redirects@1.5.10: + version "1.5.10" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" + integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== + dependencies: + debug "=3.1.0" + +follow-redirects@^1.0.0, follow-redirects@^1.14.9: + version "1.15.3" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" + integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== + +fork-ts-checker-webpack-plugin@^6.5.0: + version "6.5.3" + resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz#eda2eff6e22476a2688d10661688c47f611b37f3" + integrity sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ== + dependencies: + "@babel/code-frame" "^7.8.3" + "@types/json-schema" "^7.0.5" + chalk "^4.1.0" + chokidar "^3.4.2" + cosmiconfig "^6.0.0" + deepmerge "^4.2.2" + fs-extra "^9.0.0" + glob "^7.1.6" + memfs "^3.1.2" + minimatch "^3.0.4" + schema-utils "2.7.0" + semver "^7.3.2" + tapable "^1.0.0" + +form-data-encoder@^2.1.2: + version "2.1.4" + resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-2.1.4.tgz#261ea35d2a70d48d30ec7a9603130fa5515e9cd5" + integrity sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw== + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +format@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" + integrity sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww== + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fraction.js@^4.3.6: + version "4.3.7" + resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" + integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs-extra@^10.0.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" + integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-extra@^11.1.1: + version "11.1.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.1.tgz#da69f7c39f3b002378b0954bb6ae7efdc0876e2d" + integrity sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-extra@^9.0.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-monkey@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.5.tgz#fe450175f0db0d7ea758102e1d84096acb925788" + integrity sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-intrinsic@^1.0.2, get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.2.tgz#281b7622971123e1ef4b3c90fd7539306da93f3b" + integrity sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA== + dependencies: + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +get-own-enumerable-property-symbols@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" + integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== + +get-stream@^6.0.0, get-stream@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +github-slugger@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.5.0.tgz#17891bbc73232051474d68bd867a34625c955f7d" + integrity sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw== + +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.1: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +glob@^7.0.0, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +global-dirs@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.1.tgz#0c488971f066baceda21447aecb1a8b911d22485" + integrity sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA== + dependencies: + ini "2.0.0" + +global-modules@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" + integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== + dependencies: + global-prefix "^3.0.0" + +global-prefix@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" + integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== + dependencies: + ini "^1.3.5" + kind-of "^6.0.2" + which "^1.3.1" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globby@^11.0.1, globby@^11.0.4, globby@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +globby@^13.1.1: + version "13.2.2" + resolved "https://registry.yarnpkg.com/globby/-/globby-13.2.2.tgz#63b90b1bf68619c2135475cbd4e71e66aa090592" + integrity sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w== + dependencies: + dir-glob "^3.0.1" + fast-glob "^3.3.0" + ignore "^5.2.4" + merge2 "^1.4.1" + slash "^4.0.0" + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +got@^12.1.0: + version "12.6.1" + resolved "https://registry.yarnpkg.com/got/-/got-12.6.1.tgz#8869560d1383353204b5a9435f782df9c091f549" + integrity sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ== + dependencies: + "@sindresorhus/is" "^5.2.0" + "@szmarczak/http-timer" "^5.0.1" + cacheable-lookup "^7.0.0" + cacheable-request "^10.2.8" + decompress-response "^6.0.0" + form-data-encoder "^2.1.2" + get-stream "^6.0.1" + http2-wrapper "^2.1.10" + lowercase-keys "^3.0.0" + p-cancelable "^3.0.0" + responselike "^3.0.0" + +graceful-fs@4.2.10: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + +graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +gray-matter@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-4.0.3.tgz#e893c064825de73ea1f5f7d88c7a9f7274288798" + integrity sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q== + dependencies: + js-yaml "^3.13.1" + kind-of "^6.0.2" + section-matter "^1.0.0" + strip-bom-string "^1.0.0" + +gzip-size@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462" + integrity sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q== + dependencies: + duplexer "^0.1.2" + +handle-thing@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" + integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz#52ba30b6c5ec87fd89fa574bc1c39125c6f65340" + integrity sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg== + dependencies: + get-intrinsic "^1.2.2" + +has-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" + integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-yarn@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-3.0.0.tgz#c3c21e559730d1d3b57e28af1f30d06fac38147d" + integrity sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA== + +hasown@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" + integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== + dependencies: + function-bind "^1.1.2" + +hast-util-from-parse5@^8.0.0: + version "8.0.1" + resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-8.0.1.tgz#654a5676a41211e14ee80d1b1758c399a0327651" + integrity sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ== + dependencies: + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + devlop "^1.0.0" + hastscript "^8.0.0" + property-information "^6.0.0" + vfile "^6.0.0" + vfile-location "^5.0.0" + web-namespaces "^2.0.0" + +hast-util-parse-selector@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz#352879fa86e25616036037dd8931fb5f34cb4a27" + integrity sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A== + dependencies: + "@types/hast" "^3.0.0" + +hast-util-raw@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/hast-util-raw/-/hast-util-raw-9.0.1.tgz#2ba8510e4ed2a1e541cde2a4ebb5c38ab4c82c2d" + integrity sha512-5m1gmba658Q+lO5uqL5YNGQWeh1MYWZbZmWrM5lncdcuiXuo5E2HT/CIOp0rLF8ksfSwiCVJ3twlgVRyTGThGA== + dependencies: + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + "@ungap/structured-clone" "^1.0.0" + hast-util-from-parse5 "^8.0.0" + hast-util-to-parse5 "^8.0.0" + html-void-elements "^3.0.0" + mdast-util-to-hast "^13.0.0" + parse5 "^7.0.0" + unist-util-position "^5.0.0" + unist-util-visit "^5.0.0" + vfile "^6.0.0" + web-namespaces "^2.0.0" + zwitch "^2.0.0" + +hast-util-to-estree@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/hast-util-to-estree/-/hast-util-to-estree-3.1.0.tgz#f2afe5e869ddf0cf690c75f9fc699f3180b51b19" + integrity sha512-lfX5g6hqVh9kjS/B9E2gSkvHH4SZNiQFiqWS0x9fENzEl+8W12RqdRxX6d/Cwxi30tPQs3bIO+aolQJNp1bIyw== + dependencies: + "@types/estree" "^1.0.0" + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + comma-separated-tokens "^2.0.0" + devlop "^1.0.0" + estree-util-attach-comments "^3.0.0" + estree-util-is-identifier-name "^3.0.0" + hast-util-whitespace "^3.0.0" + mdast-util-mdx-expression "^2.0.0" + mdast-util-mdx-jsx "^3.0.0" + mdast-util-mdxjs-esm "^2.0.0" + property-information "^6.0.0" + space-separated-tokens "^2.0.0" + style-to-object "^0.4.0" + unist-util-position "^5.0.0" + zwitch "^2.0.0" + +hast-util-to-jsx-runtime@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.2.0.tgz#ffd59bfcf0eb8321c6ed511bfc4b399ac3404bc2" + integrity sha512-wSlp23N45CMjDg/BPW8zvhEi3R+8eRE1qFbjEyAUzMCzu2l1Wzwakq+Tlia9nkCtEl5mDxa7nKHsvYJ6Gfn21A== + dependencies: + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + comma-separated-tokens "^2.0.0" + hast-util-whitespace "^3.0.0" + property-information "^6.0.0" + space-separated-tokens "^2.0.0" + style-to-object "^0.4.0" + unist-util-position "^5.0.0" + vfile-message "^4.0.0" + +hast-util-to-parse5@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz#477cd42d278d4f036bc2ea58586130f6f39ee6ed" + integrity sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw== + dependencies: + "@types/hast" "^3.0.0" + comma-separated-tokens "^2.0.0" + devlop "^1.0.0" + property-information "^6.0.0" + space-separated-tokens "^2.0.0" + web-namespaces "^2.0.0" + zwitch "^2.0.0" + +hast-util-whitespace@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz#7778ed9d3c92dd9e8c5c8f648a49c21fc51cb621" + integrity sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw== + dependencies: + "@types/hast" "^3.0.0" + +hastscript@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-8.0.0.tgz#4ef795ec8dee867101b9f23cc830d4baf4fd781a" + integrity sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw== + dependencies: + "@types/hast" "^3.0.0" + comma-separated-tokens "^2.0.0" + hast-util-parse-selector "^4.0.0" + property-information "^6.0.0" + space-separated-tokens "^2.0.0" + +he@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +history@^4.9.0: + version "4.10.1" + resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" + integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew== + dependencies: + "@babel/runtime" "^7.1.2" + loose-envify "^1.2.0" + resolve-pathname "^3.0.0" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + value-equal "^1.0.1" + +hoist-non-react-statics@^3.1.0: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + +hpack.js@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" + integrity sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ== + dependencies: + inherits "^2.0.1" + obuf "^1.0.0" + readable-stream "^2.0.1" + wbuf "^1.1.0" + +html-entities@^2.3.2: + version "2.4.0" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.4.0.tgz#edd0cee70402584c8c76cc2c0556db09d1f45061" + integrity sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ== + +html-minifier-terser@^6.0.2: + version "6.1.0" + resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#bfc818934cc07918f6b3669f5774ecdfd48f32ab" + integrity sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw== + dependencies: + camel-case "^4.1.2" + clean-css "^5.2.2" + commander "^8.3.0" + he "^1.2.0" + param-case "^3.0.4" + relateurl "^0.2.7" + terser "^5.10.0" + +html-minifier-terser@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz#18752e23a2f0ed4b0f550f217bb41693e975b942" + integrity sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA== + dependencies: + camel-case "^4.1.2" + clean-css "~5.3.2" + commander "^10.0.0" + entities "^4.4.0" + param-case "^3.0.4" + relateurl "^0.2.7" + terser "^5.15.1" + +html-tags@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce" + integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ== + +html-void-elements@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7" + integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg== + +html-webpack-plugin@^5.5.3: + version "5.5.3" + resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.5.3.tgz#72270f4a78e222b5825b296e5e3e1328ad525a3e" + integrity sha512-6YrDKTuqaP/TquFH7h4srYWsZx+x6k6+FbsTm0ziCwGHDP78Unr1r9F/H4+sGmMbX08GQcJ+K64x55b+7VM/jg== + dependencies: + "@types/html-minifier-terser" "^6.0.0" + html-minifier-terser "^6.0.2" + lodash "^4.17.21" + pretty-error "^4.0.0" + tapable "^2.0.0" + +htmlparser2@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" + integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.0.0" + domutils "^2.5.2" + entities "^2.0.0" + +htmlparser2@^8.0.1: + version "8.0.2" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" + integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.0.1" + entities "^4.4.0" + +http-cache-semantics@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" + integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== + +http-deceiver@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" + integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +http-parser-js@>=0.5.1: + version "0.5.8" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" + integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== + +http-proxy-middleware@^2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" + integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== + dependencies: + "@types/http-proxy" "^1.17.8" + http-proxy "^1.18.1" + is-glob "^4.0.1" + is-plain-obj "^3.0.0" + micromatch "^4.0.2" + +http-proxy@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +http2-wrapper@^2.1.10: + version "2.2.0" + resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-2.2.0.tgz#b80ad199d216b7d3680195077bd7b9060fa9d7f3" + integrity sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ== + dependencies: + quick-lru "^5.1.1" + resolve-alpn "^1.2.0" + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +iconfont-parser@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/iconfont-parser/-/iconfont-parser-1.0.0.tgz#1fa61be02677005a9a014653ef2eeb7503c3538a" + integrity sha512-3RJceYHEjaqYyeDdfSAb1vP1x1Eb7ZtC9Xwetj+axm85sBlJU7HMvdNLVpwm/3g5eghYOdkQK+epUITZGAIqKQ== + dependencies: + axios "^0.19.0" + colors "^1.4.0" + tslib "^1.10.0" + xml2js "^0.4.22" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +icss-utils@^5.0.0, icss-utils@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" + integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== + +ignore@^5.2.0, ignore@^5.2.4: + version "5.2.4" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" + integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== + +image-size@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/image-size/-/image-size-1.0.2.tgz#d778b6d0ab75b2737c1556dd631652eb963bc486" + integrity sha512-xfOoWjceHntRb3qFCrh5ZFORYH8XCdYpASltMhZ/Q0KZiOwjdE/Yl2QCiWdwD+lygV5bMCvauzgu5PxBX/Yerg== + dependencies: + queue "6.0.2" + +immediate@^3.2.3: + version "3.3.0" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.3.0.tgz#1aef225517836bcdf7f2a2de2600c79ff0269266" + integrity sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q== + +immer@^9.0.7: + version "9.0.21" + resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176" + integrity sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA== + +import-fresh@^3.1.0, import-fresh@^3.2.1, import-fresh@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +import-lazy@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-4.0.0.tgz#e8eb627483a0a43da3c03f3e35548be5cb0cc153" + integrity sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw== + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +infima@0.2.0-alpha.43: + version "0.2.0-alpha.43" + resolved "https://registry.yarnpkg.com/infima/-/infima-0.2.0-alpha.43.tgz#f7aa1d7b30b6c08afef441c726bac6150228cbe0" + integrity sha512-2uw57LvUqW0rK/SWYnd/2rRfxNA5DDNOh33jxF7fy46VWoNhGxiUQyVZHbBMjQ33mQem0cjdDVwgWVAmlRfgyQ== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== + +ini@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" + integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== + +ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +inline-style-parser@0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" + integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== + +interpret@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" + integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== + +invariant@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + dependencies: + loose-envify "^1.0.0" + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +ipaddr.js@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.1.0.tgz#2119bc447ff8c257753b196fc5f1ce08a4cdf39f" + integrity sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ== + +is-alphabetical@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz#01072053ea7c1036df3c7d19a6daaec7f19e789b" + integrity sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ== + +is-alphanumerical@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz#7c03fbe96e3e931113e57f964b0a368cc2dfd875" + integrity sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw== + dependencies: + is-alphabetical "^2.0.0" + is-decimal "^2.0.0" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-ci@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.1.tgz#db6ecbed1bd659c43dac0f45661e7674103d1867" + integrity sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ== + dependencies: + ci-info "^3.2.0" + +is-core-module@^2.13.0: + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== + dependencies: + hasown "^2.0.0" + +is-decimal@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-2.0.1.tgz#9469d2dc190d0214fd87d78b78caecc0cc14eef7" + integrity sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A== + +is-docker@^2.0.0, is-docker@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + +is-extendable@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + integrity sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-hexadecimal@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz#86b5bf668fca307498d319dfc03289d781a90027" + integrity sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg== + +is-installed-globally@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520" + integrity sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ== + dependencies: + global-dirs "^3.0.0" + is-path-inside "^3.0.2" + +is-npm@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-6.0.0.tgz#b59e75e8915543ca5d881ecff864077cba095261" + integrity sha512-JEjxbSmtPSt1c8XTkVrlujcXdKV1/tvuQ7GwKcAlyiVLeYFQ2VHat8xfrDJsIkhCdF/tZ7CiIR3sy141c6+gPQ== + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + integrity sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg== + +is-obj@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" + integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== + +is-path-cwd@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" + integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== + +is-path-inside@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-plain-obj@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" + integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== + +is-plain-obj@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" + integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== + +is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-plain-object@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" + integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== + +is-reference@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-3.0.2.tgz#154747a01f45cd962404ee89d43837af2cba247c" + integrity sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg== + dependencies: + "@types/estree" "*" + +is-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" + integrity sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA== + +is-root@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-root/-/is-root-2.1.0.tgz#809e18129cf1129644302a4f8544035d51984a9c" + integrity sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg== + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-typedarray@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== + +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + +is-yarn-global@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.4.1.tgz#b312d902b313f81e4eaf98b6361ba2b45cd694bb" + integrity sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ== + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== + +jest-util@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc" + integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + +jest-worker@^27.4.5: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jest-worker@^29.1.2: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.7.0.tgz#acad073acbbaeb7262bd5389e1bcf43e10058d4a" + integrity sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw== + dependencies: + "@types/node" "*" + jest-util "^29.7.0" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jiti@^1.18.2, jiti@^1.20.0: + version "1.21.0" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d" + integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== + +joi@^17.7.0, joi@^17.9.2: + version "17.11.0" + resolved "https://registry.yarnpkg.com/joi/-/joi-17.11.0.tgz#aa9da753578ec7720e6f0ca2c7046996ed04fc1a" + integrity sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ== + dependencies: + "@hapi/hoek" "^9.0.0" + "@hapi/topo" "^5.0.0" + "@sideway/address" "^4.1.3" + "@sideway/formula" "^3.0.1" + "@sideway/pinpoint" "^2.0.0" + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== + +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + +json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json5@^2.1.2, json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +keyv@^4.5.3: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + +kind-of@^6.0.0, kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +klaw-sync@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/klaw-sync/-/klaw-sync-6.0.0.tgz#1fd2cfd56ebb6250181114f0a581167099c2b28c" + integrity sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ== + dependencies: + graceful-fs "^4.1.11" + +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + +latest-version@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-7.0.0.tgz#843201591ea81a4d404932eeb61240fe04e9e5da" + integrity sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg== + dependencies: + package-json "^8.1.0" + +launch-editor@^2.6.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.6.1.tgz#f259c9ef95cbc9425620bbbd14b468fcdb4ffe3c" + integrity sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw== + dependencies: + picocolors "^1.0.0" + shell-quote "^1.8.1" + +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + +lilconfig@^2.0.3: + version "2.1.0" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" + integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +loader-runner@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== + +loader-utils@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" + integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^2.1.2" + +loader-utils@^3.2.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.2.1.tgz#4fb104b599daafd82ef3e1a41fb9265f87e1f576" + integrity sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw== + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +locate-path@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-7.2.0.tgz#69cb1779bd90b35ab1e771e1f2f89a202c2a8a8a" + integrity sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA== + dependencies: + p-locate "^6.0.0" + +lodash.curry@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.curry/-/lodash.curry-4.1.1.tgz#248e36072ede906501d75966200a86dab8b23170" + integrity sha512-/u14pXGviLaweY5JI0IUzgzF2J6Ne8INyzAZjImcryjgkZ+ebruBxy2/JaOOkTqScddcYtakjhSaeemV8lR0tA== + +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + +lodash.escape@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98" + integrity sha512-nXEOnb/jK9g0DYMr1/Xvq6l5xMD7GDG55+GSYIYmS0G4tBk/hURD4JR9WCavs04t33WmJx9kCyp9vJ+mr4BOUw== + +lodash.flatten@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" + integrity sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g== + +lodash.flow@^3.3.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/lodash.flow/-/lodash.flow-3.5.0.tgz#87bf40292b8cf83e4e8ce1a3ae4209e20071675a" + integrity sha512-ff3BX/tSioo+XojX4MOsOMhJw0nZoUEF011LX8g8d3gvjVbxd89cCio4BCXronjxcTUIJUoqKEUA+n4CqvvRPw== + +lodash.invokemap@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.invokemap/-/lodash.invokemap-4.6.0.tgz#1748cda5d8b0ef8369c4eb3ec54c21feba1f2d62" + integrity sha512-CfkycNtMqgUlfjfdh2BhKO/ZXrP8ePOX5lEU/g0R3ItJcnuxWDwokMGKx1hWcfOikmyOVx6X9IwWnDGlgKl61w== + +lodash.memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== + +lodash.pullall@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.pullall/-/lodash.pullall-4.2.0.tgz#9d98b8518b7c965b0fae4099bd9fb7df8bbf38ba" + integrity sha512-VhqxBKH0ZxPpLhiu68YD1KnHmbhQJQctcipvmFnqIBDYzcIHzf3Zpu0tpeOKtR4x76p9yohc506eGdOjTmyIBg== + +lodash.uniq@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== + +lodash.uniqby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302" + integrity sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww== + +lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +longest-streak@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4" + integrity sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g== + +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +lower-case@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" + integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== + dependencies: + tslib "^2.0.3" + +lowercase-keys@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-3.0.0.tgz#c5e7d442e37ead247ae9db117a9d0a467c89d4f2" + integrity sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ== + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +lunr-languages@^1.4.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/lunr-languages/-/lunr-languages-1.14.0.tgz#6e97635f434631729dd0e5654daedd291cd6f2d0" + integrity sha512-hWUAb2KqM3L7J5bcrngszzISY4BxrXn/Xhbb9TTCJYEGqlR1nG67/M14sp09+PTIRklobrn57IAxcdcO/ZFyNA== + +lunr@^2.3.9: + version "2.3.9" + resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1" + integrity sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow== + +mark.js@^8.11.1: + version "8.11.1" + resolved "https://registry.yarnpkg.com/mark.js/-/mark.js-8.11.1.tgz#180f1f9ebef8b0e638e4166ad52db879beb2ffc5" + integrity sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ== + +markdown-extensions@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/markdown-extensions/-/markdown-extensions-2.0.0.tgz#34bebc83e9938cae16e0e017e4a9814a8330d3c4" + integrity sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q== + +markdown-table@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.3.tgz#e6331d30e493127e031dd385488b5bd326e4a6bd" + integrity sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw== + +mdast-util-directive@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-directive/-/mdast-util-directive-3.0.0.tgz#3fb1764e705bbdf0afb0d3f889e4404c3e82561f" + integrity sha512-JUpYOqKI4mM3sZcNxmF/ox04XYFFkNwr0CFlrQIkCwbvH0xzMCqkMqAde9wRd80VAhaUrwFwKm2nxretdT1h7Q== + dependencies: + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + devlop "^1.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + parse-entities "^4.0.0" + stringify-entities "^4.0.0" + unist-util-visit-parents "^6.0.0" + +mdast-util-find-and-replace@^3.0.0, mdast-util-find-and-replace@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz#a6fc7b62f0994e973490e45262e4bc07607b04e0" + integrity sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA== + dependencies: + "@types/mdast" "^4.0.0" + escape-string-regexp "^5.0.0" + unist-util-is "^6.0.0" + unist-util-visit-parents "^6.0.0" + +mdast-util-from-markdown@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.0.tgz#52f14815ec291ed061f2922fd14d6689c810cb88" + integrity sha512-n7MTOr/z+8NAX/wmhhDji8O3bRvPTV/U0oTCaZJkjhPSKTPhS3xufVhKGF8s1pJ7Ox4QgoIU7KHseh09S+9rTA== + dependencies: + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + mdast-util-to-string "^4.0.0" + micromark "^4.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-decode-string "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + unist-util-stringify-position "^4.0.0" + +mdast-util-frontmatter@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz#f5f929eb1eb36c8a7737475c7eb438261f964ee8" + integrity sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA== + dependencies: + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + escape-string-regexp "^5.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + micromark-extension-frontmatter "^2.0.0" + +mdast-util-gfm-autolink-literal@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.0.tgz#5baf35407421310a08e68c15e5d8821e8898ba2a" + integrity sha512-FyzMsduZZHSc3i0Px3PQcBT4WJY/X/RCtEJKuybiC6sjPqLv7h1yqAkmILZtuxMSsUyaLUWNp71+vQH2zqp5cg== + dependencies: + "@types/mdast" "^4.0.0" + ccount "^2.0.0" + devlop "^1.0.0" + mdast-util-find-and-replace "^3.0.0" + micromark-util-character "^2.0.0" + +mdast-util-gfm-footnote@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.0.0.tgz#25a1753c7d16db8bfd53cd84fe50562bd1e6d6a9" + integrity sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ== + dependencies: + "@types/mdast" "^4.0.0" + devlop "^1.1.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + +mdast-util-gfm-strikethrough@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz#d44ef9e8ed283ac8c1165ab0d0dfd058c2764c16" + integrity sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg== + dependencies: + "@types/mdast" "^4.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-gfm-table@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz#7a435fb6223a72b0862b33afbd712b6dae878d38" + integrity sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg== + dependencies: + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + markdown-table "^3.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-gfm-task-list-item@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz#e68095d2f8a4303ef24094ab642e1047b991a936" + integrity sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ== + dependencies: + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-gfm@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-gfm/-/mdast-util-gfm-3.0.0.tgz#3f2aecc879785c3cb6a81ff3a243dc11eca61095" + integrity sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw== + dependencies: + mdast-util-from-markdown "^2.0.0" + mdast-util-gfm-autolink-literal "^2.0.0" + mdast-util-gfm-footnote "^2.0.0" + mdast-util-gfm-strikethrough "^2.0.0" + mdast-util-gfm-table "^2.0.0" + mdast-util-gfm-task-list-item "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-mdx-expression@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.0.tgz#4968b73724d320a379110d853e943a501bfd9d87" + integrity sha512-fGCu8eWdKUKNu5mohVGkhBXCXGnOTLuFqOvGMvdikr+J1w7lDJgxThOKpwRWzzbyXAU2hhSwsmssOY4yTokluw== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-mdx-jsx@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.0.0.tgz#f73631fa5bb7a36712ff1e9cedec0cafed03401c" + integrity sha512-XZuPPzQNBPAlaqsTTgRrcJnyFbSOBovSadFgbFu8SnuNgm+6Bdx1K+IWoitsmj6Lq6MNtI+ytOqwN70n//NaBA== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + ccount "^2.0.0" + devlop "^1.1.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + parse-entities "^4.0.0" + stringify-entities "^4.0.0" + unist-util-remove-position "^5.0.0" + unist-util-stringify-position "^4.0.0" + vfile-message "^4.0.0" + +mdast-util-mdx@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz#792f9cf0361b46bee1fdf1ef36beac424a099c41" + integrity sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w== + dependencies: + mdast-util-from-markdown "^2.0.0" + mdast-util-mdx-expression "^2.0.0" + mdast-util-mdx-jsx "^3.0.0" + mdast-util-mdxjs-esm "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-mdxjs-esm@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz#019cfbe757ad62dd557db35a695e7314bcc9fa97" + integrity sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-phrasing@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-phrasing/-/mdast-util-phrasing-4.0.0.tgz#468cbbb277375523de807248b8ad969feb02a5c7" + integrity sha512-xadSsJayQIucJ9n053dfQwVu1kuXg7jCTdYsMK8rqzKZh52nLfSH/k0sAxE0u+pj/zKZX+o5wB+ML5mRayOxFA== + dependencies: + "@types/mdast" "^4.0.0" + unist-util-is "^6.0.0" + +mdast-util-to-hast@^13.0.0: + version "13.0.2" + resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-13.0.2.tgz#74c0a9f014bb2340cae6118f6fccd75467792be7" + integrity sha512-U5I+500EOOw9e3ZrclN3Is3fRpw8c19SMyNZlZ2IS+7vLsNzb2Om11VpIVOR+/0137GhZsFEF6YiKD5+0Hr2Og== + dependencies: + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + "@ungap/structured-clone" "^1.0.0" + devlop "^1.0.0" + micromark-util-sanitize-uri "^2.0.0" + trim-lines "^3.0.0" + unist-util-position "^5.0.0" + unist-util-visit "^5.0.0" + +mdast-util-to-markdown@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz#9813f1d6e0cdaac7c244ec8c6dabfdb2102ea2b4" + integrity sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ== + dependencies: + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + longest-streak "^3.0.0" + mdast-util-phrasing "^4.0.0" + mdast-util-to-string "^4.0.0" + micromark-util-decode-string "^2.0.0" + unist-util-visit "^5.0.0" + zwitch "^2.0.0" + +mdast-util-to-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz#7a5121475556a04e7eddeb67b264aae79d312814" + integrity sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg== + dependencies: + "@types/mdast" "^4.0.0" + +mdn-data@2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" + integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== + +mdn-data@2.0.28: + version "2.0.28" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.28.tgz#5ec48e7bef120654539069e1ae4ddc81ca490eba" + integrity sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g== + +mdn-data@2.0.30: + version "2.0.30" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc" + integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +medium-zoom@^1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/medium-zoom/-/medium-zoom-1.0.8.tgz#2bd1fbcf2961fa7b0e318fe284462aa9b8608ed2" + integrity sha512-CjFVuFq/IfrdqesAXfg+hzlDKu6A2n80ZIq0Kl9kWjoHh9j1N9Uvk5X0/MmN0hOfm5F9YBswlClhcwnmtwz7gA== + +memfs@^3.1.2, memfs@^3.4.3: + version "3.6.0" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.6.0.tgz#d7a2110f86f79dd950a8b6df6d57bc984aa185f6" + integrity sha512-EGowvkkgbMcIChjMTMkESFDbZeSh8xZ7kNSF0hAiAN4Jh6jgHCRS0Ga/+C8y6Au+oqpezRHCfPsmJ2+DwAgiwQ== + dependencies: + fs-monkey "^1.0.4" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +micromark-core-commonmark@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-2.0.0.tgz#50740201f0ee78c12a675bf3e68ffebc0bf931a3" + integrity sha512-jThOz/pVmAYUtkroV3D5c1osFXAMv9e0ypGDOIZuCeAe91/sD6BoE2Sjzt30yuXtwOYUmySOhMas/PVyh02itA== + dependencies: + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + micromark-factory-destination "^2.0.0" + micromark-factory-label "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-factory-title "^2.0.0" + micromark-factory-whitespace "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-classify-character "^2.0.0" + micromark-util-html-tag-name "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-subtokenize "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-directive@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-directive/-/micromark-extension-directive-3.0.0.tgz#527869de497a6de9024138479091bc885dae076b" + integrity sha512-61OI07qpQrERc+0wEysLHMvoiO3s2R56x5u7glHq2Yqq6EHbH4dW25G9GfDdGCDYqA21KE6DWgNSzxSwHc2hSg== + dependencies: + devlop "^1.0.0" + micromark-factory-space "^2.0.0" + micromark-factory-whitespace "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + parse-entities "^4.0.0" + +micromark-extension-frontmatter@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz#651c52ffa5d7a8eeed687c513cd869885882d67a" + integrity sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg== + dependencies: + fault "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm-autolink-literal@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.0.0.tgz#f1e50b42e67d441528f39a67133eddde2bbabfd9" + integrity sha512-rTHfnpt/Q7dEAK1Y5ii0W8bhfJlVJFnJMHIPisfPK3gpVNuOP0VnRl96+YJ3RYWV/P4gFeQoGKNlT3RhuvpqAg== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-sanitize-uri "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm-footnote@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.0.0.tgz#91afad310065a94b636ab1e9dab2c60d1aab953c" + integrity sha512-6Rzu0CYRKDv3BfLAUnZsSlzx3ak6HAoI85KTiijuKIz5UxZxbUI+pD6oHgw+6UtQuiRwnGRhzMmPRv4smcz0fg== + dependencies: + devlop "^1.0.0" + micromark-core-commonmark "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-sanitize-uri "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm-strikethrough@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.0.0.tgz#6917db8e320da70e39ffbf97abdbff83e6783e61" + integrity sha512-c3BR1ClMp5fxxmwP6AoOY2fXO9U8uFMKs4ADD66ahLTNcwzSCyRVU4k7LPV5Nxo/VJiR4TdzxRQY2v3qIUceCw== + dependencies: + devlop "^1.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-classify-character "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm-table@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.0.0.tgz#2cf3fe352d9e089b7ef5fff003bdfe0da29649b7" + integrity sha512-PoHlhypg1ItIucOaHmKE8fbin3vTLpDOUg8KAr8gRCF1MOZI9Nquq2i/44wFvviM4WuxJzc3demT8Y3dkfvYrw== + dependencies: + devlop "^1.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm-tagfilter@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz#f26d8a7807b5985fba13cf61465b58ca5ff7dc57" + integrity sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg== + dependencies: + micromark-util-types "^2.0.0" + +micromark-extension-gfm-task-list-item@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.0.1.tgz#ee8b208f1ced1eb9fb11c19a23666e59d86d4838" + integrity sha512-cY5PzGcnULaN5O7T+cOzfMoHjBW7j+T9D2sucA5d/KbsBTPcYdebm9zUd9zzdgJGCwahV+/W78Z3nbulBYVbTw== + dependencies: + devlop "^1.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz#3e13376ab95dd7a5cfd0e29560dfe999657b3c5b" + integrity sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w== + dependencies: + micromark-extension-gfm-autolink-literal "^2.0.0" + micromark-extension-gfm-footnote "^2.0.0" + micromark-extension-gfm-strikethrough "^2.0.0" + micromark-extension-gfm-table "^2.0.0" + micromark-extension-gfm-tagfilter "^2.0.0" + micromark-extension-gfm-task-list-item "^2.0.0" + micromark-util-combine-extensions "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-mdx-expression@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.0.tgz#1407b9ce69916cf5e03a196ad9586889df25302a" + integrity sha512-sI0nwhUDz97xyzqJAbHQhp5TfaxEvZZZ2JDqUo+7NvyIYG6BZ5CPPqj2ogUoPJlmXHBnyZUzISg9+oUmU6tUjQ== + dependencies: + "@types/estree" "^1.0.0" + devlop "^1.0.0" + micromark-factory-mdx-expression "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-events-to-acorn "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-mdx-jsx@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.0.tgz#4aba0797c25efb2366a3fd2d367c6b1c1159f4f5" + integrity sha512-uvhhss8OGuzR4/N17L1JwvmJIpPhAd8oByMawEKx6NVdBCbesjH4t+vjEp3ZXft9DwvlKSD07fCeI44/N0Vf2w== + dependencies: + "@types/acorn" "^4.0.0" + "@types/estree" "^1.0.0" + devlop "^1.0.0" + estree-util-is-identifier-name "^3.0.0" + micromark-factory-mdx-expression "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + vfile-message "^4.0.0" + +micromark-extension-mdx-md@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz#1d252881ea35d74698423ab44917e1f5b197b92d" + integrity sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ== + dependencies: + micromark-util-types "^2.0.0" + +micromark-extension-mdxjs-esm@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz#de21b2b045fd2059bd00d36746081de38390d54a" + integrity sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A== + dependencies: + "@types/estree" "^1.0.0" + devlop "^1.0.0" + micromark-core-commonmark "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-events-to-acorn "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + unist-util-position-from-estree "^2.0.0" + vfile-message "^4.0.0" + +micromark-extension-mdxjs@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz#b5a2e0ed449288f3f6f6c544358159557549de18" + integrity sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ== + dependencies: + acorn "^8.0.0" + acorn-jsx "^5.0.0" + micromark-extension-mdx-expression "^3.0.0" + micromark-extension-mdx-jsx "^3.0.0" + micromark-extension-mdx-md "^2.0.0" + micromark-extension-mdxjs-esm "^3.0.0" + micromark-util-combine-extensions "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-destination@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz#857c94debd2c873cba34e0445ab26b74f6a6ec07" + integrity sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-label@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-2.0.0.tgz#17c5c2e66ce39ad6f4fc4cbf40d972f9096f726a" + integrity sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw== + dependencies: + devlop "^1.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-mdx-expression@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.1.tgz#f2a9724ce174f1751173beb2c1f88062d3373b1b" + integrity sha512-F0ccWIUHRLRrYp5TC9ZYXmZo+p2AM13ggbsW4T0b5CRKP8KHVRB8t4pwtBgTxtjRmwrK0Irwm7vs2JOZabHZfg== + dependencies: + "@types/estree" "^1.0.0" + devlop "^1.0.0" + micromark-util-character "^2.0.0" + micromark-util-events-to-acorn "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + unist-util-position-from-estree "^2.0.0" + vfile-message "^4.0.0" + +micromark-factory-space@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz#c8f40b0640a0150751d3345ed885a080b0d15faf" + integrity sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-factory-space@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz#5e7afd5929c23b96566d0e1ae018ae4fcf81d030" + integrity sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-title@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-2.0.0.tgz#726140fc77892af524705d689e1cf06c8a83ea95" + integrity sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A== + dependencies: + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-whitespace@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.0.tgz#9e92eb0f5468083381f923d9653632b3cfb5f763" + integrity sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA== + dependencies: + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-character@^1.0.0, micromark-util-character@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-1.2.0.tgz#4fedaa3646db249bc58caeb000eb3549a8ca5dcc" + integrity sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg== + dependencies: + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-util-character@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-2.0.1.tgz#52b824c2e2633b6fb33399d2ec78ee2a90d6b298" + integrity sha512-3wgnrmEAJ4T+mGXAUfMvMAbxU9RDG43XmGce4j6CwPtVxB3vfwXSZ6KhFwDzZ3mZHhmPimMAXg71veiBGzeAZw== + dependencies: + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-chunked@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz#e51f4db85fb203a79dbfef23fd41b2f03dc2ef89" + integrity sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-classify-character@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz#8c7537c20d0750b12df31f86e976d1d951165f34" + integrity sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-combine-extensions@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz#75d6ab65c58b7403616db8d6b31315013bfb7ee5" + integrity sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ== + dependencies: + micromark-util-chunked "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-decode-numeric-character-reference@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.1.tgz#2698bbb38f2a9ba6310e359f99fcb2b35a0d2bd5" + integrity sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-decode-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-2.0.0.tgz#7dfa3a63c45aecaa17824e656bcdb01f9737154a" + integrity sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA== + dependencies: + decode-named-character-reference "^1.0.0" + micromark-util-character "^2.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-symbol "^2.0.0" + +micromark-util-encode@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz#0921ac7953dc3f1fd281e3d1932decfdb9382ab1" + integrity sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA== + +micromark-util-events-to-acorn@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.2.tgz#4275834f5453c088bd29cd72dfbf80e3327cec07" + integrity sha512-Fk+xmBrOv9QZnEDguL9OI9/NQQp6Hz4FuQ4YmCb/5V7+9eAh1s6AYSvL20kHkD67YIg7EpE54TiSlcsf3vyZgA== + dependencies: + "@types/acorn" "^4.0.0" + "@types/estree" "^1.0.0" + "@types/unist" "^3.0.0" + devlop "^1.0.0" + estree-util-visit "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + vfile-message "^4.0.0" + +micromark-util-html-tag-name@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.0.tgz#ae34b01cbe063363847670284c6255bb12138ec4" + integrity sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw== + +micromark-util-normalize-identifier@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.0.tgz#91f9a4e65fe66cc80c53b35b0254ad67aa431d8b" + integrity sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-resolve-all@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz#189656e7e1a53d0c86a38a652b284a252389f364" + integrity sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA== + dependencies: + micromark-util-types "^2.0.0" + +micromark-util-sanitize-uri@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz#ec8fbf0258e9e6d8f13d9e4770f9be64342673de" + integrity sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-encode "^2.0.0" + micromark-util-symbol "^2.0.0" + +micromark-util-subtokenize@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.0.tgz#9f412442d77e0c5789ffdf42377fa8a2bcbdf581" + integrity sha512-vc93L1t+gpR3p8jxeVdaYlbV2jTYteDje19rNSS/H5dlhxUYll5Fy6vJ2cDwP8RnsXi818yGty1ayP55y3W6fg== + dependencies: + devlop "^1.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-symbol@^1.0.0, micromark-util-symbol@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz#813cd17837bdb912d069a12ebe3a44b6f7063142" + integrity sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag== + +micromark-util-symbol@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz#12225c8f95edf8b17254e47080ce0862d5db8044" + integrity sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw== + +micromark-util-types@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-1.1.0.tgz#e6676a8cae0bb86a2171c498167971886cb7e283" + integrity sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg== + +micromark-util-types@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-2.0.0.tgz#63b4b7ffeb35d3ecf50d1ca20e68fc7caa36d95e" + integrity sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w== + +micromark@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/micromark/-/micromark-4.0.0.tgz#84746a249ebd904d9658cfabc1e8e5f32cbc6249" + integrity sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ== + dependencies: + "@types/debug" "^4.0.0" + debug "^4.0.0" + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + micromark-core-commonmark "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-combine-extensions "^2.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-encode "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-sanitize-uri "^2.0.0" + micromark-util-subtokenize "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-db@~1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" + integrity sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ== + +mime-types@2.1.18: + version "2.1.18" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" + integrity sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ== + dependencies: + mime-db "~1.33.0" + +mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +mimic-response@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" + integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== + +mimic-response@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-4.0.0.tgz#35468b19e7c75d10f5165ea25e75a5ceea7cf70f" + integrity sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg== + +mini-css-extract-plugin@^2.7.6: + version "2.7.6" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.6.tgz#282a3d38863fddcd2e0c220aaed5b90bc156564d" + integrity sha512-Qk7HcgaPkGG6eD77mLvZS1nmxlao3j+9PkrT9Uc7HAE1id3F41+DdBRYRYkbyfNRGzm8/YWtzhw7nVPmwhqTQw== + dependencies: + schema-utils "^4.0.0" + +minimalistic-assert@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimatch@3.1.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6, minimist@^1.2.7: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +mkdirp@^0.5.1: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +mrmime@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-1.0.1.tgz#5f90c825fad4bdd41dc914eff5d1a8cfdaf24f27" + integrity sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multicast-dns@^7.2.5: + version "7.2.5" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced" + integrity sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg== + dependencies: + dns-packet "^5.2.2" + thunky "^1.0.2" + +nanoid@^3.3.6: + version "3.3.6" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" + integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +no-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" + integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== + dependencies: + lower-case "^2.0.2" + tslib "^2.0.3" + +node-emoji@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-2.1.0.tgz#93c99b0d3dfe7d5e37c056aded389e013c72d0c5" + integrity sha512-tcsBm9C6FmPN5Wo7OjFi9lgMyJjvkAeirmjR/ax8Ttfqy4N8PoFic26uqFTIgayHPNI5FH4ltUvfh9kHzwcK9A== + dependencies: + "@sindresorhus/is" "^3.1.2" + char-regex "^1.0.2" + emojilib "^2.4.0" + skin-tone "^2.0.0" + +node-fetch@^2.6.12: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + +node-forge@^1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== + +node-releases@^2.0.13: + version "2.0.13" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d" + integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +normalize-range@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" + integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== + +normalize-url@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" + integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== + +normalize-url@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-8.0.0.tgz#593dbd284f743e8dcf6a5ddf8fadff149c82701a" + integrity sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw== + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +nprogress@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/nprogress/-/nprogress-0.2.0.tgz#cb8f34c53213d895723fcbab907e9422adbcafb1" + integrity sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA== + +nth-check@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== + dependencies: + boolbase "^1.0.0" + +object-assign@^4.1.0, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.9.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" + integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== + +object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.0: + version "4.1.4" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" + integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + has-symbols "^1.0.3" + object-keys "^1.1.1" + +obuf@^1.0.0, obuf@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +open@^8.0.9, open@^8.4.0: + version "8.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" + integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== + dependencies: + define-lazy-prop "^2.0.0" + is-docker "^2.1.1" + is-wsl "^2.2.0" + +opener@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" + integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== + +p-cancelable@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-3.0.0.tgz#63826694b54d61ca1c20ebcb6d3ecf5e14cd8050" + integrity sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw== + +p-limit@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-limit@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-4.0.0.tgz#914af6544ed32bfa54670b061cafcbd04984b644" + integrity sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ== + dependencies: + yocto-queue "^1.0.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-locate@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-6.0.0.tgz#3da9a49d4934b901089dca3302fa65dc5a05c04f" + integrity sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw== + dependencies: + p-limit "^4.0.0" + +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + +p-retry@^4.5.0: + version "4.6.2" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16" + integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ== + dependencies: + "@types/retry" "0.12.0" + retry "^0.13.1" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +package-json@^8.1.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/package-json/-/package-json-8.1.1.tgz#3e9948e43df40d1e8e78a85485f1070bf8f03dc8" + integrity sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA== + dependencies: + got "^12.1.0" + registry-auth-token "^5.0.1" + registry-url "^6.0.0" + semver "^7.3.7" + +param-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" + integrity sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-entities@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-4.0.1.tgz#4e2a01111fb1c986549b944af39eeda258fc9e4e" + integrity sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w== + dependencies: + "@types/unist" "^2.0.0" + character-entities "^2.0.0" + character-entities-legacy "^3.0.0" + character-reference-invalid "^2.0.0" + decode-named-character-reference "^1.0.0" + is-alphanumerical "^2.0.0" + is-decimal "^2.0.0" + is-hexadecimal "^2.0.0" + +parse-json@^5.0.0, parse-json@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +parse-numeric-range@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz#7c63b61190d61e4d53a1197f0c83c47bb670ffa3" + integrity sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ== + +parse5-htmlparser2-tree-adapter@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz#23c2cc233bcf09bb7beba8b8a69d46b08c62c2f1" + integrity sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g== + dependencies: + domhandler "^5.0.2" + parse5 "^7.0.0" + +parse5@^7.0.0: + version "7.1.2" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" + integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== + dependencies: + entities "^4.4.0" + +parseurl@~1.3.2, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +pascal-case@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb" + integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-exists@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-5.0.0.tgz#a6aad9489200b21fab31e49cf09277e5116fb9e7" + integrity sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-is-inside@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + integrity sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w== + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-root-regex@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/path-root-regex/-/path-root-regex-0.1.2.tgz#bfccdc8df5b12dc52c8b43ec38d18d72c04ba96d" + integrity sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ== + +path-root@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/path-root/-/path-root-0.1.1.tgz#9a4a6814cac1c0cd73360a95f32083c8ea4745b7" + integrity sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg== + dependencies: + path-root-regex "^0.1.0" + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +path-to-regexp@2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-2.2.1.tgz#90b617025a16381a879bc82a38d4e8bdeb2bcf45" + integrity sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ== + +path-to-regexp@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" + integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== + dependencies: + isarray "0.0.1" + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +periscopic@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/periscopic/-/periscopic-3.1.0.tgz#7e9037bf51c5855bd33b48928828db4afa79d97a" + integrity sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw== + dependencies: + "@types/estree" "^1.0.0" + estree-walker "^3.0.0" + is-reference "^3.0.0" + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pkg-dir@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-7.0.0.tgz#8f0c08d6df4476756c5ff29b3282d0bab7517d11" + integrity sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA== + dependencies: + find-up "^6.3.0" + +pkg-up@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" + integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA== + dependencies: + find-up "^3.0.0" + +postcss-calc@^8.2.3: + version "8.2.4" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.2.4.tgz#77b9c29bfcbe8a07ff6693dc87050828889739a5" + integrity sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q== + dependencies: + postcss-selector-parser "^6.0.9" + postcss-value-parser "^4.2.0" + +postcss-colormin@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.3.1.tgz#86c27c26ed6ba00d96c79e08f3ffb418d1d1988f" + integrity sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ== + dependencies: + browserslist "^4.21.4" + caniuse-api "^3.0.0" + colord "^2.9.1" + postcss-value-parser "^4.2.0" + +postcss-convert-values@^5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz#04998bb9ba6b65aa31035d669a6af342c5f9d393" + integrity sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA== + dependencies: + browserslist "^4.21.4" + postcss-value-parser "^4.2.0" + +postcss-discard-comments@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz#8df5e81d2925af2780075840c1526f0660e53696" + integrity sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ== + +postcss-discard-duplicates@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz#9eb4fe8456706a4eebd6d3b7b777d07bad03e848" + integrity sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw== + +postcss-discard-empty@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz#e57762343ff7f503fe53fca553d18d7f0c369c6c" + integrity sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A== + +postcss-discard-overridden@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz#7e8c5b53325747e9d90131bb88635282fb4a276e" + integrity sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw== + +postcss-discard-unused@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-discard-unused/-/postcss-discard-unused-5.1.0.tgz#8974e9b143d887677304e558c1166d3762501142" + integrity sha512-KwLWymI9hbwXmJa0dkrzpRbSJEh0vVUd7r8t0yOGPcfKzyJJxFM8kLyC5Ev9avji6nY95pOp1W6HqIrfT+0VGw== + dependencies: + postcss-selector-parser "^6.0.5" + +postcss-loader@^7.3.3: + version "7.3.3" + resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-7.3.3.tgz#6da03e71a918ef49df1bb4be4c80401df8e249dd" + integrity sha512-YgO/yhtevGO/vJePCQmTxiaEwER94LABZN0ZMT4A0vsak9TpO+RvKRs7EmJ8peIlB9xfXCsS7M8LjqncsUZ5HA== + dependencies: + cosmiconfig "^8.2.0" + jiti "^1.18.2" + semver "^7.3.8" + +postcss-merge-idents@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-merge-idents/-/postcss-merge-idents-5.1.1.tgz#7753817c2e0b75d0853b56f78a89771e15ca04a1" + integrity sha512-pCijL1TREiCoog5nQp7wUe+TUonA2tC2sQ54UGeMmryK3UFGIYKqDyjnqd6RcuI4znFn9hWSLNN8xKE/vWcUQw== + dependencies: + cssnano-utils "^3.1.0" + postcss-value-parser "^4.2.0" + +postcss-merge-longhand@^5.1.7: + version "5.1.7" + resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz#24a1bdf402d9ef0e70f568f39bdc0344d568fb16" + integrity sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ== + dependencies: + postcss-value-parser "^4.2.0" + stylehacks "^5.1.1" + +postcss-merge-rules@^5.1.4: + version "5.1.4" + resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz#2f26fa5cacb75b1402e213789f6766ae5e40313c" + integrity sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g== + dependencies: + browserslist "^4.21.4" + caniuse-api "^3.0.0" + cssnano-utils "^3.1.0" + postcss-selector-parser "^6.0.5" + +postcss-minify-font-values@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz#f1df0014a726083d260d3bd85d7385fb89d1f01b" + integrity sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-minify-gradients@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz#f1fe1b4f498134a5068240c2f25d46fcd236ba2c" + integrity sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw== + dependencies: + colord "^2.9.1" + cssnano-utils "^3.1.0" + postcss-value-parser "^4.2.0" + +postcss-minify-params@^5.1.4: + version "5.1.4" + resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz#c06a6c787128b3208b38c9364cfc40c8aa5d7352" + integrity sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw== + dependencies: + browserslist "^4.21.4" + cssnano-utils "^3.1.0" + postcss-value-parser "^4.2.0" + +postcss-minify-selectors@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz#d4e7e6b46147b8117ea9325a915a801d5fe656c6" + integrity sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg== + dependencies: + postcss-selector-parser "^6.0.5" + +postcss-modules-extract-imports@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d" + integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw== + +postcss-modules-local-by-default@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz#b08eb4f083050708998ba2c6061b50c2870ca524" + integrity sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA== + dependencies: + icss-utils "^5.0.0" + postcss-selector-parser "^6.0.2" + postcss-value-parser "^4.1.0" + +postcss-modules-scope@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06" + integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg== + dependencies: + postcss-selector-parser "^6.0.4" + +postcss-modules-values@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" + integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== + dependencies: + icss-utils "^5.0.0" + +postcss-normalize-charset@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz#9302de0b29094b52c259e9b2cf8dc0879879f0ed" + integrity sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg== + +postcss-normalize-display-values@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz#72abbae58081960e9edd7200fcf21ab8325c3da8" + integrity sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-positions@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz#ef97279d894087b59325b45c47f1e863daefbb92" + integrity sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-repeat-style@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz#e9eb96805204f4766df66fd09ed2e13545420fb2" + integrity sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-string@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz#411961169e07308c82c1f8c55f3e8a337757e228" + integrity sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-timing-functions@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz#d5614410f8f0b2388e9f240aa6011ba6f52dafbb" + integrity sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-unicode@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz#f67297fca3fea7f17e0d2caa40769afc487aa030" + integrity sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA== + dependencies: + browserslist "^4.21.4" + postcss-value-parser "^4.2.0" + +postcss-normalize-url@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz#ed9d88ca82e21abef99f743457d3729a042adcdc" + integrity sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew== + dependencies: + normalize-url "^6.0.1" + postcss-value-parser "^4.2.0" + +postcss-normalize-whitespace@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz#08a1a0d1ffa17a7cc6efe1e6c9da969cc4493cfa" + integrity sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-ordered-values@^5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz#b6fd2bd10f937b23d86bc829c69e7732ce76ea38" + integrity sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ== + dependencies: + cssnano-utils "^3.1.0" + postcss-value-parser "^4.2.0" + +postcss-reduce-idents@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/postcss-reduce-idents/-/postcss-reduce-idents-5.2.0.tgz#c89c11336c432ac4b28792f24778859a67dfba95" + integrity sha512-BTrLjICoSB6gxbc58D5mdBK8OhXRDqud/zodYfdSi52qvDHdMwk+9kB9xsM8yJThH/sZU5A6QVSmMmaN001gIg== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-reduce-initial@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz#798cd77b3e033eae7105c18c9d371d989e1382d6" + integrity sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg== + dependencies: + browserslist "^4.21.4" + caniuse-api "^3.0.0" + +postcss-reduce-transforms@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz#333b70e7758b802f3dd0ddfe98bb1ccfef96b6e9" + integrity sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.9: + version "6.0.13" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz#d05d8d76b1e8e173257ef9d60b706a8e5e99bf1b" + integrity sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss-sort-media-queries@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/postcss-sort-media-queries/-/postcss-sort-media-queries-4.4.1.tgz#04a5a78db3921eb78f28a1a781a2e68e65258128" + integrity sha512-QDESFzDDGKgpiIh4GYXsSy6sek2yAwQx1JASl5AxBtU1Lq2JfKBljIPNdil989NcSKRQX1ToiaKphImtBuhXWw== + dependencies: + sort-css-media-queries "2.1.0" + +postcss-svgo@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-5.1.0.tgz#0a317400ced789f233a28826e77523f15857d80d" + integrity sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA== + dependencies: + postcss-value-parser "^4.2.0" + svgo "^2.7.0" + +postcss-unique-selectors@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz#a9f273d1eacd09e9aa6088f4b0507b18b1b541b6" + integrity sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA== + dependencies: + postcss-selector-parser "^6.0.5" + +postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== + +postcss-zindex@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-zindex/-/postcss-zindex-5.1.0.tgz#4a5c7e5ff1050bd4c01d95b1847dfdcc58a496ff" + integrity sha512-fgFMf0OtVSBR1va1JNHYgMxYk73yhn/qb4uQDq1DLGYolz8gHCyr/sesEuGUaYs58E3ZJRcpoGuPVoB7Meiq9A== + +postcss@^8.4.17, postcss@^8.4.21, postcss@^8.4.26: + version "8.4.31" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" + integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== + dependencies: + nanoid "^3.3.6" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +pretty-error@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-4.0.0.tgz#90a703f46dd7234adb46d0f84823e9d1cb8f10d6" + integrity sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw== + dependencies: + lodash "^4.17.20" + renderkid "^3.0.0" + +pretty-time@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/pretty-time/-/pretty-time-1.1.0.tgz#ffb7429afabb8535c346a34e41873adf3d74dd0e" + integrity sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA== + +prism-react-renderer@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-2.1.0.tgz#a2f418451647412ea73d18cfe363fea20e419f9d" + integrity sha512-I5cvXHjA1PVGbGm1MsWCpvBCRrYyxEri0MC7/JbfIfYfcXAxHyO5PaUjs3A8H5GW6kJcLhTHxxMaOZZpRZD2iQ== + dependencies: + "@types/prismjs" "^1.26.0" + clsx "^1.2.1" + +prism-react-renderer@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-2.2.0.tgz#f199b15716e0b8d0ccfd1986ff6fa226fb7ff2b1" + integrity sha512-j4AN0VkEr72598+47xDvpzeYyeh/wPPRNTt9nJFZqIZUxwGKwYqYgt7RVigZ3ZICJWJWN84KEuMKPNyypyhNIw== + dependencies: + "@types/prismjs" "^1.26.0" + clsx "^1.2.1" + +prismjs@^1.29.0: + version "1.29.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12" + integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q== + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +promise@^7.1.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" + integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg== + dependencies: + asap "~2.0.3" + +prompts@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + +prop-types@^15.6.2, prop-types@^15.7.2: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + +property-information@^6.0.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.3.0.tgz#ba4a06ec6b4e1e90577df9931286953cdf4282c3" + integrity sha512-gVNZ74nqhRMiIUYWGQdosYetaKc83x8oT41a0LlV3AAFCAZwCpg4vmGkq8t34+cUhp3cnM4XDiU/7xlgK7HGrg== + +proto-list@~1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" + integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +punycode@^1.3.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== + +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +pupa@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/pupa/-/pupa-3.1.0.tgz#f15610274376bbcc70c9a3aa8b505ea23f41c579" + integrity sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug== + dependencies: + escape-goat "^4.0.0" + +pure-color@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/pure-color/-/pure-color-1.3.0.tgz#1fe064fb0ac851f0de61320a8bf796836422f33e" + integrity sha512-QFADYnsVoBMw1srW7OVKEYjG+MbIa49s54w1MA1EDY6r2r/sTcKKYqRX1f4GYvnXP7eN/Pe9HFcX+hwzmrXRHA== + +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +queue@6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/queue/-/queue-6.0.2.tgz#b91525283e2315c7553d2efa18d83e76432fed65" + integrity sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA== + dependencies: + inherits "~2.0.3" + +quick-lru@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" + integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +range-parser@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" + integrity sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A== + +range-parser@^1.2.1, range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +rc@1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +react-base16-styling@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/react-base16-styling/-/react-base16-styling-0.6.0.tgz#ef2156d66cf4139695c8a167886cb69ea660792c" + integrity sha512-yvh/7CArceR/jNATXOKDlvTnPKPmGZz7zsenQ3jUwLzHkNUR0CvY3yGYJbWJ/nnxsL8Sgmt5cO3/SILVuPO6TQ== + dependencies: + base16 "^1.0.0" + lodash.curry "^4.0.1" + lodash.flow "^3.3.0" + pure-color "^1.2.0" + +react-dev-utils@^12.0.1: + version "12.0.1" + resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-12.0.1.tgz#ba92edb4a1f379bd46ccd6bcd4e7bc398df33e73" + integrity sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ== + dependencies: + "@babel/code-frame" "^7.16.0" + address "^1.1.2" + browserslist "^4.18.1" + chalk "^4.1.2" + cross-spawn "^7.0.3" + detect-port-alt "^1.1.6" + escape-string-regexp "^4.0.0" + filesize "^8.0.6" + find-up "^5.0.0" + fork-ts-checker-webpack-plugin "^6.5.0" + global-modules "^2.0.0" + globby "^11.0.4" + gzip-size "^6.0.0" + immer "^9.0.7" + is-root "^2.1.0" + loader-utils "^3.2.0" + open "^8.4.0" + pkg-up "^3.1.0" + prompts "^2.4.2" + react-error-overlay "^6.0.11" + recursive-readdir "^2.2.2" + shell-quote "^1.7.3" + strip-ansi "^6.0.1" + text-table "^0.2.0" + +react-dom@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" + integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.0" + +react-error-overlay@^6.0.11: + version "6.0.11" + resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb" + integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg== + +react-fast-compare@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49" + integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ== + +react-helmet-async@*, react-helmet-async@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/react-helmet-async/-/react-helmet-async-1.3.0.tgz#7bd5bf8c5c69ea9f02f6083f14ce33ef545c222e" + integrity sha512-9jZ57/dAn9t3q6hneQS0wukqC2ENOBgMNVEhb/ZG9ZSxUetzVIw4iAmEU38IaVg3QGYauQPhSeUTuIUtFglWpg== + dependencies: + "@babel/runtime" "^7.12.5" + invariant "^2.2.4" + prop-types "^15.7.2" + react-fast-compare "^3.2.0" + shallowequal "^1.1.0" + +react-iconfont-cli@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/react-iconfont-cli/-/react-iconfont-cli-2.0.2.tgz#547d872e5d76796230877176e397bc6ed3337467" + integrity sha512-43NP+dsk08XwJf8oXfrDwHk1WeE76vy1Xzbd2zz7WjjxsdblF9h/oalFMPclZSibIirwsckz3L0IV+42Yu//iQ== + dependencies: + colors "^1.3.3" + glob "^7.1.4" + iconfont-parser "^1.0.0" + lodash "^4.17.15" + minimist "^1.2.5" + mkdirp "^0.5.1" + tslib "^1.10.0" + +react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-lifecycles-compat@~3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== + +react-loadable-ssr-addon-v5-slorber@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.1.tgz#2cdc91e8a744ffdf9e3556caabeb6e4278689883" + integrity sha512-lq3Lyw1lGku8zUEJPDxsNm1AfYHBrO9Y1+olAYwpUJ2IGFBskM0DMKok97A6LWUpHm+o7IvQBOWu9MLenp9Z+A== + dependencies: + "@babel/runtime" "^7.10.3" + +react-router-config@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/react-router-config/-/react-router-config-5.1.1.tgz#0f4263d1a80c6b2dc7b9c1902c9526478194a988" + integrity sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg== + dependencies: + "@babel/runtime" "^7.1.2" + +react-router-dom@^5.3.4: + version "5.3.4" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.3.4.tgz#2ed62ffd88cae6db134445f4a0c0ae8b91d2e5e6" + integrity sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ== + dependencies: + "@babel/runtime" "^7.12.13" + history "^4.9.0" + loose-envify "^1.3.1" + prop-types "^15.6.2" + react-router "5.3.4" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + +react-router@5.3.4, react-router@^5.3.4: + version "5.3.4" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.3.4.tgz#8ca252d70fcc37841e31473c7a151cf777887bb5" + integrity sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA== + dependencies: + "@babel/runtime" "^7.12.13" + history "^4.9.0" + hoist-non-react-statics "^3.1.0" + loose-envify "^1.3.1" + path-to-regexp "^1.7.0" + prop-types "^15.6.2" + react-is "^16.6.0" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + +react-textarea-autosize@~8.3.2: + version "8.3.4" + resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.3.4.tgz#270a343de7ad350534141b02c9cb78903e553524" + integrity sha512-CdtmP8Dc19xL8/R6sWvtknD/eCXkQr30dtvC4VmGInhRsfF8X/ihXCq6+9l9qbxmKRiq407/7z5fxE7cVWQNgQ== + dependencies: + "@babel/runtime" "^7.10.2" + use-composed-ref "^1.3.0" + use-latest "^1.2.1" + +react-transition-group@~4.4.2: + version "4.4.5" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" + integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + +react@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" + integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== + dependencies: + loose-envify "^1.1.0" + +readable-stream@^2.0.1: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.0.6: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +reading-time@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/reading-time/-/reading-time-1.5.0.tgz#d2a7f1b6057cb2e169beaf87113cc3411b5bc5bb" + integrity sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg== + +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + integrity sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw== + dependencies: + resolve "^1.1.6" + +recursive-readdir@^2.2.2: + version "2.2.3" + resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.3.tgz#e726f328c0d69153bcabd5c322d3195252379372" + integrity sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA== + dependencies: + minimatch "^3.0.5" + +regenerate-unicode-properties@^10.1.0: + version "10.1.1" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz#6b0e05489d9076b04c436f318d9b067bba459480" + integrity sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q== + dependencies: + regenerate "^1.4.2" + +regenerate@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" + integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== + +regenerator-runtime@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" + integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA== + +regenerator-transform@^0.15.2: + version "0.15.2" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.2.tgz#5bbae58b522098ebdf09bca2f83838929001c7a4" + integrity sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg== + dependencies: + "@babel/runtime" "^7.8.4" + +regexpu-core@^5.3.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.2.tgz#11a2b06884f3527aec3e93dbbf4a3b958a95546b" + integrity sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ== + dependencies: + "@babel/regjsgen" "^0.8.0" + regenerate "^1.4.2" + regenerate-unicode-properties "^10.1.0" + regjsparser "^0.9.1" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.1.0" + +registry-auth-token@^5.0.1: + version "5.0.2" + resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-5.0.2.tgz#8b026cc507c8552ebbe06724136267e63302f756" + integrity sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ== + dependencies: + "@pnpm/npm-conf" "^2.1.0" + +registry-url@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-6.0.1.tgz#056d9343680f2f64400032b1e199faa692286c58" + integrity sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q== + dependencies: + rc "1.2.8" + +regjsparser@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709" + integrity sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ== + dependencies: + jsesc "~0.5.0" + +rehype-raw@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/rehype-raw/-/rehype-raw-7.0.0.tgz#59d7348fd5dbef3807bbaa1d443efd2dd85ecee4" + integrity sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww== + dependencies: + "@types/hast" "^3.0.0" + hast-util-raw "^9.0.0" + vfile "^6.0.0" + +relateurl@^0.2.7: + version "0.2.7" + resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" + integrity sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog== + +remark-directive@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/remark-directive/-/remark-directive-3.0.0.tgz#34452d951b37e6207d2e2a4f830dc33442923268" + integrity sha512-l1UyWJ6Eg1VPU7Hm/9tt0zKtReJQNOA4+iDMAxTyZNWnJnFlbS/7zhiel/rogTLQ2vMYwDzSJa4BiVNqGlqIMA== + dependencies: + "@types/mdast" "^4.0.0" + mdast-util-directive "^3.0.0" + micromark-extension-directive "^3.0.0" + unified "^11.0.0" + +remark-emoji@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/remark-emoji/-/remark-emoji-4.0.1.tgz#671bfda668047689e26b2078c7356540da299f04" + integrity sha512-fHdvsTR1dHkWKev9eNyhTo4EFwbUvJ8ka9SgeWkMPYFX4WoI7ViVBms3PjlQYgw5TLvNQso3GUB/b/8t3yo+dg== + dependencies: + "@types/mdast" "^4.0.2" + emoticon "^4.0.1" + mdast-util-find-and-replace "^3.0.1" + node-emoji "^2.1.0" + unified "^11.0.4" + +remark-frontmatter@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/remark-frontmatter/-/remark-frontmatter-5.0.0.tgz#b68d61552a421ec412c76f4f66c344627dc187a2" + integrity sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ== + dependencies: + "@types/mdast" "^4.0.0" + mdast-util-frontmatter "^2.0.0" + micromark-extension-frontmatter "^2.0.0" + unified "^11.0.0" + +remark-gfm@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-4.0.0.tgz#aea777f0744701aa288b67d28c43565c7e8c35de" + integrity sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA== + dependencies: + "@types/mdast" "^4.0.0" + mdast-util-gfm "^3.0.0" + micromark-extension-gfm "^3.0.0" + remark-parse "^11.0.0" + remark-stringify "^11.0.0" + unified "^11.0.0" + +remark-mdx@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/remark-mdx/-/remark-mdx-3.0.0.tgz#146905a3925b078970e05fc89b0e16b9cc3bfddd" + integrity sha512-O7yfjuC6ra3NHPbRVxfflafAj3LTwx3b73aBvkEFU5z4PsD6FD4vrqJAkE5iNGLz71GdjXfgRqm3SQ0h0VuE7g== + dependencies: + mdast-util-mdx "^3.0.0" + micromark-extension-mdxjs "^3.0.0" + +remark-parse@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-11.0.0.tgz#aa60743fcb37ebf6b069204eb4da304e40db45a1" + integrity sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA== + dependencies: + "@types/mdast" "^4.0.0" + mdast-util-from-markdown "^2.0.0" + micromark-util-types "^2.0.0" + unified "^11.0.0" + +remark-rehype@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-11.0.0.tgz#7f21c08738bde024be5f16e4a8b13e5d7a04cf6b" + integrity sha512-vx8x2MDMcxuE4lBmQ46zYUDfcFMmvg80WYX+UNLeG6ixjdCCLcw1lrgAukwBTuOFsS78eoAedHGn9sNM0w7TPw== + dependencies: + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + mdast-util-to-hast "^13.0.0" + unified "^11.0.0" + vfile "^6.0.0" + +remark-stringify@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-11.0.0.tgz#4c5b01dd711c269df1aaae11743eb7e2e7636fd3" + integrity sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw== + dependencies: + "@types/mdast" "^4.0.0" + mdast-util-to-markdown "^2.0.0" + unified "^11.0.0" + +renderkid@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-3.0.0.tgz#5fd823e4d6951d37358ecc9a58b1f06836b6268a" + integrity sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg== + dependencies: + css-select "^4.1.3" + dom-converter "^0.2.0" + htmlparser2 "^6.1.0" + lodash "^4.17.21" + strip-ansi "^6.0.1" + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +"require-like@>= 0.1.1": + version "0.1.2" + resolved "https://registry.yarnpkg.com/require-like/-/require-like-0.1.2.tgz#ad6f30c13becd797010c468afa775c0c0a6b47fa" + integrity sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + +resolve-alpn@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9" + integrity sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g== + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-package-path@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/resolve-package-path/-/resolve-package-path-4.0.3.tgz#31dab6897236ea6613c72b83658d88898a9040aa" + integrity sha512-SRpNAPW4kewOaNUt8VPqhJ0UMxawMwzJD8V7m1cJfdSTK9ieZwS6K7Dabsm4bmLFM96Z5Y/UznrpG5kt1im8yA== + dependencies: + path-root "^0.1.1" + +resolve-pathname@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" + integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== + +resolve@^1.1.6, resolve@^1.14.2: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +responselike@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-3.0.0.tgz#20decb6c298aff0dbee1c355ca95461d42823626" + integrity sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg== + dependencies: + lowercase-keys "^3.0.0" + +retry@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +rtl-detect@^1.0.4: + version "1.1.2" + resolved "https://registry.yarnpkg.com/rtl-detect/-/rtl-detect-1.1.2.tgz#ca7f0330af5c6bb626c15675c642ba85ad6273c6" + integrity sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ== + +rtlcss@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/rtlcss/-/rtlcss-4.1.1.tgz#f20409fcc197e47d1925996372be196fee900c0c" + integrity sha512-/oVHgBtnPNcggP2aVXQjSy6N1mMAfHg4GSag0QtZBlD5bdDgAHwr4pydqJGd+SUCu9260+Pjqbjwtvu7EMH1KQ== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + postcss "^8.4.21" + strip-json-comments "^3.1.1" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +rxjs@^7.8.0: + version "7.8.1" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" + integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== + dependencies: + tslib "^2.1.0" + +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sax@>=0.6.0, sax@^1.2.4: + version "1.3.0" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.3.0.tgz#a5dbe77db3be05c9d1ee7785dbd3ea9de51593d0" + integrity sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA== + +scheduler@^0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" + integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== + dependencies: + loose-envify "^1.1.0" + +schema-utils@2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7" + integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== + dependencies: + "@types/json-schema" "^7.0.4" + ajv "^6.12.2" + ajv-keywords "^3.4.1" + +schema-utils@^3.0.0, schema-utils@^3.1.1, schema-utils@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +schema-utils@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.2.0.tgz#70d7c93e153a273a805801882ebd3bff20d89c8b" + integrity sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw== + dependencies: + "@types/json-schema" "^7.0.9" + ajv "^8.9.0" + ajv-formats "^2.1.1" + ajv-keywords "^5.1.0" + +section-matter@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/section-matter/-/section-matter-1.0.0.tgz#e9041953506780ec01d59f292a19c7b850b84167" + integrity sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA== + dependencies: + extend-shallow "^2.0.1" + kind-of "^6.0.0" + +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" + integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== + +selfsigned@^2.1.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.4.1.tgz#560d90565442a3ed35b674034cec4e95dceb4ae0" + integrity sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q== + dependencies: + "@types/node-forge" "^1.3.0" + node-forge "^1" + +semver-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-4.0.0.tgz#3afcf5ed6d62259f5c72d0d5d50dffbdc9680df5" + integrity sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA== + dependencies: + semver "^7.3.5" + +semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.4: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + dependencies: + lru-cache "^6.0.0" + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serialize-javascript@^6.0.0, serialize-javascript@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c" + integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w== + dependencies: + randombytes "^2.1.0" + +serve-handler@^6.1.5: + version "6.1.5" + resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.5.tgz#a4a0964f5c55c7e37a02a633232b6f0d6f068375" + integrity sha512-ijPFle6Hwe8zfmBxJdE+5fta53fdIY0lHISJvuikXB3VYFafRjMRpOffSPvCYsbKyBA7pvy9oYr/BT1O3EArlg== + dependencies: + bytes "3.0.0" + content-disposition "0.5.2" + fast-url-parser "1.1.3" + mime-types "2.1.18" + minimatch "3.1.2" + path-is-inside "1.0.2" + path-to-regexp "2.2.1" + range-parser "1.2.0" + +serve-index@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" + integrity sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw== + dependencies: + accepts "~1.3.4" + batch "0.6.1" + debug "2.6.9" + escape-html "~1.0.3" + http-errors "~1.6.2" + mime-types "~2.1.17" + parseurl "~1.3.2" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +set-function-length@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.1.1.tgz#4bc39fafb0307224a33e106a7d35ca1218d659ed" + integrity sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ== + dependencies: + define-data-property "^1.1.1" + get-intrinsic "^1.2.1" + gopd "^1.0.1" + has-property-descriptors "^1.0.0" + +setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +shallowequal@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" + integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shell-quote@^1.7.3, shell-quote@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" + integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== + +shelljs@^0.8.5: + version "0.8.5" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" + integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +signal-exit@^3.0.2, signal-exit@^3.0.3: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +sirv@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/sirv/-/sirv-2.0.3.tgz#ca5868b87205a74bef62a469ed0296abceccd446" + integrity sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA== + dependencies: + "@polka/url" "^1.0.0-next.20" + mrmime "^1.0.0" + totalist "^3.0.0" + +sisteransi@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" + integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + +sitemap@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/sitemap/-/sitemap-7.1.1.tgz#eeed9ad6d95499161a3eadc60f8c6dce4bea2bef" + integrity sha512-mK3aFtjz4VdJN0igpIJrinf3EO8U8mxOPsTBzSsy06UtjZQJ3YY3o3Xa7zSc5nMqcMrRwlChHZ18Kxg0caiPBg== + dependencies: + "@types/node" "^17.0.5" + "@types/sax" "^1.2.1" + arg "^5.0.0" + sax "^1.2.4" + +skin-tone@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/skin-tone/-/skin-tone-2.0.0.tgz#4e3933ab45c0d4f4f781745d64b9f4c208e41237" + integrity sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA== + dependencies: + unicode-emoji-modifier-base "^1.0.0" + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +slash@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" + integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== + +snake-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" + integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" + +sockjs@^0.3.24: + version "0.3.24" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" + integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== + dependencies: + faye-websocket "^0.11.3" + uuid "^8.3.2" + websocket-driver "^0.7.4" + +sort-css-media-queries@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/sort-css-media-queries/-/sort-css-media-queries-2.1.0.tgz#7c85e06f79826baabb232f5560e9745d7a78c4ce" + integrity sha512-IeWvo8NkNiY2vVYdPa27MCQiR0MN0M80johAYFVxWWXQ44KU84WNxjslwBHmc/7ZL2ccwkM7/e6S5aiKZXm7jA== + +source-map-js@^1.0.1, source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +source-map@^0.7.0: + version "0.7.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" + integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== + +space-separated-tokens@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" + integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== + +spdy-transport@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" + integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== + dependencies: + debug "^4.1.0" + detect-node "^2.0.4" + hpack.js "^2.1.6" + obuf "^1.1.2" + readable-stream "^3.0.6" + wbuf "^1.7.3" + +spdy@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" + integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== + dependencies: + debug "^4.1.0" + handle-thing "^2.0.0" + http-deceiver "^1.2.7" + select-hose "^2.0.0" + spdy-transport "^3.0.0" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + +srcset@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/srcset/-/srcset-4.0.0.tgz#336816b665b14cd013ba545b6fe62357f86e65f4" + integrity sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw== + +stable@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" + integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +"statuses@>= 1.4.0 < 2": + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + +std-env@^3.0.1: + version "3.4.3" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.4.3.tgz#326f11db518db751c83fd58574f449b7c3060910" + integrity sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q== + +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +stringify-entities@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-4.0.3.tgz#cfabd7039d22ad30f3cc435b0ca2c1574fc88ef8" + integrity sha512-BP9nNHMhhfcMbiuQKCqMjhDP5yBCAxsPu4pHFFzJ6Alo9dZgY4VLDPutXqIjpRiMoKdp7Av85Gr73Q5uH9k7+g== + dependencies: + character-entities-html4 "^2.0.0" + character-entities-legacy "^3.0.0" + +stringify-object@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" + integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw== + dependencies: + get-own-enumerable-property-symbols "^3.0.0" + is-obj "^1.0.1" + is-regexp "^1.0.0" + +strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + +strip-bom-string@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz#e5211e9224369fbb81d633a2f00044dc8cedad92" + integrity sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g== + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== + +style-to-object@^0.4.0: + version "0.4.4" + resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.4.4.tgz#266e3dfd56391a7eefb7770423612d043c3f33ec" + integrity sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg== + dependencies: + inline-style-parser "0.1.1" + +stylehacks@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.1.1.tgz#7934a34eb59d7152149fa69d6e9e56f2fc34bcc9" + integrity sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw== + dependencies: + browserslist "^4.21.4" + postcss-selector-parser "^6.0.4" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +svg-parser@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5" + integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== + +svgo@^2.7.0, svgo@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24" + integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg== + dependencies: + "@trysound/sax" "0.2.0" + commander "^7.2.0" + css-select "^4.1.3" + css-tree "^1.1.3" + csso "^4.2.0" + picocolors "^1.0.0" + stable "^0.1.8" + +svgo@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-3.0.2.tgz#5e99eeea42c68ee0dc46aa16da093838c262fe0a" + integrity sha512-Z706C1U2pb1+JGP48fbazf3KxHrWOsLme6Rv7imFBn5EnuanDW1GPaA/P1/dvObE670JDePC3mnj0k0B7P0jjQ== + dependencies: + "@trysound/sax" "0.2.0" + commander "^7.2.0" + css-select "^5.1.0" + css-tree "^2.2.1" + csso "^5.0.5" + picocolors "^1.0.0" + +tapable@^1.0.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" + integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== + +tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +terser-webpack-plugin@^5.3.7, terser-webpack-plugin@^5.3.9: + version "5.3.9" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz#832536999c51b46d468067f9e37662a3b96adfe1" + integrity sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA== + dependencies: + "@jridgewell/trace-mapping" "^0.3.17" + jest-worker "^27.4.5" + schema-utils "^3.1.1" + serialize-javascript "^6.0.1" + terser "^5.16.8" + +terser@^5.10.0, terser@^5.15.1, terser@^5.16.8: + version "5.23.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.23.0.tgz#a9c02bc3087d0f5b1cc63bbfb4fe0f7e5dbbde82" + integrity sha512-Iyy83LN0uX9ZZLCX4Qbu5JiHiWjOCTwrmM9InWOzVeM++KNWEsqV4YgN9U9E8AlohQ6Gs42ztczlWOG/lwDAMA== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" + commander "^2.20.0" + source-map-support "~0.5.20" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +thunky@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" + integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== + +tiny-invariant@^1.0.2: + version "1.3.1" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642" + integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== + +tiny-warning@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" + integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +totalist@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.1.tgz#ba3a3d600c915b1a97872348f79c127475f6acf8" + integrity sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ== + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +trim-lines@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338" + integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg== + +trough@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/trough/-/trough-2.1.0.tgz#0f7b511a4fde65a46f18477ab38849b22c554876" + integrity sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g== + +tslib@^1.10.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.6.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + +type-fest@^1.0.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" + integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== + +type-fest@^2.13.0, type-fest@^2.5.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" + integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typedarray-to-buffer@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" + integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== + dependencies: + is-typedarray "^1.0.0" + +ua-parser-js@^1.0.35: + version "1.0.37" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.37.tgz#b5dc7b163a5c1f0c510b08446aed4da92c46373f" + integrity sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ== + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +unicode-canonical-property-names-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" + integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== + +unicode-emoji-modifier-base@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz#dbbd5b54ba30f287e2a8d5a249da6c0cef369459" + integrity sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g== + +unicode-match-property-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" + integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== + dependencies: + unicode-canonical-property-names-ecmascript "^2.0.0" + unicode-property-aliases-ecmascript "^2.0.0" + +unicode-match-property-value-ecmascript@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz#cb5fffdcd16a05124f5a4b0bf7c3770208acbbe0" + integrity sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA== + +unicode-property-aliases-ecmascript@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" + integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== + +unified@^11.0.0, unified@^11.0.3, unified@^11.0.4: + version "11.0.4" + resolved "https://registry.yarnpkg.com/unified/-/unified-11.0.4.tgz#f4be0ac0fe4c88cb873687c07c64c49ed5969015" + integrity sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ== + dependencies: + "@types/unist" "^3.0.0" + bail "^2.0.0" + devlop "^1.0.0" + extend "^3.0.0" + is-plain-obj "^4.0.0" + trough "^2.0.0" + vfile "^6.0.0" + +unique-string@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-3.0.0.tgz#84a1c377aff5fd7a8bc6b55d8244b2bd90d75b9a" + integrity sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ== + dependencies: + crypto-random-string "^4.0.0" + +unist-util-is@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-6.0.0.tgz#b775956486aff107a9ded971d996c173374be424" + integrity sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-position-from-estree@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz#d94da4df596529d1faa3de506202f0c9a23f2200" + integrity sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-position@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-5.0.0.tgz#678f20ab5ca1207a97d7ea8a388373c9cf896be4" + integrity sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-remove-position@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz#fea68a25658409c9460408bc6b4991b965b52163" + integrity sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q== + dependencies: + "@types/unist" "^3.0.0" + unist-util-visit "^5.0.0" + +unist-util-stringify-position@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz#449c6e21a880e0855bf5aabadeb3a740314abac2" + integrity sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-visit-parents@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz#4d5f85755c3b8f0dc69e21eca5d6d82d22162815" + integrity sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + +unist-util-visit@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6" + integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + unist-util-visit-parents "^6.0.0" + +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +update-browserslist-db@^1.0.13: + version "1.0.13" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4" + integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + +update-notifier@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-6.0.2.tgz#a6990253dfe6d5a02bd04fbb6a61543f55026b60" + integrity sha512-EDxhTEVPZZRLWYcJ4ZXjGFN0oP7qYvbXWzEgRm/Yql4dHX5wDbvh89YHP6PK1lzZJYrMtXUuZZz8XGK+U6U1og== + dependencies: + boxen "^7.0.0" + chalk "^5.0.1" + configstore "^6.0.0" + has-yarn "^3.0.0" + import-lazy "^4.0.0" + is-ci "^3.0.1" + is-installed-globally "^0.4.0" + is-npm "^6.0.0" + is-yarn-global "^0.4.0" + latest-version "^7.0.0" + pupa "^3.1.0" + semver "^7.3.7" + semver-diff "^4.0.0" + xdg-basedir "^5.1.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +url-loader@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-4.1.1.tgz#28505e905cae158cf07c92ca622d7f237e70a4e2" + integrity sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA== + dependencies: + loader-utils "^2.0.0" + mime-types "^2.1.27" + schema-utils "^3.0.0" + +use-composed-ref@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.3.0.tgz#3d8104db34b7b264030a9d916c5e94fbe280dbda" + integrity sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ== + +use-isomorphic-layout-effect@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb" + integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA== + +use-latest@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/use-latest/-/use-latest-1.2.1.tgz#d13dfb4b08c28e3e33991546a2cee53e14038cf2" + integrity sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw== + dependencies: + use-isomorphic-layout-effect "^1.1.1" + +util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +utila@~0.4: + version "0.4.0" + resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" + integrity sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA== + +utility-types@^3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.10.0.tgz#ea4148f9a741015f05ed74fd615e1d20e6bed82b" + integrity sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +validate-peer-dependencies@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/validate-peer-dependencies/-/validate-peer-dependencies-2.2.0.tgz#47b8ff008f66a66fc5d8699123844522c1d874f4" + integrity sha512-8X1OWlERjiUY6P6tdeU9E0EwO8RA3bahoOVG7ulOZT5MqgNDUO/BQoVjYiHPcNe+v8glsboZRIw9iToMAA2zAA== + dependencies: + resolve-package-path "^4.0.3" + semver "^7.3.8" + +value-equal@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" + integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +vfile-location@^5.0.0: + version "5.0.2" + resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-5.0.2.tgz#220d9ca1ab6f8b2504a4db398f7ebc149f9cb464" + integrity sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg== + dependencies: + "@types/unist" "^3.0.0" + vfile "^6.0.0" + +vfile-message@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.2.tgz#c883c9f677c72c166362fd635f21fc165a7d1181" + integrity sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw== + dependencies: + "@types/unist" "^3.0.0" + unist-util-stringify-position "^4.0.0" + +vfile@^6.0.0, vfile@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/vfile/-/vfile-6.0.1.tgz#1e8327f41eac91947d4fe9d237a2dd9209762536" + integrity sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw== + dependencies: + "@types/unist" "^3.0.0" + unist-util-stringify-position "^4.0.0" + vfile-message "^4.0.0" + +wait-on@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-7.0.1.tgz#5cff9f8427e94f4deacbc2762e6b0a489b19eae9" + integrity sha512-9AnJE9qTjRQOlTZIldAaf/da2eW0eSRSgcqq85mXQja/DW3MriHxkpODDSUEg+Gri/rKEcXUZHe+cevvYItaog== + dependencies: + axios "^0.27.2" + joi "^17.7.0" + lodash "^4.17.21" + minimist "^1.2.7" + rxjs "^7.8.0" + +watchpack@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" + integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +wbuf@^1.1.0, wbuf@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" + integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== + dependencies: + minimalistic-assert "^1.0.0" + +web-namespaces@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692" + integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ== + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +webpack-bundle-analyzer@^4.9.0: + version "4.9.1" + resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.9.1.tgz#d00bbf3f17500c10985084f22f1a2bf45cb2f09d" + integrity sha512-jnd6EoYrf9yMxCyYDPj8eutJvtjQNp8PHmni/e/ulydHBWhT5J3menXt3HEkScsu9YqMAcG4CfFjs3rj5pVU1w== + dependencies: + "@discoveryjs/json-ext" "0.5.7" + acorn "^8.0.4" + acorn-walk "^8.0.0" + commander "^7.2.0" + escape-string-regexp "^4.0.0" + gzip-size "^6.0.0" + is-plain-object "^5.0.0" + lodash.debounce "^4.0.8" + lodash.escape "^4.0.1" + lodash.flatten "^4.4.0" + lodash.invokemap "^4.6.0" + lodash.pullall "^4.2.0" + lodash.uniqby "^4.7.0" + opener "^1.5.2" + picocolors "^1.0.0" + sirv "^2.0.3" + ws "^7.3.1" + +webpack-dev-middleware@^5.3.1: + version "5.3.3" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz#efae67c2793908e7311f1d9b06f2a08dcc97e51f" + integrity sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA== + dependencies: + colorette "^2.0.10" + memfs "^3.4.3" + mime-types "^2.1.31" + range-parser "^1.2.1" + schema-utils "^4.0.0" + +webpack-dev-server@^4.15.1: + version "4.15.1" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz#8944b29c12760b3a45bdaa70799b17cb91b03df7" + integrity sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA== + dependencies: + "@types/bonjour" "^3.5.9" + "@types/connect-history-api-fallback" "^1.3.5" + "@types/express" "^4.17.13" + "@types/serve-index" "^1.9.1" + "@types/serve-static" "^1.13.10" + "@types/sockjs" "^0.3.33" + "@types/ws" "^8.5.5" + ansi-html-community "^0.0.8" + bonjour-service "^1.0.11" + chokidar "^3.5.3" + colorette "^2.0.10" + compression "^1.7.4" + connect-history-api-fallback "^2.0.0" + default-gateway "^6.0.3" + express "^4.17.3" + graceful-fs "^4.2.6" + html-entities "^2.3.2" + http-proxy-middleware "^2.0.3" + ipaddr.js "^2.0.1" + launch-editor "^2.6.0" + open "^8.0.9" + p-retry "^4.5.0" + rimraf "^3.0.2" + schema-utils "^4.0.0" + selfsigned "^2.1.1" + serve-index "^1.9.1" + sockjs "^0.3.24" + spdy "^4.0.2" + webpack-dev-middleware "^5.3.1" + ws "^8.13.0" + +webpack-merge@^5.9.0: + version "5.10.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.10.0.tgz#a3ad5d773241e9c682803abf628d4cd62b8a4177" + integrity sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA== + dependencies: + clone-deep "^4.0.1" + flat "^5.0.2" + wildcard "^2.0.0" + +webpack-sources@^3.2.2, webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack@^5.88.1: + version "5.89.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.89.0.tgz#56b8bf9a34356e93a6625770006490bf3a7f32dc" + integrity sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw== + dependencies: + "@types/eslint-scope" "^3.7.3" + "@types/estree" "^1.0.0" + "@webassemblyjs/ast" "^1.11.5" + "@webassemblyjs/wasm-edit" "^1.11.5" + "@webassemblyjs/wasm-parser" "^1.11.5" + acorn "^8.7.1" + acorn-import-assertions "^1.9.0" + browserslist "^4.14.5" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.15.0" + es-module-lexer "^1.2.1" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.9" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.2.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.3.7" + watchpack "^2.4.0" + webpack-sources "^3.2.3" + +webpackbar@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/webpackbar/-/webpackbar-5.0.2.tgz#d3dd466211c73852741dfc842b7556dcbc2b0570" + integrity sha512-BmFJo7veBDgQzfWXl/wwYXr/VFus0614qZ8i9znqcl9fnEdiVkdbi0TedLQ6xAK92HZHDJ0QmyQ0fmuZPAgCYQ== + dependencies: + chalk "^4.1.0" + consola "^2.15.3" + pretty-time "^1.1.0" + std-env "^3.0.1" + +websocket-driver@>=0.5.1, websocket-driver@^0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + dependencies: + http-parser-js ">=0.5.1" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +which@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +widest-line@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-4.0.1.tgz#a0fc673aaba1ea6f0a0d35b3c2795c9a9cc2ebf2" + integrity sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig== + dependencies: + string-width "^5.0.1" + +wildcard@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" + integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== + +wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +write-file-atomic@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" + integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== + dependencies: + imurmurhash "^0.1.4" + is-typedarray "^1.0.0" + signal-exit "^3.0.2" + typedarray-to-buffer "^3.1.5" + +ws@^7.3.1: + version "7.5.9" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" + integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== + +ws@^8.13.0: + version "8.14.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.2.tgz#6c249a806eb2db7a20d26d51e7709eab7b2e6c7f" + integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g== + +xdg-basedir@^5.0.1, xdg-basedir@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-5.1.0.tgz#1efba19425e73be1bc6f2a6ceb52a3d2c884c0c9" + integrity sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ== + +xml-js@^1.6.11: + version "1.6.11" + resolved "https://registry.yarnpkg.com/xml-js/-/xml-js-1.6.11.tgz#927d2f6947f7f1c19a316dd8eea3614e8b18f8e9" + integrity sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g== + dependencies: + sax "^1.2.4" + +xml2js@^0.4.22: + version "0.4.23" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" + integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== + dependencies: + sax ">=0.6.0" + xmlbuilder "~11.0.0" + +xmlbuilder@~11.0.0: + version "11.0.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" + integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +yocto-queue@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" + integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== + +zwitch@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" + integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A== diff --git "a/\346\272\220\347\240\201\347\233\256\345\275\225\350\257\264\346\230\216.md" "b/\346\272\220\347\240\201\347\233\256\345\275\225\350\257\264\346\230\216.md" index ae96fed6d99a8dee7b5a6624059d415edaaaf3e4..6c6bb623912aa8ff306bff24879df55af0caaad8 100644 --- "a/\346\272\220\347\240\201\347\233\256\345\275\225\350\257\264\346\230\216.md" +++ "b/\346\272\220\347\240\201\347\233\256\345\275\225\350\257\264\346\230\216.md" @@ -6,6 +6,7 @@ `├──` **`clients`** — 客户端请求代码工具目录
`├──` **`docs`** — 针对 `Github` 平台的 `Furion` 文档发布后代码
`├──` **`framework`** — `Furion` 框架代码
+`├──` **`handbook`** — `Furion` 文档源码,内部的 `build` 文件夹是 `Gitee` 平台文档发布后代码
`├──` **`references`** — `Furion` API 文档源码
`├──` **`samples`** — `Furion` 框架演示例子
`├──` **`schema`** — `Furion` 配置文件 `JSON Schema`