Compare commits

...

46 Commits

Author SHA1 Message Date
Diego
5aff6461a1 fix: opcuaServer 业务设备刷新变量时可能导致内存泄露 2025-05-16 19:26:33 +08:00
Diego
6cf53fefec 优化内存占用 2025-05-16 18:00:36 +08:00
Diego
45132f3503 更新依赖 2025-05-16 10:51:44 +08:00
Diego
f1e78a0e8a 恢复配置json 2025-05-15 12:17:43 +08:00
Diego
0bf28ec275 更新语言资源 2025-05-15 12:17:09 +08:00
Diego
41f8412c97 支持达梦数据库 2025-05-15 12:15:32 +08:00
Diego
c535974362 更新规则引擎示例 2025-05-15 10:44:12 +08:00
Diego
1860c5f215 build:10.6.1 2025-05-15 09:12:33 +08:00
Diego
6d778b2d39 增加initDatabase配置项 2025-05-15 09:08:08 +08:00
Diego
f48b99c259 更新示例 2025-05-14 21:12:51 +08:00
Diego
3c73b93051 更新依赖 2025-05-14 18:52:19 +08:00
Diego
98f3f2d519 添加 `过滤离线变量` 插件属性 2025-05-14 13:01:12 +08:00
Diego
b76b4e8d68 变量表索引删除语句兼容性增强 2025-05-13 19:27:50 +08:00
Diego
07285a7c61 mqtt增加qos属性 2025-05-13 19:27:27 +08:00
Diego
03c0dfef37 增加业务设备日志 2025-05-12 15:26:51 +08:00
Diego
6ef6929c35 优化api权限树 2025-05-12 10:21:41 +08:00
Diego
e3c0c173f0 更新依赖 2025-05-12 08:53:25 +08:00
Diego
7d64e058d4 更新依赖 2025-05-08 16:34:48 +08:00
Diego
e97ee9b64b 10.5.15 2025-05-07 22:08:54 +08:00
Diego
6a03e39eeb 10.5.14 2025-05-07 22:00:31 +08:00
Diego
525ec740b5 添加脚本demo 2025-05-06 11:43:56 +08:00
2248356998 qq.com
b790cf5f4e 更新依赖 2025-05-05 20:25:43 +08:00
Diego
d1248811fd build: 10.5.11 2025-04-30 23:05:36 +08:00
2248356998 qq.com
022d016e8e feat: 添加采集组 2025-04-30 23:04:51 +08:00
Diego
f73245e650 build: 10.5.10 2025-04-30 15:31:39 +08:00
2248356998 qq.com
484461fa05 更新依赖 2025-04-30 15:29:19 +08:00
Diego
7e0b7aff2a feat: sqldb支持数组 2025-04-28 15:52:32 +08:00
Diego
6c450dcb09 feat: sqldb支持数组类型 2025-04-28 15:52:11 +08:00
Diego
227f44283f build: 10.5.8 2025-04-28 15:30:10 +08:00
Diego
74f6e79625 build: 10.5.7
优化opcua变量缓存
修复cron表达式间隔
2025-04-27 16:35:58 +08:00
Diego
cec43e2ce8 feat: 防呆设计,强制设置通道的最大并发数 2025-04-27 10:13:35 +08:00
Diego
7553b258bb build: 10.5.5 2025-04-26 17:46:31 +08:00
Diego
8bdbdc117e 支持bind链路设置多个通道 2025-04-26 17:10:46 +08:00
Diego
0e206be296 更新依赖 2025-04-26 16:17:02 +08:00
Diego
00b7353433 兼容性增强 2025-04-26 15:53:20 +08:00
Diego
44e7a83593 更新依赖 2025-04-26 15:02:57 +08:00
Diego
dd68d555d4 feat: 支持字节数组上传 2025-04-24 10:25:36 +08:00
2248356998 qq.com
0456296103 更新依赖包 2025-04-23 23:07:27 +08:00
Diego
a1b66277ff nuget 2025-04-23 15:28:53 +08:00
Diego
50758b79bc 更新依赖包 2025-04-23 11:48:45 +08:00
Diego
06a1f902ad 修改Startup方法名称 2025-04-23 10:56:03 +08:00
Diego
58f8b23b7c build:10.5.1
fix: 反向代理正确获取客户端IP

refactor: 添加demo站点域名
2025-04-22 11:45:14 +08:00
Diego
9e7c348b15 添加大数据写入方法 2025-04-21 23:05:10 +08:00
Diego
5f5ff8b43b 删除实体部分特性 2025-04-21 17:53:33 +08:00
Diego
f626b4e5fc sql插件增加分表模式 属性 2025-04-21 11:00:05 +08:00
2248356998 qq.com
bc23200e66 更新依赖 2025-04-20 20:32:41 +08:00
349 changed files with 8102 additions and 5271 deletions

View File

@@ -20,11 +20,14 @@ A cross-platform, high-performance edge data collection gateway based on net9.
## Demo

[Demo](http://47.119.161.158:5000/)
[Demo](https://demo.thingsgateway.cn/)

Account: **SuperAdmin**

Password: **111111**

**In the upper-right corner, switch to the IoT Gateway module in the personal popup box**

View File

@@ -13,7 +13,7 @@
## 演示
[ThingsGateway演示地址](http://47.119.161.158:5000/)
[ThingsGateway演示地址](https://demo.thingsgateway.cn/)
账户 : **SuperAdmin**

View File

@@ -137,7 +137,7 @@ public sealed class OperDescAttribute : MoAttribute
Name = (localizerType == null ? App.CreateLocalizerByType(typeof(OperDescAttribute)) : App.CreateLocalizerByType(localizerType))![Description],
Category = LogCateGoryEnum.Operate,
ExeStatus = true,
OpIp = AppService?.RemoteIpAddress?.MapToIPv4()?.ToString() ?? string.Empty,
OpIp = AppService?.RemoteIpAddress ?? string.Empty,
OpBrowser = clientInfo?.UA?.Family + clientInfo?.UA?.Major,
OpOs = clientInfo?.OS?.Family + clientInfo?.OS?.Major,
OpTime = DateTime.Now,

View File

@@ -26,6 +26,7 @@ namespace ThingsGateway.Admin.Application;
[Route("openapi/auth")]
[Authorize(AuthenticationSchemes = "Bearer")]
[LoggingMonitor]
[ApiController]
public class OpenApiController : ControllerBase
{
private readonly IAuthService _authService;

View File

@@ -15,16 +15,13 @@ namespace ThingsGateway.Admin.Application;
[Route("api/[controller]/[action]")]
[AllowAnonymous]
[ApiController]
public class TestController : ControllerBase
{
[HttpPost]
public Task Test(string data)
[HttpGet]
public void Test()
{
for (int i = 0; i < 3; i++)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
return Task.CompletedTask;
GC.Collect();
GC.WaitForPendingFinalizers();
}
}

View File

@@ -23,7 +23,7 @@ public class SysDict : BaseEntity
/// <summary>
/// 类型
///</summary>
[SugarColumn(ColumnDescription = "类型", Length = 200)]
[SugarColumn(ColumnDescription = "类型")]
[AutoGenerateColumn(Ignore = true, Filterable = true, Sortable = true)]
public virtual DictTypeEnum DictType { get; set; }

View File

@@ -24,7 +24,7 @@ public class SysOperateLog
/// <summary>
/// 日志分类
///</summary>
[SugarColumn(ColumnDescription = "日志分类", Length = 200)]
[SugarColumn(ColumnDescription = "日志分类")]
[AutoGenerateColumn(Order = 1, Filterable = true, Sortable = true)]
public LogCateGoryEnum Category { get; set; }

View File

@@ -54,7 +54,7 @@ public class SysPosition : BaseEntity
/// <summary>
/// 分类
///</summary>
[SugarColumn(ColumnName = "Category", ColumnDescription = "分类", Length = 200)]
[SugarColumn(ColumnName = "Category", ColumnDescription = "分类")]
[AutoGenerateColumn(Visible = true, Sortable = true, Filterable = true)]
public virtual PositionCategoryEnum Category { get; set; }

View File

@@ -22,7 +22,7 @@ public class SysRelation : PrimaryKeyEntity
/// <summary>
/// 分类
///</summary>
[SugarColumn(ColumnDescription = "分类", Length = 200)]
[SugarColumn(ColumnDescription = "分类")]
public RelationCategoryEnum Category { get; set; }
/// <summary>

View File

@@ -41,7 +41,7 @@ public class SysRole : BaseEntity
/// <summary>
/// 分类
///</summary>
[SugarColumn(ColumnDescription = "分类", Length = 200, IsNullable = false)]
[SugarColumn(ColumnDescription = "分类", IsNullable = false)]
[AutoGenerateColumn(Visible = true, Sortable = true, Filterable = true)]
public virtual RoleCategoryEnum Category { get; set; }

View File

@@ -1,4 +1,15 @@
{
"ThingsGateway.Admin.Application.BaseDataEntity": {
"CreateOrgId": "CreateOrgId"
},
"ThingsGateway.Admin.Application.BaseEntity": {
"SortCode": "SortCode",
"CreateTime": "CreateTime",
"CreateUser": "CreateUser",
"UpdateTime": "UpdateTime",
"UpdateUser": "UpdateUser"
},
"ThingsGateway.Admin.Application.BlazorAuthenticationHandler": {
"UserExpire": "User expired, please login again"
},
@@ -24,9 +35,6 @@
"LatestLoginTime": "LatestLoginTime",
"LatestLoginDevice": "LatestLoginDevice",
"LatestLoginAddress": "LatestLoginAddress",
"SortCode": "SortCode",
"CreateTime": "CreateTime",
"UpdateTime": "UpdateTime",
"OrgNames": "OrgNames",
"PositionName": "PositionName",
"OrgId": "Org",
@@ -60,9 +68,6 @@
"Name": "Name",
"Name.Required": "{0} is required",
"Category": "Category",
"SortCode": "Sort",
"CreateTime": "CreateTime",
"UpdateTime": "UpdateTime",
"OrgId": "Org",
"Global": "Global",
"Status": "Status",
@@ -105,9 +110,6 @@
"Category": "Category",
"Target": "Target",
"NavLinkMatch": "NavLinkMatch",
"SortCode": "Sort",
"CreateTime": "CreateTime",
"UpdateTime": "UpdateTime",
"ParentId": "Parent",
"ResourceDup": "Duplicate name {0} exists",
"ResourceParentChoiceSelf": "Parent cannot choose itself",
@@ -134,9 +136,6 @@
"Status": "Status",
"OrgId": "Organization",
"Remark": "Remarks",
"SortCode": "SortCode",
"CreateTime": "CreateTime",
"UpdateTime": "UpdateTime",
"Dup": "Duplicate position exists with Category {0} and Name {1}",
"CodeDup": "Duplicate code {0} exists",
"NameDup": "Duplicate name {0} exists",
@@ -159,9 +158,6 @@
"Names": "Names",
"Remark": "Remarks",
"DirectorId": "Director",
"SortCode": "SortCode",
"CreateTime": "CreateTime",
"UpdateTime": "UpdateTime",
"Dup": "Duplicate organization exists with Category {0} and Name {1}",
"CodeDup": "Duplicate code {0} exists",
"NameDup": "Duplicate name {0} exists",
@@ -358,9 +354,6 @@
"Name": "Name",
"Code": "Code",
"Remark": "Remark",
"SortCode": "Sort",
"CreateTime": "CreateTime",
"UpdateTime": "UpdateTime",
"DemoCanotUpdateWebsitePolicy": "DEMO environment does not allow modifying website settings",
"DictDup": "Duplicate configuration exists, category {0}, name {1}"
},

View File

@@ -1,469 +1,462 @@
{
"ThingsGateway.Admin.Application.BaseDataEntity": {
"CreateOrgId": "创建机构Id"
},
"ThingsGateway.Admin.Application.BaseEntity": {
"SortCode": "排序",
"CreateTime": "创建时间",
"CreateUser": "创建人",
"UpdateTime": "更新时间",
"UpdateUser": "更新人"
},
"ThingsGateway.Admin.Application.BlazorAuthenticationHandler": {
"UserExpire": "用户登录已过期,请重新登录"
},
"UserExpire": "用户登录已过期,请重新登录"
},
"ThingsGateway.Admin.Application.SysUser": {
"Disable": "禁用",
"Enable": "启用",
"GrantRole": "分配角色",
"ExitVerificat": "您已被强制下线",
"PasswordEdited": "密码被修改,已退出登录",
"Avatar": "头像",
"Account": "账号",
"Account.Required": " {0} 是必填项",
"Password": "密码",
"Status": "状态",
"Phone": "手机",
"Email": "邮箱",
"LastLoginIp": "上次登录ip",
"LastLoginDevice": "上次登录设备",
"LastLoginTime": "上次登录时间",
"LastLoginAddress": "上次登录地点",
"LatestLoginIp": "最新登录ip",
"LatestLoginTime": "最新登录时间",
"LatestLoginDevice": "最新登录设备",
"LatestLoginAddress": "最新登录地点",
"SortCode": "排序",
"CreateTime": "创建时间",
"UpdateTime": "更新时间",
"OrgNames": "机构名称",
"PositionName": "职位名称",
"OrgId": "机构",
"PositionId": "职位",
"DirectorId": "主管",
"CheckSelf": "禁止 {0} 自己",
"CanotDeleteAdminUser": "不可删除系统内置超管用户",
"CanotEditAdminUser": "不可编辑超管用户",
"CanotGrantAdmin": "不能分配超管角色",
"EmailDup": "存在重复的邮箱 {0}",
"AccountDup": "存在重复的账号 {0}",
"CanotDeleteSelf": "不可删除自己",
"EmailError": "邮箱 {0} 格式错误",
"PhoneError": "手机号码 {0} 格式错误",
"NoOrg": "组织机构不存在",
"DirectorSelf": "不能设置自己为主管",
"ThingsGateway.Admin.Application.SysUser": {
"Disable": "禁用",
"Enable": "启用",
"GrantRole": "分配角色",
"ExitVerificat": "您已被强制下线",
"PasswordEdited": "密码被修改,已退出登录",
"Avatar": "头像",
"Account": "账号",
"Account.Required": " {0} 是必填项",
"Password": "密码",
"Status": "状态",
"Phone": "手机",
"Email": "邮箱",
"LastLoginIp": "上次登录ip",
"LastLoginDevice": "上次登录设备",
"LastLoginTime": "上次登录时间",
"LastLoginAddress": "上次登录地点",
"LatestLoginIp": "最新登录ip",
"LatestLoginTime": "最新登录时间",
"LatestLoginDevice": "最新登录设备",
"LatestLoginAddress": "最新登录地点",
"OrgNames": "机构名称",
"PositionName": "职位名称",
"OrgId": "机构",
"PositionId": "职位",
"DirectorId": "主管",
"CheckSelf": "禁止 {0} 自己",
"CanotDeleteAdminUser": "不可删除系统内置超管用户",
"CanotEditAdminUser": "不可编辑超管用户",
"CanotGrantAdmin": "不能分配超管角色",
"EmailDup": "存在重复的邮箱 {0}",
"AccountDup": "存在重复的账号 {0}",
"CanotDeleteSelf": "不可删除自己",
"EmailError": "邮箱 {0} 格式错误",
"PhoneError": "手机号码 {0} 格式错误",
"NoOrg": "组织机构不存在",
"DirectorSelf": "不能设置自己为主管",
"DemoCanotUpdatePassword": "DEMO环境不允许修改密码",
"OldPasswordError": "原密码错误",
"ConfirmPasswordDiff": "两次输入的密码不一致",
"PasswordLengthLess": "密码长度不能小于 {0} ",
"PasswordMustNum ": "密码必须包含数字",
"PasswordMustLow": "密码必须包含小写字母",
"PasswordMustUpp": "密码必须包含大写字母",
"PasswordMustSpecial": "密码必须包含特殊字符"
},
"DemoCanotUpdatePassword": "DEMO环境不允许修改密码",
"OldPasswordError": "原密码错误",
"ConfirmPasswordDiff": "两次输入的密码不一致",
"PasswordLengthLess": "密码长度不能小于 {0} ",
"PasswordMustNum ": "密码必须包含数字",
"PasswordMustLow": "密码必须包含小写字母",
"PasswordMustUpp": "密码必须包含大写字母",
"PasswordMustSpecial": "密码必须包含特殊字符"
},
"ThingsGateway.Admin.Application.SysRole": {
"Code": "编码",
"Name": "名称",
"Name.Required": " {0} 是必填项",
"Category": "分类",
"SortCode": "排序",
"Global": "全局",
"Status": "状态",
"OrgId": "机构",
"CreateTime": "创建时间",
"UpdateTime": "更新时间",
"ThingsGateway.Admin.Application.SysRole": {
"Code": "编码",
"Name": "名称",
"Name.Required": " {0} 是必填项",
"Category": "分类",
"Global": "全局",
"Status": "状态",
"OrgId": "机构",
"CanotDeleteAdmin": "不可删除系统内置超管角色",
"CanotEditAdmin": "不可编辑超管角色",
"CanotGrantAdmin": "不能分配超管角色",
"CanotDeleteAdmin": "不可删除系统内置超管角色",
"CanotEditAdmin": "不可编辑超管角色",
"CanotGrantAdmin": "不能分配超管角色",
"NameDup": "存在重复的角色名称 {0}",
"OrgNotNull": "机构不能为空",
"SameOrgNameDup": "存在重复的角色名称 {0}",
"CannotRoleScopeAll": "机构角色不能选择全局数据范围",
"CodeDup": "存在重复的编码 {0}"
},
"ThingsGateway.Admin.Application.RoleCategoryEnum": {
"Global": "全局",
"Org": "机构"
},
"ThingsGateway.Admin.Application.DataScopeEnum": {
"SCOPE_SELF": "仅自己",
"SCOPE_ALL": "全部",
"SCOPE_ORG": "仅所属组织",
"SCOPE_ORG_CHILD": "所属组织及以下",
"SCOPE_ORG_DEFINE": "自定义"
},
"ThingsGateway.Admin.Application.DefaultDataScope": {
"ScopeCategory": "数据范围",
"ScopeDefineOrgIdList": "自定义列表"
},
"ThingsGateway.Admin.Application.SysResource": {
"Title": "标题",
"Module": "模块",
"Title.Required": "{0} 是必填项",
"Href.Required": "{0} 是必填项",
"Icon": "图标",
"Href": "路径",
"Code": "编码",
"Category": "分类",
"Target": "跳转类型",
"NavLinkMatch": "匹配类型",
"SortCode": "排序",
"ParentId": "上级菜单",
"CreateTime": "创建时间",
"UpdateTime": "更新时间",
"ResourceDup": "存在重复的名称 {0}",
"ResourceParentChoiceSelf": "父级不能选择自己",
"ResourceParentNull": "父级不存在 {0}",
"NotFoundResource": "系统异常,没找到该菜单",
"ModuleIdDiff": "模块与上级菜单不一致",
"CanotDeleteSystemResource": "不可删除系统资源 {0}",
"ResourceMenuHrefNotNull": "菜单的路径不能为空"
},
"NameDup": "存在重复的角色名称 {0}",
"OrgNotNull": "机构不能为空",
"SameOrgNameDup": "存在重复的角色名称 {0}",
"CannotRoleScopeAll": "机构角色不能选择全局数据范围",
"CodeDup": "存在重复的编码 {0}"
},
"ThingsGateway.Admin.Application.RoleCategoryEnum": {
"Global": "全局",
"Org": "机构"
},
"ThingsGateway.Admin.Application.DataScopeEnum": {
"SCOPE_SELF": "仅自己",
"SCOPE_ALL": "全部",
"SCOPE_ORG": "仅所属组织",
"SCOPE_ORG_CHILD": "所属组织及以下",
"SCOPE_ORG_DEFINE": "自定义"
},
"ThingsGateway.Admin.Application.DefaultDataScope": {
"ScopeCategory": "数据范围",
"ScopeDefineOrgIdList": "自定义列表"
},
"ThingsGateway.Admin.Application.SysResource": {
"Title": "标题",
"Module": "模块",
"Title.Required": "{0} 是必填项",
"Href.Required": "{0} 是必填项",
"Icon": "图标",
"Href": "路径",
"Code": "编码",
"Category": "分类",
"Target": "跳转类型",
"NavLinkMatch": "匹配类型",
"ParentId": "上级菜单",
"ResourceDup": "存在重复的名称 {0}",
"ResourceParentChoiceSelf": "父级不能选择自己",
"ResourceParentNull": "父级不存在 {0}",
"NotFoundResource": "系统异常,没找到该菜单",
"ModuleIdDiff": "模块与上级菜单不一致",
"CanotDeleteSystemResource": "不可删除系统资源 {0}",
"ResourceMenuHrefNotNull": "菜单的路径不能为空"
},
"ThingsGateway.Admin.Application.SysOrgCopyInput": {
"TargetId": "目标机构",
"ContainsChild": "包含下级",
"ContainsPosition": "包含职位"
},
"ThingsGateway.Admin.Application.SysPosition": {
"Category.Required": "{0} 是必填项",
"Name.Required": "{0} 是必填项",
"Code.Required": "{0} 是必填项",
"OrgId.MinValue": "{0} 是必填项",
"Category": "分类",
"Name": "名称",
"Code": "代码",
"Status": "状态",
"OrgId": "机构",
"Remark": "备注",
"SortCode": "排序",
"CreateTime": "创建时间",
"UpdateTime": "更新时间",
"Dup": "存在重复的岗位 分类 {0} 名称 {1}",
"CodeDup": "存在重复的编码 {0}",
"NameDup": "存在重复的名称 {0}",
"CanotContainsSelf": "不可包含自己",
"TargetNameDup": "目标节点存在重复的名称 {0}",
"ParentChoiceSelf": "父级不能选择自己",
"ParentNull": "父级不存在 {0}",
"DeleteUserFirst": "请先删除职位下的用户"
"ThingsGateway.Admin.Application.SysOrgCopyInput": {
"TargetId": "目标机构",
"ContainsChild": "包含下级",
"ContainsPosition": "包含职位"
},
"ThingsGateway.Admin.Application.SysPosition": {
"Category.Required": "{0} 是必填项",
"Name.Required": "{0} 是必填项",
"Code.Required": "{0} 是必填项",
"OrgId.MinValue": "{0} 是必填项",
"Category": "分类",
"Name": "名称",
"Code": "代码",
"Status": "状态",
"OrgId": "机构",
"Remark": "备注",
"Dup": "存在重复的岗位 分类 {0} 名称 {1}",
"CodeDup": "存在重复的编码 {0}",
"NameDup": "存在重复的名称 {0}",
"CanotContainsSelf": "不可包含自己",
"TargetNameDup": "目标节点存在重复的名称 {0}",
"ParentChoiceSelf": "父级不能选择自己",
"ParentNull": "父级不存在 {0}",
"DeleteUserFirst": "请先删除职位下的用户"
},
},
"ThingsGateway.Admin.Application.SysOrg": {
"Category.Required": "{0} 是必填项",
"Name.Required": "{0} 是必填项",
"Code.Required": "{0} 是必填项",
"Category": "分类",
"Name": "名称",
"Code": "代码",
"Status": "状态",
"ParentId": "上级机构",
"Names": "机构全称",
"Remark": "备注",
"DirectorId": "主管",
"SortCode": "排序",
"CreateTime": "创建时间",
"UpdateTime": "更新时间",
"Dup": "存在重复的机构 分类 {0} 名称 {1}",
"CodeDup": "存在重复的编码 {0}",
"NameDup": "存在重复的名称 {0}",
"CanotContainsSelf": "不可包含自己",
"TargetNameDup": "目标节点存在重复的名称 {0}",
"ParentChoiceSelf": "父级不能选择自己",
"ParentNull": "父级不存在 {0}",
"DeleteUserFirst": "请先删除机构下的用户",
"DeleteRoleFirst": "请先删除机构下的角色",
"DeletePositionFirst": "请先删除机构下的职位",
"RootOrg": "无法创建顶层机构"
},
"ThingsGateway.Admin.Application.OrgEnum": {
"COMPANY": "公司",
"DEPT": "部门"
},
"ThingsGateway.Admin.Application.PositionCategoryEnum": {
"HIGH": "高层",
"MIDDLE": "中层",
"LOW": "低层"
},
"ThingsGateway.Admin.Application.SysOrg": {
"Category.Required": "{0} 是必填项",
"Name.Required": "{0} 是必填项",
"Code.Required": "{0} 是必填项",
"Category": "分类",
"Name": "名称",
"Code": "代码",
"Status": "状态",
"ParentId": "上级机构",
"Names": "机构全称",
"Remark": "备注",
"DirectorId": "主管",
"Dup": "存在重复的机构 分类 {0} 名称 {1}",
"CodeDup": "存在重复的编码 {0}",
"NameDup": "存在重复的名称 {0}",
"CanotContainsSelf": "不可包含自己",
"TargetNameDup": "目标节点存在重复的名称 {0}",
"ParentChoiceSelf": "父级不能选择自己",
"ParentNull": "父级不存在 {0}",
"DeleteUserFirst": "请先删除机构下的用户",
"DeleteRoleFirst": "请先删除机构下的角色",
"DeletePositionFirst": "请先删除机构下的职位",
"RootOrg": "无法创建顶层机构"
},
"ThingsGateway.Admin.Application.OrgEnum": {
"COMPANY": "公司",
"DEPT": "部门"
},
"ThingsGateway.Admin.Application.PositionCategoryEnum": {
"HIGH": "高层",
"MIDDLE": "中层",
"LOW": "低层"
},
//controller
"ThingsGateway.Admin.Application.AuthController": {
//auth
"AuthController": "登录API",
"LoginAsync": "登录",
"LogoutAsync": "注销"
},
"ThingsGateway.Admin.Application.TestController": {
//auth
"TestController": "测试API",
"Test": "测试"
},
"ThingsGateway.Admin.Application.OpenApiAuthController": {
//auth
"OpenApiAuthController": "登录API",
"LoginAsync": "登录",
"LogoutAsync": "注销"
},
//controller
"ThingsGateway.Admin.Application.AuthController": {
//auth
"AuthController": "登录API",
"LoginAsync": "登录",
"LogoutAsync": "注销"
},
"ThingsGateway.Admin.Application.TestController": {
//auth
"TestController": "测试API",
"Test": "测试"
},
"ThingsGateway.Admin.Application.OpenApiAuthController": {
//auth
"OpenApiAuthController": "登录API",
"LoginAsync": "登录",
"LogoutAsync": "注销"
},
"ThingsGateway.Admin.Application.FileService": {
"FileNullError": "文件不能为空",
"FileLengthError": "文件大小不允许超过 {0} M",
"FileTypeError": "不支持 {0} 格式"
},
"ThingsGateway.Admin.Application.FileService": {
"FileNullError": "文件不能为空",
"FileLengthError": "文件大小不允许超过 {0} M",
"FileTypeError": "不支持 {0} 格式"
},
"ThingsGateway.Admin.Application.UnifyResultProvider": {
"TokenOver": "登录已过期,请重新登录",
"NoPermission": "禁止访问,没有权限"
},
"ThingsGateway.Admin.Application.UnifyResultProvider": {
"TokenOver": "登录已过期,请重新登录",
"NoPermission": "禁止访问,没有权限"
},
"ThingsGateway.Admin.Application.AuthService": {
"TenantNull": "租户不存在",
"OrgDisable": "所属公司/部门已停用,请联系管理员",
"SingleLoginWarn": "您的账号已在别处登录",
"UserNull": "用户 {0} 不存在",
"PasswordError": "密码错误次数过多,请 {0} 分钟后再试",
"AuthErrorMax": "账号密码错误,超过 {0} 次后将锁定 {1} 分钟,错误次数 {2} ",
"UserDisable": "账号 {0} 已停用",
"MustDesc": "密码需要DESC加密后传入",
"UserNoModule": "该账号未分配模块,请联系管理员"
},
"ThingsGateway.Admin.Application.AuthService": {
"TenantNull": "租户不存在",
"OrgDisable": "所属公司/部门已停用,请联系管理员",
"SingleLoginWarn": "您的账号已在别处登录",
"UserNull": "用户 {0} 不存在",
"PasswordError": "密码错误次数过多,请 {0} 分钟后再试",
"AuthErrorMax": "账号密码错误,超过 {0} 次后将锁定 {1} 分钟,错误次数 {2} ",
"UserDisable": "账号 {0} 已停用",
"MustDesc": "密码需要DESC加密后传入",
"UserNoModule": "该账号未分配模块,请联系管理员"
},
"ThingsGateway.Admin.Application.HardwareInfo": {
"Environment": "主机环境",
"FrameworkDescription": "NET框架",
"OsArchitecture": "系统架构",
"UUID": "唯一编码",
"UpdateTime": "更新时间"
},
"ThingsGateway.Admin.Application.HistoryHardwareInfo": {
"DriveUsage": "磁盘使用率",
"MemoryUsage": "内存",
"CpuUsage": "CPU使用率",
"Temperature": "温度",
"Battery": "电池"
},
"ThingsGateway.Admin.Application.HardwareInfo": {
"Environment": "主机环境",
"FrameworkDescription": "NET框架",
"OsArchitecture": "系统架构",
"UUID": "唯一编码",
"UpdateTime": "更新时间"
},
"ThingsGateway.Admin.Application.HistoryHardwareInfo": {
"DriveUsage": "磁盘使用率",
"MemoryUsage": "内存",
"CpuUsage": "CPU使用率",
"Temperature": "温度",
"Battery": "电池"
},
//oper
"ThingsGateway.Admin.Application.OperDescAttribute": {
//dict
"SaveDict": "修改字典",
"DeleteDict": "删除字典",
"EditLoginPolicy": "修改登录策略",
"EditPasswordPolicy": "修改密码策略",
"EditPagePolicy": "修改页面策略",
"EditWebsitePolicy": "修改网站设置",
//operlog
"DeleteOperLog": "删除操作日志",
"ExportOperLog": "导出操作日志",
//oper
"ThingsGateway.Admin.Application.OperDescAttribute": {
//dict
"SaveDict": "修改字典",
"DeleteDict": "删除字典",
"EditLoginPolicy": "修改登录策略",
"EditPasswordPolicy": "修改密码策略",
"EditPagePolicy": "修改页面策略",
"EditWebsitePolicy": "修改网站设置",
//operlog
"DeleteOperLog": "删除操作日志",
"ExportOperLog": "导出操作日志",
//resource
"SaveResource": "修改资源",
"DeleteResource": "删除资源",
//resource
"SaveResource": "修改资源",
"DeleteResource": "删除资源",
//role
"SaveRole": "修改角色",
"DeleteRole": "删除角色",
"RoleGrantResource": "角色授权资源",
"RoleGrantUser": "角色授权用户",
"RoleGrantApiPermission": "角色授权OpenApi",
"GrantApi": "API",
"GrantUser": "用户",
"GrantRole": "角色",
"GrantResource": "资源",
//user
"SaveUser": "修改用户",
"DeleteuSER": "删除用户",
"ResetPassword": "重置密码",
"UserGrantRole": "用户授权角色",
"UserGrantResource": "用户授权资源",
"UserGrantApiPermission": "用户授权OpenApi",
//role
"SaveRole": "修改角色",
"DeleteRole": "删除角色",
"RoleGrantResource": "角色授权资源",
"RoleGrantUser": "角色授权用户",
"RoleGrantApiPermission": "角色授权OpenApi",
"GrantApi": "API",
"GrantUser": "用户",
"GrantRole": "角色",
"GrantResource": "资源",
//user
"SaveUser": "修改用户",
"DeleteuSER": "删除用户",
"ResetPassword": "重置密码",
"UserGrantRole": "用户授权角色",
"UserGrantResource": "用户授权资源",
"UserGrantApiPermission": "用户授权OpenApi",
//usercenter
"UpdateUserInfo": "更新个人信息",
"WorkbenchInfo": "更新个人工作台",
"UpdatePassword": "更新个人密码",
//usercenter
"UpdateUserInfo": "更新个人信息",
"WorkbenchInfo": "更新个人工作台",
"UpdatePassword": "更新个人密码",
//session
"ExitVerificat": "强退令牌",
"ExitSession": "强退会话",
//session
"ExitVerificat": "强退令牌",
"ExitSession": "强退会话",
"CopyOrg": "复制机构",
"DeleteOrg": "删除机构",
"SaveOrg": "保存机构",
"CopyOrg": "复制机构",
"DeleteOrg": "删除机构",
"SaveOrg": "保存机构",
"DeletePosition": "删除岗位",
"SavePosition": "保存岗位",
"DeletePosition": "删除岗位",
"SavePosition": "保存岗位",
"NoPermission": "无权限操作",
"NoPermission": "无权限操作",
"CopyResource": "复制资源",
"ChangeParentResource": "更改父节点"
},
"CopyResource": "复制资源",
"ChangeParentResource": "更改父节点"
},
//service
//service
"ThingsGateway.Admin.Application.HardwareJob": {
"GetHardwareInfoFail": "获取硬件信息出错"
},
"ThingsGateway.Admin.Application.HardwareJob": {
"GetHardwareInfoFail": "获取硬件信息出错"
},
//dto
"ThingsGateway.Admin.Application.UserSelectorOutput": {
"Account": "账号",
"OrgId": "机构"
},
"ThingsGateway.Admin.Application.ResourceTableSearchModel": {
"Module": "模块",
"Href": "路径",
"Title": "标题"
},
"ThingsGateway.Admin.Application.WorkbenchInfo": {
"Razor": "主页",
"Shortcuts": "快捷方式"
},
"ThingsGateway.Admin.Application.UpdatePasswordInput": {
"Password": "密码",
"NewPassword": "新密码",
"ConfirmPassword": "确认密码",
"Password.Required": " {0} 是必填项",
"NewPassword.Required": " {0} 是必填项",
"ConfirmPassword.Required": " {0} 是必填项"
},
//dto
"ThingsGateway.Admin.Application.UserSelectorOutput": {
"Account": "账号",
"OrgId": "机构"
},
"ThingsGateway.Admin.Application.ResourceTableSearchModel": {
"Module": "模块",
"Href": "路径",
"Title": "标题"
},
"ThingsGateway.Admin.Application.WorkbenchInfo": {
"Razor": "主页",
"Shortcuts": "快捷方式"
},
"ThingsGateway.Admin.Application.UpdatePasswordInput": {
"Password": "密码",
"NewPassword": "新密码",
"ConfirmPassword": "确认密码",
"Password.Required": " {0} 是必填项",
"NewPassword.Required": " {0} 是必填项",
"ConfirmPassword.Required": " {0} 是必填项"
},
"ThingsGateway.Admin.Application.VerificatInfo": {
"Expire": "过期时间(分)",
"Online": "在线状态",
"VerificatTimeout": "超时时间",
"Device": "登录设备",
"LoginIp": "登录IP",
"LoginTime": "登录时间"
},
"ThingsGateway.Admin.Application.VerificatInfo": {
"Expire": "过期时间(分)",
"Online": "在线状态",
"VerificatTimeout": "超时时间",
"Device": "登录设备",
"LoginIp": "登录IP",
"LoginTime": "登录时间"
},
"ThingsGateway.Admin.Application.SessionOutput": {
"Account": "账号",
"Online": "在线状态",
"LatestLoginIp": "最新登录ip",
"LatestLoginTime": "最新登录时间",
"VerificatCount": "令牌数量"
},
"ThingsGateway.Admin.Application.SessionOutput": {
"Account": "账号",
"Online": "在线状态",
"LatestLoginIp": "最新登录ip",
"LatestLoginTime": "最新登录时间",
"VerificatCount": "令牌数量"
},
"ThingsGateway.Admin.Application.SysDict": {
"Category.Required": "{0} 是必填项",
"Name.Required": "{0} 是必填项",
"Code.Required": "{0} 是必填项",
"Category": "分类",
"Name": "名称",
"Code": "代码",
"Remark": "备注",
"SortCode": "排序",
"CreateTime": "创建时间",
"UpdateTime": "更新时间",
"DictDup": "存在重复的配置 分类 {0} 名称 {1}",
"DemoCanotUpdateWebsitePolicy": "DEMO环境不允许修改网站设置"
},
"ThingsGateway.Admin.Application.SysDict": {
"Category.Required": "{0} 是必填项",
"Name.Required": "{0} 是必填项",
"Code.Required": "{0} 是必填项",
"Category": "分类",
"Name": "名称",
"Code": "代码",
"Remark": "备注",
"DictDup": "存在重复的配置 分类 {0} 名称 {1}",
"DemoCanotUpdateWebsitePolicy": "DEMO环境不允许修改网站设置"
},
"ThingsGateway.Admin.Application.SysOperateLog": {
"ClassName": "类名",
"ExeMessage": "具体消息",
"MethodName": "方法名称",
"ParamJson": "请求参数",
"ReqMethod": "请求方式",
"ReqUrl": "请求地址",
"ResultJson": "返回结果",
"Category": "日志分类",
"ExeStatus": "执行状态",
"Name": "日志名称",
"OpAccount": "账号",
"OpBrowser": "浏览器",
"OpIp": "ip",
"OpOs": "系统",
"OpTime": "操作时间",
"VerificatId": "验证Id"
"ThingsGateway.Admin.Application.SysOperateLog": {
"ClassName": "类名",
"ExeMessage": "具体消息",
"MethodName": "方法名称",
"ParamJson": "请求参数",
"ReqMethod": "请求方式",
"ReqUrl": "请求地址",
"ResultJson": "返回结果",
"Category": "日志分类",
"ExeStatus": "执行状态",
"Name": "日志名称",
"OpAccount": "账号",
"OpBrowser": "浏览器",
"OpIp": "ip",
"OpOs": "系统",
"OpTime": "操作时间",
"VerificatId": "验证Id"
},
"ThingsGateway.Admin.Application.OperateLogPageInput": {
"SearchDate": "时间范围",
"Account": "操作账号",
"Category": "分类"
},
"ThingsGateway.Admin.Application.LoginInput": {
"Account": "登录账号",
"Password": "登录密码",
"Account.Required": "{0} 是必填项",
"Password.Required": "{0} 是必填项"
},
"ThingsGateway.Admin.Application.LogoutInput": {
"VerificatId.Required": "{0} 是必填项"
},
"ThingsGateway.Admin.Application.AppConfig": {
"LoginPolicy": "登录策略",
"PasswordPolicy": "密码策略",
"PagePolicy": "页面设置",
"WebsitePolicy": "网站设置"
},
"ThingsGateway.Admin.Application.LoginPolicy": {
"SingleOpen": "单用户登录开关",
"ErrorLockTime": "登录错误锁定时长(分)",
"ErrorResetTime": "登录错误次数过期时长(分)",
"ErrorCount": "登录错误次数锁定阈值",
"VerificatExpireTime": "登录过期时间(分)",
"ErrorLockTime.MinValue": " {0} 值太小",
"ErrorResetTime.MinValue": " {0} 值太小",
"ErrorCount.MinValue": " {0} 值太小",
"VerificatExpireTime.MinValue": " {0} 值太小"
},
"ThingsGateway.Admin.Application.PagePolicy": {
"Shortcuts": "默认快捷方式",
"Razor": "默认主页"
},
"ThingsGateway.Admin.Application.PasswordPolicy": {
"DefaultPassword": "默认用户密码",
"DefaultPassword.Required": " {0} 是必填项",
"PasswordMinLen": "密码最小长度",
"PasswordMinLen.MinValue": " {0} 值太小",
"PasswordContainNum": "包含数字",
"PasswordContainLower": "包含小写字母",
"PasswordContainUpper": "包含大写字母",
"PasswordContainChar": "包含特殊字符"
},
"ThingsGateway.Admin.Application.WebsitePolicy": {
"WebStatus": "是否开放",
"CloseTip": "关闭提示",
"CloseTip.Required": " {0} 是必填项"
},
},
"ThingsGateway.Admin.Application.OperateLogPageInput": {
"SearchDate": "时间范围",
"Account": "操作账号",
"Category": "分类"
},
"ThingsGateway.Admin.Application.LoginInput": {
"Account": "登录账号",
"Password": "登录密码",
"Account.Required": "{0} 是必填项",
"Password.Required": "{0} 是必填项"
},
"ThingsGateway.Admin.Application.LogoutInput": {
"VerificatId.Required": "{0} 是必填项"
},
"ThingsGateway.Admin.Application.AppConfig": {
"LoginPolicy": "登录策略",
"PasswordPolicy": "密码策略",
"PagePolicy": "页面设置",
"WebsitePolicy": "网站设置"
},
"ThingsGateway.Admin.Application.LoginPolicy": {
"SingleOpen": "单用户登录开关",
"ErrorLockTime": "登录错误锁定时长(分)",
"ErrorResetTime": "登录错误次数过期时长(分)",
"ErrorCount": "登录错误次数锁定阈值",
"VerificatExpireTime": "登录过期时间(分)",
"ErrorLockTime.MinValue": " {0} 值太小",
"ErrorResetTime.MinValue": " {0} 值太小",
"ErrorCount.MinValue": " {0} 值太小",
"VerificatExpireTime.MinValue": " {0} 值太小"
},
"ThingsGateway.Admin.Application.PagePolicy": {
"Shortcuts": "默认快捷方式",
"Razor": "默认主页"
},
"ThingsGateway.Admin.Application.PasswordPolicy": {
"DefaultPassword": "默认用户密码",
"DefaultPassword.Required": " {0} 是必填项",
"PasswordMinLen": "密码最小长度",
"PasswordMinLen.MinValue": " {0} 值太小",
"PasswordContainNum": "包含数字",
"PasswordContainLower": "包含小写字母",
"PasswordContainUpper": "包含大写字母",
"PasswordContainChar": "包含特殊字符"
},
"ThingsGateway.Admin.Application.WebsitePolicy": {
"WebStatus": "是否开放",
"CloseTip": "关闭提示",
"CloseTip.Required": " {0} 是必填项"
},
//enum
"ThingsGateway.Admin.Application.ResourceCategoryEnum": {
"Module": "模块",
"Menu": "菜单",
"Button": "按钮"
},
//enum
"ThingsGateway.Admin.Application.ResourceCategoryEnum": {
"Module": "模块",
"Menu": "菜单",
"Button": "按钮"
},
"ThingsGateway.Admin.Application.TargetEnum": {
"_self": "本窗口",
"_blank": "新窗口",
"_parent": "父级窗口",
"_top": "顶级窗口"
},
"ThingsGateway.Admin.Application.DictTypeEnum": {
"System": "系统配置",
"Define": "业务配置"
},
"ThingsGateway.Admin.Application.TargetEnum": {
"_self": "本窗口",
"_blank": "新窗口",
"_parent": "父级窗口",
"_top": "顶级窗口"
},
"ThingsGateway.Admin.Application.DictTypeEnum": {
"System": "系统配置",
"Define": "业务配置"
},
"ThingsGateway.Admin.Application.LogCateGoryEnum": {
"Login": "登录",
"Logout": "注销",
"Operate": "操作",
"Exception": "异常"
},
"ThingsGateway.Admin.Application.LogCateGoryEnum": {
"Login": "登录",
"Logout": "注销",
"Operate": "操作",
"Exception": "异常"
},
"ThingsGateway.Admin.Application.LogEnum": {
"SUCCESS": "成功",
"FAIL": "失败"
}
"ThingsGateway.Admin.Application.LogEnum": {
"SUCCESS": "成功",
"FAIL": "失败"
}
}

View File

@@ -22,26 +22,19 @@ using System.Globalization;
using System.Reflection;
using ThingsGateway.Extension;
using ThingsGateway.SpecificationDocument;
namespace ThingsGateway.Admin.Application;
internal sealed class ApiPermissionService : IApiPermissionService
{
private readonly IApiDescriptionGroupCollectionProvider _apiDescriptionGroupCollectionProvider;
private readonly SwaggerGeneratorOptions _generatorOptions;
public ApiPermissionService(
IOptions<SwaggerGeneratorOptions> generatorOptions,
IApiDescriptionGroupCollectionProvider apiDescriptionGroupCollectionProvider)
{
_generatorOptions = generatorOptions.Value;
_apiDescriptionGroupCollectionProvider = apiDescriptionGroupCollectionProvider;
}
private IEnumerable<string> GetDocumentNames()
{
return _generatorOptions.SwaggerDocs.Keys;
}
/// <inheritdoc />
public List<OpenApiPermissionTreeSelector> ApiPermissionTreeSelector()
@@ -53,37 +46,37 @@ internal sealed class ApiPermissionService : IApiPermissionService
permissions = new();
Dictionary<string, OpenApiPermissionTreeSelector> groupOpenApis = new();
foreach (var item in GetDocumentNames())
{
OpenApiPermissionTreeSelector openApiPermissionTreeSelector = new() { ApiName = item ?? "Default" };
groupOpenApis.TryAdd(openApiPermissionTreeSelector.ApiName, openApiPermissionTreeSelector);
}
var apiDescriptions = _apiDescriptionGroupCollectionProvider.ApiDescriptionGroups.Items;
foreach (var item1 in apiDescriptions)
{
foreach (var item in item1.Items)
{
if (item.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor)
{
OpenApiPermissionTreeSelector openApiPermissionTreeSelector = new() { ApiName = controllerActionDescriptor.ControllerName ?? "Default" };
groupOpenApis.TryAdd(openApiPermissionTreeSelector.ApiName, openApiPermissionTreeSelector);
}
}
}
// 获取所有需要数据权限的控制器
var controllerTypes =
App.EffectiveTypes.Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass && u.IsDefined(typeof(RolePermissionAttribute), false));
foreach (var groupOpenApi in groupOpenApis)
//foreach (var groupOpenApi in groupOpenApis)
{
foreach (var apiDescriptionGroup in apiDescriptions)
{
var routes = apiDescriptionGroup.Items.Where(api => api.ActionDescriptor is ControllerActionDescriptor);
OpenApiPermissionTreeSelector openApiPermissionTreeSelector = groupOpenApi.Value;
Dictionary<string, OpenApiPermissionTreeSelector> openApiPermissionTreeSelectorDict = new();
foreach (var route in routes)
{
if (!SpecificationDocumentBuilder.CheckApiDescriptionInCurrentGroup(groupOpenApi.Key, route))
{
continue;
}
var actionDesc = (ControllerActionDescriptor)route.ActionDescriptor;
if (!actionDesc.ControllerTypeInfo.CustomAttributes.Any(a => a.AttributeType == typeof(RolePermissionAttribute)))
continue;
@@ -116,10 +109,8 @@ internal sealed class ApiPermissionService : IApiPermissionService
}
openApiPermissionTreeSelector.Children.AddRange(openApiPermissionTreeSelectorDict.Values);
if (openApiPermissionTreeSelector.Children.Any(a => a.Children.Count > 0))
permissions.Add(openApiPermissionTreeSelector);
if (openApiPermissionTreeSelectorDict.Values.Any(a => a.Children.Count > 0))
permissions.AddRange(openApiPermissionTreeSelectorDict.Values);
}

View File

@@ -13,7 +13,6 @@ using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
using System.Net;
using System.Security.Claims;
using UAParser;
@@ -72,7 +71,7 @@ public class AppService : IAppService
}
public ClaimsPrincipal? User => App.User;
public IPAddress? RemoteIpAddress => App.HttpContext?.Connection?.RemoteIpAddress;
public string? RemoteIpAddress => App.HttpContext?.GetRemoteIpAddressToIPv4();
public int LocalPort => App.HttpContext.Connection.LocalPort;
}

View File

@@ -11,7 +11,6 @@
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.DependencyInjection;
using System.Net;
using System.Security.Claims;
using UAParser;
@@ -24,7 +23,7 @@ public class HybridAppService : IAppService
{
var str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0";
ClientInfo = Parser.GetDefault().Parse(str);
RemoteIpAddress = IPAddress.Parse("127.0.0.1");
RemoteIpAddress = "127.0.0.1";
}
public ClientInfo? ClientInfo { get; }
@@ -56,7 +55,7 @@ public class HybridAppService : IAppService
}
}
public IPAddress? RemoteIpAddress { get; }
public string? RemoteIpAddress { get; }
public string GetReturnUrl(string returnUrl)
{

View File

@@ -9,7 +9,6 @@
//------------------------------------------------------------------------------
using System.Net;
using System.Security.Claims;
using UAParser;
@@ -31,7 +30,7 @@ public interface IAppService
/// <summary>
/// RemoteIpAddress
/// </summary>
public IPAddress? RemoteIpAddress { get; }
public string? RemoteIpAddress { get; }
/// <summary>
/// GetReturnUrl

View File

@@ -96,16 +96,16 @@ public class AuthService : IAuthService
/// </summary>
public async Task LoginOutAsync()
{
if (UserManager.UserId == 0)
if (UserManager.VerificatId == 0)
return;
var verificatId = UserManager.UserId;
var verificatId = UserManager.VerificatId;
//获取用户信息
var userinfo = await _sysUserService.GetUserByAccountAsync(UserManager.UserAccount, UserManager.TenantId).ConfigureAwait(false);
if (userinfo != null)
{
var loginEvent = new LoginEvent
{
Ip = _appService.RemoteIpAddress?.MapToIPv4()?.ToString(),
Ip = _appService.RemoteIpAddress,
SysUser = userinfo,
VerificatId = verificatId
};
@@ -236,7 +236,7 @@ public class AuthService : IAuthService
//登录事件参数
var logingEvent = new LoginEvent
{
Ip = _appService.RemoteIpAddress?.MapToIPv4()?.ToString(),
Ip = _appService.RemoteIpAddress,
Device = App.GetService<IAppService>().ClientInfo?.OS?.ToString(),
Expire = expire,
SysUser = sysUser,

View File

@@ -47,12 +47,10 @@ public class BaseService<T> : IDataService<T>, IDisposable where T : class, new(
}
/// <inheritdoc/>
public Task<bool> DeleteAsync(IEnumerable<T> models)
public async Task<bool> DeleteAsync(IEnumerable<T> models)
{
if (models.FirstOrDefault() is IPrimaryIdEntity)
return DeleteAsync(models.Select(a => ((IPrimaryIdEntity)a).Id));
else
return Task.FromResult(false);
using var db = GetDB();
return await db.Deleteable<T>().In(models.ToList()).ExecuteCommandHasChangeAsync().ConfigureAwait(false);
}
/// <inheritdoc/>
@@ -165,4 +163,6 @@ public class BaseService<T> : IDataService<T>, IDisposable where T : class, new(
{
return DbContext.Db.GetConnectionScopeWithAttr<T>().CopyNew();
}
}

View File

@@ -169,7 +169,7 @@ internal sealed class VerificatInfoService : BaseService<VerificatInfo>, IVerifi
public void RemoveAllClientId()
{
using var db = GetDB();
db.Updateable<VerificatInfo>().SetColumns("ClientIds", new ConcurrentList<long>().ToJsonNetString()).Where(a => a.Id >= 0).ExecuteCommand();
db.Updateable<VerificatInfo>().SetColumns(a=>a.ClientIds==null).Where(a => a.Id > 0).ExecuteCommand();
VerificatInfoService.RemoveCache();
}

View File

@@ -103,4 +103,46 @@ public static class DbContext
Console.WriteLine("【Sql执行时间】" + DateTime.Now.ToDefaultDateTimeFormat());
Console.WriteLine("【Sql语句】" + msg + Environment.NewLine);
}
public static async Task BulkCopyAsync<TITEM>(this SqlSugarClient db, List<TITEM> datas, int size) where TITEM : class, new()
{
switch (db.CurrentConnectionConfig.DbType)
{
case DbType.MySql:
case DbType.SqlServer:
case DbType.Sqlite:
case DbType.Oracle:
case DbType.PostgreSQL:
case DbType.Dm:
case DbType.MySqlConnector:
case DbType.Kdbndp:
await db.Fastest<TITEM>().PageSize(size).BulkCopyAsync(datas).ConfigureAwait(false);
break;
default:
await db.Insertable(datas).PageSize(size).ExecuteCommandAsync().ConfigureAwait(false);
break;
}
}
public static async Task BulkUpdateAsync<TITEM>(this SqlSugarClient db, List<TITEM> datas, int size) where TITEM : class, new()
{
switch (db.CurrentConnectionConfig.DbType)
{
case DbType.MySql:
case DbType.SqlServer:
case DbType.Sqlite:
case DbType.Oracle:
case DbType.PostgreSQL:
case DbType.Dm:
case DbType.MySqlConnector:
case DbType.Kdbndp:
await db.Fastest<TITEM>().PageSize(size).BulkUpdateAsync(datas).ConfigureAwait(false);
break;
default:
await db.Updateable(datas).PageSize(size).ExecuteCommandAsync().ConfigureAwait(false);
break;
}
}
}

View File

@@ -24,6 +24,11 @@ public sealed class SqlSugarOption : ConnectionConfig
/// </summary>
public bool InitSeedData { get; set; } = false;
/// <summary>
/// 初始化数据库
/// </summary>
public bool InitDatabase { get; set; } = false;
/// <summary>
/// 初始化表
/// </summary>

View File

@@ -10,6 +10,7 @@
using BootstrapBlazor.Components;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using SqlSugar;
@@ -23,7 +24,7 @@ namespace ThingsGateway.Admin.Application;
[AppStartup(1000000000)]
public class Startup : AppStartup
{
public void ConfigureAdminApp(IServiceCollection services)
public void Configure(IServiceCollection services)
{
Directory.CreateDirectory("DB");
@@ -75,7 +76,7 @@ public class Startup : AppStartup
}
public void UseAdminCore(IServiceProvider serviceProvider)
public void Use(IApplicationBuilder applicationBuilder)
{
//检查ConfigId
var configIdGroup = DbContext.DbConfigs.GroupBy(it => it.ConfigId);
@@ -88,7 +89,7 @@ public class Startup : AppStartup
DbContext.DbConfigs?.ForEach(it =>
{
var connection = DbContext.Db.GetConnection(it.ConfigId);//获取数据库连接对象
if (it.InitTable == true)
if (it.InitDatabase == true)
connection.DbMaintenance.CreateDatabase();//创建数据库,如果存在则不创建
});

View File

@@ -18,11 +18,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="BootstrapBlazor.TableExport" Version="9.2.2" />
<!--<PackageReference Include="MiniExcel" Version="1.39.0" />-->
<PackageReference Include="BootstrapBlazor.TableExport" Version="9.2.4" />
<PackageReference Include="UAParser" Version="3.1.47" />
<PackageReference Include="Rougamo.Fody" Version="5.0.0" />
<PackageReference Include="SqlSugarCore" Version="5.1.4.188" />
<PackageReference Include="SqlSugarCore" Version="5.1.4.193" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" />
@@ -31,9 +30,9 @@
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net9.0' ">
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.4" />
<PackageReference Include="System.Formats.Asn1" Version="9.0.4" />
<PackageReference Include="System.Threading.RateLimiting" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.5" />
<PackageReference Include="System.Formats.Asn1" Version="9.0.5" />
<PackageReference Include="System.Threading.RateLimiting" Version="9.0.5" />
</ItemGroup>
<ItemGroup>
<Content Remove="SeedData\Admin\*.json" />

View File

@@ -41,7 +41,7 @@ public partial class SessionPage
{
var op = new DialogOption()
{
IsScrolling = false,
IsScrolling = true,
Title = Localizer[nameof(VerificatInfo)],
ShowMaximizeButton = true,
Class = "dialog-table",

View File

@@ -15,7 +15,7 @@ namespace ThingsGateway.Admin.Razor;
[AppStartup(-1)]
public class Startup : AppStartup
{
public void ConfigureAdminApp(IServiceCollection services)
public void Configure(IServiceCollection services)
{
services.AddScoped<IMenuService, MenuService>();
services.AddBootstrapBlazorTableExportService();

View File

@@ -9,10 +9,10 @@
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)'=='net8.0'">
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.14" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.16" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)'=='net9.0'">
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.4" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.5" />
</ItemGroup>
<PropertyGroup>

View File

@@ -10,6 +10,9 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.StaticFiles;
@@ -18,6 +21,7 @@ using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Unicode;
@@ -291,6 +295,21 @@ public class Startup : AppStartup
services.AddAuthorizationCore();
services.AddScoped<IAuthorizationHandler, BlazorServerAuthenticationHandler>();
services.AddScoped<AuthenticationStateProvider, BlazorServerAuthenticationStateProvider>();
#if NET9_0_OR_GREATER
var certificate = X509CertificateLoader.LoadPkcs12FromFile("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
#else
var certificate = new X509Certificate2("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
#endif
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo("../keys"))
.ProtectKeysWithCertificate(certificate)
.UseCryptographicAlgorithms(new AuthenticatedEncryptorConfiguration
{
EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
ValidationAlgorithm = ValidationAlgorithm.HMACSHA256
});
}
@@ -298,9 +317,9 @@ public class Startup : AppStartup
public void Use(IApplicationBuilder applicationBuilder, IWebHostEnvironment env)
{
var app = (WebApplication)applicationBuilder;
app.UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.All, KnownNetworks = { }, KnownProxies = { } });
app.UseBootstrapBlazor();
app.UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.All });
// 启用本地化
var option = app.Services.GetService<IOptions<RequestLocalizationOptions>>();

View File

@@ -45,7 +45,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.4" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.5" />
</ItemGroup>
<!--安装服务守护-->
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
@@ -54,8 +54,8 @@
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.1" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net9.0' ">
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.5" />
</ItemGroup>
<ItemGroup>
@@ -72,6 +72,9 @@
<None Update="pm2-linux.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="ThingsGateway.pfx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="thingsgateway.service">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>

Binary file not shown.

View File

@@ -0,0 +1,81 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
using Microsoft.Extensions.DependencyInjection;
using ThingsGateway;
namespace Microsoft.Extensions.Hosting;
/// <summary>
/// HostApplication 拓展
/// </summary>
public static class AppHostApplicationBuilderExtensions
{
/// <summary>
/// Host 应用注入
/// </summary>
/// <param name="hostApplicationBuilder">Host 应用构建器</param>
/// <param name="autoRegisterBackgroundService"></param>
/// <returns>HostApplicationBuilder</returns>
public static HostApplicationBuilder Inject(this HostApplicationBuilder hostApplicationBuilder, bool autoRegisterBackgroundService = true)
{
// 初始化配置
InternalApp.ConfigureApplication(hostApplicationBuilder, autoRegisterBackgroundService);
return hostApplicationBuilder;
}
/// <summary>
/// 注册依赖组件
/// </summary>
/// <typeparam name="TComponent">派生自 <see cref="IServiceComponent"/></typeparam>
/// <param name="hostApplicationBuilder">Host 应用构建器</param>
/// <param name="options">组件参数</param>
/// <returns></returns>
public static HostApplicationBuilder AddComponent<TComponent>(this HostApplicationBuilder hostApplicationBuilder, object options = default)
where TComponent : class, IServiceComponent, new()
{
hostApplicationBuilder.Services.AddComponent<TComponent>(options);
return hostApplicationBuilder;
}
/// <summary>
/// 注册依赖组件
/// </summary>
/// <typeparam name="TComponent">派生自 <see cref="IServiceComponent"/></typeparam>
/// <typeparam name="TComponentOptions">组件参数</typeparam>
/// <param name="hostApplicationBuilder">Host 应用构建器</param>
/// <param name="options">组件参数</param>
/// <returns><see cref="HostApplicationBuilder"/></returns>
public static HostApplicationBuilder AddComponent<TComponent, TComponentOptions>(this HostApplicationBuilder hostApplicationBuilder, TComponentOptions options = default)
where TComponent : class, IServiceComponent, new()
{
hostApplicationBuilder.Services.AddComponent<TComponent, TComponentOptions>(options);
return hostApplicationBuilder;
}
/// <summary>
/// 注册依赖组件
/// </summary>
/// <param name="hostApplicationBuilder">Host 应用构建器</param>
/// <param name="componentType">组件类型</param>
/// <param name="options">组件参数</param>
/// <returns><see cref="HostApplicationBuilder"/></returns>
public static HostApplicationBuilder AddComponent(this HostApplicationBuilder hostApplicationBuilder, Type componentType, object options = default)
{
hostApplicationBuilder.Services.AddComponent(componentType, options);
return hostApplicationBuilder;
}
}

View File

@@ -467,18 +467,20 @@ public static class ObjectExtensions
return obj;
}
/// <summary>
/// 查找方法指定特性,如果没找到则继续查找声明类
/// </summary>
/// <typeparam name="TAttribute"></typeparam>
/// <param name="method"></param>
/// <param name="inherit"></param>
/// <param name="searchFromReflectedType">searchFromRuntimeType</param>
/// <returns></returns>
internal static TAttribute GetFoundAttribute<TAttribute>(this MethodInfo method, bool inherit)
internal static TAttribute GetFoundAttribute<TAttribute>(this MethodInfo method, bool inherit, bool searchFromReflectedType = false)
where TAttribute : Attribute
{
// 获取方法所在类型
var declaringType = method.DeclaringType;
var declaringType = !searchFromReflectedType ? method.DeclaringType : method.ReflectedType; // 解决嵌套继承问题
var attributeType = typeof(TAttribute);
@@ -493,7 +495,6 @@ public static class ObjectExtensions
return foundAttribute;
}
/// <summary>
/// 格式化字符串
/// </summary>

View File

@@ -132,6 +132,34 @@ internal static class InternalApp
});
}
/// <summary>
/// 配置 Furion 框架(非 Web
/// </summary>
/// <param name="hostApplicationBuilder"></param>
/// <param name="autoRegisterBackgroundService"></param>
internal static void ConfigureApplication(IHostApplicationBuilder hostApplicationBuilder, bool autoRegisterBackgroundService = true)
{
// 存储环境对象
HostEnvironment = hostApplicationBuilder.Environment;
// 加载配置
AddJsonFiles(hostApplicationBuilder.Configuration, hostApplicationBuilder.Environment);
// 存储配置对象
Configuration = hostApplicationBuilder.Configuration;
// 存储服务提供器
InternalServices = hostApplicationBuilder.Services;
// 存储根服务
hostApplicationBuilder.Services.AddHostedService<GenericHostLifetimeEventsHostedService>();
// 初始化应用服务
hostApplicationBuilder.Services.AddApp();
// 自动注册 BackgroundService
if (autoRegisterBackgroundService) hostApplicationBuilder.Services.AddAppHostedService();
}
/// <summary>
/// 自动装载主机配置
/// </summary>

View File

@@ -106,7 +106,7 @@ public static class HttpContextExtensions
/// <param name="context"></param>
/// <param name="xff">是否优先取 X-Forwarded-For</param>
/// <returns></returns>
public static string GetRemoteIpAddressToIPv4(this HttpContext context, bool xff = false)
public static string GetRemoteIpAddressToIPv4(this HttpContext context, bool xff = true)
{
var ipv4 = context.Connection.RemoteIpAddress?.MapToIPv4()?.ToString();

View File

@@ -9,7 +9,7 @@
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;
@@ -29,34 +29,36 @@ public class AESEncryption
/// <param name="iv">偏移量</param>
/// <param name="mode">模式</param>
/// <param name="padding">填充</param>
/// <param name="isBase64"></param>
/// <returns></returns>
public static string Encrypt(string text, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7)
public static string Encrypt(string text, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7, bool isBase64 = false)
{
var bKey = Encoding.UTF8.GetBytes(skey);
var bKey = !isBase64 ? Encoding.UTF8.GetBytes(skey) : Convert.FromBase64String(skey);
if (bKey.Length != 16 && bKey.Length != 24 && bKey.Length != 32) throw new ArgumentException("The key length must be 16, 24, or 32 bytes.");
using var aesAlg = Aes.Create();
aesAlg.Key = bKey;
aesAlg.Mode = mode;
aesAlg.Padding = padding;
// 如果是 ECB 模式,不需要 IV
if (mode != CipherMode.ECB)
{
aesAlg.IV = iv ?? aesAlg.IV; // 如果未提供 IV则使用随机生成的 IV
aesAlg.IV = iv ?? aesAlg.IV;
if (iv != null && iv.Length != 16) throw new ArgumentException("The IV length must be 16 bytes.");
}
using var encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
using var encryptor = aesAlg.CreateEncryptor();
using var msEncrypt = new MemoryStream();
using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
using (var swEncrypt = new StreamWriter(csEncrypt))
using (var swEncrypt = new StreamWriter(csEncrypt, Encoding.UTF8))
{
swEncrypt.Write(text);
}
var encryptedContent = msEncrypt.ToArray();
// 如果是 CBC 模式,将 IV 和密文拼接在一起
if (mode != CipherMode.ECB)
// 仅在未提供 IV 时拼接 IV
if (mode != CipherMode.ECB && iv == null)
{
var result = new byte[aesAlg.IV.Length + encryptedContent.Length];
Buffer.BlockCopy(aesAlg.IV, 0, result, 0, aesAlg.IV.Length);
@@ -76,35 +78,43 @@ public class AESEncryption
/// <param name="iv">偏移量</param>
/// <param name="mode">模式</param>
/// <param name="padding">填充</param>
/// <param name="isBase64"></param>
/// <returns></returns>
public static string Decrypt(string hash, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7)
public static string Decrypt(string hash, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7, bool isBase64 = false)
{
var fullCipher = Convert.FromBase64String(hash);
var bKey = Encoding.UTF8.GetBytes(skey);
var bKey = !isBase64 ? Encoding.UTF8.GetBytes(skey) : Convert.FromBase64String(skey);
if (bKey.Length != 16 && bKey.Length != 24 && bKey.Length != 32) throw new ArgumentException("The key length must be 16, 24, or 32 bytes.");
using var aesAlg = Aes.Create();
aesAlg.Key = bKey;
aesAlg.Mode = mode;
aesAlg.Padding = padding;
// 如果是 ECB 模式,不需要 IV
if (mode != CipherMode.ECB)
{
var bVector = new byte[16];
var cipher = new byte[fullCipher.Length - bVector.Length];
if (iv == null)
{
if (fullCipher.Length < aesAlg.BlockSize / 8) throw new ArgumentException("The ciphertext length is insufficient to extract the IV.");
Unsafe.CopyBlock(ref bVector[0], ref fullCipher[0], (uint)bVector.Length);
Unsafe.CopyBlock(ref cipher[0], ref fullCipher[bVector.Length], (uint)(fullCipher.Length - bVector.Length));
aesAlg.IV = iv ?? bVector;
fullCipher = cipher;
iv = new byte[aesAlg.BlockSize / 8];
var cipher = new byte[fullCipher.Length - iv.Length];
Buffer.BlockCopy(fullCipher, 0, iv, 0, iv.Length);
Buffer.BlockCopy(fullCipher, iv.Length, cipher, 0, cipher.Length);
aesAlg.IV = iv;
fullCipher = cipher;
}
else
{
if (iv.Length != 16) throw new ArgumentException("The IV length must be 16 bytes.");
aesAlg.IV = iv;
}
}
using var decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
using var decryptor = aesAlg.CreateDecryptor();
using var msDecrypt = new MemoryStream(fullCipher);
using var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read);
using var srDecrypt = new StreamReader(csDecrypt);
using var srDecrypt = new StreamReader(csDecrypt, Encoding.UTF8);
return srDecrypt.ReadToEnd();
}
@@ -117,19 +127,13 @@ public class AESEncryption
/// <param name="iv">偏移量</param>
/// <param name="mode">模式</param>
/// <param name="padding">填充</param>
/// <param name="isBase64"></param>
/// <returns>加密后的字节数组</returns>
public static byte[] Encrypt(byte[] bytes, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7)
public static byte[] Encrypt(byte[] bytes, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7, bool isBase64 = false)
{
// 确保密钥长度为 128 位、192 位或 256 位
var bKey = new byte[32]; // 256 位密钥
var keyBytes = Encoding.UTF8.GetBytes(skey);
Array.Copy(keyBytes, bKey, Math.Min(keyBytes.Length, bKey.Length));
// 如果是 ECB 模式,不需要 IV
if (mode != CipherMode.ECB)
{
iv ??= GenerateRandomIV(); // 生成随机 IV
}
// 验证密钥长度
var bKey = !isBase64 ? Encoding.UTF8.GetBytes(skey) : Convert.FromBase64String(skey);
if (bKey.Length != 16 && bKey.Length != 24 && bKey.Length != 32) throw new ArgumentException("The key length must be 16, 24, or 32 bytes.");
using var aesAlg = Aes.Create();
aesAlg.Key = bKey;
@@ -138,34 +142,29 @@ public class AESEncryption
if (mode != CipherMode.ECB)
{
aesAlg.IV = iv;
aesAlg.IV = iv ?? GenerateRandomIV();
if (aesAlg.IV.Length != 16) throw new ArgumentException("The IV length must be 16 bytes.");
}
using var memoryStream = new MemoryStream();
using var cryptoStream = new CryptoStream(memoryStream, aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV), CryptoStreamMode.Write);
cryptoStream.Write(bytes, 0, bytes.Length);
cryptoStream.FlushFinalBlock();
// 如果是 CBC 模式,将 IV 和密文拼接在一起
if (mode != CipherMode.ECB)
using (var cryptoStream = new CryptoStream(memoryStream, aesAlg.CreateEncryptor(), CryptoStreamMode.Write))
{
var result = new byte[aesAlg.IV.Length + memoryStream.ToArray().Length];
cryptoStream.Write(bytes, 0, bytes.Length);
cryptoStream.FlushFinalBlock();
}
var encryptedContent = memoryStream.ToArray();
// 仅在未提供 IV 时拼接 IV
if (mode != CipherMode.ECB && iv == null)
{
var result = new byte[aesAlg.IV.Length + encryptedContent.Length];
Buffer.BlockCopy(aesAlg.IV, 0, result, 0, aesAlg.IV.Length);
Buffer.BlockCopy(memoryStream.ToArray(), 0, result, aesAlg.IV.Length, memoryStream.ToArray().Length);
Buffer.BlockCopy(encryptedContent, 0, result, aesAlg.IV.Length, encryptedContent.Length);
return result;
}
// 如果是 ECB 模式,直接返回密文
return memoryStream.ToArray();
}
// 生成随机 IV
private static byte[] GenerateRandomIV()
{
using var aes = Aes.Create();
aes.GenerateIV();
return aes.IV;
return encryptedContent;
}
/// <summary>
@@ -176,25 +175,13 @@ public class AESEncryption
/// <param name="iv">偏移量</param>
/// <param name="mode">模式</param>
/// <param name="padding">填充</param>
/// <param name="isBase64"></param>
/// <returns></returns>
public static byte[] Decrypt(byte[] bytes, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7)
public static byte[] Decrypt(byte[] bytes, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7, bool isBase64 = false)
{
// 确保密钥长度为 128 位、192 位或 256 位
var bKey = new byte[32]; // 256 位密钥
var keyBytes = Encoding.UTF8.GetBytes(skey);
Array.Copy(keyBytes, bKey, Math.Min(keyBytes.Length, bKey.Length));
// 如果是 ECB 模式,不需要 IV
if (mode != CipherMode.ECB)
{
if (iv == null)
{
// 从密文中提取 IV
iv = new byte[16];
Array.Copy(bytes, iv, iv.Length);
bytes = bytes.Skip(iv.Length).ToArray();
}
}
// 验证密钥长度
var bKey = !isBase64 ? Encoding.UTF8.GetBytes(skey) : Convert.FromBase64String(skey);
if (bKey.Length != 16 && bKey.Length != 24 && bKey.Length != 32) throw new ArgumentException("The key length must be 16, 24, or 32 bytes.");
using var aesAlg = Aes.Create();
aesAlg.Key = bKey;
@@ -203,21 +190,36 @@ public class AESEncryption
if (mode != CipherMode.ECB)
{
if (iv == null)
{
// 提取IV
if (bytes.Length < 16) throw new ArgumentException("The ciphertext length is insufficient to extract the IV.");
iv = bytes.Take(16).ToArray();
bytes = bytes.Skip(16).ToArray();
}
else
{
if (iv.Length != 16) throw new ArgumentException("The IV length must be 16 bytes.");
}
aesAlg.IV = iv;
}
using var memoryStream = new MemoryStream(bytes);
using var cryptoStream = new CryptoStream(memoryStream, aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV), CryptoStreamMode.Read);
using var cryptoStream = new CryptoStream(memoryStream, aesAlg.CreateDecryptor(), CryptoStreamMode.Read);
using var originalStream = new MemoryStream();
var buffer = new byte[1024];
var readBytes = 0;
while ((readBytes = cryptoStream.Read(buffer, 0, buffer.Length)) > 0)
{
originalStream.Write(buffer, 0, readBytes);
}
cryptoStream.CopyTo(originalStream);
return originalStream.ToArray();
}
/// <summary>
/// 生成随机 IV
/// </summary>
/// <returns></returns>
private static byte[] GenerateRandomIV()
{
using var aes = Aes.Create();
aes.GenerateIV();
return aes.IV;
}
}

View File

@@ -0,0 +1,92 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
using System.IO.Compression;
using System.Text;
namespace ThingsGateway.DataEncryption;
/// <summary>
/// GZip 压缩解压
/// </summary>
[SuppressSniffer]
public static class GzipEncryption
{
/// <summary>
/// 压缩字符串并返回字节数组
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public static byte[] Compress(string text)
{
var buffer = Encoding.UTF8.GetBytes(text);
using var ms = new MemoryStream();
using (var zip = new GZipStream(ms, CompressionMode.Compress, true))
{
zip.Write(buffer, 0, buffer.Length);
}
return ms.ToArray();
}
/// <summary>
/// 从字节数组解压
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
public static string Decompress(byte[] bytes)
{
using var ms = new MemoryStream(bytes);
using var zip = new GZipStream(ms, CompressionMode.Decompress);
using var outStream = new MemoryStream();
zip.CopyTo(outStream);
return Encoding.UTF8.GetString(outStream.ToArray());
}
/// <summary>
/// 压缩字符串并返回 Base64 字符串
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public static string CompressToBase64(string text)
{
var buffer = Encoding.UTF8.GetBytes(text);
using var ms = new MemoryStream();
using (var zip = new GZipStream(ms, CompressionMode.Compress, true))
{
zip.Write(buffer, 0, buffer.Length);
}
return Convert.ToBase64String(ms.ToArray());
}
/// <summary>
/// 从 Base64 字符串解压
/// </summary>
/// <param name="base64String"></param>
/// <returns></returns>
public static string DecompressFromBase64(string base64String)
{
var compressedData = Convert.FromBase64String(base64String);
using var ms = new MemoryStream(compressedData);
using var zip = new GZipStream(ms, CompressionMode.Decompress);
using var outStream = new MemoryStream();
zip.CopyTo(outStream);
return Encoding.UTF8.GetString(outStream.ToArray());
}
}

View File

@@ -77,10 +77,11 @@ public static class StringEncryptionExtensions
/// <param name="iv">偏移量</param>
/// <param name="mode">模式</param>
/// <param name="padding">填充</param>
/// <param name="isBase64"></param>
/// <returns>string</returns>
public static string ToAESEncrypt(this string text, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7)
public static string ToAESEncrypt(this string text, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7, bool isBase64 = false)
{
return AESEncryption.Encrypt(text, skey, iv, mode, padding);
return AESEncryption.Encrypt(text, skey, iv, mode, padding, isBase64);
}
/// <summary>
@@ -91,10 +92,11 @@ public static class StringEncryptionExtensions
/// <param name="iv">偏移量</param>
/// <param name="mode">模式</param>
/// <param name="padding">填充</param>
/// <param name="isBase64"></param>
/// <returns>string</returns>
public static string ToAESDecrypt(this string text, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7)
public static string ToAESDecrypt(this string text, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7, bool isBase64 = false)
{
return AESEncryption.Decrypt(text, skey, iv, mode, padding);
return AESEncryption.Decrypt(text, skey, iv, mode, padding, isBase64);
}
/// <summary>
@@ -105,10 +107,11 @@ public static class StringEncryptionExtensions
/// <param name="iv">偏移量</param>
/// <param name="mode">模式</param>
/// <param name="padding">填充</param>
/// <param name="isBase64"></param>
/// <returns>string</returns>
public static byte[] ToAESEncrypt(this byte[] bytes, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7)
public static byte[] ToAESEncrypt(this byte[] bytes, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7, bool isBase64 = false)
{
return AESEncryption.Encrypt(bytes, skey, iv, mode, padding);
return AESEncryption.Encrypt(bytes, skey, iv, mode, padding, isBase64);
}
/// <summary>
@@ -119,10 +122,11 @@ public static class StringEncryptionExtensions
/// <param name="iv">偏移量</param>
/// <param name="mode">模式</param>
/// <param name="padding">填充</param>
/// <param name="isBase64"></param>
/// <returns>string</returns>
public static byte[] ToAESDecrypt(this byte[] bytes, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7)
public static byte[] ToAESDecrypt(this byte[] bytes, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7, bool isBase64 = false)
{
return AESEncryption.Decrypt(bytes, skey, iv, mode, padding);
return AESEncryption.Decrypt(bytes, skey, iv, mode, padding, isBase64);
}
/// <summary>
@@ -243,4 +247,44 @@ public static class StringEncryptionExtensions
{
return PBKDF2Encryption.Compare(text, hash, saltSize, iterationCount, derivedKeyLength);
}
/// <summary>
/// Gzip 压缩字符串并返回字节数组
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public static byte[] ToGzipCompress(this string text)
{
return GzipEncryption.Compress(text);
}
/// <summary>
/// Gzip 从字节数组解压
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
public static string ToGzipDecompress(this byte[] bytes)
{
return GzipEncryption.Decompress(bytes);
}
/// <summary>
/// Gzip 压缩字符串并返回 Base64 字符串
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public static string ToGzipCompressToBase64(this string text)
{
return GzipEncryption.CompressToBase64(text);
}
/// <summary>
/// Gzip 从 Base64 字符串解压
/// </summary>
/// <param name="base64String"></param>
/// <returns></returns>
public static string ToGzipDecompressFromBase64(this string base64String)
{
return GzipEncryption.DecompressFromBase64(base64String);
}
}

View File

@@ -565,10 +565,10 @@ internal sealed class DynamicApiControllerApplicationModelConvention : IApplicat
if (isLowerCamelCase) parameterModel.ParameterName = parameterModel.ParameterName.ToLowerCamelCase();
// 判断是否贴有任何 [FromXXX] 特性了
var hasFormAttribute = parameterAttributes.Any(u => typeof(IBindingSourceMetadata).IsAssignableFrom(u.GetType()));
var hasFromAttribute = parameterAttributes.Any(u => typeof(IBindingSourceMetadata).IsAssignableFrom(u.GetType()));
// 判断方法贴有 [QueryParameters] 特性且当前参数没有任何 [FromXXX] 特性,则添加 [FromQuery] 特性
if (isQueryParametersAction && !hasFormAttribute)
if (isQueryParametersAction && !hasFromAttribute)
{
parameterModel.BindingInfo = BindingInfo.GetBindingInfo(new[] { new FromQueryAttribute() });
continue;
@@ -577,7 +577,7 @@ internal sealed class DynamicApiControllerApplicationModelConvention : IApplicat
// 如果没有贴 [FromRoute] 特性且不是基元类型,则跳过
// 如果没有贴 [FromRoute] 特性且有任何绑定特性,则跳过
if (!parameterAttributes.Any(u => u is FromRouteAttribute)
&& (!parameterType.IsRichPrimitive() || hasFormAttribute)) continue;
&& (!parameterType.IsRichPrimitive() || hasFromAttribute)) continue;
// 处理基元数组数组类型,还有全局配置参数问题
if (_dynamicApiControllerSettings?.UrlParameterization == true || parameterType.IsArray)
@@ -588,7 +588,7 @@ internal sealed class DynamicApiControllerApplicationModelConvention : IApplicat
// 处理 [ApiController] 特性情况
// https://docs.microsoft.com/en-US/aspnet/core/web-api/?view=aspnetcore-5.0#binding-source-parameter-inference
if (!hasFormAttribute && hasApiControllerAttribute) continue;
if (!hasFromAttribute && hasApiControllerAttribute) continue;
// 处理默认基元参数绑定方式,若是 query[FromQuery])则跳过
if (_dynamicApiControllerSettings?.DefaultBindingInfo?.ToLower() == "query")

View File

@@ -84,8 +84,6 @@ internal sealed class DynamicApiRuntimeChangeProvider : IDynamicApiRuntimeChange
if (applicationPart != null) _applicationPartManager.ApplicationParts.Remove(applicationPart);
}
GC.Collect();
GC.WaitForPendingFinalizers();
}
}

View File

@@ -72,7 +72,7 @@ public sealed class EventBusOptionsBuilder
/// <summary>
/// 是否启用执行完成触发 GC 回收
/// </summary>
public bool GCCollect { get; set; } = true;
public bool GCCollect { get; set; } = false;
/// <summary>
/// 是否启用日志记录

View File

@@ -10,6 +10,7 @@
// ------------------------------------------------------------------------
using System.Reflection;
using System.Text.Json;
namespace ThingsGateway.EventBus;
@@ -57,4 +58,31 @@ public abstract class EventHandlerContext
/// </summary>
/// <remarks><remarks>如果是动态订阅,可能为 null</remarks></remarks>
public EventSubscribeAttribute Attribute { get; }
private static JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions(JsonSerializerOptions.Default)
{
PropertyNameCaseInsensitive = true
};
/// <summary>
/// 获取负载数据
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T GetPayload<T>()
{
var rawPayload = Source.Payload;
if (rawPayload is null)
{
return default;
}
else if (rawPayload is JsonElement jsonElement)
{
return JsonSerializer.Deserialize<T>(jsonElement.GetRawText(), JsonSerializerOptions);
}
else
{
return (T)rawPayload;
}
}
}

View File

@@ -38,4 +38,18 @@ public sealed class EventHandlerExecutingContext : EventHandlerContext
/// 执行前时间
/// </summary>
public DateTime ExecutingTime { get; internal set; }
/// <summary>
/// 执行结果
/// </summary>
internal object Result { get; private set; }
/// <summary>
/// 设置执行结果
/// </summary>
/// <param name="result"></param>
public void SetResult(object result)
{
Result = result;
}
}

View File

@@ -42,4 +42,10 @@ public sealed class EventHandlerEventArgs : EventArgs
/// 异常信息
/// </summary>
public Exception Exception { get; internal set; }
/// <summary>
/// 执行结果
/// </summary>
public object Result { get; internal set; }
}

View File

@@ -304,7 +304,10 @@ internal sealed class EventBusHostedService : BackgroundService
}
// 触发事件处理程序事件
_eventPublisher.InvokeEvents(new(eventSource, true));
_eventPublisher.InvokeEvents(new(eventSource, true)
{
Result = eventHandlerExecutingContext.Result
});
}
catch (Exception ex)
{

View File

@@ -198,8 +198,9 @@ public class JWTEncryption
/// <param name="refreshTokenExpiredTime">新刷新 Token 有效期(分钟)</param>
/// <param name="tokenPrefix"></param>
/// <param name="clockSkew"></param>
/// <param name="onRefreshing">当刷新时触发</param>
/// <returns></returns>
public static async Task<bool> AutoRefreshToken(AuthorizationHandlerContext context, DefaultHttpContext httpContext, long? expiredTime = null, int refreshTokenExpiredTime = 43200, string tokenPrefix = "Bearer ", long clockSkew = 5)
public static async Task<bool> AutoRefreshToken(AuthorizationHandlerContext context, DefaultHttpContext httpContext, long? expiredTime = null, int refreshTokenExpiredTime = 43200, string tokenPrefix = "Bearer ", long clockSkew = 5, Action<string, string> onRefreshing = null)
{
// 如果验证有效,则跳过刷新
if (context.User.Identity.IsAuthenticated)
@@ -245,7 +246,11 @@ public class JWTEncryption
// 返回新的 Token
httpContext.Response.Headers[accessTokenKey] = accessToken;
// 返回新的 刷新Token
httpContext.Response.Headers[xAccessTokenKey] = GenerateRefreshToken(accessToken, refreshTokenExpiredTime);
var refreshAccessToken = GenerateRefreshToken(accessToken, refreshTokenExpiredTime); ;
httpContext.Response.Headers[xAccessTokenKey] = refreshAccessToken;
// 调用刷新后回调函数
onRefreshing?.Invoke(accessToken, refreshAccessToken);
// 处理 axios 问题
httpContext.Response.Headers.TryGetValue(accessControlExposeKey, out var acehs);

View File

@@ -90,7 +90,8 @@ public sealed class ConsoleFormatterExtend : ConsoleFormatter, IDisposable
, true
, _disableColors
, _formatterOptions.WithTraceId
, _formatterOptions.WithStackFrame);
, _formatterOptions.WithStackFrame
, _formatterOptions.FormatProvider);
}
// 判断是否自定义了日志筛选器,如果是则检查是否符合条件

View File

@@ -12,6 +12,8 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Console;
using System.Globalization;
namespace ThingsGateway.Logging;
/// <summary>
@@ -69,4 +71,10 @@ public sealed class ConsoleFormatterExtendOptions : ConsoleFormatterOptions
/// 日志消息内容转换(如脱敏处理)
/// </summary>
public Func<string, string> MessageProcess { get; set; }
/// <summary>
/// 格式化提供器
/// </summary>
/// <remarks></remarks>
public IFormatProvider? FormatProvider { get; set; } = CultureInfo.InvariantCulture;
}

View File

@@ -118,7 +118,7 @@ public sealed class DatabaseLogger : ILogger
// 设置日志消息模板
logMsg.Message = _options.MessageFormat != null
? _options.MessageFormat(logMsg)
: Penetrates.OutputStandardMessage(logMsg, _options.DateFormat, withTraceId: _options.WithTraceId, withStackFrame: _options.WithStackFrame);
: Penetrates.OutputStandardMessage(logMsg, _options.DateFormat, withTraceId: _options.WithTraceId, withStackFrame: _options.WithStackFrame, provider: _options.FormatProvider);
// 空检查
if (logMsg.Message is null)

View File

@@ -11,6 +11,8 @@
using Microsoft.Extensions.Logging;
using System.Globalization;
namespace ThingsGateway.Logging;
/// <summary>
@@ -80,4 +82,10 @@ public sealed class DatabaseLoggerOptions
/// 日志消息内容转换(如脱敏处理)
/// </summary>
public Func<string, string> MessageProcess { get; set; }
/// <summary>
/// 格式化提供器
/// </summary>
/// <remarks></remarks>
public IFormatProvider? FormatProvider { get; set; } = CultureInfo.InvariantCulture;
}

View File

@@ -116,7 +116,7 @@ public sealed class FileLogger : ILogger
// 设置日志消息模板
logMsg.Message = _options.MessageFormat != null
? _options.MessageFormat(logMsg)
: Penetrates.OutputStandardMessage(logMsg, _options.DateFormat, withTraceId: _options.WithTraceId, withStackFrame: _options.WithStackFrame);
: Penetrates.OutputStandardMessage(logMsg, _options.DateFormat, withTraceId: _options.WithTraceId, withStackFrame: _options.WithStackFrame, provider: _options.FormatProvider);
// 空检查
if (logMsg.Message is null)

View File

@@ -11,6 +11,8 @@
using Microsoft.Extensions.Logging;
using System.Globalization;
namespace ThingsGateway.Logging;
/// <summary>
@@ -104,4 +106,10 @@ public sealed class FileLoggerOptions
/// 日志消息内容转换(如脱敏处理)
/// </summary>
public Func<string, string> MessageProcess { get; set; }
/// <summary>
/// 格式化提供器
/// </summary>
/// <remarks></remarks>
public IFormatProvider? FormatProvider { get; set; } = CultureInfo.InvariantCulture;
}

View File

@@ -11,6 +11,8 @@
using Microsoft.Extensions.Logging;
using System.Globalization;
namespace ThingsGateway.Logging;
/// <summary>
@@ -120,6 +122,6 @@ public struct LogMessage
/// <returns><see cref="string"/></returns>
public override readonly string ToString()
{
return Penetrates.OutputStandardMessage(this);
return Penetrates.OutputStandardMessage(this, provider: CultureInfo.InvariantCulture);
}
}

View File

@@ -192,7 +192,7 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
/// <param name="claimsPrincipal"></param>
/// <param name="authorization"></param>
/// <returns></returns>
private static List<string> GenerateAuthorizationTemplate(Utf8JsonWriter writer, ClaimsPrincipal claimsPrincipal, StringValues authorization)
private List<string> GenerateAuthorizationTemplate(Utf8JsonWriter writer, ClaimsPrincipal claimsPrincipal, StringValues authorization)
{
var templates = new List<string>();
@@ -219,7 +219,7 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
var succeed = long.TryParse(value, out var seconds);
if (succeed)
{
value = $"{value} ({DateTimeOffset.FromUnixTimeSeconds(seconds).ToLocalTime():yyyy-MM-dd HH:mm:ss:ffff(zzz) dddd} L)";
value = $"{value} ({DateTimeOffset.FromUnixTimeSeconds(seconds).ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss:ffff(zzz) dddd", Settings.FormatProvider)} L)";
}
}

View File

@@ -12,6 +12,7 @@
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
using System.Globalization;
using System.Text.Encodings.Web;
using System.Text.Json;
@@ -143,4 +144,11 @@ public sealed class LoggingMonitorSettings
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
SkipValidation = true
};
/// <summary>
/// 格式化提供器
/// </summary>
/// <remarks></remarks>
public IFormatProvider? FormatProvider { get; set; } = CultureInfo.InvariantCulture;
}

View File

@@ -107,13 +107,15 @@ internal static class Penetrates
/// <param name="isConsole"></param>
/// <param name="withTraceId"></param>
/// <param name="withStackFrame"></param>
/// <param name="provider"></param>
/// <returns></returns>
internal static string OutputStandardMessage(LogMessage logMsg
, string dateFormat = "yyyy-MM-dd HH:mm:ss.fffffff zzz dddd"
, bool isConsole = false
, bool disableColors = true
, bool withTraceId = false
, bool withStackFrame = false)
, bool withStackFrame = false
, IFormatProvider? provider = null)
{
// 空检查
if (logMsg.Message is null) return null;
@@ -127,7 +129,7 @@ internal static class Penetrates
_ = AppendWithColor(formatString, GetLogLevelString(logMsg.LogLevel), logLevelColors);
formatString.Append(": ");
formatString.Append(logMsg.LogDateTime.ToString(dateFormat));
formatString.Append(logMsg.LogDateTime.ToString(dateFormat, provider));
formatString.Append(' ');
formatString.Append(logMsg.UseUtcTimestamp ? "U" : "L");
formatString.Append(' ');

View File

@@ -78,9 +78,9 @@ public partial interface ISchedulerFactory
/// <returns><see cref="IJob"/></returns>
IJob CreateJob(IServiceProvider serviceProvider, JobFactoryContext context);
/// <summary>
/// GC 垃圾回收器回收处理
/// </summary>
/// <remarks>避免频繁 GC 回收</remarks>
void GCCollect();
///// <summary>
///// GC 垃圾回收器回收处理
///// </summary>
///// <remarks>避免频繁 GC 回收</remarks>
//void GCCollect();
}

View File

@@ -183,9 +183,10 @@ internal sealed partial class SchedulerFactory : ISchedulerFactory
// 标记当前方法初始化完成
PreloadCompleted = true;
// 释放引用内存并立即回收GC
// 释放引用内存
_schedulerBuilders.Clear();
GCCollect();
//GCCollect();
// 输出作业调度器初始化日志
if (!preloadSucceed) _logger.LogWarning("Schedule hosted service preload completed, and a total of <{Count}> schedulers are appended.", _schedulers.Count);
@@ -393,22 +394,22 @@ internal sealed partial class SchedulerFactory : ISchedulerFactory
return jobHandler;
}
/// <summary>
/// GC 垃圾回收器回收处理
/// </summary>
/// <remarks>避免频繁 GC 回收</remarks>
public void GCCollect()
{
var nowTime = DateTime.UtcNow;
if ((LastGCCollectTime == null || (nowTime - LastGCCollectTime.Value).TotalMilliseconds > GC_COLLECT_INTERVAL_MILLISECONDS))
{
LastGCCollectTime = nowTime;
///// <summary>
///// GC 垃圾回收器回收处理
///// </summary>
///// <remarks>避免频繁 GC 回收</remarks>
//public void GCCollect()
//{
// var nowTime = DateTime.UtcNow;
// if ((LastGCCollectTime == null || (nowTime - LastGCCollectTime.Value).TotalMilliseconds > GC_COLLECT_INTERVAL_MILLISECONDS))
// {
// LastGCCollectTime = nowTime;
// 通知 GC 垃圾回收器立即回收
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
// // 通知 GC 垃圾回收器立即回收
// GC.Collect();
// GC.WaitForPendingFinalizers();
// }
//}
/// <summary>
/// 释放非托管资源
@@ -535,7 +536,7 @@ internal sealed partial class SchedulerFactory : ISchedulerFactory
//_logger.LogWarning("Schedule hosted service cancels hibernation.");
// 通知 GC 垃圾回收器立即回收
GCCollect();
//GCCollect();
});
}

View File

@@ -147,7 +147,7 @@ internal sealed class ScheduleHostedService : BackgroundService
await BackgroundProcessing(stoppingToken).ConfigureAwait(false);
}
_logger.LogCritical($"Schedule hosted service is stopped.");
_logger.LogInformation($"Schedule hosted service is stopped.");
}
/// <summary>
@@ -389,7 +389,7 @@ internal sealed class ScheduleHostedService : BackgroundService
_jobCancellationToken.Cancel(jobId, triggerId, false);
// 通知 GC 垃圾回收器回收
_schedulerFactory.GCCollect();
//_schedulerFactory.GCCollect();
}
}, stoppingToken);
});

View File

@@ -113,10 +113,8 @@ public static class SpecificationDocumentBuilder
}
// 处理贴有 [ApiExplorerSettings(IgnoreApi = true)] 或者 [ApiDescriptionSettings(false)] 特性的接口
var apiExplorerSettings = method.GetFoundAttribute<ApiExplorerSettingsAttribute>(true);
var apiDescriptionSettings = method.GetFoundAttribute<ApiDescriptionSettingsAttribute>(true);
var apiExplorerSettings = method.GetFoundAttribute<ApiExplorerSettingsAttribute>(true, true);
var apiDescriptionSettings = method.GetFoundAttribute<ApiDescriptionSettingsAttribute>(true, true);
if (apiExplorerSettings?.IgnoreApi == true || apiDescriptionSettings?.IgnoreApi == true) return false;
if (currentGroup == AllGroupsKey)

View File

@@ -43,18 +43,18 @@
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.14" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.16" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.16" />
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="8.0.2" />
<PackageReference Include="System.Reflection.MetadataLoadContext" Version="8.0.1" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net9.0' ">
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="9.0.4" />
<PackageReference Include="System.Reflection.MetadataLoadContext" Version="9.0.4" />
<PackageReference Include="System.Text.Json" Version="9.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="9.0.5" />
<PackageReference Include="System.Reflection.MetadataLoadContext" Version="9.0.5" />
<PackageReference Include="System.Text.Json" Version="9.0.5" />
</ItemGroup>

View File

@@ -433,10 +433,15 @@ public partial class Crontab
{
newValue = newValue.AddSeconds(-newValue.Second);
}
// 初始化是否存在随机 R 标识符
var randomSecond = false;
var randomMinute = false;
var randomHour = false;
// 获取分钟、小时所有字符解析器
var minuteParsers = Parsers[CrontabFieldKind.Minute].Where(x => x is ITimeParser).Cast<ITimeParser>().ToList();
randomMinute = minuteParsers.OfType<RandomParser>().Any();
var hourParsers = Parsers[CrontabFieldKind.Hour].Where(x => x is ITimeParser).Cast<ITimeParser>().ToList();
randomHour = hourParsers.OfType<RandomParser>().Any();
// 获取秒、分钟、小时解析器中最小起始值
// 该值主要用来获取下一个发生值的输入参数
@@ -456,7 +461,7 @@ public partial class Crontab
{
// 获取秒所有字符解析器
var secondParsers = Parsers[CrontabFieldKind.Second].Where(x => x is ITimeParser).Cast<ITimeParser>().ToList();
randomSecond = secondParsers.OfType<RandomParser>().Any();
// 获取秒解析器最小起始值
firstSecondValue = secondParsers.Select(x => x.First()).Min();
@@ -519,8 +524,8 @@ public partial class Crontab
// 设置起始时间为下一个小时时间
newValue = new DateTime(newValue.Year, newValue.Month, newValue.Day, newHours,
overflow ? firstMinuteValue : newMinutes,
overflow ? firstSecondValue : newSeconds);
overflow && !randomMinute ? firstMinuteValue : newMinutes,
overflow && !randomSecond ? firstSecondValue : newSeconds);
// 如果当前小时并没有进入下一轮循环但存在不匹配的字符过滤器
if (!overflow && !IsMatch(newValue))
@@ -534,7 +539,7 @@ public partial class Crontab
}
// 如果程序到达这里,说明并没有进入上面分支,则直接返回下一小时时间
if (!overflow)
if (!randomHour && !overflow)
{
return MinDate(newValue, endTime);
}
@@ -788,8 +793,15 @@ public partial class Crontab
/// <param name="defaultValue">默认值</param>
/// <param name="overflow">控制秒、分钟、小时到达59秒/分和23小时开关</param>
/// <returns><see cref="int"/></returns>
private static int Increment(IEnumerable<ITimeParser> parsers, int value, int defaultValue, out bool overflow)
private static int Increment(List<ITimeParser> parsers, int value, int defaultValue, out bool overflow)
{
// 检查是否是随机 R 字符解析器
if (parsers.Count == 1 && parsers.First() is RandomParser randomParser)
{
overflow = true;
return randomParser.Next(value).Value;
}
var nextValue = parsers.Select(x => x.Next(value))
.Where(x => x > value)
.Min()
@@ -808,7 +820,7 @@ public partial class Crontab
/// <param name="defaultValue">默认值</param>
/// <param name="overflow">控制秒、分钟、小时到达59秒/分和23小时开关</param>
/// <returns><see cref="int"/></returns>
private static int Decrement(IEnumerable<ITimeParser> parsers, int value, int defaultValue, out bool overflow)
private static int Decrement(List<ITimeParser> parsers, int value, int defaultValue, out bool overflow)
{
var previousValue = parsers.Select(x => x.Previous(value))
.Where(x => x < value)

View File

@@ -69,7 +69,7 @@ internal sealed class RandomParser : ICronParser, ITimeParser
/// <returns><see cref="bool"/></returns>
public bool IsMatch(DateTime datetime)
{
return true;
return Kind is not CrontabFieldKind.Hour;
}
/// <summary>

View File

@@ -168,7 +168,7 @@ public static class UnifyContext
if (context.ActionDescriptor is not ControllerActionDescriptor actionDescriptor) return null;
// 获取序列化配置
var unifySerializerSettingAttribute = actionDescriptor.MethodInfo.GetFoundAttribute<UnifySerializerSettingAttribute>(true);
var unifySerializerSettingAttribute = actionDescriptor.MethodInfo.GetFoundAttribute<UnifySerializerSettingAttribute>(true, true);
if (unifySerializerSettingAttribute == null || string.IsNullOrWhiteSpace(unifySerializerSettingAttribute.Name)) return null;
// 解析全局配置
@@ -225,7 +225,8 @@ public static class UnifyContext
|| method.GetRealReturnType().HasImplementedRawGeneric(unityMetadata.ResultType)
|| method.CustomAttributes.Any(x => typeof(NonUnifyAttribute).IsAssignableFrom(x.AttributeType) || typeof(ProducesResponseTypeAttribute).IsAssignableFrom(x.AttributeType) || typeof(IApiResponseMetadataProvider).IsAssignableFrom(x.AttributeType))
|| method.ReflectedType.IsDefined(typeof(NonUnifyAttribute), true)
|| method.DeclaringType.Assembly.GetName().Name.StartsWith("Microsoft.AspNetCore.OData");
|| method.DeclaringType.Assembly.GetName().Name.StartsWith("Microsoft.AspNetCore.OData")
|| method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>);
if (!isWebRequest)
{
@@ -255,7 +256,8 @@ public static class UnifyContext
!method.CustomAttributes.Any(x => typeof(ProducesResponseTypeAttribute).IsAssignableFrom(x.AttributeType) || typeof(IApiResponseMetadataProvider).IsAssignableFrom(x.AttributeType))
&& method.ReflectedType.IsDefined(typeof(NonUnifyAttribute), true)
)
|| method.DeclaringType.Assembly.GetName().Name.StartsWith("Microsoft.AspNetCore.OData");
|| method.DeclaringType.Assembly.GetName().Name.StartsWith("Microsoft.AspNetCore.OData")
|| method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>);
unifyResult = isSkip ? null : App.RootServices.GetService(unityMetadata.ProviderType) as IUnifyResultProvider;
return unifyResult == null || isSkip;
@@ -398,7 +400,7 @@ public static class UnifyContext
{
if (method == default) return default;
var unityProviderAttribute = method.GetFoundAttribute<UnifyProviderAttribute>(true);
var unityProviderAttribute = method.GetFoundAttribute<UnifyProviderAttribute>(true, true);
// 获取元数据
var isExists = UnifyProviders.TryGetValue(unityProviderAttribute?.Name ?? string.Empty, out var metadata);

View File

@@ -0,0 +1,38 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
using System.Text.Json;
using System.Text.Json.Serialization;
namespace ThingsGateway.Converters.Json;
/// <summary>
/// <see cref="DateTime" /> JSON 序列化转换器
/// </summary>
/// <remarks>在不符合 <c>ISO 8601-1:2019</c> 格式的 <see cref="DateTime" /> 时间使用 <c>DateTime.Parse</c> 作为回退。</remarks>
public sealed class DateTimeConverterUsingDateTimeParseAsFallback : JsonConverter<DateTime>
{
/// <inheritdoc />
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// 尝试获取 ISO 8601-1:2019 格式时间
if (!reader.TryGetDateTime(out var value))
{
value = DateTime.Parse(reader.GetString()!);
}
return value;
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) =>
JsonSerializer.Serialize(writer, value);
}

View File

@@ -0,0 +1,38 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
using System.Text.Json;
using System.Text.Json.Serialization;
namespace ThingsGateway.Converters.Json;
/// <summary>
/// <see cref="DateTimeOffset" /> JSON 序列化转换器
/// </summary>
/// <remarks>在不符合 <c>ISO 8601-1:2019</c> 格式的 <see cref="DateTimeOffset" /> 时间使用 <c>DateTimeOffset.Parse</c> 作为回退。</remarks>
public sealed class DateTimeOffsetConverterUsingDateTimeOffsetParseAsFallback : JsonConverter<DateTimeOffset>
{
/// <inheritdoc />
public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// 尝试获取 ISO 8601-1:2019 格式时间
if (!reader.TryGetDateTimeOffset(out var value))
{
value = DateTimeOffset.Parse(reader.GetString()!);
}
return value;
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) =>
JsonSerializer.Serialize(writer, value);
}

View File

@@ -0,0 +1,37 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
using System.Text.Json;
using System.Text.Json.Serialization;
using ThingsGateway.Extensions;
namespace ThingsGateway.Converters.Json;
/// <summary>
/// <see cref="string" /> JSON 序列化转换器
/// </summary>
/// <remarks>解决 Number 类型和 Boolean 类型转 String 类型时异常。</remarks>
public sealed class StringJsonConverter : JsonConverter<string>
{
/// <inheritdoc />
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
reader.TokenType switch
{
JsonTokenType.True or JsonTokenType.False => reader.GetBoolean().ToString(),
JsonTokenType.Number => reader.ConvertRawValueToString(),
_ => reader.GetString()
};
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) =>
writer.WriteStringValue(value);
}

View File

@@ -10,6 +10,7 @@
// ------------------------------------------------------------------------
using System.Linq.Expressions;
using System.Reflection;
namespace ThingsGateway.Extensions;
@@ -43,7 +44,7 @@ internal static class LinqExpressionExtensions
}
/// <summary>
/// 解析表达式属性名称
/// 解析表达式并获取属性的 <see cref="PropertyInfo" /> 实例
/// </summary>
/// <typeparam name="T">对象类型</typeparam>
/// <typeparam name="TProperty">属性类型</typeparam>
@@ -51,48 +52,54 @@ internal static class LinqExpressionExtensions
/// <see cref="Expression{TDelegate}" />
/// </param>
/// <returns>
/// <see cref="string" />
/// <see cref="PropertyInfo" />
/// </returns>
/// <exception cref="ArgumentException"></exception>
internal static string GetPropertyName<T, TProperty>(this Expression<Func<T, TProperty?>> propertySelector) =>
internal static PropertyInfo GetProperty<T, TProperty>(this Expression<Func<T, TProperty?>> propertySelector) =>
propertySelector.Body switch
{
// 检查 Lambda 表达式的主体是否是 MemberExpression 类型
MemberExpression memberExpression => GetPropertyName<T>(memberExpression),
MemberExpression memberExpression => GetProperty<T>(memberExpression),
// 如果主体是 UnaryExpression 类型,则继续解析
UnaryExpression { Operand: MemberExpression nestedMemberExpression } => GetPropertyName<T>(
UnaryExpression { Operand: MemberExpression nestedMemberExpression } => GetProperty<T>(
nestedMemberExpression),
_ => throw new ArgumentException("Expression is not valid for property selection.")
_ => throw new ArgumentException("Expression must be a simple member access (e.g. x => x.Property).",
nameof(propertySelector))
};
/// <summary>
/// 解析表达式属性名称
/// 从成员表达式中提取 <see cref="PropertyInfo" /> 实例
/// </summary>
/// <typeparam name="T">对象类型</typeparam>
/// <param name="memberExpression">
/// <see cref="MemberExpression" />
/// </param>
/// <typeparam name="T">对象类型</typeparam>
/// <returns>
/// <see cref="string" />
/// <see cref="PropertyInfo" />
/// </returns>
/// <exception cref="ArgumentException"></exception>
internal static string GetPropertyName<T>(MemberExpression memberExpression)
internal static PropertyInfo GetProperty<T>(MemberExpression memberExpression)
{
// 空检查
ArgumentNullException.ThrowIfNull(memberExpression);
// 获取属性声明类型
var propertyType = memberExpression.Member.DeclaringType;
// 检查是否越界访问属性
if (propertyType != typeof(T))
// 确保表达式根是 T 类型的参数
if (memberExpression.Expression is not ParameterExpression parameterExpression ||
parameterExpression.Type != typeof(T))
{
throw new ArgumentException("Invalid property selection.");
throw new ArgumentException(
$"Expression '{memberExpression}' must refer to a member of type '{typeof(T)}'.",
nameof(memberExpression));
}
// 返回属性名称
return memberExpression.Member.Name;
// 确保成员是属性(非字段)
if (memberExpression.Member is not PropertyInfo propertyInfo)
{
throw new ArgumentException(
$"Expression '{memberExpression}' refers to a field. Only properties are supported.",
nameof(memberExpression));
}
return propertyInfo;
}
}

View File

@@ -11,6 +11,7 @@
using Microsoft.Extensions.Configuration;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Reflection;
using System.Text;
@@ -149,7 +150,7 @@ internal static partial class StringExtensions
var pairs = (trimChar is null ? keyValueString : keyValueString.TrimStart(trimChar.Value)).Split(separators);
return (from pair in pairs
select pair.Split('=')
select pair.Split('=', 2) // 限制只分割一次
into keyValue
where keyValue.Length == 2
select new KeyValuePair<string, string?>(keyValue[0].Trim(), keyValue[1])).ToList();
@@ -328,6 +329,18 @@ internal static partial class StringExtensions
});
}
/// <summary>
/// 转换输入字符串中的任何转义字符
/// </summary>
/// <param name="input">
/// <see cref="string" />
/// </param>
/// <returns>
/// <see cref="string" />
/// </returns>
internal static string? Unescape([NotNullIfNotNull(nameof(input))] this string? input) =>
string.IsNullOrWhiteSpace(input) ? input : Regex.Unescape(input);
/// <summary>
/// 占位符匹配正则表达式
/// </summary>

View File

@@ -9,6 +9,8 @@
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
using System.Buffers;
using System.Text;
using System.Text.Json;
namespace ThingsGateway.Extensions;
@@ -34,4 +36,17 @@ internal static class Utf8JsonReaderExtensions
return jsonDocument.RootElement.Clone().GetRawText();
}
/// <summary>
/// 从 <see cref="Utf8JsonReader" /> 中提取原始值,并将其转换为字符串
/// </summary>
/// <remarks>支持处理各种类型的原始值(例如数字、布尔值等)。</remarks>
/// <param name="reader">
/// <see cref="Utf8JsonReader" />
/// </param>
/// <returns>
/// <see cref="string" />
/// </returns>
internal static string ConvertRawValueToString(this Utf8JsonReader reader) =>
Encoding.UTF8.GetString(reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan);
}

View File

@@ -97,16 +97,45 @@ internal static class V5_ObjectExtensions
case ICollection collection:
count = collection.Count;
return true;
// 检查对象是否实现了 IEnumerable 接口
case IEnumerable enumerable:
// 获取集合枚举数
var enumerator = enumerable.GetEnumerator();
try
{
// 检查枚举数是否可以推进到下一个元素
if (!enumerator.MoveNext())
{
count = 0;
return true;
}
// 枚举数循环推进到下一个元素并叠加推进次数
var c = 1;
while (enumerator.MoveNext())
{
c++;
}
count = c;
return true;
}
finally
{
// 检查枚举数是否实现了 IDisposable 接口
if (enumerator is IDisposable disposable)
{
disposable.Dispose();
}
}
}
// 反射查找是否存在 Count 属性
var runtimeProperty = obj.GetType()
.GetRuntimeProperty("Count");
var runtimeProperty = obj.GetType().GetRuntimeProperty("Count");
// 反射获取 Count 属性值
if (runtimeProperty is not null
&& runtimeProperty.CanRead
&& runtimeProperty.PropertyType == typeof(int))
if (runtimeProperty is not null && runtimeProperty.CanRead && runtimeProperty.PropertyType == typeof(int))
{
count = (int)runtimeProperty.GetValue(obj)!;
return true;

View File

@@ -38,7 +38,7 @@ public sealed class HttpContextForwardBuilder
/// <summary>
/// 忽略在转发时需要跳过的请求标头列表
/// </summary>
internal static HashSet<string> _ignoreRequestHeaders =
internal static readonly HashSet<string> _ignoreRequestHeaders =
[
Constants.X_FORWARD_TO_HEADER, "Host", "Accept", "Accept-CH", "Accept-Charset", "Accept-Encoding",
"Accept-Language", "Accept-Patch", "Accept-Post", "Accept-Ranges"
@@ -356,8 +356,7 @@ public sealed class HttpContextForwardBuilder
if (multipartSection.AsFileSection() is not null)
{
// 复制多部分表单内容文件节内容
await CopyFileMultipartSectionAsync(multipartSection, httpMultipartFormDataBuilder, httpRequestBuilder,
cancellationToken).ConfigureAwait(false);
await CopyFileMultipartSectionAsync(multipartSection, httpMultipartFormDataBuilder, cancellationToken).ConfigureAwait(false);
}
else
{
@@ -410,15 +409,11 @@ public sealed class HttpContextForwardBuilder
/// <param name="httpMultipartFormDataBuilder">
/// <see cref="HttpMultipartFormDataBuilder" />
/// </param>
/// <param name="httpRequestBuilder">
/// <see cref="HttpRequestBuilder" />
/// </param>
/// <param name="cancellationToken">
/// <see cref="CancellationToken" />
/// </param>
internal static async Task CopyFileMultipartSectionAsync(MultipartSection multipartSection,
HttpMultipartFormDataBuilder httpMultipartFormDataBuilder, HttpRequestBuilder httpRequestBuilder,
CancellationToken cancellationToken)
HttpMultipartFormDataBuilder httpMultipartFormDataBuilder, CancellationToken cancellationToken)
{
// 初始化 MemoryStream 实例
var memoryStream = new MemoryStream();
@@ -433,10 +428,8 @@ public sealed class HttpContextForwardBuilder
var fileMultipartSection = multipartSection.AsFileSection()!;
// 添加文件流
httpMultipartFormDataBuilder.AddStream(memoryStream, fileMultipartSection.Name, fileMultipartSection.FileName);
// 添加文件流到请求结束时需要释放的集合中
httpRequestBuilder.AddDisposable(memoryStream);
httpMultipartFormDataBuilder.AddStream(memoryStream, fileMultipartSection.Name, fileMultipartSection.FileName,
disposeStreamOnRequestCompletion: true);
}
/// <summary>

View File

@@ -124,12 +124,9 @@ public sealed class HttpMultipartFormDataBuilder
/// <see cref="HttpMultipartFormDataBuilder" />
/// </returns>
/// <exception cref="JsonException"></exception>
public HttpMultipartFormDataBuilder AddJson(object rawJson, string? name = null, Encoding? contentEncoding = null,
public HttpMultipartFormDataBuilder AddJson(object? rawJson, string? name = null, Encoding? contentEncoding = null,
string? contentType = null)
{
// 空检查
ArgumentNullException.ThrowIfNull(rawJson);
// 检查是否配置表单名或不是字符串类型
if (!string.IsNullOrWhiteSpace(name) || rawJson is not string rawString)
{
@@ -292,10 +289,8 @@ public sealed class HttpMultipartFormDataBuilder
// 从互联网 URL 地址中加载流
var fileStream = Helpers.GetStreamFromRemote(url);
// 添加文件流到请求结束时需要释放的集合中
_httpRequestBuilder.AddDisposable(fileStream);
return AddStream(fileStream, name, newFileName, contentType, contentEncoding);
return AddStream(fileStream, name, newFileName, contentType, contentEncoding,
true);
}
/// <summary>
@@ -365,10 +360,8 @@ public sealed class HttpMultipartFormDataBuilder
// 读取文件流(没有 using
var fileStream = File.OpenRead(filePath);
// 添加文件流到请求结束时需要释放的集合中
_httpRequestBuilder.AddDisposable(fileStream);
return AddStream(fileStream, name, newFileName, contentType, contentEncoding);
return AddStream(fileStream, name, newFileName, contentType, contentEncoding,
true);
}
/// <summary>
@@ -407,10 +400,8 @@ public sealed class HttpMultipartFormDataBuilder
// 初始化带读写进度的文件流
var progressFileStream = new ProgressFileStream(fileStream, filePath, progressChannel, newFileName);
// 添加文件流到请求结束时需要释放的集合中
_httpRequestBuilder.AddDisposable(progressFileStream);
return AddStream(progressFileStream, name, newFileName, contentType, contentEncoding);
return AddStream(progressFileStream, name, newFileName, contentType, contentEncoding,
true);
}
/// <summary>
@@ -500,11 +491,12 @@ public sealed class HttpMultipartFormDataBuilder
/// <param name="fileName">文件的名称</param>
/// <param name="contentType">内容类型</param>
/// <param name="contentEncoding">内容编码</param>
/// <param name="disposeStreamOnRequestCompletion">是否在请求结束后自动释放流。默认值为:<c>false</c></param>
/// <returns>
/// <see cref="HttpMultipartFormDataBuilder" />
/// </returns>
public HttpMultipartFormDataBuilder AddStream(Stream stream, string name = "file", string? fileName = null,
string? contentType = null, Encoding? contentEncoding = null)
string? contentType = null, Encoding? contentEncoding = null, bool disposeStreamOnRequestCompletion = false)
{
// 空检查
ArgumentNullException.ThrowIfNull(stream);
@@ -529,6 +521,12 @@ public sealed class HttpMultipartFormDataBuilder
FileName = fileName
});
// 是否在请求结束后自动释放流
if (disposeStreamOnRequestCompletion)
{
_httpRequestBuilder.AddDisposable(stream);
}
return this;
}
@@ -697,6 +695,20 @@ public sealed class HttpMultipartFormDataBuilder
return this;
}
/// <summary>
/// 设置是否移除默认的多部分内容的 <c>Content-Type</c>
/// </summary>
/// <param name="omit">如果为 <c>true</c> 则移除,默认为 <c>false</c></param>
/// <returns>
/// <see cref="HttpMultipartFormDataBuilder" />
/// </returns>
public HttpMultipartFormDataBuilder SetOmitContentType(bool omit)
{
OmitContentType = omit;
return this;
}
/// <summary>
/// 构建 <see cref="MultipartFormDataContent" /> 实例
/// </summary>

View File

@@ -327,23 +327,20 @@ public sealed partial class HttpRequestBuilder
/// <param name="key">键</param>
/// <param name="value">值</param>
/// <param name="escape">是否转义字符串,默认 <c>false</c></param>
/// <param name="replace">是否替换已存在的请求标头。默认值为 <c>false</c></param>
/// <param name="culture">
/// <see cref="CultureInfo" />
/// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <param name="replace">是否替换已存在的请求标头。默认值为 <c>false</c>。</param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder WithHeader(string key, object? value, bool escape = false, CultureInfo? culture = null,
IEqualityComparer<string>? comparer = null, bool replace = false)
public HttpRequestBuilder WithHeader(string key, object? value, bool escape = false, bool replace = false,
CultureInfo? culture = null)
{
// 空检查
ArgumentException.ThrowIfNullOrWhiteSpace(key);
return WithHeaders(new Dictionary<string, object?> { { key, value } }, escape, culture, comparer, replace);
return WithHeaders(new Dictionary<string, object?> { { key, value } }, escape, replace, culture);
}
/// <summary>
@@ -352,25 +349,22 @@ public sealed partial class HttpRequestBuilder
/// <remarks>支持多次调用。</remarks>
/// <param name="headers">请求标头集合</param>
/// <param name="escape">是否转义字符串,默认 <c>false</c></param>
/// <param name="replace">是否替换已存在的请求标头。默认值为 <c>false</c></param>
/// <param name="culture">
/// <see cref="CultureInfo" />
/// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <param name="replace">是否替换已存在的请求标头。默认值为 <c>false</c>。</param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder WithHeaders(IDictionary<string, object?> headers, bool escape = false,
CultureInfo? culture = null, IEqualityComparer<string>? comparer = null, bool replace = false)
bool replace = false, CultureInfo? culture = null)
{
// 空检查
ArgumentNullException.ThrowIfNull(headers);
// 初始化请求标头
Headers ??= new Dictionary<string, List<string?>>(comparer);
var objectHeaders = new Dictionary<string, List<object?>>(comparer);
Headers ??= new Dictionary<string, List<string?>>(StringComparer.OrdinalIgnoreCase);
var objectHeaders = new Dictionary<string, List<object?>>(StringComparer.OrdinalIgnoreCase);
// 存在则合并否则添加
objectHeaders.AddOrUpdate(Headers.ToDictionary(u => u.Key, object? (u) => u.Value), false);
@@ -380,7 +374,7 @@ public sealed partial class HttpRequestBuilder
Headers = objectHeaders.ToDictionary(kvp => kvp.Key,
kvp => kvp.Value.Select(u =>
u.ToCultureString(culture ?? CultureInfo.InvariantCulture)?.EscapeDataString(escape)).ToList(),
comparer);
StringComparer.OrdinalIgnoreCase);
return this;
}
@@ -391,26 +385,23 @@ public sealed partial class HttpRequestBuilder
/// <remarks>支持多次调用。</remarks>
/// <param name="headerSource">请求标头源对象</param>
/// <param name="escape">是否转义字符串,默认 <c>false</c></param>
/// <param name="replace">是否替换已存在的请求标头。默认值为 <c>false</c></param>
/// <param name="culture">
/// <see cref="CultureInfo" />
/// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <param name="replace">是否替换已存在的请求标头。默认值为 <c>false</c>。</param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder WithHeaders(object headerSource, bool escape = false, CultureInfo? culture = null,
IEqualityComparer<string>? comparer = null, bool replace = false)
public HttpRequestBuilder WithHeaders(object headerSource, bool escape = false, bool replace = false,
CultureInfo? culture = null)
{
// 空检查
ArgumentNullException.ThrowIfNull(headerSource);
return WithHeaders(
headerSource.ObjectToDictionary()!.ToDictionary(
u => u.Key.ToCultureString(culture ?? CultureInfo.InvariantCulture)!, u => u.Value), escape, culture,
comparer, replace);
u => u.Key.ToCultureString(culture ?? CultureInfo.InvariantCulture)!, u => u.Value), escape, replace,
culture);
}
/// <summary>
@@ -474,6 +465,7 @@ public sealed partial class HttpRequestBuilder
public HttpRequestBuilder SetTimeout(TimeSpan timeout)
{
Timeout = timeout;
TimeoutAction = null;
return this;
}
@@ -494,6 +486,43 @@ public sealed partial class HttpRequestBuilder
}
Timeout = TimeSpan.FromMilliseconds(timeoutMilliseconds);
TimeoutAction = null;
return this;
}
/// <summary>
/// 设置超时时间
/// </summary>
/// <param name="timeout">超时时间</param>
/// <param name="onTimeout">超时发生时要执行的操作</param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder SetTimeout(TimeSpan timeout, Action onTimeout)
{
// 空检查
ArgumentNullException.ThrowIfNull(onTimeout);
SetTimeout(timeout).TimeoutAction = onTimeout;
return this;
}
/// <summary>
/// 设置超时时间
/// </summary>
/// <param name="timeoutMilliseconds">超时时间(毫秒)</param>
/// <param name="onTimeout">超时发生时要执行的操作</param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder SetTimeout(double timeoutMilliseconds, Action onTimeout)
{
// 空检查
ArgumentNullException.ThrowIfNull(onTimeout);
SetTimeout(timeoutMilliseconds).TimeoutAction = onTimeout;
return this;
}
@@ -570,26 +599,22 @@ public sealed partial class HttpRequestBuilder
/// <param name="key">键</param>
/// <param name="value">值</param>
/// <param name="escape">是否转义字符串,默认 <c>false</c></param>
/// <param name="replace">是否替换已存在的查询参数。默认值为 <c>false</c></param>
/// <param name="ignoreNullValues">是否忽略空值。默认值为 <c>false</c></param>
/// <param name="culture">
/// <see cref="CultureInfo" />
/// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <param name="replace">是否替换已存在的查询参数。默认值为 <c>false</c>。</param>
/// <param name="ignoreNullValues">是否忽略空值。默认值为 <c>false</c>。</param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder WithQueryParameter(string key, object? value, bool escape = false,
CultureInfo? culture = null, IEqualityComparer<string>? comparer = null, bool replace = false,
bool ignoreNullValues = false)
public HttpRequestBuilder WithQueryParameter(string key, object? value, bool escape = false, bool replace = false,
bool ignoreNullValues = false, CultureInfo? culture = null)
{
// 空检查
ArgumentException.ThrowIfNullOrWhiteSpace(key);
return WithQueryParameters(new Dictionary<string, object?> { { key, value } }, escape, culture, comparer,
replace, ignoreNullValues);
return WithQueryParameters(new Dictionary<string, object?> { { key, value } }, escape, replace,
ignoreNullValues, culture);
}
/// <summary>
@@ -598,27 +623,23 @@ public sealed partial class HttpRequestBuilder
/// <remarks>支持多次调用。</remarks>
/// <param name="parameters">查询参数集合</param>
/// <param name="escape">是否转义字符串,默认 <c>false</c></param>
/// <param name="replace">是否替换已存在的查询参数。默认值为 <c>false</c></param>
/// <param name="ignoreNullValues">是否忽略空值。默认值为 <c>false</c></param>
/// <param name="culture">
/// <see cref="CultureInfo" />
/// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <param name="replace">是否替换已存在的查询参数。默认值为 <c>false</c>。</param>
/// <param name="ignoreNullValues">是否忽略空值。默认值为 <c>false</c>。</param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder WithQueryParameters(IDictionary<string, object?> parameters, bool escape = false,
CultureInfo? culture = null, IEqualityComparer<string>? comparer = null, bool replace = false,
bool ignoreNullValues = false)
bool replace = false, bool ignoreNullValues = false, CultureInfo? culture = null)
{
// 空检查
ArgumentNullException.ThrowIfNull(parameters);
// 初始化查询参数
QueryParameters ??= new Dictionary<string, List<string?>>(comparer);
var objectQueryParameters = new Dictionary<string, List<object?>>(comparer);
QueryParameters ??= new Dictionary<string, List<string?>>(StringComparer.OrdinalIgnoreCase);
var objectQueryParameters = new Dictionary<string, List<object?>>(StringComparer.OrdinalIgnoreCase);
// 存在则合并否则添加
objectQueryParameters.AddOrUpdate(QueryParameters.ToDictionary(u => u.Key, object? (u) => u.Value), false);
@@ -629,7 +650,7 @@ public sealed partial class HttpRequestBuilder
QueryParameters = objectQueryParameters.ToDictionary(kvp => kvp.Key,
kvp => kvp.Value.Select(u =>
u.ToCultureString(culture ?? CultureInfo.InvariantCulture)?.EscapeDataString(escape)).ToList(),
comparer);
StringComparer.OrdinalIgnoreCase);
return this;
}
@@ -641,20 +662,16 @@ public sealed partial class HttpRequestBuilder
/// <param name="parameterSource">查询参数集合</param>
/// <param name="prefix">参数前缀。对于对象类型可生成如 <c>prefix.Name=furion</c> 与 <c>prefix.Age=30</c> 参数格式。</param>
/// <param name="escape">是否转义字符串,默认 <c>false</c></param>
/// <param name="replace">是否替换已存在的查询参数。默认值为 <c>false</c></param>
/// <param name="ignoreNullValues">是否忽略空值。默认值为 <c>false</c></param>
/// <param name="culture">
/// <see cref="CultureInfo" />
/// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <param name="replace">是否替换已存在的查询参数。默认值为 <c>false</c>。</param>
/// <param name="ignoreNullValues">是否忽略空值。默认值为 <c>false</c>。</param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder WithQueryParameters(object parameterSource, string? prefix = null, bool escape = false,
CultureInfo? culture = null, IEqualityComparer<string>? comparer = null, bool replace = false,
bool ignoreNullValues = false)
bool replace = false, bool ignoreNullValues = false, CultureInfo? culture = null)
{
// 空检查
ArgumentNullException.ThrowIfNull(parameterSource);
@@ -663,7 +680,7 @@ public sealed partial class HttpRequestBuilder
parameterSource.ObjectToDictionary()!.ToDictionary(
u =>
$"{(string.IsNullOrWhiteSpace(prefix) ? null : $"{prefix}.")}{u.Key.ToCultureString(culture ?? CultureInfo.InvariantCulture)!}",
u => u.Value), escape, culture, comparer, replace, ignoreNullValues);
u => u.Value), escape, replace, ignoreNullValues, culture);
}
/// <summary>
@@ -709,19 +726,16 @@ public sealed partial class HttpRequestBuilder
/// <param name="culture">
/// <see cref="CultureInfo" />
/// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder WithPathParameter(string key, object? value, bool escape = false,
CultureInfo? culture = null, IEqualityComparer<string>? comparer = null)
CultureInfo? culture = null)
{
// 空检查
ArgumentException.ThrowIfNullOrWhiteSpace(key);
return WithPathParameters(new Dictionary<string, object?> { { key, value } }, escape, culture, comparer);
return WithPathParameters(new Dictionary<string, object?> { { key, value } }, escape, culture);
}
/// <summary>
@@ -733,26 +747,21 @@ public sealed partial class HttpRequestBuilder
/// <param name="culture">
/// <see cref="CultureInfo" />
/// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder WithPathParameters(IDictionary<string, object?> parameters,
bool escape = false,
CultureInfo? culture = null,
IEqualityComparer<string>? comparer = null)
public HttpRequestBuilder WithPathParameters(IDictionary<string, object?> parameters, bool escape = false,
CultureInfo? culture = null)
{
// 空检查
ArgumentNullException.ThrowIfNull(parameters);
PathParameters ??= new Dictionary<string, string?>(comparer);
PathParameters ??= new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
// 存在则更新否则添加
PathParameters.AddOrUpdate(parameters.ToDictionary(u => u.Key,
u => u.Value?.ToCultureString(culture ?? CultureInfo.InvariantCulture)?.EscapeDataString(escape),
comparer));
StringComparer.OrdinalIgnoreCase));
return this;
}
@@ -767,15 +776,11 @@ public sealed partial class HttpRequestBuilder
/// <param name="culture">
/// <see cref="CultureInfo" />
/// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder WithPathParameters(object? parameterSource, string? prefix = null, bool escape = false,
CultureInfo? culture = null,
IEqualityComparer<string>? comparer = null)
CultureInfo? culture = null)
{
// 检查是否设置了模板字符串前缀
if (string.IsNullOrWhiteSpace(prefix))
@@ -786,7 +791,7 @@ public sealed partial class HttpRequestBuilder
return WithPathParameters(
parameterSource.ObjectToDictionary()!.ToDictionary(
u => u.Key.ToCultureString(culture ?? CultureInfo.InvariantCulture)!, u => u.Value), escape,
culture, comparer);
culture);
}
ObjectPathParameters ??= new Dictionary<string, object?>();
@@ -823,19 +828,15 @@ public sealed partial class HttpRequestBuilder
/// <param name="culture">
/// <see cref="CultureInfo" />
/// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder WithCookie(string key, object? value, bool escape = false, CultureInfo? culture = null,
IEqualityComparer<string>? comparer = null)
public HttpRequestBuilder WithCookie(string key, object? value, bool escape = false, CultureInfo? culture = null)
{
// 空检查
ArgumentException.ThrowIfNullOrWhiteSpace(key);
return WithCookies(new Dictionary<string, object?> { { key, value } }, escape, culture, comparer);
return WithCookies(new Dictionary<string, object?> { { key, value } }, escape, culture);
}
/// <summary>
@@ -847,26 +848,21 @@ public sealed partial class HttpRequestBuilder
/// <param name="culture">
/// <see cref="CultureInfo" />
/// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder WithCookies(IDictionary<string, object?> cookies,
bool escape = false,
CultureInfo? culture = null,
IEqualityComparer<string>? comparer = null)
public HttpRequestBuilder WithCookies(IDictionary<string, object?> cookies, bool escape = false,
CultureInfo? culture = null)
{
// 空检查
ArgumentNullException.ThrowIfNull(cookies);
Cookies ??= new Dictionary<string, string?>(comparer);
Cookies ??= new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
// 存在则更新否则添加
Cookies.AddOrUpdate(cookies.ToDictionary(u => u.Key,
u => u.Value?.ToCultureString(culture ?? CultureInfo.InvariantCulture)?.EscapeDataString(escape),
comparer));
StringComparer.OrdinalIgnoreCase));
return this;
}
@@ -880,15 +876,10 @@ public sealed partial class HttpRequestBuilder
/// <param name="culture">
/// <see cref="CultureInfo" />
/// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder WithCookies(object cookieSource, bool escape = false,
CultureInfo? culture = null,
IEqualityComparer<string>? comparer = null)
public HttpRequestBuilder WithCookies(object cookieSource, bool escape = false, CultureInfo? culture = null)
{
// 空检查
ArgumentNullException.ThrowIfNull(cookieSource);
@@ -896,8 +887,7 @@ public sealed partial class HttpRequestBuilder
// 存在则更新否则添加
return WithCookies(
cookieSource.ObjectToDictionary()!.ToDictionary(
u => u.Key.ToCultureString(culture ?? CultureInfo.InvariantCulture)!, u => u.Value), escape, culture,
comparer);
u => u.Key.ToCultureString(culture ?? CultureInfo.InvariantCulture)!, u => u.Value), escape, culture);
}
/// <summary>
@@ -1193,6 +1183,17 @@ public sealed partial class HttpRequestBuilder
return this;
}
/// <summary>
/// 设置身份验证凭据请求授权标头
/// </summary>
/// <param name="scheme">身份验证的方案</param>
/// <param name="parameter">身份验证的凭证</param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder AddAuthentication(string scheme, string? parameter) =>
AddAuthentication(new AuthenticationHeaderValue(scheme, parameter));
/// <summary>
/// 设置身份验证凭据请求授权标头
/// </summary>
@@ -1328,6 +1329,17 @@ public sealed partial class HttpRequestBuilder
ReleaseDisposables();
}
/// <summary>
/// 设置请求来源地址
/// </summary>
/// <remarks>设置此配置后,将在单次请求标头中添加 <c>Referer</c> 标头。</remarks>
/// <param name="referer">请求来源地址,当设置为 <c>"{BASE_ADDRESS}"</c> 时将替换为基地址</param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder SetReferer(string? referer) =>
WithHeader(HeaderNames.Referer, referer, replace: true);
/// <summary>
/// 设置模拟浏览器环境
/// </summary>
@@ -1364,6 +1376,17 @@ public sealed partial class HttpRequestBuilder
public HttpRequestBuilder WithAnyStatusCodeHandler(Func<HttpResponseMessage, CancellationToken, Task> handler) =>
WithStatusCodeHandler(["*"], handler);
/// <summary>
/// 添加请求成功200-299状态码处理程序
/// </summary>
/// <param name="handler">自定义处理程序</param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder
WithSuccessStatusCodeHandler(Func<HttpResponseMessage, CancellationToken, Task> handler) =>
WithStatusCodeHandler("200-299", handler);
/// <summary>
/// 添加状态码处理程序
/// </summary>
@@ -1590,6 +1613,107 @@ public sealed partial class HttpRequestBuilder
? null
: new Uri(baseAddress, UriKind.RelativeOrAbsolute));
/// <summary>
/// 设置 HTTP 版本
/// </summary>
/// <param name="version">版本号</param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder SetVersion(string? version) =>
SetVersion(string.IsNullOrWhiteSpace(version) ? null : new Version(version));
/// <summary>
/// 设置 HTTP 版本
/// </summary>
/// <param name="version">
/// <see cref="Version" />
/// </param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder SetVersion(Version? version)
{
Version = version;
return this;
}
/// <summary>
/// 设置异常抑制
/// </summary>
/// <remarks>抑制所有异常。重复调用仅最后一次调用生效。</remarks>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder SuppressExceptions() => SuppressExceptions(true);
/// <summary>
/// 设置异常抑制
/// </summary>
/// <remarks>重复调用仅最后一次调用生效。</remarks>
/// <param name="enable">是否启用异常抑制。当设置为 <c>false</c> 时,将禁用异常抑制机制。</param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder SuppressExceptions(bool enable) => SuppressExceptions(enable ? [typeof(Exception)] : []);
/// <summary>
/// 设置是否移除默认的内容的 <c>Content-Type</c>
/// </summary>
/// <param name="omit">如果为 <c>true</c> 则移除,默认为 <c>false</c></param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder SetOmitContentType(bool omit)
{
OmitContentType = omit;
return this;
}
/// <summary>
/// 设置异常抑制
/// </summary>
/// <remarks>重复调用仅最后一次调用生效。</remarks>
/// <param name="exceptionTypes">异常抑制类型集合</param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
/// <exception cref="ArgumentException"></exception>
public HttpRequestBuilder SuppressExceptions(Type[] exceptionTypes)
{
// 空检查
ArgumentNullException.ThrowIfNull(exceptionTypes);
// 检查是否包含 null 或者不是 Exception 类型的元素
if (exceptionTypes.Any(u => (Type?)u is null || !typeof(Exception).IsAssignableFrom(u)))
{
throw new ArgumentException(
"All elements in exceptionTypes must be non-null and assignable to System.Exception.");
}
// 释放引用(无关紧要)
SuppressExceptionTypes = null;
// 空检查
if (exceptionTypes.Length == 0)
{
return this;
}
// 确保每次都能覆盖
SuppressExceptionTypes = [];
// 遍历异常抑制类型集合逐条追加
foreach (var exceptionType in exceptionTypes)
{
SuppressExceptionTypes.Add(exceptionType);
}
return this;
}
/// <summary>
/// 释放可释放的对象集合
/// </summary>

View File

@@ -157,6 +157,11 @@ public sealed partial class HttpRequestBuilder
/// </summary>
public Uri? BaseAddress { get; private set; }
/// <summary>
/// HTTP 版本
/// </summary>
public Version? Version { get; private set; }
/// <summary>
/// <see cref="HttpClient" /> 实例提供器
/// </summary>
@@ -181,7 +186,7 @@ public sealed partial class HttpRequestBuilder
/// <summary>
/// 用于处理在设置 <see cref="HttpRequestMessage" /> 的请求消息的内容时的操作
/// </summary>
public Action<HttpContent?>? OnPreSetContent { get; private set; }
public Action<HttpContent>? OnPreSetContent { get; private set; }
/// <summary>
/// 用于处理在发送 HTTP 请求之前的操作
@@ -201,7 +206,13 @@ public sealed partial class HttpRequestBuilder
/// <summary>
/// <inheritdoc cref="HttpMultipartFormDataBuilder" />
/// </summary>
internal HttpMultipartFormDataBuilder? MultipartFormDataBuilder { get; private set; }
public HttpMultipartFormDataBuilder? MultipartFormDataBuilder { get; private set; }
/// <summary>
/// 是否移除默认的内容的 <c>Content-Type</c>
/// </summary>
/// <remarks>默认值为:<c>false</c>。</remarks>
public bool OmitContentType { get; private set; }
/// <summary>
/// 如果 HTTP 响应的 <c>IsSuccessStatusCode</c> 属性是 <c>false</c>,则引发异常。
@@ -273,4 +284,15 @@ public sealed partial class HttpRequestBuilder
get;
private set;
}
/// <summary>
/// 异常抑制类型集合
/// </summary>
/// <remarks>当配置了异常抑制类型集合后,框架将抑制(即不抛出)该集合中匹配的异常类型。</remarks>
internal HashSet<Type>? SuppressExceptionTypes { get; private set; }
/// <summary>
/// 超时发生时要执行的操作
/// </summary>
internal Action? TimeoutAction { get; private set; }
}

View File

@@ -10,6 +10,9 @@
// ------------------------------------------------------------------------
using System.Reflection;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace ThingsGateway.HttpRemote;
@@ -614,4 +617,116 @@ public sealed partial class HttpRequestBuilder
/// </returns>
public static HttpDeclarativeBuilder Declarative(MethodInfo method, object?[] args) =>
new(method, args);
/// <summary>
/// 从 JSON 中创建 <see cref="HttpRequestBuilder" /> 实例
/// </summary>
/// <param name="json">JSON 字符串</param>
/// <param name="configure">自定义配置委托</param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
/// <exception cref="ArgumentException"></exception>
/// <exception cref="InvalidOperationException"></exception>
public static HttpRequestBuilder FromJson(string json, Action<HttpRequestBuilder>? configure = null)
{
// 空检查
ArgumentException.ThrowIfNullOrWhiteSpace(json);
/*
* 手动解析 JSON 字符串
*
* 不采用 JSON 反序列化的原因如下:
* 1. HttpRequestBuilder 的属性设计为只读,无法直接通过反序列化赋值。
* 2. 避免引入 [JsonInclude] 特性对 System.Text.Json 的强耦合,保持依赖解耦。
* 3. 简化 JSON 字符串的结构定义,无需严格遵循 HttpRequestBuilder 的属性定义,从而省略 [JsonPropertyName] 等自定义映射。
* 4. 精确控制需要解析的键,减少不必要的自定义 JsonConverter 操作,提升性能与可维护性。
*/
var jsonObject = JsonNode.Parse(json, new JsonNodeOptions { PropertyNameCaseInsensitive = true },
new JsonDocumentOptions { AllowTrailingCommas = true })?.AsObject();
// 空检查
ArgumentNullException.ThrowIfNull(jsonObject);
// 验证必填字段
if (!jsonObject.TryGetPropertyValue("method", out var methodNode) || methodNode is not JsonValue methodValue)
{
throw new ArgumentException("Missing required `method` in JSON.");
}
// 允许 "url" 为 null但必须定义
if (!jsonObject.ContainsKey("url"))
{
throw new ArgumentException("Missing required `url` in JSON.");
}
// 初始化 HttpRequestBuilder 实例
var httpRequestBuilder = Create(methodValue.ToString(), jsonObject["url"]?.GetValue<string?>());
// 处理可选字段
HandleJsonNode(jsonObject, "baseAddress", node => httpRequestBuilder.SetBaseAddress(node.GetValue<string>()));
HandleJsonNode(jsonObject, "headers", node => httpRequestBuilder.WithHeaders(node));
HandleJsonNode(jsonObject, "queries", node => httpRequestBuilder.WithQueryParameters(node));
HandleJsonNode(jsonObject, "cookies", node => httpRequestBuilder.WithCookies(node));
HandleJsonNode(jsonObject, "timeout", node => httpRequestBuilder.SetTimeout(node.GetValue<double>()));
HandleJsonNode(jsonObject, "client", node => httpRequestBuilder.SetHttpClientName(node.GetValue<string?>()));
HandleJsonNode(jsonObject, "profiler", node => httpRequestBuilder.Profiler(node.GetValue<bool>()));
// 处理请求内容
if (jsonObject.TryGetPropertyValue("data", out var dataNode))
{
// "data" 和 "contentType" 必须同时存在或同时不存在
if (!jsonObject.TryGetPropertyValue("contentType", out var contentTypeNode) ||
contentTypeNode is not JsonValue contentTypeValue)
{
throw new InvalidOperationException("The `contentType` key is required when `data` is present.");
}
// 设置请求内容
httpRequestBuilder
.SetContent(
dataNode?.ToJsonString(new JsonSerializerOptions
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
}), contentTypeValue.ToString()).AddStringContentForFormUrlEncodedContentProcessor();
// 设置内容编码
HandleJsonNode(jsonObject, "encoding",
node => httpRequestBuilder.SetContentEncoding(node.GetValue<string>()));
}
// 处理多部分表单
if (jsonObject.TryGetPropertyValue("multipart", out var multipartNode))
{
// 设置多部分表单内容
httpRequestBuilder.SetMultipartContent(multipart => multipart.AddJson(multipartNode?.AsObject()
.ToJsonString(new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping })));
}
// 调用自定义配置委托
configure?.Invoke(httpRequestBuilder);
return httpRequestBuilder;
}
/// <summary>
/// 处理 <see cref="JsonNode" />
/// </summary>
/// <param name="jsonObject">
/// <see cref="JsonObject" />
/// </param>
/// <param name="propertyName">属性名</param>
/// <param name="action">自定义操作</param>
internal static void HandleJsonNode(JsonObject jsonObject, string propertyName, Action<JsonNode> action)
{
// 空检查
ArgumentNullException.ThrowIfNull(jsonObject);
ArgumentNullException.ThrowIfNull(propertyName);
ArgumentNullException.ThrowIfNull(action);
if (jsonObject.TryGetPropertyValue(propertyName, out var node) && node is not null)
{
action(node);
}
}
}

View File

@@ -76,6 +76,12 @@ public sealed partial class HttpRequestBuilder
// 初始化 HttpRequestMessage 实例
var httpRequestMessage = new HttpRequestMessage(HttpMethod, finalRequestUri);
// 设置 HTTP 版本
if (Version is not null)
{
httpRequestMessage.Version = Version;
}
// 启用性能优化
EnablePerformanceOptimization(httpRequestMessage);
@@ -160,18 +166,44 @@ public sealed partial class HttpRequestBuilder
/// </param>
internal void AppendPathSegments(UriBuilder uriBuilder)
{
// 空检查
if ((PathSegments == null || PathSegments.Count == 0) &&
(PathSegmentsToRemove == null || PathSegmentsToRemove.Count == 0))
{
return;
}
// 记录原路径是否以斜杠结尾(修复核心逻辑)
var originalPath = uriBuilder.Uri.AbsolutePath;
var endsWithSlash = originalPath.Length > 1 && originalPath.EndsWith('/');
// 解析 URL 中的路径片段列表
var pathSegments = uriBuilder.Path.Split('/', StringSplitOptions.RemoveEmptyEntries).Concat([]);
var pathSegments = uriBuilder.Path.Split('/', StringSplitOptions.RemoveEmptyEntries);
// 追加路径片段
pathSegments = pathSegments.Concat(PathSegments.ConcatIgnoreNull([]).Where(u => !string.IsNullOrWhiteSpace(u))
.Select(u => u.TrimStart('/').TrimEnd('/')));
// 追加并处理新路径片段
var newPathSegments = pathSegments.Concat(PathSegments.ConcatIgnoreNull([])
.Where(u => !string.IsNullOrWhiteSpace(u)).Select(u => u.TrimStart('/').TrimEnd('/')));
// 构建路径片段赋值给 UriBuilder 的 Path 属性
uriBuilder.Path = '/' + string.Join('/',
// 过滤已标记为移除的路径片段
pathSegments.WhereIf(PathSegmentsToRemove is { Count: > 0 },
u => PathSegmentsToRemove?.TryGetValue(u, out _) == false));
// 过滤需要移除的路径片段
var filteredSegments = newPathSegments.WhereIf(PathSegmentsToRemove is { Count: > 0 },
u => PathSegmentsToRemove?.Contains(u) == false).ToArray();
// 构建最终路径
if (filteredSegments.Length != 0)
{
uriBuilder.Path = $"/{string.Join('/', filteredSegments)}";
// 恢复原路径的结尾斜杠(当存在路径片段时)
if (endsWithSlash)
{
uriBuilder.Path += "/";
}
}
// 没有路径片段时设置为根路径
else
{
uriBuilder.Path = "/";
}
}
/// <summary>
@@ -182,6 +214,13 @@ public sealed partial class HttpRequestBuilder
/// </param>
internal void AppendQueryParameters(UriBuilder uriBuilder)
{
// 空检查
if ((QueryParameters is null || QueryParameters.Count == 0) &&
(QueryParametersToRemove is null || QueryParametersToRemove.Count == 0))
{
return;
}
// 解析 URL 中的查询字符串为键值对列表
var queryParameters = uriBuilder.Query.ParseFormatKeyValueString(['&'], '?');
@@ -300,6 +339,16 @@ public sealed partial class HttpRequestBuilder
// 遍历请求标头集合并追加到 HttpRequestMessage.Headers 中
foreach (var (key, values) in Headers)
{
// 替换 Referer 标头的 "{BASE_ADDRESS}" 模板字符串
if (key.IsIn([HeaderNames.Referer], StringComparer.OrdinalIgnoreCase) &&
values.FirstOrDefault() == Constants.REFERER_HEADER_BASE_ADDRESS_TEMPLATE)
{
httpRequestMessage.Headers.Referrer = new Uri(
$"{httpRequestMessage.RequestUri?.Scheme}://{httpRequestMessage.RequestUri?.Host}{(httpRequestMessage.RequestUri?.IsDefaultPort != true ? $":{httpRequestMessage.RequestUri?.Port}" : string.Empty)}",
UriKind.RelativeOrAbsolute);
continue;
}
httpRequestMessage.Headers.TryAddWithoutValidation(key, values);
}
}
@@ -486,6 +535,18 @@ public sealed partial class HttpRequestBuilder
// 构建 HttpContent 实例
var httpContent = httpContentProcessorFactory.Build(RawContent, ContentType!, ContentEncoding, processors);
// 空检查
if (httpContent is null)
{
return;
}
// 检查是否移除默认的内容的 Content-Type解决对接 Java 程序时可能出现失败问题
if (OmitContentType)
{
httpContent.Headers.ContentType = null;
}
// 调用用于处理在设置请求消息的内容时的操作
OnPreSetContent?.Invoke(httpContent);
@@ -513,6 +574,9 @@ public sealed partial class HttpRequestBuilder
{
httpRequestMessage.Options.AddOrUpdate(Constants.DISABLED_PROFILER_KEY, "TRUE");
}
// 添加 HttpClient 实例的配置名称
httpRequestMessage.Options.AddOrUpdate(Constants.HTTP_CLIENT_NAME, HttpClientName ?? string.Empty);
}
/// <summary>

View File

@@ -88,15 +88,26 @@ internal static class Constants
/// <remarks>被用于从 <see cref="HttpRequestMessage" /> 的 <c>Options</c> 属性中读取。</remarks>
internal const string DECLARATIVE_METHOD_KEY = "__DECLARATIVE_METHOD__";
/// <summary>
/// HTTP 请求 <see cref="HttpClient" /> 实例的配置名称键
/// </summary>
/// <remarks>被用于从 <see cref="HttpRequestMessage" /> 的 <c>Options</c> 属性中读取。</remarks>
internal const string HTTP_CLIENT_NAME = "__HTTP_CLIENT_NAME__";
/// <summary>
/// 浏览器的 <c>User-Agent</c> 标头值
/// </summary>
internal const string USER_AGENT_OF_BROWSER =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0";
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 Edg/135.0.0.0";
/// <summary>
/// 移动端浏览器的 <c>User-Agent</c> 标头值
/// </summary>
internal const string USER_AGENT_OF_MOBILE_BROWSER =
"Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Mobile Safari/537.36 Edg/133.0.0.0";
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Mobile Safari/537.36 Edg/135.0.0.0";
/// <summary>
/// <c>Referer</c> 标头请求基地址模板
/// </summary>
internal const string REFERER_HEADER_BASE_ADDRESS_TEMPLATE = "{BASE_ADDRESS}";
}

View File

@@ -13,6 +13,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using System.Net.Http.Json;
using System.Text.Json;
namespace ThingsGateway.HttpRemote;
@@ -27,16 +28,45 @@ public class ObjectContentConverter : IHttpContentConverter
/// <inheritdoc />
public virtual object? Read(Type resultType, HttpResponseMessage httpResponseMessage,
CancellationToken cancellationToken = default) =>
httpResponseMessage.Content.ReadFromJsonAsync(resultType,
ServiceProvider?.GetRequiredService<IOptions<HttpRemoteOptions>>().Value.JsonSerializerOptions ??
HttpRemoteOptions.JsonSerializerOptionsDefault, cancellationToken).GetAwaiter().GetResult();
httpResponseMessage.Content
.ReadFromJsonAsync(resultType, GetJsonSerializerOptions(httpResponseMessage), cancellationToken)
.GetAwaiter().GetResult();
/// <inheritdoc />
public virtual async Task<object?> ReadAsync(Type resultType, HttpResponseMessage httpResponseMessage,
CancellationToken cancellationToken = default) =>
await httpResponseMessage.Content.ReadFromJsonAsync(resultType,
ServiceProvider?.GetRequiredService<IOptions<HttpRemoteOptions>>().Value.JsonSerializerOptions ??
HttpRemoteOptions.JsonSerializerOptionsDefault, cancellationToken).ConfigureAwait(false);
await httpResponseMessage.Content.ReadFromJsonAsync(resultType, GetJsonSerializerOptions(httpResponseMessage),
cancellationToken).ConfigureAwait(false);
/// <summary>
/// 获取 JSON 序列化选项实例
/// </summary>
/// <param name="httpResponseMessage">
/// <see cref="HttpResponseMessage" />
/// </param>
/// <returns>
/// <see cref="JsonSerializerOptions" />
/// </returns>
protected virtual JsonSerializerOptions GetJsonSerializerOptions(HttpResponseMessage httpResponseMessage)
{
// 空检查
ArgumentNullException.ThrowIfNull(httpResponseMessage);
// 获取 HttpClient 实例的配置名称
if (httpResponseMessage.RequestMessage?.Options.TryGetValue(
new HttpRequestOptionsKey<string>(Constants.HTTP_CLIENT_NAME), out var httpClientName) != true)
{
httpClientName = string.Empty;
}
// 获取 HttpClientOptions 实例
var httpClientOptions = ServiceProvider?.GetService<IOptionsMonitor<HttpClientOptions>>()?.Get(httpClientName);
// 优先级:指定名称的 HttpClientOptions -> HttpRemoteOptions -> 默认值
return (httpClientOptions?.IsDefault != false ? null : httpClientOptions.JsonSerializerOptions) ??
ServiceProvider?.GetRequiredService<IOptions<HttpRemoteOptions>>().Value.JsonSerializerOptions ??
HttpRemoteOptions.JsonSerializerOptionsDefault;
}
}
/// <summary>
@@ -48,14 +78,13 @@ public class ObjectContentConverter<TResult> : ObjectContentConverter, IHttpCont
/// <inheritdoc />
public virtual TResult? Read(HttpResponseMessage httpResponseMessage,
CancellationToken cancellationToken = default) =>
httpResponseMessage.Content.ReadFromJsonAsync<TResult>(
ServiceProvider?.GetRequiredService<IOptions<HttpRemoteOptions>>().Value.JsonSerializerOptions ??
HttpRemoteOptions.JsonSerializerOptionsDefault, cancellationToken).GetAwaiter().GetResult();
httpResponseMessage.Content
.ReadFromJsonAsync<TResult>(GetJsonSerializerOptions(httpResponseMessage), cancellationToken).GetAwaiter()
.GetResult();
/// <inheritdoc />
public virtual async Task<TResult?> ReadAsync(HttpResponseMessage httpResponseMessage,
CancellationToken cancellationToken = default) =>
await httpResponseMessage.Content.ReadFromJsonAsync<TResult>(
ServiceProvider?.GetRequiredService<IOptions<HttpRemoteOptions>>().Value.JsonSerializerOptions ??
HttpRemoteOptions.JsonSerializerOptionsDefault, cancellationToken).ConfigureAwait(false);
await httpResponseMessage.Content.ReadFromJsonAsync<TResult>(GetJsonSerializerOptions(httpResponseMessage),
cancellationToken).ConfigureAwait(false);
}

View File

@@ -18,10 +18,10 @@ public class VoidContentConverter : HttpContentConverterBase<VoidContent>
{
/// <inheritdoc />
public override VoidContent? Read(HttpResponseMessage httpResponseMessage,
CancellationToken cancellationToken = default) => default;
CancellationToken cancellationToken = default) => null;
/// <inheritdoc />
public override Task<VoidContent?> ReadAsync(HttpResponseMessage httpResponseMessage,
CancellationToken cancellationToken = default) =>
Task.FromResult<VoidContent?>(default);
Task.FromResult<VoidContent?>(null);
}

View File

@@ -0,0 +1,30 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
namespace ThingsGateway.HttpRemote;
/// <summary>
/// HTTP 声明式 HTTP 版本特性
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Interface)]
public sealed class HttpVersionAttribute : Attribute
{
/// <summary>
/// <inheritdoc cref="HttpVersionAttribute" />
/// </summary>
/// <param name="version">HTTP 版本</param>
public HttpVersionAttribute(string? version) => Version = version;
/// <summary>
/// HTTP 版本
/// </summary>
public string? Version { get; set; }
}

View File

@@ -0,0 +1,30 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
namespace ThingsGateway.HttpRemote;
/// <summary>
/// HTTP 声明式请求来源地址特性
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Interface)]
public sealed class RefererAttribute : Attribute
{
/// <summary>
/// <inheritdoc cref="RefererAttribute" />
/// </summary>
/// <param name="referer">请求来源地址,当设置为 <c>"{BASE_ADDRESS}"</c> 时将替换为基地址</param>
public RefererAttribute(string? referer) => Referer = referer;
/// <summary>
/// 请求来源地址,当设置为 <c>"{BASE_ADDRESS}"</c> 时将替换为基地址
/// </summary>
public string? Referer { get; set; }
}

View File

@@ -0,0 +1,45 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
namespace ThingsGateway.HttpRemote;
/// <summary>
/// HTTP 声明式异常抑制特性
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Interface)]
public sealed class SuppressExceptionsAttribute : Attribute
{
/// <summary>
/// <inheritdoc cref="SuppressExceptionsAttribute" />
/// </summary>
/// <remarks>抑制所有异常。</remarks>
public SuppressExceptionsAttribute()
: this(true)
{
}
/// <summary>
/// <inheritdoc cref="SuppressExceptionsAttribute" />
/// </summary>
/// <param name="enabled">是否启用异常抑制。当设置为 <c>false</c> 时,将禁用异常抑制机制。</param>
public SuppressExceptionsAttribute(bool enabled) => Types = enabled ? [typeof(Exception)] : [];
/// <summary>
/// <inheritdoc cref="SuppressExceptionsAttribute" />
/// </summary>
/// <param name="types">异常抑制类型集合</param>
public SuppressExceptionsAttribute(params Type[] types) => Types = types;
/// <summary>
/// 异常抑制类型集合
/// </summary>
public Type[] Types { get; set; }
}

View File

@@ -44,8 +44,11 @@ public sealed class HttpDeclarativeBuilder
new(typeof(QueryDeclarativeExtractor), new QueryDeclarativeExtractor()),
new(typeof(PathDeclarativeExtractor), new PathDeclarativeExtractor()),
new(typeof(CookieDeclarativeExtractor), new CookieDeclarativeExtractor()),
new(typeof(RefererDeclarativeExtractor), new RefererDeclarativeExtractor()),
new(typeof(HeaderDeclarativeExtractor), new HeaderDeclarativeExtractor()),
new(typeof(PropertyDeclarativeExtractor), new PropertyDeclarativeExtractor()),
new(typeof(HttpVersionDeclarativeExtractor), new HttpVersionDeclarativeExtractor()),
new(typeof(SuppressExceptionsDeclarativeExtractor), new SuppressExceptionsDeclarativeExtractor()),
new(typeof(BodyDeclarativeExtractor), new BodyDeclarativeExtractor())
]);

View File

@@ -45,7 +45,7 @@ internal sealed class HeaderDeclarativeExtractor : IHttpDeclarativeExtractor
if (headerAttribute.HasSetValue)
{
httpRequestBuilder.WithHeader(headerName, headerAttribute.Value, headerAttribute.Escape,
replace: headerAttribute.Replace);
headerAttribute.Replace);
}
// 移除请求标头
else
@@ -91,7 +91,7 @@ internal sealed class HeaderDeclarativeExtractor : IHttpDeclarativeExtractor
if (parameter.ParameterType.IsBaseTypeOrEnumOrCollection())
{
httpRequestBuilder.WithHeader(parameterName, value ?? headerAttribute.Value,
headerAttribute.Escape, replace: headerAttribute.Replace);
headerAttribute.Escape, headerAttribute.Replace);
continue;
}
@@ -99,7 +99,7 @@ internal sealed class HeaderDeclarativeExtractor : IHttpDeclarativeExtractor
// 空检查
if (value is not null)
{
httpRequestBuilder.WithHeaders(value, headerAttribute.Escape, replace: headerAttribute.Replace);
httpRequestBuilder.WithHeaders(value, headerAttribute.Escape, headerAttribute.Replace);
}
}
}

View File

@@ -0,0 +1,31 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
namespace ThingsGateway.HttpRemote;
/// <summary>
/// HTTP 声明式 <see cref="HttpVersionAttribute" /> 特性提取器
/// </summary>
internal sealed class HttpVersionDeclarativeExtractor : IHttpDeclarativeExtractor
{
/// <inheritdoc />
public void Extract(HttpRequestBuilder httpRequestBuilder, HttpDeclarativeExtractorContext context)
{
// 检查方法或接口是否贴有 [HttpVersion] 特性
if (!context.IsMethodDefined<HttpVersionAttribute>(out var versionAttribute, true))
{
return;
}
// 设置 HTTP 版本
httpRequestBuilder.SetVersion(versionAttribute.Version);
}
}

View File

@@ -45,7 +45,7 @@ internal sealed class QueryDeclarativeExtractor : IHttpDeclarativeExtractor
if (queryAttribute.HasSetValue)
{
httpRequestBuilder.WithQueryParameter(queryName, queryAttribute.Value, queryAttribute.Escape,
replace: queryAttribute.Replace, ignoreNullValues: queryAttribute.IgnoreNullValues);
queryAttribute.Replace, queryAttribute.IgnoreNullValues);
}
// 移除查询参数
else
@@ -91,8 +91,7 @@ internal sealed class QueryDeclarativeExtractor : IHttpDeclarativeExtractor
if (parameter.ParameterType.IsBaseTypeOrEnumOrCollection())
{
httpRequestBuilder.WithQueryParameter(parameterName, value ?? queryAttribute.Value,
queryAttribute.Escape, replace: queryAttribute.Replace,
ignoreNullValues: queryAttribute.IgnoreNullValues);
queryAttribute.Escape, queryAttribute.Replace, queryAttribute.IgnoreNullValues);
continue;
}
@@ -101,7 +100,7 @@ internal sealed class QueryDeclarativeExtractor : IHttpDeclarativeExtractor
if (value is not null)
{
httpRequestBuilder.WithQueryParameters(value, queryAttribute.Prefix, queryAttribute.Escape,
replace: queryAttribute.Replace, ignoreNullValues: queryAttribute.IgnoreNullValues);
queryAttribute.Replace, queryAttribute.IgnoreNullValues);
}
}
}

View File

@@ -0,0 +1,31 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
namespace ThingsGateway.HttpRemote;
/// <summary>
/// HTTP 声明式 <see cref="RefererAttribute" /> 特性提取器
/// </summary>
internal sealed class RefererDeclarativeExtractor : IHttpDeclarativeExtractor
{
/// <inheritdoc />
public void Extract(HttpRequestBuilder httpRequestBuilder, HttpDeclarativeExtractorContext context)
{
// 检查方法或接口是否贴有 [Referer] 特性
if (!context.IsMethodDefined<RefererAttribute>(out var refererAttribute, true))
{
return;
}
// 设置请求来源地址
httpRequestBuilder.SetReferer(refererAttribute.Referer);
}
}

View File

@@ -0,0 +1,31 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
namespace ThingsGateway.HttpRemote;
/// <summary>
/// HTTP 声明式 <see cref="SuppressExceptionsAttribute" /> 特性提取器
/// </summary>
internal sealed class SuppressExceptionsDeclarativeExtractor : IHttpDeclarativeExtractor
{
/// <inheritdoc />
public void Extract(HttpRequestBuilder httpRequestBuilder, HttpDeclarativeExtractorContext context)
{
// 检查方法或接口是否贴有 [SuppressExceptions] 特性
if (!context.IsMethodDefined<SuppressExceptionsAttribute>(out var suppressExceptionsAttribute, true))
{
return;
}
// 设置异常抑制
httpRequestBuilder.SuppressExceptions(suppressExceptionsAttribute.Types);
}
}

View File

@@ -25,7 +25,7 @@ public sealed class HttpDeclarativeExtractorContext
/// 冻结参数类型集合
/// </summary>
/// <remarks>此类参数类型不应作为外部提取对象。</remarks>
internal static Type[] _frozenParameterTypes =
internal static readonly Type[] _frozenParameterTypes =
[
typeof(Action<HttpRequestBuilder>), typeof(Action<HttpMultipartFormDataBuilder>), typeof(HttpCompletionOption),
typeof(CancellationToken)

View File

@@ -42,7 +42,7 @@ public static partial class HttpContextExtensions
/// </item>
/// </list>
/// </remarks>
internal static HashSet<string> _ignoreResponseHeaders =
internal static readonly HashSet<string> _ignoreResponseHeaders =
[
"Content-Type", "Connection", "Transfer-Encoding", "Keep-Alive", "Upgrade", "Proxy-Connection"
];
@@ -64,7 +64,7 @@ public static partial class HttpContextExtensions
/// <returns>
/// <see cref="HttpResponseMessage" />
/// </returns>
public static HttpResponseMessage Forward(this HttpContext? httpContext, string? requestUri = null,
public static HttpResponseMessage? Forward(this HttpContext? httpContext, string? requestUri = null,
Action<HttpRequestBuilder>? configure = null,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
HttpContextForwardOptions? forwardOptions = null) =>
@@ -90,7 +90,7 @@ public static partial class HttpContextExtensions
/// <returns>
/// <see cref="HttpResponseMessage" />
/// </returns>
public static HttpResponseMessage Forward(this HttpContext? httpContext, HttpMethod httpMethod,
public static HttpResponseMessage? Forward(this HttpContext? httpContext, HttpMethod httpMethod,
string? requestUri = null, Action<HttpRequestBuilder>? configure = null,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
HttpContextForwardOptions? forwardOptions = null) =>
@@ -115,7 +115,7 @@ public static partial class HttpContextExtensions
/// <returns>
/// <see cref="HttpResponseMessage" />
/// </returns>
public static HttpResponseMessage Forward(this HttpContext? httpContext, Uri? requestUri = null,
public static HttpResponseMessage? Forward(this HttpContext? httpContext, Uri? requestUri = null,
Action<HttpRequestBuilder>? configure = null,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
HttpContextForwardOptions? forwardOptions = null) =>
@@ -140,7 +140,7 @@ public static partial class HttpContextExtensions
/// <returns>
/// <see cref="HttpResponseMessage" />
/// </returns>
public static HttpResponseMessage Forward(this HttpContext? httpContext, HttpMethod httpMethod,
public static HttpResponseMessage? Forward(this HttpContext? httpContext, HttpMethod httpMethod,
Uri? requestUri = null, Action<HttpRequestBuilder>? configure = null,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
HttpContextForwardOptions? forwardOptions = null)
@@ -180,7 +180,7 @@ public static partial class HttpContextExtensions
/// <returns>
/// <see cref="HttpResponseMessage" />
/// </returns>
public static Task<HttpResponseMessage> ForwardAsync(this HttpContext? httpContext, string? requestUri = null,
public static Task<HttpResponseMessage?> ForwardAsync(this HttpContext? httpContext, string? requestUri = null,
Action<HttpRequestBuilder>? configure = null,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
HttpContextForwardOptions? forwardOptions = null) =>
@@ -206,7 +206,7 @@ public static partial class HttpContextExtensions
/// <returns>
/// <see cref="HttpResponseMessage" />
/// </returns>
public static Task<HttpResponseMessage> ForwardAsync(this HttpContext? httpContext, HttpMethod httpMethod,
public static Task<HttpResponseMessage?> ForwardAsync(this HttpContext? httpContext, HttpMethod httpMethod,
string? requestUri = null, Action<HttpRequestBuilder>? configure = null,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
HttpContextForwardOptions? forwardOptions = null) =>
@@ -231,7 +231,7 @@ public static partial class HttpContextExtensions
/// <returns>
/// <see cref="HttpResponseMessage" />
/// </returns>
public static Task<HttpResponseMessage> ForwardAsync(this HttpContext? httpContext, Uri? requestUri = null,
public static Task<HttpResponseMessage?> ForwardAsync(this HttpContext? httpContext, Uri? requestUri = null,
Action<HttpRequestBuilder>? configure = null,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
HttpContextForwardOptions? forwardOptions = null) =>
@@ -256,7 +256,7 @@ public static partial class HttpContextExtensions
/// <returns>
/// <see cref="HttpResponseMessage" />
/// </returns>
public static async Task<HttpResponseMessage> ForwardAsync(this HttpContext? httpContext, HttpMethod httpMethod,
public static async Task<HttpResponseMessage?> ForwardAsync(this HttpContext? httpContext, HttpMethod httpMethod,
Uri? requestUri = null, Action<HttpRequestBuilder>? configure = null,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
HttpContextForwardOptions? forwardOptions = null)
@@ -297,7 +297,7 @@ public static partial class HttpContextExtensions
/// <returns>
/// <see cref="HttpRemoteResult{TResult}" />
/// </returns>
public static HttpRemoteResult<TResult> Forward<TResult>(this HttpContext? httpContext, string? requestUri = null,
public static HttpRemoteResult<TResult>? Forward<TResult>(this HttpContext? httpContext, string? requestUri = null,
Action<HttpRequestBuilder>? configure = null,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
HttpContextForwardOptions? forwardOptions = null) =>
@@ -324,7 +324,7 @@ public static partial class HttpContextExtensions
/// <returns>
/// <see cref="HttpRemoteResult{TResult}" />
/// </returns>
public static HttpRemoteResult<TResult> Forward<TResult>(this HttpContext? httpContext, HttpMethod httpMethod,
public static HttpRemoteResult<TResult>? Forward<TResult>(this HttpContext? httpContext, HttpMethod httpMethod,
string? requestUri = null, Action<HttpRequestBuilder>? configure = null,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
HttpContextForwardOptions? forwardOptions = null) =>
@@ -350,7 +350,7 @@ public static partial class HttpContextExtensions
/// <returns>
/// <see cref="HttpRemoteResult{TResult}" />
/// </returns>
public static HttpRemoteResult<TResult> Forward<TResult>(this HttpContext? httpContext, Uri? requestUri = null,
public static HttpRemoteResult<TResult>? Forward<TResult>(this HttpContext? httpContext, Uri? requestUri = null,
Action<HttpRequestBuilder>? configure = null,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
HttpContextForwardOptions? forwardOptions = null) =>
@@ -376,7 +376,7 @@ public static partial class HttpContextExtensions
/// <returns>
/// <see cref="HttpRemoteResult{TResult}" />
/// </returns>
public static HttpRemoteResult<TResult> Forward<TResult>(this HttpContext? httpContext, HttpMethod httpMethod,
public static HttpRemoteResult<TResult>? Forward<TResult>(this HttpContext? httpContext, HttpMethod httpMethod,
Uri? requestUri = null, Action<HttpRequestBuilder>? configure = null,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
HttpContextForwardOptions? forwardOptions = null)
@@ -393,7 +393,7 @@ public static partial class HttpContextExtensions
var result = httpRemoteService.Send<TResult>(httpRequestBuilder, completionOption, httpContext.RequestAborted);
// 根据配置选项将 HttpResponseMessage 信息转发到 HttpContext 中
ForwardResponseMessage(httpContext, result.ResponseMessage, httpContextForwardBuilder.ForwardOptions);
ForwardResponseMessage(httpContext, result?.ResponseMessage, httpContextForwardBuilder.ForwardOptions);
return result;
}
@@ -416,7 +416,7 @@ public static partial class HttpContextExtensions
/// <returns>
/// <see cref="HttpRemoteResult{TResult}" />
/// </returns>
public static Task<HttpRemoteResult<TResult>> ForwardAsync<TResult>(this HttpContext? httpContext,
public static Task<HttpRemoteResult<TResult>?> ForwardAsync<TResult>(this HttpContext? httpContext,
string? requestUri = null, Action<HttpRequestBuilder>? configure = null,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
HttpContextForwardOptions? forwardOptions = null) =>
@@ -443,7 +443,7 @@ public static partial class HttpContextExtensions
/// <returns>
/// <see cref="HttpRemoteResult{TResult}" />
/// </returns>
public static Task<HttpRemoteResult<TResult>> ForwardAsync<TResult>(this HttpContext? httpContext,
public static Task<HttpRemoteResult<TResult>?> ForwardAsync<TResult>(this HttpContext? httpContext,
HttpMethod httpMethod, string? requestUri = null, Action<HttpRequestBuilder>? configure = null,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
HttpContextForwardOptions? forwardOptions = null) =>
@@ -469,7 +469,7 @@ public static partial class HttpContextExtensions
/// <returns>
/// <see cref="HttpRemoteResult{TResult}" />
/// </returns>
public static Task<HttpRemoteResult<TResult>> ForwardAsync<TResult>(this HttpContext? httpContext,
public static Task<HttpRemoteResult<TResult>?> ForwardAsync<TResult>(this HttpContext? httpContext,
Uri? requestUri = null, Action<HttpRequestBuilder>? configure = null,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
HttpContextForwardOptions? forwardOptions = null) =>
@@ -495,7 +495,7 @@ public static partial class HttpContextExtensions
/// <returns>
/// <see cref="HttpRemoteResult{TResult}" />
/// </returns>
public static async Task<HttpRemoteResult<TResult>> ForwardAsync<TResult>(this HttpContext? httpContext,
public static async Task<HttpRemoteResult<TResult>?> ForwardAsync<TResult>(this HttpContext? httpContext,
HttpMethod httpMethod, Uri? requestUri = null, Action<HttpRequestBuilder>? configure = null,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
HttpContextForwardOptions? forwardOptions = null)
@@ -513,7 +513,7 @@ public static partial class HttpContextExtensions
httpContext.RequestAborted).ConfigureAwait(false);
// 根据配置选项将 HttpResponseMessage 信息转发到 HttpContext 中
ForwardResponseMessage(httpContext, result.ResponseMessage, httpContextForwardBuilder.ForwardOptions);
ForwardResponseMessage(httpContext, result?.ResponseMessage, httpContextForwardBuilder.ForwardOptions);
return result;
}
@@ -605,14 +605,22 @@ public static partial class HttpContextExtensions
/// <param name="forwardOptions">
/// <see cref="HttpContextForwardOptions" />
/// </param>
internal static void ForwardResponseMessage(HttpContext httpContext, HttpResponseMessage httpResponseMessage,
internal static void ForwardResponseMessage(HttpContext httpContext, HttpResponseMessage? httpResponseMessage,
HttpContextForwardOptions forwardOptions)
{
// 空检查
ArgumentNullException.ThrowIfNull(httpContext);
ArgumentNullException.ThrowIfNull(httpResponseMessage);
ArgumentNullException.ThrowIfNull(forwardOptions);
// 空检查
if (httpResponseMessage is null)
{
// 输出调试信息
Debugging.Error("The response content was not read, as it was empty.");
return;
}
// 获取 HttpResponse 实例
var httpResponse = httpContext.Response;

View File

@@ -0,0 +1,88 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
using Microsoft.AspNetCore.Http;
using System.Text;
namespace ThingsGateway.HttpRemote;
/// <summary>
/// <see cref="HttpMultipartFormDataBuilder" /> 拓展类
/// </summary>
public static class HttpMultipartFormDataBuilderExtensions
{
/// <summary>
/// 添加文件
/// </summary>
/// <param name="httpMultipartFormDataBuilder">
/// <see cref="HttpMultipartFormDataBuilder" />
/// </param>
/// <param name="formFile">
/// <see cref="IFormFile" />
/// </param>
/// <param name="name">表单名称</param>
/// <param name="fileName">文件的名称</param>
/// <param name="contentType">内容类型</param>
/// <param name="contentEncoding">内容编码</param>
/// <returns>
/// <see cref="HttpMultipartFormDataBuilder" />
/// </returns>
public static HttpMultipartFormDataBuilder AddFile(this HttpMultipartFormDataBuilder httpMultipartFormDataBuilder,
IFormFile formFile, string? name = null, string? fileName = null, string? contentType = null,
Encoding? contentEncoding = null)
{
// 空检查
ArgumentNullException.ThrowIfNull(formFile);
// 初始化 MemoryStream 实例
var memoryStream = new MemoryStream();
// 将 IFormFile 内容复制到内存流
formFile.CopyTo(memoryStream);
// 将内存流的位置重置到起始位置
memoryStream.Position = 0;
// 添加文件流
return httpMultipartFormDataBuilder.AddStream(memoryStream, name ?? formFile.Name,
fileName ?? formFile.FileName, contentType ?? formFile.ContentType, contentEncoding,
true);
}
/// <summary>
/// 添加多个文件
/// </summary>
/// <param name="httpMultipartFormDataBuilder">
/// <see cref="HttpMultipartFormDataBuilder" />
/// </param>
/// <param name="formFiles">
/// <see cref="IFormFileCollection" />
/// </param>
/// <param name="name">表单名称</param>
/// <returns>
/// <see cref="HttpMultipartFormDataBuilder" />
/// </returns>
public static HttpMultipartFormDataBuilder AddFiles(this HttpMultipartFormDataBuilder httpMultipartFormDataBuilder,
IEnumerable<IFormFile> formFiles, string? name = null)
{
// 空检查
ArgumentNullException.ThrowIfNull(formFiles);
// 逐条添加文件
foreach (var formFile in formFiles)
{
httpMultipartFormDataBuilder.AddFile(formFile, name ?? formFile.Name);
}
return httpMultipartFormDataBuilder;
}
}

View File

@@ -16,6 +16,7 @@ using Microsoft.Net.Http.Headers;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http.Headers;
using System.Text;
using System.Text.RegularExpressions;
using ThingsGateway.Extensions;
using ThingsGateway.Utilities;
@@ -27,7 +28,7 @@ namespace ThingsGateway.HttpRemote.Extensions;
/// <summary>
/// HTTP 远程服务拓展类
/// </summary>
public static class HttpRemoteExtensions
public static partial class HttpRemoteExtensions
{
/// <summary>
/// 添加 HTTP 远程请求分析工具处理委托
@@ -70,6 +71,56 @@ public static class HttpRemoteExtensions
builder.AddProfilerDelegatingHandler(() =>
disableInProduction && GetHostEnvironmentName(builder.Services)?.ToLower() == "production");
/// <summary>
/// 配置 <see cref="HttpClient" /> 额外选项
/// </summary>
/// <param name="builder">
/// <see cref="IHttpClientBuilder" />
/// </param>
/// <param name="configure">自定义配置选项</param>
/// <returns>
/// <see cref="IHttpClientBuilder" />
/// </returns>
public static IHttpClientBuilder ConfigureOptions(this IHttpClientBuilder builder,
Action<HttpClientOptions> configure)
{
// 空检查
ArgumentNullException.ThrowIfNull(configure);
builder.Services.AddOptions<HttpClientOptions>(builder.Name).Configure(options =>
{
options.IsDefault = false;
configure.Invoke(options);
});
return builder;
}
/// <summary>
/// 配置 <see cref="HttpClient" /> 额外选项
/// </summary>
/// <param name="builder">
/// <see cref="IHttpClientBuilder" />
/// </param>
/// <param name="configure">自定义配置选项</param>
/// <returns>
/// <see cref="IHttpClientBuilder" />
/// </returns>
public static IHttpClientBuilder ConfigureOptions(this IHttpClientBuilder builder,
Action<HttpClientOptions, IServiceProvider> configure)
{
// 空检查
ArgumentNullException.ThrowIfNull(configure);
builder.Services.AddOptions<HttpClientOptions>(builder.Name).Configure<IServiceProvider>((options, provider) =>
{
options.IsDefault = false;
configure.Invoke(options, provider);
});
return builder;
}
/// <summary>
/// 为 <see cref="HttpClient" /> 启用性能优化
/// </summary>
@@ -160,17 +211,26 @@ public static class HttpRemoteExtensions
? [new KeyValuePair<string, IEnumerable<string>>("Declarative", [methodSignature])]
: null;
// 格式化 HttpClient 实例的配置条目
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? httpClientKeyValues =
httpRequestMessage.Options.TryGetValue(new HttpRequestOptionsKey<string>(Constants.HTTP_CLIENT_NAME),
out var httpClientName)
? [new KeyValuePair<string, IEnumerable<string>>("HttpClient Name", [httpClientName])]
: null;
// 格式化常规条目
var generalEntry = StringUtility.FormatKeyValuesSummary(new[]
{
new KeyValuePair<string, IEnumerable<string>>("Request URL",
[httpRequestMessage.RequestUri?.OriginalString!]),
new KeyValuePair<string, IEnumerable<string>>("HTTP Method", [httpRequestMessage.Method.ToString()]),
new KeyValuePair<string, IEnumerable<string>>("Status Code",
[$"{(int)httpResponseMessage.StatusCode} {httpResponseMessage.StatusCode}"]),
new KeyValuePair<string, IEnumerable<string>>("HTTP Content",
[$"{httpContent?.GetType().Name}"])
}.ConcatIgnoreNull(declarativeKeyValues).ConcatIgnoreNull(generalCustomKeyValues), generalSummary);
{
new KeyValuePair<string, IEnumerable<string>>("Request URL",
[httpRequestMessage.RequestUri?.OriginalString!]),
new KeyValuePair<string, IEnumerable<string>>("HTTP Method", [httpRequestMessage.Method.ToString()]),
new KeyValuePair<string, IEnumerable<string>>("Status Code",
[$"{(int)httpResponseMessage.StatusCode} {httpResponseMessage.StatusCode}"]),
new KeyValuePair<string, IEnumerable<string>>("HTTP Version", [httpResponseMessage.Version.ToString()]),
new KeyValuePair<string, IEnumerable<string>>("HTTP Content",
[$"{httpContent?.GetType().Name}"])
}.ConcatIgnoreNull(httpClientKeyValues).ConcatIgnoreNull(declarativeKeyValues)
.ConcatIgnoreNull(generalCustomKeyValues), generalSummary);
// 格式化响应条目
var responseEntry = httpResponseMessage.ProfilerHeaders(responseSummary);
@@ -203,19 +263,43 @@ public static class HttpRemoteExtensions
// 默认只读取 5KB 的内容
const int maxBytesToDisplay = 5120;
// 读取内容为字节数组
/*
* 读取内容为字节数组
*
* 由于 HttpContent 的流设计为单次读取(即流内容在首次读取后会被消耗,无法重复读取),
* 当前实现(即使用 ReadAsByteArrayAsync(cancellationToken))中对于较大内容会一次性加载至内存,
* 这可能导致性能问题(如内存占用过高或响应延迟),不过目前尚未找到更优的解决方案。
*
* 强烈建议在生产环境中禁用或关闭此类一次性读取操作,尤其是对于高并发或大流量场景,
* 以避免因内存溢出OOM或线程阻塞导致的服务不可用风险。
*/
var buffer = await httpContent.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
var total = buffer.Length;
// 计算要显示的部分
var bytesToShow = Math.Min(buffer.Length, maxBytesToDisplay);
var partialContent = Encoding.UTF8.GetString(buffer, 0, bytesToShow);
var bytesToShow = Math.Min(total, maxBytesToDisplay);
// 注册 CodePagesEncodingProvider使得程序能够识别并使用 Windows 代码页中的各种编码
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
// 获取内容编码
var charset = httpContent.Headers.ContentType?.CharSet ?? "utf-8";
var partialContent = Encoding.GetEncoding(charset).GetString(buffer, 0, bytesToShow);
// 检查是否是完整的 Unicode 转义字符串
if (total == bytesToShow && UnicodeEscapeRegex().IsMatch(partialContent))
{
partialContent = Regex.Unescape(partialContent);
}
// 如果实际读取的数据小于最大显示大小,则直接返回;否则,添加省略号表示内容被截断
var bodyString = buffer.Length <= maxBytesToDisplay ? partialContent : partialContent + " ... [truncated]";
var bodyString = total <= maxBytesToDisplay
? partialContent
: partialContent + $" ... [truncated, total: {total} bytes]";
return StringUtility.FormatKeyValuesSummary(
[new KeyValuePair<string, IEnumerable<string>>(string.Empty, [bodyString])],
$"{summary} ({httpContent.GetType().Name})");
$"{summary} ({httpContent.GetType().Name}, total: {total} bytes)");
}
/// <summary>
@@ -359,4 +443,13 @@ public static class HttpRemoteExtensions
? null
: Convert.ToString(hostEnvironment.GetType().GetProperty("EnvironmentName")?.GetValue(hostEnvironment));
}
/// <summary>
/// Unicode 转义正则表达式
/// </summary>
/// <returns>
/// <see cref="Regex" />
/// </returns>
[GeneratedRegex(@"\\u([0-9a-fA-F]{4})")]
private static partial Regex UnicodeEscapeRegex();
}

View File

@@ -52,27 +52,44 @@ internal sealed class HttpContentConverterFactory : IHttpContentConverterFactory
public IServiceProvider ServiceProvider { get; }
/// <inheritdoc />
public TResult? Read<TResult>(HttpResponseMessage httpResponseMessage, IHttpContentConverter[]? converters = null,
public TResult? Read<TResult>(HttpResponseMessage? httpResponseMessage, IHttpContentConverter[]? converters = null,
CancellationToken cancellationToken = default) =>
GetConverter<TResult>(converters).Read(httpResponseMessage, cancellationToken);
httpResponseMessage is null
? default
: GetConverter<TResult>(converters).Read(httpResponseMessage, cancellationToken);
/// <inheritdoc />
public object? Read(Type resultType, HttpResponseMessage httpResponseMessage,
IHttpContentConverter[]? converters = null,
CancellationToken cancellationToken = default) =>
GetConverter(resultType, converters).Read(resultType, httpResponseMessage, cancellationToken);
public object? Read(Type resultType, HttpResponseMessage? httpResponseMessage,
IHttpContentConverter[]? converters = null, CancellationToken cancellationToken = default) =>
httpResponseMessage is null
? null
: GetConverter(resultType, converters).Read(resultType, httpResponseMessage, cancellationToken);
/// <inheritdoc />
public async Task<TResult?> ReadAsync<TResult>(HttpResponseMessage httpResponseMessage,
IHttpContentConverter[]? converters = null,
CancellationToken cancellationToken = default) =>
await GetConverter<TResult>(converters).ReadAsync(httpResponseMessage, cancellationToken).ConfigureAwait(false);
public async Task<TResult?> ReadAsync<TResult>(HttpResponseMessage? httpResponseMessage,
IHttpContentConverter[]? converters = null, CancellationToken cancellationToken = default)
{
// 空检查
if (httpResponseMessage is null)
{
return default;
}
return await GetConverter<TResult>(converters).ReadAsync(httpResponseMessage, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task<object?> ReadAsync(Type resultType, HttpResponseMessage httpResponseMessage,
IHttpContentConverter[]? converters = null,
CancellationToken cancellationToken = default) =>
await GetConverter(resultType, converters).ReadAsync(resultType, httpResponseMessage, cancellationToken).ConfigureAwait(false);
public async Task<object?> ReadAsync(Type resultType, HttpResponseMessage? httpResponseMessage,
IHttpContentConverter[]? converters = null, CancellationToken cancellationToken = default)
{
// 空检查
if (httpResponseMessage is null)
{
return null;
}
return await GetConverter(resultType, converters).ReadAsync(resultType, httpResponseMessage, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// 获取 <see cref="IHttpContentConverter{TResult}" /> 实例

View File

@@ -37,7 +37,7 @@ public interface IHttpContentConverterFactory
/// <returns>
/// <typeparamref name="TResult" />
/// </returns>
TResult? Read<TResult>(HttpResponseMessage httpResponseMessage,
TResult? Read<TResult>(HttpResponseMessage? httpResponseMessage,
IHttpContentConverter[]? converters = null, CancellationToken cancellationToken = default);
/// <summary>
@@ -54,7 +54,7 @@ public interface IHttpContentConverterFactory
/// <returns>
/// <see cref="object" />
/// </returns>
object? Read(Type resultType, HttpResponseMessage httpResponseMessage,
object? Read(Type resultType, HttpResponseMessage? httpResponseMessage,
IHttpContentConverter[]? converters = null, CancellationToken cancellationToken = default);
/// <summary>
@@ -73,7 +73,7 @@ public interface IHttpContentConverterFactory
/// <returns>
/// <typeparamref name="TResult" />
/// </returns>
Task<TResult?> ReadAsync<TResult>(HttpResponseMessage httpResponseMessage,
Task<TResult?> ReadAsync<TResult>(HttpResponseMessage? httpResponseMessage,
IHttpContentConverter[]? converters = null, CancellationToken cancellationToken = default);
/// <summary>
@@ -90,6 +90,6 @@ public interface IHttpContentConverterFactory
/// <returns>
/// <see cref="object" />
/// </returns>
Task<object?> ReadAsync(Type resultType, HttpResponseMessage httpResponseMessage,
Task<object?> ReadAsync(Type resultType, HttpResponseMessage? httpResponseMessage,
IHttpContentConverter[]? converters = null, CancellationToken cancellationToken = default);
}

View File

@@ -115,6 +115,15 @@ internal sealed class FileDownloadManager
var httpResponseMessage = _httpRemoteService.Send(RequestBuilder, HttpCompletionOption.ResponseHeadersRead,
cancellationToken);
// 空检查
if (httpResponseMessage is null)
{
// 输出调试信息
Debugging.Error("The response content was not read, as it was empty.");
return;
}
// 根据文件是否存在及配置的行为来决定是否应继续进行文件下载
if (!ShouldContinueWithDownload(httpResponseMessage, out var destinationPath))
{
@@ -228,6 +237,15 @@ internal sealed class FileDownloadManager
var httpResponseMessage = await _httpRemoteService.SendAsync(RequestBuilder,
HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
// 空检查
if (httpResponseMessage is null)
{
// 输出调试信息
Debugging.Error("The response content was not read, as it was empty.");
return;
}
// 根据文件是否存在及配置的行为来决定是否应继续进行文件下载
if (!ShouldContinueWithDownload(httpResponseMessage, out var destinationPath))
{
@@ -246,7 +264,7 @@ internal sealed class FileDownloadManager
bufferSize, true);
// 获取 HTTP 响应体中的内容流
using var contentStream = (await httpResponseMessage.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false));
using var contentStream = await httpResponseMessage.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
// 循环读取数据直到取消请求或读取完毕
int numBytesRead;
@@ -485,6 +503,9 @@ internal sealed class FileDownloadManager
/// </returns>
internal string GetFileName(HttpResponseMessage httpResponseMessage)
{
// 空检查
ArgumentNullException.ThrowIfNull(httpResponseMessage);
// 获取文件下载保存的文件的名称
var fileName = Path.GetFileName(_httpFileDownloadBuilder.DestinationPath);

View File

@@ -88,7 +88,7 @@ internal sealed class FileUploadManager
/// <see cref="HttpResponseMessage" />
/// </returns>
/// <exception cref="NotImplementedException"></exception>
internal HttpResponseMessage Start(CancellationToken cancellationToken = default)
internal HttpResponseMessage? Start(CancellationToken cancellationToken = default)
{
// 创建进度报告任务取消标识
using var progressCancellationTokenSource = new CancellationTokenSource();
@@ -102,7 +102,7 @@ internal sealed class FileUploadManager
// 初始化 Stopwatch 实例并开启计时操作
var stopwatch = Stopwatch.StartNew();
HttpResponseMessage httpResponseMessage;
HttpResponseMessage? httpResponseMessage;
try
{
@@ -147,7 +147,7 @@ internal sealed class FileUploadManager
/// <returns>
/// <see cref="Task{TResult}" />
/// </returns>
internal async Task<HttpResponseMessage> StartAsync(CancellationToken cancellationToken = default)
internal async Task<HttpResponseMessage?> StartAsync(CancellationToken cancellationToken = default)
{
// 创建进度报告任务取消标识
using var progressCancellationTokenSource = new CancellationTokenSource();
@@ -161,7 +161,7 @@ internal sealed class FileUploadManager
// 初始化 Stopwatch 实例并开启计时操作
var stopwatch = Stopwatch.StartNew();
HttpResponseMessage httpResponseMessage;
HttpResponseMessage? httpResponseMessage;
try
{

View File

@@ -97,6 +97,15 @@ internal sealed class LongPollingManager
// 发送 HTTP 远程请求
var httpResponseMessage = _httpRemoteService.Send(RequestBuilder, cancellationToken);
// 空检查
if (httpResponseMessage is null)
{
// 输出调试信息
Debugging.Error("The response content was not read, as it was empty.");
continue;
}
// 发送响应数据对象到通道
dataChannel.Writer.TryWrite(httpResponseMessage);
@@ -167,6 +176,15 @@ internal sealed class LongPollingManager
// 发送 HTTP 远程请求
var httpResponseMessage = await _httpRemoteService.SendAsync(RequestBuilder, cancellationToken).ConfigureAwait(false);
// 空检查
if (httpResponseMessage is null)
{
// 输出调试信息
Debugging.Error("The response content was not read, as it was empty.");
continue;
}
// 发送响应数据对象到通道
await dataChannel.Writer.WriteAsync(httpResponseMessage, cancellationToken).ConfigureAwait(false);

View File

@@ -109,6 +109,15 @@ internal sealed class ServerSentEventsManager
var httpResponseMessage = _httpRemoteService.Send(RequestBuilder, HttpCompletionOption.ResponseHeadersRead,
cancellationToken);
// 空检查
if (httpResponseMessage is null)
{
// 输出调试信息
Debugging.Error("The response content was not read, as it was empty.");
return;
}
// 获取 HTTP 响应体中的内容流
using var contentStream = httpResponseMessage.Content.ReadAsStream(cancellationToken);
@@ -203,9 +212,17 @@ internal sealed class ServerSentEventsManager
var httpResponseMessage = await _httpRemoteService.SendAsync(RequestBuilder,
HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
// 获取 HTTP 响应体中的内容流
using var contentStream = (await httpResponseMessage.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false));
// 空检查
if (httpResponseMessage is null)
{
// 输出调试信息
Debugging.Error("The response content was not read, as it was empty.");
return;
}
// 获取 HTTP 响应体中的内容流
using var contentStream = await httpResponseMessage.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
// 初始化 StreamReader 实例
using var streamReader = new StreamReader(contentStream, Encoding.UTF8);
@@ -382,10 +399,10 @@ internal sealed class ServerSentEventsManager
? retryInterval
: _httpServerSentEventsBuilder.DefaultRetryInterval;
break;
// 所有其他的字段名都会被忽略
// 其他的字段名存储在 CustomFields 属性中
default:
// 保持数据不变
return true;
serverSentEventsData.AddCustomField(key, value);
break;
}
return true;

View File

@@ -143,6 +143,15 @@ internal sealed class StressTestHarnessManager
var httpResponseMessage =
await _httpRemoteService.SendAsync(RequestBuilder, completionOption, cancellationToken).ConfigureAwait(false);
// 空检查
if (httpResponseMessage is null)
{
// 输出调试信息
Debugging.Error("The response content was not read, as it was empty.");
return;
}
// 检查响应状态码是否是成功状态
if (httpResponseMessage.IsSuccessStatusCode)
{

View File

@@ -36,7 +36,7 @@ public sealed class HttpRemoteAnalyzer
/// <summary>
/// 分析数据
/// </summary>
public string Data => _cachedData ??= _dataBuffer.ToString();
public string Data => _cachedData ??= _dataBuffer.ToString().TrimEnd(Environment.NewLine.ToCharArray());
/// <summary>
/// 追加分析数据
@@ -44,7 +44,7 @@ public sealed class HttpRemoteAnalyzer
/// <param name="value">分析数据</param>
internal void AppendData(string? value)
{
_dataBuffer.Append(value);
_dataBuffer.AppendLine(value);
_cachedData = null;
}

View File

@@ -0,0 +1,186 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
using Microsoft.Extensions.DependencyInjection;
namespace ThingsGateway.HttpRemote;
/// <summary>
/// 提供静态访问 <see cref="IHttpRemoteService" /> 服务的方式
/// </summary>
/// <remarks>支持服务的延迟初始化、配置更新以及资源释放。</remarks>
#pragma warning disable CA1513
public static class HttpRemoteClient
{
/// <inheritdoc cref="IServiceProvider" />
internal static IServiceProvider? _serviceProvider;
/// <summary>
/// 延迟加载的 <see cref="IHttpRemoteService" /> 实例
/// </summary>
internal static Lazy<IHttpRemoteService> _lazyService;
/// <summary>
/// 并发锁对象
/// </summary>
internal static readonly object _lock = new();
/// <summary>
/// 标记服务是否已释放
/// </summary>
internal static bool _isDisposed;
/// <summary>
/// 自定义服务注册逻辑的委托
/// </summary>
internal static Action<IServiceCollection> _configure = services => services.AddHttpRemote();
/// <summary>
/// <inheritdoc cref="HttpRemoteClient" />
/// </summary>
static HttpRemoteClient() => _lazyService = new Lazy<IHttpRemoteService>(CreateService);
/// <summary>
/// 获取当前配置下的 <see cref="IHttpRemoteService" /> 实例
/// </summary>
/// <exception cref="ObjectDisposedException"></exception>
public static IHttpRemoteService Service
{
get
{
if (_isDisposed)
{
throw new ObjectDisposedException(nameof(HttpRemoteClient));
}
return _lazyService.Value;
}
}
/// <summary>
/// 自定义服务注册逻辑
/// </summary>
public static void Configure(Action<IServiceCollection> configure)
{
// 空检查
ArgumentNullException.ThrowIfNull(configure);
lock (_lock)
{
_configure = configure;
// 重新初始化服务
Reinitialize();
}
}
/// <summary>
/// 释放服务提供器及相关资源
/// </summary>
/// <remarks>通常在应用程序关闭或不再需要 HTTP 远程请求服务时调用。</remarks>
public static void Dispose()
{
lock (_lock)
{
if (_isDisposed)
{
return;
}
// 释放服务提供器
ReleaseServiceProvider();
_isDisposed = true;
}
}
/// <summary>
/// 创建 <see cref="IHttpRemoteService" /> 实例
/// </summary>
/// <returns>
/// <see cref="IHttpRemoteService" />
/// </returns>
/// <exception cref="ObjectDisposedException"></exception>
/// <exception cref="InvalidOperationException"></exception>
internal static IHttpRemoteService CreateService()
{
lock (_lock)
{
if (_isDisposed)
{
throw new ObjectDisposedException(nameof(HttpRemoteClient));
}
// 如果值已创建,直接返回
if (_lazyService.IsValueCreated)
{
return _lazyService.Value;
}
try
{
// 初始化 ServiceCollection 实例
var services = new ServiceCollection();
// 调用自定义服务注册逻辑的委托
_configure(services);
// 构建服务提供器
_serviceProvider = services.BuildServiceProvider();
// 解析 IHttpRemoteService 实例
var service = _serviceProvider.GetRequiredService<IHttpRemoteService>();
return service;
}
catch (Exception ex)
{
throw new InvalidOperationException("Failed to initialize IHttpRemoteService.", ex);
}
}
}
/// <summary>
/// 使用最新的 <see cref="Configure" /> 配置重新初始化服务
/// </summary>
/// <exception cref="ObjectDisposedException"></exception>
internal static void Reinitialize()
{
lock (_lock)
{
if (_isDisposed)
{
throw new ObjectDisposedException(nameof(HttpRemoteClient));
}
// 释放当前的服务提供器
ReleaseServiceProvider();
// 重新创建延迟加载实例
_lazyService = new Lazy<IHttpRemoteService>(CreateService, LazyThreadSafetyMode.ExecutionAndPublication);
}
}
/// <summary>
/// 释放服务提供器
/// </summary>
internal static void ReleaseServiceProvider()
{
// 如果服务提供器支持释放资源,则执行释放操作
if (_serviceProvider is IDisposable disposable)
{
disposable.Dispose();
}
_serviceProvider = null;
}
}
#pragma warning restore CA1513

Some files were not shown because too many files have changed in this diff Show More