Compare commits

...

11 Commits

Author SHA1 Message Date
Diego
b06405717d build: 10.9.9
fix: timerx 池 max值取消
feat: mqttrpc支持脚本
2025-07-02 10:03:50 +08:00
2248356998 qq.com
298a1f2ed4 更新docker文件 2025-07-02 07:32:23 +08:00
Diego
74a47a1983 build: 10.9.8
支持redis缓存
2025-07-01 17:55:03 +08:00
Diego
a48a42abe4 modbusslave 异常捕获 2025-07-01 10:51:10 +08:00
Diego
feb1d0a3c5 feat: 增加后台服务生命周期识别 2025-06-30 10:59:18 +08:00
Diego
92bca824e6 10.9.6 2025-06-29 22:19:53 +08:00
2248356998 qq.com
025c699517 feat: s7增加请求id识别 2025-06-29 22:06:53 +08:00
2248356998 qq.com
53f8fbe4b1 refactor: 变量排序导出 2025-06-28 21:43:06 +08:00
2248356998 qq.com
77bfabc41d feat: 改用Mapperly源生成,代替Mapster 2025-06-28 00:00:43 +08:00
Diego
6427ee6ee0 refactor: 降低sqlite依赖 2025-06-27 14:44:00 +08:00
Diego
4c95997d62 build: 10.9.2
fix: taos connection dispose
refactor: opcua AddSubscriptionAsync 添加延时和重试
2025-06-27 11:16:58 +08:00
460 changed files with 41347 additions and 4448 deletions

View File

@@ -17,8 +17,7 @@
</PropertyGroup>
<ItemGroup>
<None Include="$(OutputPath)\$(TargetFileName)" Pack="true"
PackagePath="analyzers/dotnet/cs" Visible="false" />
<None Include="$(OutputPath)\$(TargetFileName)" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>
@@ -27,6 +26,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.9.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.13.0" PrivateAssets="all" Private="false" />
</ItemGroup>
</Project>

View File

@@ -1,63 +1,48 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System.Text;
namespace Microsoft.AspNetCore.Components;
[Generator]
public partial class SetParametersAsyncGenerator : ISourceGenerator
public sealed partial class SetParametersAsyncGenerator : IIncrementalGenerator
{
private const string m_DoNotGenerateSetParametersAsyncAttribute = """
using System;
namespace Microsoft.AspNetCore.Components
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
internal sealed class DoNotGenerateSetParametersAsyncAttribute : Attribute { }
}
""";
private string m_DoNotGenerateSetParametersAsyncAttribute = """
using System;
namespace Microsoft.AspNetCore.Components
private const string m_GenerateSetParametersAsyncAttribute = """
using System;
namespace Microsoft.AspNetCore.Components
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
internal sealed class GenerateSetParametersAsyncAttribute : Attribute
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
internal sealed class DoNotGenerateSetParametersAsyncAttribute : Attribute
{
}
public bool RequireExactMatch { get; set; }
}
}
""";
"""
;
private string m_GenerateSetParametersAsyncAttribute = """
using System;
namespace Microsoft.AspNetCore.Components
private const string m_GlobalGenerateSetParametersAsyncAttribute = """
using System;
namespace Microsoft.AspNetCore.Components
{
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)]
internal sealed class GlobalGenerateSetParametersAsyncAttribute : Attribute
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
internal sealed class GenerateSetParametersAsyncAttribute : Attribute
{
public bool RequireExactMatch { get; set; }
}
public bool Enable { get; }
public GlobalGenerateSetParametersAsyncAttribute(bool enable = true) { Enable = enable; }
}
"""
;
private string m_GlobalGenerateSetParametersAsyncAttribute = """
using System;
namespace Microsoft.AspNetCore.Components
{
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)]
internal sealed class GlobalGenerateSetParametersAsyncAttribute : Attribute
{
public bool Enable { get; }
public GlobalGenerateSetParametersAsyncAttribute(bool enable = true)
{
Enable = enable;
}
}
}
"""
;
}
""";
private static readonly DiagnosticDescriptor ParameterNameConflict = new DiagnosticDescriptor(
id: "TG0001",
@@ -68,36 +53,64 @@ public partial class SetParametersAsyncGenerator : ISourceGenerator
isEnabledByDefault: true,
description: "Parameter names must be case insensitive to be usable in routes. Rename the parameter to not be in conflict with other parameters.");
public void Initialize(GeneratorInitializationContext context)
public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.RegisterForPostInitialization(a =>
// 注入 attribute 源码
context.RegisterPostInitializationOutput(ctx =>
{
a.AddSource(nameof(m_DoNotGenerateSetParametersAsyncAttribute), m_DoNotGenerateSetParametersAsyncAttribute);
a.AddSource(nameof(m_GenerateSetParametersAsyncAttribute), m_GenerateSetParametersAsyncAttribute);
a.AddSource(nameof(m_GlobalGenerateSetParametersAsyncAttribute), m_GlobalGenerateSetParametersAsyncAttribute);
ctx.AddSource("DoNotGenerateSetParametersAsyncAttribute.g.cs", SourceText.From(m_DoNotGenerateSetParametersAsyncAttribute, Encoding.UTF8));
ctx.AddSource("GenerateSetParametersAsyncAttribute.g.cs", SourceText.From(m_GenerateSetParametersAsyncAttribute, Encoding.UTF8));
ctx.AddSource("GlobalGenerateSetParametersAsyncAttribute.g.cs", SourceText.From(m_GlobalGenerateSetParametersAsyncAttribute, Encoding.UTF8));
});
// Register a syntax receiver that will be created for each generation pass
context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
// 筛选 ClassDeclarationSyntax
var classDeclarations = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (node, _) => node is ClassDeclarationSyntax,
transform: static (ctx, _) => (ClassDeclarationSyntax)ctx.Node)
.Where(static c => c is not null);
// 合并 Compilation
var compilationProvider = context.CompilationProvider;
var candidateClasses = classDeclarations.Combine(compilationProvider);
context.RegisterSourceOutput(candidateClasses, static (spc, tuple) =>
{
var (classDeclaration, compilation) = tuple;
Execute(spc, compilation, classDeclaration);
});
}
public void Execute(GeneratorExecutionContext context)
private static void Execute(SourceProductionContext context, Compilation compilation, ClassDeclarationSyntax classDeclaration)
{
// https://github.com/dotnet/AspNetCore.Docs/blob/1e199f340780f407a685695e6c4d953f173fa891/aspnetcore/blazor/webassembly-performance-best-practices.md#implement-setparametersasync-manually
if (context.SyntaxReceiver is not SyntaxReceiver receiver)
{
var model = compilation.GetSemanticModel(classDeclaration.SyntaxTree);
var classSymbol = model.GetDeclaredSymbol(classDeclaration);
if (classSymbol is null || classSymbol.Name == "_Imports")
return;
}
var candidate_classes = GetCandidateClasses(receiver, context);
var positiveAttr = compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Components.GenerateSetParametersAsyncAttribute");
var negativeAttr = compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Components.DoNotGenerateSetParametersAsyncAttribute");
foreach (var class_symbol in candidate_classes.Distinct(SymbolEqualityComparer.Default).Cast<INamedTypeSymbol>())
{
GenerateSetParametersAsyncMethod(context, class_symbol);
}
if (classSymbol.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, negativeAttr)))
return;
if (!IsPartial(classSymbol) || !IsComponent(classDeclaration, classSymbol, compilation))
return;
var globalEnable = compilation.Assembly.GetAttributes()
.FirstOrDefault(a => a.AttributeClass?.ToDisplayString() == "Microsoft.AspNetCore.Components.GlobalGenerateSetParametersAsyncAttribute")
?.ConstructorArguments.FirstOrDefault().Value as bool? ?? false;
var hasPositive = classSymbol.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, positiveAttr));
if (!globalEnable && !hasPositive)
return;
GenerateSetParametersAsyncMethod(context, classSymbol);
}
private static void GenerateSetParametersAsyncMethod(GeneratorExecutionContext context, INamedTypeSymbol class_symbol)
private static void GenerateSetParametersAsyncMethod(SourceProductionContext context, INamedTypeSymbol class_symbol)
{
var force_exact_match = class_symbol.GetAttributes().Any(a => a.NamedArguments.Any(na => na.Key == "RequireExactMatch" && na.Value.Value is bool v && v));
var namespaceName = class_symbol.ContainingNamespace.ToDisplayString();
@@ -386,6 +399,31 @@ namespace {namespaceName}
#pragma warning restore CS0162");
}
private static bool IsPartial(INamedTypeSymbol symbol)
{
return symbol.DeclaringSyntaxReferences
.Select(r => r.GetSyntax())
.OfType<ClassDeclarationSyntax>()
.Any(c => c.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)));
}
private static bool IsComponent(ClassDeclarationSyntax classDeclaration, INamedTypeSymbol symbol, Compilation compilation)
{
if (HasUserDefinedSetParametersAsync(symbol))
return false;
if (classDeclaration.SyntaxTree.FilePath.EndsWith(".razor") || classDeclaration.SyntaxTree.FilePath.EndsWith(".razor.cs"))
return true;
var iComponent = compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Components.IComponent");
var componentBase = compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Components.ComponentBase");
if (iComponent == null || componentBase == null)
return false;
return symbol.AllInterfaces.Contains(iComponent) || SymbolEqualityComparer.Default.Equals(symbol.BaseType, componentBase);
}
private static bool HasUserDefinedSetParametersAsync(INamedTypeSymbol classSymbol)
{
return classSymbol
@@ -398,137 +436,4 @@ namespace {namespaceName}
!m.IsStatic);
}
private static bool IsPartial(INamedTypeSymbol symbol)
{
return symbol.DeclaringSyntaxReferences
.Select(r => r.GetSyntax())
.OfType<ClassDeclarationSyntax>()
.Any(c => c.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)));
}
private static bool IsComponent(ClassDeclarationSyntax classDeclarationSyntax, INamedTypeSymbol symbol, Compilation compilation)
{
if (HasUserDefinedSetParametersAsync(symbol))
{
// 用户自己写了方法,不生成
return false;
}
if (!IsPartial(symbol))
{
return false;
}
if (classDeclarationSyntax.SyntaxTree.FilePath.EndsWith(".razor") || classDeclarationSyntax.SyntaxTree.FilePath.EndsWith(".razor.cs"))
{
return true;
}
var iComponent = compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Components.IComponent");
var componentBase = compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Components.ComponentBase");
if (iComponent == null || componentBase == null)
return false;
if (SymbolEqualityComparer.Default.Equals(symbol, iComponent))
return true;
if (SymbolEqualityComparer.Default.Equals(symbol, componentBase))
return true;
return false;
}
/// <summary>
/// Enumerate methods with at least one Group attribute
/// </summary>
private static IEnumerable<INamedTypeSymbol> GetCandidateClasses(SyntaxReceiver receiver, GeneratorExecutionContext context)
{
var compilation = context.Compilation;
var positiveAttributeSymbol = compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Components.GenerateSetParametersAsyncAttribute");
var negativeAttributeSymbol = compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Components.DoNotGenerateSetParametersAsyncAttribute");
// loop over the candidate methods, and keep the ones that are actually annotated
// 找特性
var assemblyAttributes = compilation.Assembly.GetAttributes();
var enableAttr = assemblyAttributes.FirstOrDefault(attr =>
attr.AttributeClass?.ToDisplayString() == "Microsoft.AspNetCore.Components.GlobalGenerateSetParametersAsyncAttribute");
var globalEnable = false;
if (enableAttr != null)
{
var arg = enableAttr.ConstructorArguments.FirstOrDefault();
if (arg.Value is bool b)
globalEnable = b;
}
foreach (ClassDeclarationSyntax class_declaration in receiver.CandidateClasses)
{
var model = compilation.GetSemanticModel(class_declaration.SyntaxTree);
var class_symbol = model.GetDeclaredSymbol(class_declaration);
if (class_symbol is null)
{
continue;
}
if (class_symbol.Name == "_Imports")
{
continue;
}
// 是否拒绝生成
var hasNegative = class_symbol.GetAttributes().Any(ad =>
ad.AttributeClass?.Equals(negativeAttributeSymbol, SymbolEqualityComparer.Default) == true);
if (hasNegative)
continue;
if (IsComponent(class_declaration, class_symbol, compilation))
{
if (globalEnable)
{
yield return class_symbol;
}
else
{
// 必须显式标注 Positive Attribute
var hasPositive = class_symbol.GetAttributes().Any(ad =>
ad.AttributeClass?.Equals(positiveAttributeSymbol, SymbolEqualityComparer.Default) == true);
if (hasPositive)
yield return class_symbol;
}
}
else
{
}
}
}
/// <summary>
/// Created on demand before each generation pass
/// </summary>
internal class SyntaxReceiver : ISyntaxReceiver
{
public List<ClassDeclarationSyntax> CandidateClasses { get; } = new List<ClassDeclarationSyntax>();
/// <summary>
/// Called for every syntax node in the compilation, we can inspect the nodes and save any information useful for generation
/// </summary>
public void OnVisitSyntaxNode(SyntaxNode syntax_node)
{
// any class with at least one attribute is a candidate for property generation
if (syntax_node is ClassDeclarationSyntax classDeclarationSyntax)
{
CandidateClasses.Add(classDeclarationSyntax);
}
else
{
}
}
}
}

View File

@@ -7,7 +7,7 @@ namespace Microsoft.AspNetCore.Components
{
internal static class SourceGeneratorContextExtension
{
public static void AddCode(this GeneratorExecutionContext context, string hint_name, string code)
public static void AddCode(this SourceProductionContext context, string hint_name, string code)
{
context.AddSource(hint_name.Replace("<", "_").Replace(">", "_"), SourceText.From(code, Encoding.UTF8));
}

View File

@@ -1,13 +0,0 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
namespace Microsoft.AspNetCore.Components
{
internal static class StringExtension
{
public static string NormalizeWhitespace(this string code)
{
return CSharpSyntaxTree.ParseText(code).GetRoot().NormalizeWhitespace().ToFullString();
}
}
}

View File

@@ -33,22 +33,22 @@ public static class CacheConst
/// <summary>
/// 资源表缓存Key
/// </summary>
public const string Cache_SysResource = $"{CacheConst.Cache_Prefix_Admin}SysResource:";
public const string Cache_SysResource = $"{CacheConst.Cache_Prefix_Admin}SysResource:List";
/// <summary>
/// 角色表缓存Key
/// </summary>
public const string Cache_SysRole = $"{CacheConst.Cache_Prefix_Admin}SysRole:";
public const string Cache_SysRole = $"{CacheConst.Cache_Prefix_Admin}SysRole:List";
/// <summary>
/// 用户表缓存Key
/// </summary>
public const string Cache_SysUser = $"{CacheConst.Cache_Prefix_Admin}SysUser:";
public const string Cache_SysUser = $"{CacheConst.Cache_Prefix_Admin}SysUser:Hash";
/// <summary>
/// 用户账号关系缓存Key
/// </summary>
public const string Cache_SysUserAccount = $"{CacheConst.Cache_Prefix_Admin}SysUserAccount:";
public const string Cache_SysUserAccount = $"{CacheConst.Cache_Prefix_Admin}SysUserAccount:Hash";
/// <summary>
/// 职位表缓存Key
@@ -58,7 +58,7 @@ public static class CacheConst
/// <summary>
/// 机构表缓存Key
/// </summary>
public const string Cache_SysOrg = $"{CacheConst.Cache_Prefix_Admin}SysOrg:";
public const string Cache_SysOrg = $"{CacheConst.Cache_Prefix_Admin}SysOrg:List";
/// <summary>
/// 公司表缓存Key
@@ -67,12 +67,12 @@ public static class CacheConst
/// <summary>
/// 公司表缓存Key
/// </summary>
public const string Cache_SysOrgTenant = $"{CacheConst.Cache_Prefix_Admin}OrgTenant:";
public const string Cache_SysOrgTenant = $"{CacheConst.Cache_Prefix_Admin}OrgTenant:Hash";
/// <summary>
/// Token表缓存Key
/// </summary>
public const string Cache_Token = $"{CacheConst.Cache_Prefix_Admin}Token:";
public const string Cache_Token = $"{CacheConst.Cache_Prefix_Admin}Token:Hash";
#region

View File

@@ -8,8 +8,6 @@
// QQ群605534569
//------------------------------------------------------------------------------
using Mapster;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -41,9 +39,9 @@ public class OpenApiController : ControllerBase
[AllowAnonymous]
public async Task<OpenApiLoginOutput> LoginAsync([FromBody] OpenApiLoginInput input)
{
var output = await _authService.LoginAsync(input.Adapt<LoginInput>(), false).ConfigureAwait(false);
var output = await _authService.LoginAsync(input.AdaptLoginInput(), false).ConfigureAwait(false);
var openApiLoginOutput = output.Adapt<OpenApiLoginOutput>();
var openApiLoginOutput = output.AdaptOpenApiLoginOutput();
return openApiLoginOutput;
}

View File

@@ -10,7 +10,7 @@
using BootstrapBlazor.Components;
using Mapster;
using Riok.Mapperly.Abstractions;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
@@ -31,7 +31,7 @@ public class SysUser : BaseEntity
///</summary>
[SugarColumn(ColumnDescription = "头像", ColumnDataType = StaticConfig.CodeFirst_BigString, IsNullable = true)]
[AutoGenerateColumn(Visible = true, Sortable = false, Filterable = false)]
[AdaptIgnore]
[MapperIgnore]
public virtual string? Avatar { get; set; }
/// <summary>

View File

@@ -53,8 +53,8 @@ public class HardwareJob : IJob, IHardwareJob
#endregion
private MemoryCache MemoryCache = new() { };
private const string CacheKey = "HistoryHardwareInfo";
private ICache MemoryCache => App.CacheService;
private const string CacheKey = $"{CacheConst.Cache_HardwareInfo}HistoryHardwareInfo";
/// <inheritdoc/>
public async Task<List<HistoryHardwareInfo>> GetHistoryHardwareInfos()
{
@@ -81,8 +81,7 @@ public class HardwareJob : IJob, IHardwareJob
{
if (HardwareInfo.MachineInfo == null)
{
MachineInfo.Register();
HardwareInfo.MachineInfo = MachineInfo.Current;
HardwareInfo.MachineInfo = MachineInfo.GetCurrent();
string currentPath = Directory.GetCurrentDirectory();
DriveInfo drive = new(Path.GetPathRoot(currentPath));

View File

@@ -0,0 +1,33 @@
//------------------------------------------------------------------------------
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
// 此代码版权除特别声明外的代码归作者本人Diego所有
// 源代码使用协议遵循本仓库的开源协议及附加协议
// Gitee源代码仓库https://gitee.com/diego2098/ThingsGateway
// Github源代码仓库https://github.com/kimdiego2098/ThingsGateway
// 使用文档https://thingsgateway.cn/
// QQ群605534569
//------------------------------------------------------------------------------
using BootstrapBlazor.Components;
using Riok.Mapperly.Abstractions;
namespace ThingsGateway.Admin.Application;
[Mapper(UseDeepCloning = true, EnumMappingStrategy = EnumMappingStrategy.ByName, RequiredMappingStrategy = RequiredMappingStrategy.None)]
public static partial class AdminMapper
{
public static partial LoginInput AdaptLoginInput(this OpenApiLoginInput src);
public static partial OpenApiLoginOutput AdaptOpenApiLoginOutput(this LoginOutput src);
public static partial SessionOutput AdaptSessionOutput(this SysUser src);
public static partial SysUser AdaptSysUser(this SysUser src);
public static partial UserSelectorOutput AdaptUserSelectorOutput(this SysUser src);
public static partial List<SysResource> AdaptListSysResource(this IEnumerable<SysResource> src);
public static partial AppConfig AdaptAppConfig(this AppConfig src);
public static partial WorkbenchInfo AdaptWorkbenchInfo(this WorkbenchInfo src);
public static partial QueryData<UserSelectorOutput> AdaptQueryDataUserSelectorOutput(this QueryData<SysUser> src);
public static partial LoginInput AdaptLoginInput(this LoginInput src);
}

View File

@@ -10,6 +10,8 @@
using System.Collections.Concurrent;
using ThingsGateway.NewLife.DictionaryExtensions;
namespace ThingsGateway.Admin.Application;
/// <summary>

View File

@@ -12,8 +12,6 @@ using BootstrapBlazor.Components;
using Microsoft.Extensions.DependencyInjection;
using System.Globalization;
using ThingsGateway.FriendlyException;
using ThingsGateway.SqlSugar;
@@ -23,7 +21,7 @@ internal sealed class SysResourceService : BaseService<SysResource>, ISysResourc
{
private readonly IRelationService _relationService;
private string CacheKey = $"{CacheConst.Cache_SysResource}-{CultureInfo.CurrentUICulture.Name}";
private string CacheKey = $"{CacheConst.Cache_SysResource}";
public SysResourceService(IRelationService relationService)
{
@@ -32,7 +30,6 @@ internal sealed class SysResourceService : BaseService<SysResource>, ISysResourc
#region
[OperDesc("CopyResource")]
public async Task CopyAsync(IEnumerable<long> ids, long moduleId)
{
@@ -143,12 +140,12 @@ internal sealed class SysResourceService : BaseService<SysResource>, ISysResourc
/// <returns>全部资源列表</returns>
public async Task<List<SysResource>> GetAllAsync()
{
var sysResources = App.CacheService.Get<List<SysResource>>(CacheKey);
var sysResources = App.CacheService.Get<List<SysResource>>(CacheConst.Cache_SysResource);
if (sysResources == null)
{
using var db = GetDB();
sysResources = await db.Queryable<SysResource>().ToListAsync().ConfigureAwait(false);
App.CacheService.Set(CacheKey, sysResources);
App.CacheService.Set(CacheConst.Cache_SysResource, sysResources);
}
return sysResources;
}
@@ -258,7 +255,7 @@ internal sealed class SysResourceService : BaseService<SysResource>, ISysResourc
/// </summary>
public void RefreshCache()
{
App.CacheService.Remove(CacheKey);
App.CacheService.Remove(CacheConst.Cache_SysResource);
//删除超级管理员的缓存
App.RootServices.GetRequiredService<ISysUserService>().DeleteUserFromCache(RoleConst.SuperAdminId);
}

View File

@@ -10,8 +10,6 @@
using BootstrapBlazor.Components;
using Mapster;
using ThingsGateway.SqlSugar;
namespace ThingsGateway.Admin.Application;
@@ -69,7 +67,7 @@ internal sealed class SessionService : BaseService<SysUser>, ISessionService
var r = items.Select((it) =>
{
var reuslt = it.Adapt<SessionOutput>();
var reuslt = it.AdaptSessionOutput();
if (verificatInfoDicts.TryGetValue(it.Id, out var verificatInfos))
{
reuslt.VerificatCount = verificatInfos.Count;//令牌数量
@@ -94,7 +92,7 @@ internal sealed class SessionService : BaseService<SysUser>, ISessionService
var r = items.Select((it) =>
{
var reuslt = it.Adapt<SessionOutput>();
var reuslt = it.AdaptSessionOutput();
if (verificatInfoDicts.TryGetValue(it.Id, out var verificatInfos))
{
reuslt.VerificatCount = verificatInfos.Count;//令牌数量
@@ -117,7 +115,7 @@ internal sealed class SessionService : BaseService<SysUser>, ISessionService
var r = items.Select((it) =>
{
var reuslt = it.Adapt<SessionOutput>();
var reuslt = it.AdaptSessionOutput();
if (verificatInfoDicts.TryGetValue(it.Id, out var verificatInfos))
{
reuslt.VerificatCount = verificatInfos.Count;//令牌数量

View File

@@ -10,8 +10,6 @@
using BootstrapBlazor.Components;
using Mapster;
using ThingsGateway.DataEncryption;
using ThingsGateway.Extension;
using ThingsGateway.Extension.Generic;
@@ -452,7 +450,7 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService
if (changedType == ItemChangedType.Add)
{
var sysUser = input.Adapt<SysUser>();
var sysUser = input.AdaptSysUser();
//获取默认密码
sysUser.Avatar = input.Avatar;
sysUser.Password = await GetDefaultPassWord(true).ConfigureAwait(false);//设置密码
@@ -872,7 +870,7 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService
sysUser.OrgAndPosIdList.AddRange(sysUser.OrgId, sysUser.PositionId ?? 0);//添加组织和职位Id
if (sysUser.DirectorId != null)
{
sysUser.DirectorInfo = (await GetUserByIdAsync(sysUser.DirectorId.Value).ConfigureAwait(false)).Adapt<UserSelectorOutput>();//获取主管信息
sysUser.DirectorInfo = (await GetUserByIdAsync(sysUser.DirectorId.Value).ConfigureAwait(false)).AdaptUserSelectorOutput();//获取主管信息
}
//获取按钮码

View File

@@ -11,10 +11,12 @@
using BootstrapBlazor.Components;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Reflection;
using System.Text;
using ThingsGateway.NewLife.Log;
using ThingsGateway.Extension;
using ThingsGateway.UnifyResult;
namespace ThingsGateway.Admin.Application;
@@ -65,11 +67,76 @@ public class Startup : AppStartup
services.AddSingleton(typeof(IEventService<>), typeof(EventService<>));
#region
services.AddConsoleFormatter(options =>
{
options.WriteFilter = (logMsg) =>
{
if (App.HostApplicationLifetime.ApplicationStopping.IsCancellationRequested && logMsg.LogLevel >= LogLevel.Warning) return false;
if (string.IsNullOrEmpty(logMsg.Message)) return false;
else return true;
};
options.MessageFormat = (logMsg) =>
{
//如果不是LoggingMonitor日志才格式化
if (logMsg.LogName != "System.Logging.LoggingMonitor")
{
var stringBuilder = new StringBuilder();
stringBuilder.AppendLine("【日志级别】:" + logMsg.LogLevel);
stringBuilder.AppendLine("【日志类名】:" + logMsg.LogName);
stringBuilder.AppendLine("【日志时间】:" + DateTime.Now.ToDefaultDateTimeFormat());
stringBuilder.AppendLine("【日志内容】:" + logMsg.Message);
if (logMsg.Exception != null)
{
stringBuilder.AppendLine("【异常信息】:" + logMsg.Exception);
}
return stringBuilder.ToString();
}
else
{
return logMsg.Message;
}
};
options.WriteHandler = (logMsg, scopeProvider, writer, fmtMsg, opt) =>
{
ConsoleColor consoleColor = ConsoleColor.White;
switch (logMsg.LogLevel)
{
case LogLevel.Information:
consoleColor = ConsoleColor.DarkGreen;
break;
case LogLevel.Warning:
consoleColor = ConsoleColor.DarkYellow;
break;
case LogLevel.Error:
consoleColor = ConsoleColor.DarkRed;
break;
}
writer.WriteWithColor(fmtMsg, ConsoleColor.Black, consoleColor);
};
});
#endregion
//日志写入数据库配置
services.AddDatabaseLogging<DatabaseLoggingWriter>(options =>
{
options.NameFilter = (name) =>
{
return (
name == "System.Logging.RequestAudit"
);
};
});
}
public void Use(IServiceProvider serviceProvider)
{
XTrace.UnhandledExceptionLogEnable = () => !App.HostApplicationLifetime.ApplicationStopping.IsCancellationRequested;
NewLife.Log.XTrace.UnhandledExceptionLogEnable = () => !App.HostApplicationLifetime.ApplicationStopping.IsCancellationRequested;
//检查ConfigId
var configIdGroup = DbContext.DbConfigs.GroupBy(it => it.ConfigId);

View File

@@ -18,6 +18,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Riok.Mapperly" Version="4.2.1" ExcludeAssets="runtime" PrivateAssets="all" />
<PackageReference Include="Rougamo.Fody" Version="5.0.1" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">

View File

@@ -21,7 +21,7 @@ namespace ThingsGateway.Admin.Application
Settings = new UserAgentSettings();
}
private MemoryCache MemoryCache { get; set; } = new();
private ICache MemoryCache => App.CacheService;
/// <summary>
/// Parses the specified user agent string.

View File

@@ -8,8 +8,6 @@
// QQ群605534569
//------------------------------------------------------------------------------
using Mapster;
using ThingsGateway.Admin.Application;
using ThingsGateway.NewLife;
using ThingsGateway.NewLife.Extension;
@@ -88,7 +86,7 @@ public class BlazorAppContext
if (UserManager.UserId > 0)
{
url = url.StartsWith('/') ? url : $"/{url}";
var sysResources = (await ResourceService.GetAllAsync()).Adapt<List<SysResource>>();
var sysResources = (await ResourceService.GetAllAsync()).AdaptListSysResource();
if (TitleLocalizer != null)
{
sysResources.ForEach(a =>
@@ -121,7 +119,7 @@ public class BlazorAppContext
CurrentModuleId = moduleId.Value;
}
UserWorkBench = await UserCenterService.GetLoginWorkbenchAsync(UserManager.UserId);
OwnMenus = (await UserCenterService.GetOwnMenuAsync(UserManager.UserId, 0)).Adapt<List<SysResource>>();
OwnMenus = (await UserCenterService.GetOwnMenuAsync(UserManager.UserId, 0)).AdaptListSysResource();
if (TitleLocalizer != null)
{

View File

@@ -8,8 +8,6 @@
// QQ群605534569
//------------------------------------------------------------------------------
using Mapster;
using Microsoft.AspNetCore.Components.Forms;
using ThingsGateway.Admin.Application;
@@ -30,7 +28,7 @@ public partial class AppConfigPage
protected override async Task OnParametersSetAsync()
{
AppConfig = (await SysDictService.GetAppConfigAsync()).Adapt<AppConfig>();
AppConfig = (await SysDictService.GetAppConfigAsync()).AdaptAppConfig();
await base.OnParametersSetAsync();
}

View File

@@ -8,8 +8,6 @@
// QQ群605534569
//------------------------------------------------------------------------------
using Mapster;
using ThingsGateway.Admin.Application;
namespace ThingsGateway.Admin.Razor;
@@ -47,10 +45,7 @@ public partial class RoleChoiceDialog
private ISysRoleService? SysRoleService { get; set; }
private async Task<QueryData<SysRole>> OnQueryAsync(QueryPageOptions options)
{
var data = await SysRoleService.PageAsync(options, a => a.Where(b => b.OrgId == OrgId));
QueryData<SysRole> queryData = data.Adapt<QueryData<SysRole>>();
return queryData;
return await SysRoleService.PageAsync(options, a => a.Where(b => b.OrgId == OrgId));
}
#region
private long OrgId { get; set; }

View File

@@ -8,8 +8,6 @@
// QQ群605534569
//------------------------------------------------------------------------------
using Mapster;
using ThingsGateway.Admin.Application;
namespace ThingsGateway.Admin.Razor;
@@ -56,7 +54,7 @@ public partial class UserChoiceDialog
OrgId = OrgId,
PositionId = PositionId,
});
QueryData<UserSelectorOutput> queryData = data.Adapt<QueryData<UserSelectorOutput>>();
QueryData<UserSelectorOutput> queryData = data.AdaptQueryDataUserSelectorOutput();
return queryData;
}

View File

@@ -8,8 +8,6 @@
// QQ群605534569
//------------------------------------------------------------------------------
using Mapster;
using Microsoft.AspNetCore.Components.Forms;
using ThingsGateway.Admin.Application;
@@ -37,9 +35,9 @@ public partial class UserCenterPage
protected override async Task OnParametersSetAsync()
{
SysUser = AppContext.CurrentUser.Adapt<SysUser>();
SysUser = AppContext.CurrentUser.AdaptSysUser();
SysUser.Avatar = AppContext.Avatar;
WorkbenchInfo = (await UserCenterService.GetLoginWorkbenchAsync(SysUser.Id)).Adapt<WorkbenchInfo>();
WorkbenchInfo = (await UserCenterService.GetLoginWorkbenchAsync(SysUser.Id)).AdaptWorkbenchInfo();
await base.OnParametersSetAsync();
}

View File

@@ -8,8 +8,6 @@
// QQ群605534569
//------------------------------------------------------------------------------
using Mapster;
using ThingsGateway.Admin.Application;
namespace ThingsGateway.Admin.Razor;
@@ -122,31 +120,4 @@ public static class ResourceUtil
return trees;
}
/// <summary>
/// 构建树节点
/// </summary>
public static List<TreeViewItem<T>> BuildTreeItemList<T>(IEnumerable<SysResource> sysresources, List<long> selectedItems, Microsoft.AspNetCore.Components.RenderFragment<T> render, long parentId = 0, TreeViewItem<T>? parent = null, Func<SysResource, bool> disableFunc = null) where T : class
{
if (sysresources == null) return null;
var trees = new List<TreeViewItem<T>>();
var roots = sysresources.Where(i => i.ParentId == parentId).OrderBy(i => i.SortCode);
foreach (var node in roots)
{
var item = new TreeViewItem<T>(node.Adapt<T>())
{
Text = node.Title,
Icon = node.Icon,
IsDisabled = disableFunc == null ? false : disableFunc(node),
IsActive = selectedItems.Any(v => node.Id == v),
IsExpand = true,
Parent = parent,
Template = render,
CheckedState = selectedItems.Any(i => i == node.Id) ? CheckboxState.Checked : CheckboxState.UnChecked
};
item.Items = BuildTreeItemList(sysresources, selectedItems, render, node.Id, item, disableFunc) ?? new();
trees.Add(item);
}
return trees;
}
}

View File

@@ -0,0 +1,22 @@
{
"Cache": {
"CacheType": "Memory", // 可选Memory 或 Redis
"MemoryCacheOptions": {
"Expire": 3600,
"Capacity": 100000,
"Period": 60
},
"RedisCacheOptions": {
"InstanceName": "ThingsGateway",
"Configuration": "server=127.0.0.1:6379;password=123456;db=3;timeout=3000",
"Server": "127.0.0.1:6379",
"Db": 3,
"UserName": "",
"Password": "123456",
"Timeout": 3000,
"Prefix": "ThingsGateway:"
}
}
}

View File

@@ -11,8 +11,6 @@
#pragma warning disable CA2007 // 考虑对等待的任务调用 ConfigureAwait
using BootstrapBlazor.Components;
using Mapster;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.Extensions.Localization;
@@ -71,7 +69,7 @@ public partial class Login
private async Task LoginAsync(EditContext context)
{
var model = loginModel.Adapt<LoginInput>();
var model = loginModel.AdaptLoginInput();
model.Password = DESEncryption.Encrypt(model.Password);
try
{

View File

@@ -21,14 +21,11 @@ using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Unicode;
using ThingsGateway.Admin.Application;
using ThingsGateway.Admin.Razor;
using ThingsGateway.Extension;
using ThingsGateway.NewLife.Caching;
using ThingsGateway.VirtualFileServer;
namespace ThingsGateway.AdminServer;
@@ -61,8 +58,6 @@ public class Startup : AppStartup
options.AddPersistence<JobPersistence>();
});
// 缓存
services.AddSingleton<ICache, MemoryCache>();
// 允许跨域
services.AddCorsAccessor();
@@ -153,101 +148,9 @@ public class Startup : AppStartup
#region
services.AddConsoleFormatter(options =>
{
options.WriteFilter = (logMsg) =>
{
if (App.HostApplicationLifetime.ApplicationStopping.IsCancellationRequested && logMsg.LogLevel >= LogLevel.Warning) return false;
if (string.IsNullOrEmpty(logMsg.Message)) return false;
else return true;
};
options.MessageFormat = (logMsg) =>
{
//如果不是LoggingMonitor日志才格式化
if (logMsg.LogName != "System.Logging.LoggingMonitor")
{
var stringBuilder = new StringBuilder();
stringBuilder.AppendLine("【日志级别】:" + logMsg.LogLevel);
stringBuilder.AppendLine("【日志类名】:" + logMsg.LogName);
stringBuilder.AppendLine("【日志时间】:" + DateTime.Now.ToDefaultDateTimeFormat());
stringBuilder.AppendLine("【日志内容】:" + logMsg.Message);
if (logMsg.Exception != null)
{
stringBuilder.AppendLine("【异常信息】:" + logMsg.Exception);
}
return stringBuilder.ToString();
}
else
{
return logMsg.Message;
}
};
options.WriteHandler = (logMsg, scopeProvider, writer, fmtMsg, opt) =>
{
ConsoleColor consoleColor = ConsoleColor.White;
switch (logMsg.LogLevel)
{
case LogLevel.Information:
consoleColor = ConsoleColor.DarkGreen;
break;
case LogLevel.Warning:
consoleColor = ConsoleColor.DarkYellow;
break;
case LogLevel.Error:
consoleColor = ConsoleColor.DarkRed;
break;
}
writer.WriteWithColor(fmtMsg, ConsoleColor.Black, consoleColor);
};
});
#endregion
#region api日志
//Monitor日志配置
//services.AddMonitorLogging(options =>
//{
// options.JsonIndented = true;// 是否美化 JSON
// options.GlobalEnabled = false;//全局启用
// options.ConfigureLogger((logger, logContext, context) =>
// {
// var httpContext = context.HttpContext;//获取httpContext
// //获取客户端信息
// var client = App.GetService<IAppService>().UserAgent;
// // 获取控制器/操作描述器
// var controllerActionDescriptor = context.ActionDescriptor as ControllerActionDescriptor;
// //操作名称默认是控制器名加方法名,自定义操作名称要在action上加Description特性
// var option = $"{controllerActionDescriptor.ControllerName}/{controllerActionDescriptor.ActionName}";
// var desc = App.CreateLocalizerByType(controllerActionDescriptor.ControllerTypeInfo.AsType())[controllerActionDescriptor.MethodInfo.Name];
// //获取特性
// option = desc.Value;//则将操作名称赋值为控制器上写的title
// logContext.Set(LoggingConst.CateGory, option);//传操作名称
// logContext.Set(LoggingConst.Operation, option);//传操作名称
// logContext.Set(LoggingConst.Client, client);//客户端信息
// logContext.Set(LoggingConst.Path, httpContext.Request.Path.Value);//请求地址
// logContext.Set(LoggingConst.Method, httpContext.Request.Method);//请求方法
// });
//});
//日志写入数据库配置
services.AddDatabaseLogging<DatabaseLoggingWriter>(options =>
{
options.WriteFilter = (logMsg) =>
{
return logMsg.LogName == "System.Logging.RequestAudit";
};
});
#endregion api日志
//已添加AddOptions
// 增加多语言支持配置信息
@@ -302,7 +205,7 @@ public class Startup : AppStartup
var certificate = new X509Certificate2("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
#endif
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo("keys"))
.PersistKeysToFileSystem(new DirectoryInfo("Keys"))
.ProtectKeysWithCertificate(certificate)
.UseCryptographicAlgorithms(new AuthenticatedEncryptorConfiguration
{

View File

@@ -45,9 +45,7 @@
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="$(NET9Version)" />
</ItemGroup>
<!--安装服务守护-->
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">

View File

@@ -11,8 +11,6 @@
using BootstrapBlazor.Components;
using Mapster;
using MiniExcelLibs;
using MiniExcelLibs.Attributes;
using MiniExcelLibs.OpenXml;
@@ -45,7 +43,7 @@ public static class ExportExcelExtensions
#region
var type = typeof(T);
var propertyInfos = type.GetRuntimeProperties().Where(a => a.GetCustomAttribute<IgnoreExcelAttribute>() == null)
var propertyInfos = type.GetRuntimeProperties().Where(a => a.GetCustomAttribute<IgnoreExcelAttribute>(false) == null)
.OrderBy(
a =>
{
@@ -104,7 +102,7 @@ public static class ExportExcelExtensions
int index = 0;
foreach (var item in data)
{
var ignore = item.GetCustomAttribute<IgnoreExcelAttribute>() != null;
var ignore = item.GetCustomAttribute<IgnoreExcelAttribute>(false) != null;
//描述
var desc = type.GetPropertyDisplayName(item.Name);
//数据源增加

View File

@@ -15,6 +15,8 @@ using System.Reflection;
using System.Text;
using ThingsGateway;
using ThingsGateway.NewLife.Caching;
using ThingsGateway.NewLife.Redis.Extensions;
using ThingsGateway.UnifyResult;
namespace Microsoft.Extensions.DependencyInjection;
@@ -198,11 +200,44 @@ public static class AppServiceCollectionExtensions
{
// 注册全局配置选项
services.AddConfigurableOptions<AppSettingsOptions>();
services.AddConfigurableOptions<CacheOptions>();
// 注册内存和分布式内存
services.AddMemoryCache();
services.AddDistributedMemoryCache();
var cacheOptions = App.GetConfig<CacheOptions>("Cache", true);
// 缓存
if (cacheOptions.CacheType == CacheType.Memory)
{
services.AddSingleton<ICache, MemoryCache>(a => new()
{
Capacity = cacheOptions.MemoryCacheOptions.Capacity,
Expire = cacheOptions.MemoryCacheOptions.Expire,
Period = cacheOptions.MemoryCacheOptions.Period
});
}
else if (cacheOptions.CacheType == CacheType.Redis)
{
services.AddDistributedRedisCache(options =>
{
options.Db = cacheOptions.RedisCacheOptions.Db;
options.Configuration = cacheOptions.RedisCacheOptions.Configuration;
options.UserName = cacheOptions.RedisCacheOptions.UserName;
options.Password = cacheOptions.RedisCacheOptions.Password;
options.Server = cacheOptions.RedisCacheOptions.Server;
options.Timeout = cacheOptions.RedisCacheOptions.Timeout;
options.Prefix = cacheOptions.RedisCacheOptions.Prefix;
options.InstanceName = cacheOptions.RedisCacheOptions.InstanceName;
options.Expire = cacheOptions.RedisCacheOptions.Expire;
});
}
// 注册全局依赖注入
services.AddDependencyInjection();
@@ -210,7 +245,7 @@ public static class AppServiceCollectionExtensions
services.AddStartups();
// 添加对象映射
services.AddObjectMapper();
//services.AddObjectMapper();
// 默认内置 GBKWindows-1252, Shift-JIS, GB2312 编码支持
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

View File

@@ -151,6 +151,8 @@ internal static class InternalApp
// 存储服务提供器
InternalServices = hostApplicationBuilder.Services;
// 存储根服务
hostApplicationBuilder.Services.AddHostedService<GenericHostLifetimeEventsHostedService>();

View File

@@ -93,6 +93,10 @@ public sealed class DatabaseLogger : ILogger, IDisposable
{
// 判断日志级别是否有效
if (!IsEnabled(logLevel)) return;
if (_options.NameFilter?.Invoke(_logName) == false)
{
return;
}
// 检查日志格式化器
if (formatter == null) throw new ArgumentNullException(nameof(formatter));

View File

@@ -51,7 +51,10 @@ public sealed class DatabaseLoggerOptions
/// 是否使用 UTC 时间戳,默认 false
/// </summary>
public bool UseUtcTimestamp { get; set; }
/// <summary>
/// 名称筛选
/// </summary>
public Func<string, bool> NameFilter { get; set; }
/// <summary>
/// 日期格式化
/// </summary>

View File

@@ -1,59 +1,59 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
//// ------------------------------------------------------------------------
//// 版权信息
//// 版权归百小僧及百签科技(广东)有限公司所有。
//// 所有权利保留。
//// 官方网站https://baiqian.com
////
//// 许可证信息
//// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
//// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
//// ------------------------------------------------------------------------
using Mapster;
//using Mapster;
using System.Reflection;
//using System.Reflection;
using ThingsGateway;
//using ThingsGateway;
namespace Microsoft.Extensions.DependencyInjection;
//namespace Microsoft.Extensions.DependencyInjection;
/// <summary>
/// 对象映射拓展类
/// </summary>
[SuppressSniffer]
public static class ObjectMapperServiceCollectionExtensions
{
///// <summary>
///// 对象映射拓展类
///// </summary>
//[SuppressSniffer]
//public static class ObjectMapperServiceCollectionExtensions
//{
/// <summary>
/// 添加对象映射
/// </summary>
/// <param name="services">服务集合</param>
/// <returns></returns>
public static IServiceCollection AddObjectMapper(this IServiceCollection services)
{
// 判断是否安装了 Mapster 程序集
return services.AddObjectMapper(App.Assemblies.ToArray());
}
// /// <summary>
// /// 添加对象映射
// /// </summary>
// /// <param name="services">服务集合</param>
// /// <returns></returns>
// public static IServiceCollection AddObjectMapper(this IServiceCollection services)
// {
// // 判断是否安装了 Mapster 程序集
// return services.AddObjectMapper(App.Assemblies.ToArray());
// }
/// <summary>
/// 添加对象映射
/// </summary>
/// <param name="services">服务集合</param>
/// <param name="assemblies">扫描的程序集</param>
/// <returns></returns>
public static IServiceCollection AddObjectMapper(this IServiceCollection services, params Assembly[] assemblies)
{
// 获取全局映射配置
var config = TypeAdapterConfig.GlobalSettings;
// /// <summary>
// /// 添加对象映射
// /// </summary>
// /// <param name="services">服务集合</param>
// /// <param name="assemblies">扫描的程序集</param>
// /// <returns></returns>
// public static IServiceCollection AddObjectMapper(this IServiceCollection services, params Assembly[] assemblies)
// {
// // 获取全局映射配置
// var config = TypeAdapterConfig.GlobalSettings;
//config.Compiler = exp => exp.CompileFast();
// //config.Compiler = exp => exp.CompileFast();
// 扫描所有继承 IRegister 接口的对象映射配置
if (assemblies?.Length > 0) config.Scan(assemblies);
// // 扫描所有继承 IRegister 接口的对象映射配置
// if (assemblies?.Length > 0) config.Scan(assemblies);
// 配置支持依赖注入
services.AddSingleton(config);
// // 配置支持依赖注入
// services.AddSingleton(config);
return services;
}
}
// return services;
// }
//}

View File

@@ -0,0 +1,66 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
using Microsoft.Extensions.Configuration;
using ThingsGateway.ConfigurableOptions;
using ThingsGateway.NewLife.Caching;
namespace ThingsGateway;
public enum CacheType
{
/// <summary>
/// 内存缓存
/// </summary>
Memory,
/// <summary>
/// Redis 缓存
/// </summary>
Redis
}
/// <summary>
/// 应用全局配置
/// </summary>
public sealed class CacheOptions : IConfigurableOptions<CacheOptions>
{
public CacheType CacheType { get; set; }
public MemoryCacheOptions MemoryCacheOptions { get; set; } = new MemoryCacheOptions();
public RedisCacheOptions RedisCacheOptions { get; set; } = new RedisCacheOptions();
/// <summary>
/// 后期配置
/// </summary>
/// <param name="options"></param>
/// <param name="configuration"></param>
public void PostConfigure(CacheOptions options, IConfiguration configuration)
{
}
}
public class MemoryCacheOptions
{
/// <summary>默认过期时间。避免Set操作时没有设置过期时间默认3600秒</summary>
public Int32 Expire { get; set; } = 3600;
/// <summary>容量。容量超标时采用LRU机制删除默认100_000</summary>
public Int32 Capacity { get; set; } = 100_000;
/// <summary>定时清理时间默认60秒</summary>
public Int32 Period { get; set; } = 60;
}
public class RedisCacheOptions : RedisOptions
{
}

View File

@@ -0,0 +1,208 @@

#if NET6_0_OR_GREATER
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using ThingsGateway.NewLife.Caching;
using ThingsGateway.NewLife.Caching.Services;
using ThingsGateway.NewLife.Configuration;
using ThingsGateway.NewLife.Extension;
using ThingsGateway.NewLife.Log;
namespace Microsoft.Extensions.DependencyInjection;
/// <summary>
/// DependencyInjectionExtensions
/// </summary>
public static class DependencyInjectionExtensions
{
/// <summary>注入FullRedis应用内可使用FullRedis/Redis/ICache/ICacheProvider</summary>
/// <param name="services"></param>
/// <param name="redis"></param>
/// <returns></returns>
public static IServiceCollection AddRedis(this IServiceCollection services, FullRedis? redis = null)
{
//if (redis == null) throw new ArgumentNullException(nameof(redis));
if (redis == null) return services.AddRedisCacheProvider();
services.AddBasic();
services.TryAddSingleton<ICache>(redis);
services.AddSingleton<Redis>(redis);
services.AddSingleton(redis);
// 注册Redis缓存服务
services.TryAddSingleton<ICacheProvider>(p =>
{
var provider = new RedisCacheProvider(p);
if (provider.Cache is not Redis) provider.Cache = redis;
provider.RedisQueue ??= redis;
return provider;
});
return services;
}
/// <summary>
/// Adds services for FullRedis to the specified Microsoft.Extensions.DependencyInjection.IServiceCollection.
/// </summary>
/// <param name="services"></param>
/// <param name="config"></param>
/// <param name="tracer"></param>
/// <returns></returns>
public static FullRedis AddRedis(this IServiceCollection services, String config, ITracer tracer = null!)
{
if (String.IsNullOrEmpty(config)) throw new ArgumentNullException(nameof(config));
var redis = new FullRedis();
redis.Init(config);
redis.Tracer = tracer;
services.AddRedis(redis);
return redis;
}
/// <summary>
/// Adds services for FullRedis to the specified Microsoft.Extensions.DependencyInjection.IServiceCollection.
/// </summary>
/// <param name="services"></param>
/// <param name="name"></param>
/// <param name="config"></param>
/// <param name="timeout"></param>
/// <param name="tracer"></param>
/// <returns></returns>
public static FullRedis AddRedis(this IServiceCollection services, String name, String config, Int32 timeout = 0, ITracer tracer = null!)
{
if (String.IsNullOrEmpty(config)) throw new ArgumentNullException(nameof(config));
var redis = new FullRedis();
if (!name.IsNullOrEmpty()) redis.Name = name;
redis.Init(config);
if (timeout > 0) redis.Timeout = timeout;
redis.Tracer = tracer;
services.AddRedis(redis);
return redis;
}
/// <summary>
/// Adds services for FullRedis to the specified Microsoft.Extensions.DependencyInjection.IServiceCollection.
/// </summary>
/// <param name="services"></param>
/// <param name="server"></param>
/// <param name="psssword"></param>
/// <param name="db"></param>
/// <param name="timeout"></param>
/// <param name="tracer"></param>
/// <returns></returns>
public static FullRedis AddRedis(this IServiceCollection services, String server, String psssword, Int32 db, Int32 timeout = 0, ITracer tracer = null!)
{
if (String.IsNullOrEmpty(server)) throw new ArgumentNullException(nameof(server));
var redis = new FullRedis(server, psssword, db);
if (timeout > 0) redis.Timeout = timeout;
redis.Tracer = tracer;
services.AddRedis(redis);
return redis;
}
/// <summary>添加Redis缓存</summary>
/// <param name="services"></param>
/// <param name="setupAction"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
public static IServiceCollection AddRedis(this IServiceCollection services, Action<RedisOptions> setupAction)
{
if (services == null)
throw new ArgumentNullException(nameof(services));
if (setupAction == null)
throw new ArgumentNullException(nameof(setupAction));
services.AddBasic();
services.AddOptions();
services.Configure(setupAction);
//services.Add(ServiceDescriptor.Singleton<ICache, FullRedis>());
services.AddSingleton(sp => new FullRedis(sp, sp.GetRequiredService<IOptions<RedisOptions>>().Value));
services.TryAddSingleton<ICache>(p => p.GetRequiredService<FullRedis>());
services.TryAddSingleton<Redis>(p => p.GetRequiredService<FullRedis>());
// 注册Redis缓存服务
services.TryAddSingleton<ICacheProvider>(p =>
{
var redis = p.GetRequiredService<FullRedis>();
var provider = new RedisCacheProvider(p);
if (provider.Cache is not Redis) provider.Cache = redis;
provider.RedisQueue ??= redis;
return provider;
});
return services;
}
/// <summary>
/// 添加键前缀的PrefixedRedis缓存
/// </summary>
/// <param name="services"></param>
/// <param name="setupAction"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
[Obsolete("=>AddRedis")]
public static IServiceCollection AddPrefixedRedis(this IServiceCollection services, Action<RedisOptions> setupAction)
{
if (services == null)
throw new ArgumentNullException(nameof(services));
if (setupAction == null)
throw new ArgumentNullException(nameof(setupAction));
services.AddBasic();
services.AddOptions();
services.Configure(setupAction);
services.AddSingleton(sp => new FullRedis(sp, sp.GetRequiredService<IOptions<RedisOptions>>().Value));
return services;
}
/// <summary>添加Redis缓存提供者ICacheProvider。从配置读取RedisCache和RedisQueue</summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddRedisCacheProvider(this IServiceCollection services)
{
services.AddBasic();
services.AddSingleton<ICacheProvider, RedisCacheProvider>();
services.TryAddSingleton<ICache>(p => p.GetRequiredService<ICacheProvider>().Cache);
services.TryAddSingleton<Redis>(p =>
{
var redis = p.GetRequiredService<ICacheProvider>().Cache as Redis;
if (redis == null) throw new InvalidOperationException("未配置Redis可在配置文件或配置中心指定名为RedisCache的连接字符串");
return redis;
});
services.TryAddSingleton<FullRedis>(p =>
{
var redis = p.GetRequiredService<ICacheProvider>().Cache as FullRedis;
if (redis == null) throw new InvalidOperationException("未配置Redis可在配置文件或配置中心指定名为RedisCache的连接字符串");
return redis;
});
return services;
}
static void AddBasic(this IServiceCollection services)
{
// 注册依赖项
services.TryAddSingleton<ILog>(XTrace.Log);
services.TryAddSingleton<ITracer>(DefaultTracer.Instance ??= new DefaultTracer());
if (!services.Any(e => e.ServiceType == typeof(IConfigProvider)))
services.TryAddSingleton<IConfigProvider>(JsonConfigProvider.LoadAppSettings());
}
}
#endif

View File

@@ -0,0 +1,116 @@
#if NET6_0_OR_GREATER
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Options;
using ThingsGateway.NewLife.Caching;
namespace ThingsGateway.NewLife.Redis.Extensions;
/// <summary>
/// Redis分布式缓存
/// </summary>
public class RedisCache : FullRedis, IDistributedCache, IDisposable
{
#region
/// <summary>刷新时的过期时间。默认24小时</summary>
public new TimeSpan Expire { get; set; } = TimeSpan.FromHours(24);
#endregion
#region
/// <summary>
/// 实例化Redis分布式缓存
/// </summary>
/// <param name="serviceProvider"></param>
/// <param name="optionsAccessor"></param>
/// <exception cref="ArgumentNullException"></exception>
public RedisCache(IServiceProvider serviceProvider, IOptions<RedisOptions> optionsAccessor) : base(serviceProvider, optionsAccessor.Value)
{
if (optionsAccessor == null) throw new ArgumentNullException(nameof(optionsAccessor));
}
#endregion
/// <summary>
/// 获取
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public Byte[]? Get(String key) => base.Get<Byte[]?>(key);
/// <summary>
/// 异步获取
/// </summary>
/// <param name="key"></param>
/// <param name="token"></param>
/// <returns></returns>
public Task<Byte[]?> GetAsync(String key, CancellationToken token = default) => Task.Run(() => base.Get<Byte[]>(key), token);
/// <summary>
/// 设置
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
/// <param name="options"></param>
/// <exception cref="ArgumentNullException"></exception>
public void Set(String key, Byte[] value, DistributedCacheEntryOptions options)
{
if (key == null) throw new ArgumentNullException(nameof(key));
if (value == null) throw new ArgumentNullException(nameof(value));
if (options == null)
base.Set(key, value);
else
if (options.AbsoluteExpiration != null)
base.Set(key, value, options.AbsoluteExpiration.Value - DateTime.Now);
else if (options.AbsoluteExpirationRelativeToNow != null)
base.Set(key, value, options.AbsoluteExpirationRelativeToNow.Value);
else if (options.SlidingExpiration != null)
base.Set(key, value, options.SlidingExpiration.Value);
else
base.Set(key, value);
}
/// <summary>
/// 异步设置
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
/// <param name="options"></param>
/// <param name="token"></param>
/// <returns></returns>
public Task SetAsync(String key, Byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default) => Task.Run(() => Set(key, value, options), token);
/// <summary>
/// 刷新
/// </summary>
/// <param name="key"></param>
/// <exception cref="ArgumentNullException"></exception>
public void Refresh(String key) => base.SetExpire(key, Expire);
/// <summary>
/// 异步刷新
/// </summary>
/// <param name="key"></param>
/// <param name="token"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
public Task RefreshAsync(String key, CancellationToken token = default) => Task.Run(() => Refresh(key), token);
/// <summary>
/// 删除
/// </summary>
/// <param name="key"></param>
public new void Remove(String key) => base.Remove(key);
/// <summary>
/// 异步删除
/// </summary>
/// <param name="key"></param>
/// <param name="token"></param>
/// <returns></returns>
public Task RemoveAsync(String key, CancellationToken token = default) => Task.Run(() => base.Remove(key), token);
}
#endif

View File

@@ -0,0 +1,63 @@
#if NET6_0_OR_GREATER
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using ThingsGateway.NewLife.Caching;
using ThingsGateway.NewLife.Caching.Services;
namespace ThingsGateway.NewLife.Redis.Extensions;
/// <summary>
/// Redis分布式缓存扩展
/// </summary>
public static class RedisCacheServiceCollectionExtensions
{
/// <summary>
/// 添加Redis分布式缓存应用内可使用RedisCache/FullRedis/Redis/IDistributedCache/ICache/ICacheProvider
/// </summary>
/// <param name="services"></param>
/// <param name="setupAction"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
public static IServiceCollection AddDistributedRedisCache(this IServiceCollection services, Action<RedisOptions> setupAction)
{
if (services == null)
throw new ArgumentNullException(nameof(services));
if (setupAction == null)
throw new ArgumentNullException(nameof(setupAction));
services.AddOptions();
services.Configure(setupAction);
services.AddSingleton(sp => new RedisCache(sp, sp.GetRequiredService<IOptions<RedisOptions>>()));
services.AddSingleton<IDistributedCache>(sp => sp.GetRequiredService<RedisCache>());
services.TryAddSingleton<FullRedis>(sp => sp.GetRequiredService<RedisCache>());
services.TryAddSingleton<ICache>(p =>
{
var result = p.GetRequiredService<RedisCache>();
Cache.Default = result;
return result;
});
services.TryAddSingleton<ThingsGateway.NewLife.Caching.Redis>(p => p.GetRequiredService<RedisCache>());
// 注册Redis缓存服务
services.TryAddSingleton(p =>
{
var redis = p.GetRequiredService<RedisCache>();
var provider = new RedisCacheProvider(p);
if (provider.Cache is not ThingsGateway.NewLife.Caching.Redis) provider.Cache = redis;
provider.RedisQueue ??= redis;
return provider;
});
return services;
}
}
#endif

View File

@@ -0,0 +1,72 @@
#if NET6_0_OR_GREATER
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.Extensions.DependencyInjection;
namespace ThingsGateway.NewLife.Redis.Extensions;
/// <summary>Redis数据保护扩展</summary>
public static class RedisDataProtectionBuilderExtensions
{
private const String DataProtectionKeysName = "DataProtection-Keys";
///// <summary>存储数据保护Key到Redis自动识别已注入到容器的FullRedis或Redis单例</summary>
///// <param name="builder"></param>
///// <returns></returns>
///// <exception cref="ArgumentNullException"></exception>
//public static IDataProtectionBuilder PersistKeysToRedis(this IDataProtectionBuilder builder)
//{
// if (builder == null) throw new ArgumentNullException(nameof(builder));
// var redis = builder.Services.LastOrDefault(e => e.ServiceType == typeof(FullRedis))?.ImplementationInstance as NewLife.Caching.Redis;
// redis ??= builder.Services.LastOrDefault(e => e.ServiceType == typeof(NewLife.Caching.Redis))?.ImplementationInstance as NewLife.Caching.Redis;
// if (redis == null) throw new ArgumentNullException(nameof(redis));
// return PersistKeysToRedisInternal(builder, redis, DataProtectionKeysName);
//}
/// <summary>存储数据保护Key到Redis</summary>
/// <param name="builder"></param>
/// <param name="redis"></param>
/// <param name="key"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
public static IDataProtectionBuilder PersistKeysToRedis(this IDataProtectionBuilder builder, ThingsGateway.NewLife.Caching.Redis redis, String key = DataProtectionKeysName)
{
if (builder == null) throw new ArgumentNullException(nameof(builder));
if (redis == null) throw new ArgumentNullException(nameof(redis));
return PersistKeysToRedisInternal(builder, redis, key);
}
private static IDataProtectionBuilder PersistKeysToRedisInternal(IDataProtectionBuilder builder, ThingsGateway.NewLife.Caching.Redis redis, String key)
{
builder.Services.Configure(delegate (KeyManagementOptions options)
{
options.XmlRepository = new RedisXmlRepository(redis, key);
});
return builder;
}
/// <summary>存储数据保护Key到Redis</summary>
/// <param name="builder"></param>
/// <param name="redisFactory"></param>
/// <param name="key"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
public static IDataProtectionBuilder PersistKeysToRedis(this IDataProtectionBuilder builder, Func<ThingsGateway.NewLife.Caching.Redis> redisFactory, String key = DataProtectionKeysName)
{
if (builder == null) throw new ArgumentNullException(nameof(builder));
if (redisFactory == null) throw new ArgumentNullException(nameof(redisFactory));
builder.Services.Configure(delegate (KeyManagementOptions options)
{
options.XmlRepository = new RedisXmlRepository(redisFactory, key);
});
return builder;
}
}
#endif

View File

@@ -0,0 +1,69 @@
#if NET6_0_OR_GREATER
using Microsoft.AspNetCore.DataProtection.Repositories;
using System.Xml.Linq;
using ThingsGateway.NewLife.Log;
namespace ThingsGateway.NewLife.Redis.Extensions;
/// <summary>在Redis中存储Xml</summary>
public class RedisXmlRepository : IXmlRepository
{
private readonly ThingsGateway.NewLife.Caching.Redis? _redis;
private readonly Func<ThingsGateway.NewLife.Caching.Redis>? _redisFactory;
private readonly String _key;
/// <summary>实例化</summary>
/// <param name="redis"></param>
/// <param name="key"></param>
public RedisXmlRepository(ThingsGateway.NewLife.Caching.Redis redis, String key)
{
_redis = redis;
_key = key;
XTrace.WriteLine("DataProtection使用Redis持久化密钥Key={0}", key);
}
/// <summary>实例化</summary>
/// <param name="redisFactory"></param>
/// <param name="key"></param>
public RedisXmlRepository(Func<ThingsGateway.NewLife.Caching.Redis> redisFactory, String key)
{
_redisFactory = redisFactory;
_key = key;
XTrace.WriteLine("DataProtection使用Redis持久化密钥Key={0}", key);
}
/// <summary>获取所有元素</summary>
/// <returns></returns>
public IReadOnlyCollection<XElement> GetAllElements() => GetAllElementsCore().ToList().AsReadOnly();
/// <summary>遍历元素</summary>
/// <returns></returns>
private IEnumerable<XElement> GetAllElementsCore()
{
var rds = _redis ?? _redisFactory!();
var list = rds.GetList<String>(_key) ?? [];
foreach (var item in list)
{
yield return XElement.Parse(item);
}
}
/// <summary>存储元素</summary>
/// <param name="element"></param>
/// <param name="friendlyName"></param>
public void StoreElement(XElement element, String friendlyName)
{
var rds = _redis ?? _redisFactory!();
var list = rds.GetList<String>(_key);
list.Add(element.ToString(SaveOptions.DisableFormatting));
}
}
#endif

View File

@@ -36,7 +36,7 @@
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
<!--<PackageReference Include="FastExpressionCompiler" Version="5.3.0" />-->
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageReference Include="Mapster" Version="7.4.0" />
<!--<PackageReference Include="Mapster" Version="7.4.0" />-->
<PackageReference Include="MiniProfiler.AspNetCore.Mvc" Version="4.5.4" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.1" />
</ItemGroup>

View File

@@ -0,0 +1,27 @@
namespace ThingsGateway.NewLife.Algorithms;
/// <summary>
/// 对齐模型。数据采样时X轴对齐
/// </summary>
public enum AlignModes
{
/// <summary>
/// 不对齐,原始值
/// </summary>
None,
/// <summary>
/// 左对齐
/// </summary>
Left,
/// <summary>
/// 中间对齐
/// </summary>
Center,
/// <summary>
/// 右对齐
/// </summary>
Right,
}

View File

@@ -0,0 +1,116 @@
using ThingsGateway.NewLife.Data;
namespace ThingsGateway.NewLife.Algorithms;
/// <summary>
/// 平均值采样算法
/// </summary>
public class AverageSampling : ISampling
{
/// <summary>
/// 对齐模式。每个桶X轴对齐方式
/// </summary>
public AlignModes AlignMode { get; set; }
/// <summary>
/// 插值填充算法
/// </summary>
public IInterpolation? Interpolation { get; set; } = new LinearInterpolation();
/// <summary>
/// 降采样处理。保留边界两个点
/// </summary>
/// <param name="data">原始数据</param>
/// <param name="threshold">阈值,采样数</param>
/// <returns></returns>
public TimePoint[] Down(TimePoint[] data, Int32 threshold)
{
//if (data == null || data.Length < 2) return data;
if (data.Length < 2) return data;
if (threshold < 2 || threshold >= data.Length) return data;
var buckets = SamplingHelper.SplitByAverage(data.Length, threshold, true);
// 每个桶选择一个点作为代表
var sampled = new TimePoint[buckets.Length];
for (var i = 0; i < buckets.Length; i++)
{
var item = buckets[i];
TimePoint point = default;
var vs = 0.0;
for (var j = item.Start; j < item.End; j++)
{
vs += data[j].Value;
}
point.Value = vs / (item.End - item.Start);
// 对齐
point.Time = AlignMode switch
{
AlignModes.Right => data[item.End - 1].Time,
AlignModes.Center => data[(Int32)Math.Round((item.Start + item.End - 1) / 2.0)].Time,
_ => data[item.Start].Time,
};
sampled[i] = point;
}
return sampled;
}
/// <summary>
/// 混合处理,降采样和插值,不保留边界节点
/// </summary>
/// <param name="data">原始数据</param>
/// <param name="size">桶大小。如60/3600/86400</param>
/// <param name="offset">偏移量。时间不是对齐零点时使用</param>
/// <returns></returns>
public TimePoint[] Process(TimePoint[] data, Int32 size, Int32 offset = 0)
{
//if (data == null || data.Length < 2) return data;
if (data.Length < 2) return data;
if (size <= 1) return data;
if (Interpolation == null) throw new ArgumentNullException(nameof(Interpolation));
var xs = new Int64[data.Length];
for (var i = 0; i < data.Length; i++) xs[i] = data[i].Time;
var buckets = SamplingHelper.SplitByFixedSize(xs, size, offset);
// 每个桶选择一个点作为代表
var sampled = new TimePoint[buckets.Length];
var last = 0;
for (var i = 0; i < buckets.Length; i++)
{
// 断层,插值
var item = buckets[i];
if (item.Start < 0)
{
// 取last用于插值起点如果不存在可以取0点
// 此时End指向下一个有效点即使下一个桶也是断层
sampled[i].Time = i * size;
sampled[i].Value = Interpolation.Process(data, last, item.End, i);
continue;
}
TimePoint point = default;
var vs = 0.0;
for (var j = item.Start; j < item.End; j++)
{
vs += data[j].Value;
}
last = item.End - 1;
point.Value = vs / (item.End - item.Start);
// 对齐
point.Time = AlignMode switch
{
AlignModes.Right => (i + 1) * size - 1,
AlignModes.Center => data[(Int32)Math.Round((i + 0.5) * size)].Time,
_ => i * size,
};
sampled[i] = point;
}
return sampled;
}
}

View File

@@ -0,0 +1,19 @@
using ThingsGateway.NewLife.Data;
namespace ThingsGateway.NewLife.Algorithms;
/// <summary>
/// 插值算法
/// </summary>
public interface IInterpolation
{
/// <summary>
/// 插值处理
/// </summary>
/// <param name="data">数据</param>
/// <param name="prev">上一个点索引</param>
/// <param name="next">下一个点索引</param>
/// <param name="current">当前点时间值</param>
/// <returns></returns>
Double Process(TimePoint[] data, Int32 prev, Int32 next, Int64 current);
}

View File

@@ -0,0 +1,138 @@
using ThingsGateway.NewLife.Data;
namespace ThingsGateway.NewLife.Algorithms;
/// <summary>
/// 采样接口。负责降采样和插值处理,用于处理时序数据
/// </summary>
public interface ISampling
{
/// <summary>
/// 对齐模式。每个桶X轴对齐方式
/// </summary>
AlignModes AlignMode { get; set; }
/// <summary>
/// 插值填充算法
/// </summary>
IInterpolation? Interpolation { get; set; }
/// <summary>
/// 降采样处理
/// </summary>
/// <param name="data">原始数据</param>
/// <param name="threshold">阈值,采样数</param>
/// <returns></returns>
TimePoint[] Down(TimePoint[] data, Int32 threshold);
/// <summary>
/// 混合处理,降采样和插值
/// </summary>
/// <param name="data">原始数据</param>
/// <param name="size">桶大小。如60/3600/86400</param>
/// <param name="offset">偏移量。时间不是对齐零点时使用</param>
/// <returns></returns>
TimePoint[] Process(TimePoint[] data, Int32 size, Int32 offset = 0);
}
/// <summary>
/// 采样助手
/// </summary>
public static class SamplingHelper
{
/// <summary>
/// 按照指定桶数平均分,可指定保留头尾
/// </summary>
/// <param name="dataLength"></param>
/// <param name="threshold"></param>
/// <param name="retainEdge"></param>
/// <returns></returns>
public static IndexRange[] SplitByAverage(Int32 dataLength, Int32 threshold, Boolean retainEdge = true)
{
if (dataLength == 0) throw new ArgumentNullException(nameof(dataLength));
if (threshold <= 2) throw new ArgumentNullException(nameof(threshold));
var buckets = new IndexRange[threshold];
if (retainEdge)
{
var step = (Double)(dataLength - 2) / (threshold - 2);
var v = 0d;
for (var i = 1; i < threshold - 1; i++)
{
buckets[i].Start = (Int32)Math.Round(v) + 1;
buckets[i].End = (Int32)Math.Round(v += step) + 1;
if (buckets[i].End > dataLength - 1) buckets[i].End = dataLength - 1;
}
buckets[0].Start = 0;
buckets[0].End = 1;
buckets[threshold - 1].Start = dataLength - 1;
buckets[threshold - 1].End = dataLength - 1 + 1;
}
else
{
var step = (Double)dataLength / threshold;
var v = 0d;
for (var i = 0; i < threshold; i++)
{
buckets[i].Start = (Int32)Math.Round(v);
buckets[i].End = (Int32)Math.Round(v += step);
if (buckets[i].End > dataLength) buckets[i].End = dataLength;
}
}
return buckets;
}
/// <summary>
/// 按照固定时间间隔,拆分数据轴为多个桶
/// </summary>
/// <param name="data">原始数据</param>
/// <param name="size">桶大小。如60/3600/86400</param>
/// <param name="offset">偏移量。时间不是对齐零点时使用</param>
/// <returns></returns>
public static IndexRange[] SplitByFixedSize(Int64[] data, Int32 size, Int32 offset = 0)
{
if (data == null || data.Length == 0) throw new ArgumentNullException(nameof(data));
if (size <= 0) throw new ArgumentNullException(nameof(size));
if (offset >= size) throw new ArgumentOutOfRangeException(nameof(offset));
// 计算首尾的两个桶的值
var start = data[0] / size * size + offset;
if (start > data[0]) start -= size;
var last = data[^1];
var end = last / size * size + offset;
if (end > last) end -= size;
var buckets = new IndexRange[(end - start) / size + 1];
// 计算每个桶的头尾
var p = 0;
var idx = 0;
for (var time = start; time <= end; p++)
{
IndexRange r = default;
r.Start = -1;
r.End = -1;
var next = time + size;
// 顺序遍历原始数据,这里假设原始数据为升序
for (; idx < data.Length; idx++)
{
// 如果超过了当前桶的结尾,则换下一个桶
if (data[idx] >= next)
{
r.End = idx;
break;
}
if (r.Start < 0 && time <= data[idx]) r.Start = idx;
}
if (r.End < 0) r.End = idx;
buckets[p] = r;
time = next;
}
return buckets.ToArray();
}
}

View File

@@ -0,0 +1,23 @@
using ThingsGateway.NewLife.Data;
namespace ThingsGateway.NewLife.Algorithms;
/// <summary>
/// 线性插值
/// </summary>
public class LinearInterpolation : IInterpolation
{
/// <summary>
/// 插值处理
/// </summary>
/// <param name="data">数据</param>
/// <param name="prev">上一个点索引</param>
/// <param name="next">下一个点索引</param>
/// <param name="current">当前点时间值</param>
/// <returns></returns>
public Double Process(TimePoint[] data, Int32 prev, Int32 next, Int64 current)
{
var dt = (data[next].Value - data[prev].Value) / (data[next].Time - data[prev].Time);
return data[prev].Value + (current - data[prev].Time) * dt;
}
}

View File

@@ -1,107 +0,0 @@
using System.Runtime.CompilerServices;
namespace ThingsGateway.NewLife.Buffers;
internal sealed class BufferSegment : ReadOnlySequenceSegment<Byte>
{
private IMemoryOwner<Byte>? _memoryOwner;
private Byte[]? _array;
private BufferSegment? _next;
private Int32 _end;
public Int32 End
{
get
{
return _end;
}
set
{
_end = value;
base.Memory = AvailableMemory[..value];
}
}
public BufferSegment? NextSegment
{
get
{
return _next;
}
set
{
base.Next = value;
_next = value;
}
}
internal Object? MemoryOwner => ((Object)_memoryOwner) ?? ((Object)_array);
public Memory<Byte> AvailableMemory { get; private set; }
public Int32 Length => End;
public Int32 WritableBytes
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => AvailableMemory.Length - End;
}
public void SetOwnedMemory(IMemoryOwner<Byte> memoryOwner)
{
_memoryOwner = memoryOwner;
AvailableMemory = memoryOwner.Memory;
}
public void SetOwnedMemory(Byte[] arrayPoolBuffer)
{
_array = arrayPoolBuffer;
AvailableMemory = arrayPoolBuffer;
}
public void Reset()
{
ResetMemory();
base.Next = null;
base.RunningIndex = 0L;
_next = null;
}
public void ResetMemory()
{
var memoryOwner = _memoryOwner;
if (memoryOwner != null)
{
_memoryOwner = null;
memoryOwner.Dispose();
}
else if (_array != null)
{
ArrayPool<Byte>.Shared.Return(_array);
_array = null;
}
base.Memory = default;
_end = 0;
AvailableMemory = default;
}
public void SetNext(BufferSegment segment)
{
NextSegment = segment;
segment = this;
while (segment.Next != null)
{
segment.NextSegment.RunningIndex = segment.RunningIndex + segment.Length;
segment = segment.NextSegment;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static Int64 GetLength(BufferSegment startSegment, Int32 startIndex, BufferSegment endSegment, Int32 endIndex) => endSegment.RunningIndex + (UInt32)endIndex - (startSegment.RunningIndex + (UInt32)startIndex);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static Int64 GetLength(Int64 startPosition, BufferSegment endSegment, Int32 endIndex) => endSegment.RunningIndex + (UInt32)endIndex - startPosition;
}

View File

@@ -38,7 +38,7 @@ public static class SpanHelper
{
if (bytes.IsEmpty) return String.Empty;
#if NET45
#if NET452
return encoding.GetString(bytes.ToArray());
#else
fixed (Byte* bytes2 = &MemoryMarshal.GetReference(bytes))

View File

@@ -0,0 +1,377 @@
using System.Buffers.Binary;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using ThingsGateway.NewLife.Data;
namespace ThingsGateway.NewLife.Buffers;
/// <summary>Span读取器</summary>
/// <remarks>
/// 引用结构的Span读取器确保高性能无GC读取。
/// 支持Stream扩展当数据不足时自动从数据流中读取常用于解析Redis/MySql等协议。
/// </remarks>
public ref struct SpanReader
{
#region
private ReadOnlySpan<Byte> _span;
/// <summary>数据片段</summary>
public ReadOnlySpan<Byte> Span => _span;
private Int32 _index;
/// <summary>已读取字节数</summary>
public Int32 Position { get => _index; set => _index = value; }
/// <summary>总容量</summary>
public Int32 Capacity => _span.Length;
/// <summary>空闲容量</summary>
public Int32 FreeCapacity => _span.Length - _index;
/// <summary>是否小端字节序。默认true</summary>
public Boolean IsLittleEndian { get; set; } = true;
#endregion
#region
/// <summary>实例化。暂时兼容旧版,后面使用主构造函数</summary>
/// <param name="span"></param>
public SpanReader(ReadOnlySpan<Byte> span) => _span = span;
/// <summary>实例化。暂时兼容旧版,后面删除</summary>
/// <param name="span"></param>
public SpanReader(Span<Byte> span) => _span = span;
/// <summary>实例化Span读取器</summary>
/// <param name="data"></param>
public SpanReader(IPacket data)
{
_data = data;
_span = data.GetSpan();
_total = data.Total;
}
#endregion
#region
/// <summary>最大容量。多次从数据流读取数据时,受限于此最大值</summary>
public Int32 MaxCapacity { get; set; }
private readonly Stream? _stream;
private readonly Int32 _bufferSize;
private IPacket? _data;
private Int32 _total;
/// <summary>实例化Span读取器。支持从数据流中读取更多数据突破大小限制</summary>
/// <remarks>
/// 解析网络协议时,有时候数据帧较大,超过特定缓冲区大小,导致无法一次性读取完整数据帧。
/// 加入数据流参数后在读取数据不足时SpanReader会自动从数据流中读取一批数据。
/// </remarks>
/// <param name="stream">数据流。一般是网络流</param>
/// <param name="data"></param>
/// <param name="bufferSize"></param>
public SpanReader(Stream stream, IPacket? data = null, Int32 bufferSize = 8192)
{
_stream = stream;
_bufferSize = bufferSize;
if (data != null)
{
_data = data;
_span = data.GetSpan();
_total = data.Total;
}
}
#endregion
#region
/// <summary>告知有多少数据已从缓冲区读取</summary>
/// <param name="count"></param>
public void Advance(Int32 count)
{
if (count < 0) throw new ArgumentOutOfRangeException(nameof(count));
if (count > 0) EnsureSpace(count);
if (_index + count > _span.Length) throw new ArgumentOutOfRangeException(nameof(count));
_index += count;
}
/// <summary>返回要写入到的Span其大小按 sizeHint 参数指定至少为所请求的大小</summary>
/// <param name="sizeHint"></param>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public ReadOnlySpan<Byte> GetSpan(Int32 sizeHint = 0)
{
if (sizeHint > FreeCapacity) throw new ArgumentOutOfRangeException(nameof(sizeHint));
return _span[_index..];
}
#endregion
#region
/// <summary>确保缓冲区中有足够的空间。</summary>
/// <param name="size">需要的字节数。</param>
/// <exception cref="InvalidOperationException"></exception>
public void EnsureSpace(Int32 size)
{
// 检查剩余空间大小不足时再从数据流中读取。此时需要注意创建新的OwnerPacket后需要先把之前剩余的一点数据拷贝过去然后再读取Stream
var remain = FreeCapacity;
if (remain < size && _stream != null)
{
// 申请指定大小的数据包缓冲区,至少达到缓冲区大小,但不超过最大容量
var idx = 0;
var bsize = size;
if (MaxCapacity > 0)
{
if (bsize < _bufferSize) bsize = _bufferSize;
if (bsize > MaxCapacity - _total) bsize = MaxCapacity - _total;
}
var pk = new OwnerPacket(bsize);
if (_data != null && remain > 0)
{
if (!_data.TryGetArray(out var arr)) throw new NotSupportedException();
arr.AsSpan(_index, remain).CopyTo(pk.Buffer);
idx += remain;
}
_data.TryDispose();
_data = pk;
_index = 0;
// 多次读取,直到满足需求。不要超过最大容量,否则可能读取到下一个数据帧的数据
_stream.ReadExactly(pk.Buffer, pk.Offset + idx, pk.Length - idx);
idx = pk.Length;
//while (idx < size)
//{
// var n = _stream.Read(pk.Buffer, pk.Offset + idx, pk.Length - idx);
// if (n <= 0) break;
// idx += n;
//}
if (idx < size)
throw new InvalidOperationException("Not enough data to read.");
pk.Resize(idx);
_span = pk.GetSpan();
_total += idx - remain;
}
if (_index + size > _span.Length)
throw new InvalidOperationException("Not enough data to read.");
}
/// <summary>读取单个字节</summary>
/// <returns></returns>
public Byte ReadByte()
{
var size = sizeof(Byte);
EnsureSpace(size);
var result = _span[_index];
_index += size;
return result;
}
/// <summary>读取Int16整数</summary>
/// <returns></returns>
public Int16 ReadInt16()
{
var size = sizeof(Int16);
EnsureSpace(size);
var result = IsLittleEndian ?
BinaryPrimitives.ReadInt16LittleEndian(_span.Slice(_index, size)) :
BinaryPrimitives.ReadInt16BigEndian(_span.Slice(_index, size));
_index += size;
return result;
}
/// <summary>读取UInt16整数</summary>
/// <returns></returns>
public UInt16 ReadUInt16()
{
var size = sizeof(UInt16);
EnsureSpace(size);
var result = IsLittleEndian ?
BinaryPrimitives.ReadUInt16LittleEndian(_span.Slice(_index, size)) :
BinaryPrimitives.ReadUInt16BigEndian(_span.Slice(_index, size));
_index += size;
return result;
}
/// <summary>读取Int32整数</summary>
/// <returns></returns>
public Int32 ReadInt32()
{
var size = sizeof(Int32);
EnsureSpace(size);
var result = IsLittleEndian ?
BinaryPrimitives.ReadInt32LittleEndian(_span.Slice(_index, size)) :
BinaryPrimitives.ReadInt32BigEndian(_span.Slice(_index, size));
_index += size;
return result;
}
/// <summary>读取UInt32整数</summary>
/// <returns></returns>
public UInt32 ReadUInt32()
{
var size = sizeof(UInt32);
EnsureSpace(size);
var result = IsLittleEndian ?
BinaryPrimitives.ReadUInt32LittleEndian(_span.Slice(_index, size)) :
BinaryPrimitives.ReadUInt32BigEndian(_span.Slice(_index, size));
_index += size;
return result;
}
/// <summary>读取Int64整数</summary>
/// <returns></returns>
public Int64 ReadInt64()
{
var size = sizeof(Int64);
EnsureSpace(size);
var result = IsLittleEndian ?
BinaryPrimitives.ReadInt64LittleEndian(_span.Slice(_index, size)) :
BinaryPrimitives.ReadInt64BigEndian(_span.Slice(_index, size));
_index += size;
return result;
}
/// <summary>读取UInt64整数</summary>
/// <returns></returns>
public UInt64 ReadUInt64()
{
var size = sizeof(UInt64);
EnsureSpace(size);
var result = IsLittleEndian ?
BinaryPrimitives.ReadUInt64LittleEndian(_span.Slice(_index, size)) :
BinaryPrimitives.ReadUInt64BigEndian(_span.Slice(_index, size));
_index += size;
return result;
}
/// <summary>读取单精度浮点数</summary>
/// <returns></returns>
public unsafe Single ReadSingle()
{
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP
return BitConverter.Int32BitsToSingle(ReadInt32());
#else
var result = ReadInt32();
return Unsafe.ReadUnaligned<Single>(ref Unsafe.As<Int32, Byte>(ref result));
#endif
}
/// <summary>读取双精度浮点数</summary>
/// <returns></returns>
public Double ReadDouble()
{
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP
return BitConverter.Int64BitsToDouble(ReadInt64());
#else
var result = ReadInt64();
return Unsafe.ReadUnaligned<Double>(ref Unsafe.As<Int64, Byte>(ref result));
#endif
}
/// <summary>读取字符串。支持定长、全部和长度前缀</summary>
/// <param name="length">需要读取的长度。-1表示读取全部默认0表示读取7位压缩编码整数长度</param>
/// <param name="encoding">字符串编码默认UTF8</param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
public String ReadString(Int32 length = 0, Encoding? encoding = null)
{
if (length < 0)
length = _span.Length - _index;
else if (length == 0)
length = ReadEncodedInt();
if (length == 0) return String.Empty;
EnsureSpace(length);
encoding ??= Encoding.UTF8;
var result = encoding.GetString(_span.Slice(_index, length));
_index += length;
return result;
}
/// <summary>读取字节数组</summary>
/// <param name="length"></param>
/// <returns></returns>
public ReadOnlySpan<Byte> ReadBytes(Int32 length)
{
EnsureSpace(length);
var result = _span.Slice(_index, length);
_index += length;
return result;
}
/// <summary>读取字节数组</summary>
/// <param name="data"></param>
/// <returns></returns>
public Int32 Read(Span<Byte> data)
{
var length = data.Length;
EnsureSpace(length);
var result = _span.Slice(_index, length);
result.CopyTo(data);
_index += length;
return length;
}
/// <summary>读取数据包。直接对内部数据包进行切片</summary>
/// <param name="length"></param>
/// <returns></returns>
public IPacket ReadPacket(Int32 length)
{
if (_data == null) throw new InvalidOperationException("No data stream to read!");
//EnsureSpace(length);
var result = _data.Slice(_index, length);
_index += length;
return result;
}
/// <summary>读取结构体</summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T Read<T>() where T : struct
{
var size = Unsafe.SizeOf<T>();
EnsureSpace(size);
var result = MemoryMarshal.Read<T>(_span.Slice(_index));
_index += size;
return result;
}
#endregion
#region
/// <summary>以压缩格式读取32位整数</summary>
/// <returns></returns>
public Int32 ReadEncodedInt()
{
Byte b;
UInt32 rs = 0;
Byte n = 0;
while (true)
{
var bt = ReadByte();
b = (Byte)bt;
// 必须转为Int32否则可能溢出
rs |= (UInt32)((b & 0x7f) << n);
if ((b & 0x80) == 0) break;
n += 7;
if (n >= 32) throw new FormatException("The number value is too large to read in compressed format!");
}
return (Int32)rs;
}
#endregion
}

View File

@@ -0,0 +1,338 @@
using System.Buffers.Binary;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using ThingsGateway.NewLife.Collections;
using ThingsGateway.NewLife.Data;
namespace ThingsGateway.NewLife.Buffers;
/// <summary>Span写入器</summary>
/// <param name="buffer"></param>
public ref struct SpanWriter(Span<Byte> buffer)
{
#region
private readonly Span<Byte> _span = buffer;
/// <summary>数据片段</summary>
public Span<Byte> Span => _span;
private Int32 _index;
/// <summary>已写入字节数</summary>
public Int32 Position { get => _index; set => _index = value; }
/// <summary>总容量</summary>
public readonly Int32 Capacity => _span.Length;
/// <summary>空闲容量</summary>
public readonly Int32 FreeCapacity => _span.Length - _index;
/// <summary>是否小端字节序。默认true</summary>
public Boolean IsLittleEndian { get; set; } = true;
#endregion
#region
/// <summary>实例化Span读取器</summary>
/// <param name="data"></param>
public SpanWriter(IPacket data) : this(data.GetSpan()) { }
#endregion
#region
/// <summary>告知有多少数据已写入缓冲区</summary>
/// <param name="count"></param>
public void Advance(Int32 count)
{
if (count < 0) throw new ArgumentOutOfRangeException(nameof(count));
if (_index + count > _span.Length) throw new ArgumentOutOfRangeException(nameof(count));
_index += count;
}
/// <summary>返回要写入到的Span其大小按 sizeHint 参数指定至少为所请求的大小</summary>
/// <param name="sizeHint"></param>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public readonly Span<Byte> GetSpan(Int32 sizeHint = 0)
{
if (sizeHint > FreeCapacity) throw new ArgumentOutOfRangeException(nameof(sizeHint));
return _span[_index..];
}
#endregion
#region
/// <summary>确保缓冲区中有足够的空间。</summary>
/// <param name="size">需要的字节数。</param>
private readonly void EnsureSpace(Int32 size)
{
if (_index + size > _span.Length)
throw new InvalidOperationException("Not enough data to write.");
}
/// <summary>写入字节</summary>
public Int32 WriteByte(Int32 value) => Write((Byte)value);
/// <summary>写入字节</summary>
/// <param name="value">要写入的字节值。</param>
public Int32 Write(Byte value)
{
var size = sizeof(Byte);
EnsureSpace(size);
_span[_index] = value;
_index += size;
return size;
}
/// <summary>写入 16 位整数。</summary>
/// <param name="value">要写入的整数值。</param>
public Int32 Write(Int16 value)
{
var size = sizeof(Int16);
EnsureSpace(size);
if (IsLittleEndian)
BinaryPrimitives.WriteInt16LittleEndian(_span[_index..], value);
else
BinaryPrimitives.WriteInt16BigEndian(_span[_index..], value);
_index += size;
return size;
}
/// <summary>写入无符号 16 位整数。</summary>
/// <param name="value">要写入的无符号整数值。</param>
public Int32 Write(UInt16 value)
{
var size = sizeof(UInt16);
EnsureSpace(size);
if (IsLittleEndian)
BinaryPrimitives.WriteUInt16LittleEndian(_span[_index..], value);
else
BinaryPrimitives.WriteUInt16BigEndian(_span[_index..], value);
_index += size;
return size;
}
/// <summary>写入 32 位整数。</summary>
/// <param name="value">要写入的整数值。</param>
public Int32 Write(Int32 value)
{
var size = sizeof(Int32);
EnsureSpace(size);
if (IsLittleEndian)
BinaryPrimitives.WriteInt32LittleEndian(_span[_index..], value);
else
BinaryPrimitives.WriteInt32BigEndian(_span[_index..], value);
_index += size;
return size;
}
/// <summary>写入无符号 32 位整数。</summary>
/// <param name="value">要写入的无符号整数值。</param>
public Int32 Write(UInt32 value)
{
var size = sizeof(UInt32);
EnsureSpace(size);
if (IsLittleEndian)
BinaryPrimitives.WriteUInt32LittleEndian(_span[_index..], value);
else
BinaryPrimitives.WriteUInt32BigEndian(_span[_index..], value);
_index += size;
return size;
}
/// <summary>写入 64 位整数。</summary>
/// <param name="value">要写入的整数值。</param>
public Int32 Write(Int64 value)
{
var size = sizeof(Int64);
EnsureSpace(size);
if (IsLittleEndian)
BinaryPrimitives.WriteInt64LittleEndian(_span[_index..], value);
else
BinaryPrimitives.WriteInt64BigEndian(_span[_index..], value);
_index += size;
return size;
}
/// <summary>写入无符号 64 位整数。</summary>
/// <param name="value">要写入的无符号整数值。</param>
public Int32 Write(UInt64 value)
{
var size = sizeof(UInt64);
EnsureSpace(size);
if (IsLittleEndian)
BinaryPrimitives.WriteUInt64LittleEndian(_span[_index..], value);
else
BinaryPrimitives.WriteUInt64BigEndian(_span[_index..], value);
_index += size;
return size;
}
/// <summary>写入单精度浮点数。</summary>
/// <param name="value">要写入的浮点值。</param>
public unsafe Int32 Write(Single value)
{
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP
return Write(BitConverter.SingleToInt32Bits(value));
#else
return Write(*(Int32*)&value);
#endif
}
/// <summary>写入双精度浮点数。</summary>
/// <param name="value">要写入的浮点值。</param>
public unsafe Int32 Write(Double value)
{
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP
return Write(BitConverter.DoubleToInt64Bits(value));
#else
return Write(*(Int64*)&value);
#endif
}
/// <summary>写入字符串。支持定长、全部和长度前缀</summary>
/// <param name="value">要写入的字符串</param>
/// <param name="length">最大长度。字节数,-1表示写入全部默认0表示写入7位压缩编码整数长度。不足时填充字节0超长时截取</param>
/// <param name="encoding">字符串编码默认UTF8</param>
/// <returns>返回写入字节数,包括头部长度和字符串部分</returns>
/// <exception cref="ArgumentNullException"></exception>
public Int32 Write(String value, Int32 length = 0, Encoding? encoding = null)
{
var p = _index;
encoding ??= Encoding.UTF8;
if (length < 0)
{
// 写入字符串全部内容
var count = encoding.GetBytes(value.AsSpan(), _span[_index..]);
_index += count;
return _index - p;
}
else if (length == 0)
{
// 先写入长度,再写入内容
if (value.IsNullOrEmpty())
{
WriteEncodedInt(0);
return _index - p;
}
length = encoding.GetByteCount(value);
WriteEncodedInt(length);
EnsureSpace(length);
var count = encoding.GetBytes(value.AsSpan(), _span[_index..]);
_index += count;
return _index - p;
}
else
{
// 写入指定长度不足是填充字节0超长时截取
var span = GetSpan(length);
if (span.Length > length) span = span[..length];
// 输出缓冲区不能过小,否则报错。大小足够时,直接把字符串写入到目标
var source = value.AsSpan();
var max = encoding.GetMaxByteCount(source.Length);
if (max <= length)
encoding.GetBytes(source, span);
else
{
// 目标大小可能不足,申请临时缓冲区,输出后做局部拷贝
var buf = Pool.Shared.Rent(max);
var count = encoding.GetBytes(source, buf);
// 局部拷贝,仅拷贝需要部分,抛弃超长部分
new Span<Byte>(buf, 0, length).CopyTo(span);
Pool.Shared.Return(buf, true);
}
_index += length;
return length;
}
}
/// <summary>写入字节数组</summary>
/// <param name="value"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
public Int32 Write(Byte[] value)
{
if (value == null)
throw new ArgumentNullException(nameof(value));
value.CopyTo(_span[_index..]);
_index += value.Length;
return value.Length;
}
/// <summary>写入Span</summary>
/// <param name="span"></param>
/// <returns></returns>
public Int32 Write(ReadOnlySpan<Byte> span)
{
span.CopyTo(_span[_index..]);
_index += span.Length;
return span.Length;
}
/// <summary>写入Span</summary>
/// <param name="span"></param>
/// <returns></returns>
public Int32 Write(Span<Byte> span)
{
span.CopyTo(_span[_index..]);
_index += span.Length;
return span.Length;
}
/// <summary>写入结构体</summary>
/// <typeparam name="T"></typeparam>
/// <param name="value"></param>
/// <returns></returns>
public Int32 Write<T>(T value) where T : struct
{
var size = Unsafe.SizeOf<T>();
EnsureSpace(size);
#if NET8_0_OR_GREATER
MemoryMarshal.Write(_span.Slice(_index, size), in value);
#else
MemoryMarshal.Write(_span.Slice(_index, size), ref value);
#endif
_index += size;
return size;
}
#endregion
#region
/// <summary>写入7位压缩编码整数</summary>
/// <remarks>
/// 以7位压缩格式写入32位整数小于7位用1个字节小于14位用2个字节。
/// 由每次写入的一个字节的第一位标记后面的字节是否还是当前数据所以每个字节实际可利用存储空间只有后7位。
/// </remarks>
/// <param name="value">数值</param>
/// <returns>实际写入字节数</returns>
public Int32 WriteEncodedInt(Int64 value)
{
var span = _span[_index..];
var count = 0;
var num = (UInt32)value;
while (num >= 0x80)
{
span[count++] = (Byte)(num | 0x80);
num >>= 7;
}
span[count++] = (Byte)num;
_index += count;
return count;
}
#endregion
}

View File

@@ -1,5 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using ThingsGateway.NewLife.Messaging;
namespace ThingsGateway.NewLife.Caching;
/// <summary>缓存</summary>
@@ -100,9 +102,9 @@ public abstract class Cache : DisposeBase, ICache
/// <typeparam name="T"></typeparam>
/// <param name="keys"></param>
/// <returns></returns>
public virtual IDictionary<String, T?> GetAll<T>(IEnumerable<String> keys)
public virtual IDictionary<String, T> GetAll<T>(IEnumerable<String> keys)
{
var dic = new Dictionary<String, T?>();
var dic = new Dictionary<String, T>();
foreach (var key in keys)
{
dic[key] = Get<T>(key);
@@ -152,6 +154,14 @@ public abstract class Cache : DisposeBase, ICache
/// <param name="key"></param>
/// <returns></returns>
public virtual ICollection<T> GetSet<T>(String key) => throw new NotSupportedException();
/// <summary>获取事件总线,可发布消息或订阅消息</summary>
/// <typeparam name="T"></typeparam>
/// <param name="topic">事件主题</param>
/// <param name="clientId">客户标识/消息分组</param>
/// <returns></returns>
/// <exception cref="NotSupportedException"></exception>
public virtual IEventBus<T> GetEventBus<T>(String topic, String clientId = "") => throw new NotSupportedException();
#endregion
#region
@@ -310,7 +320,7 @@ public abstract class Cache : DisposeBase, ICache
if (!rlock.Acquire(msTimeout, msExpire))
{
if (throwOnFailure) throw new InvalidOperationException($"Lock [{key}] failed! msTimeout={msTimeout}");
rlock.Dispose();
return null;
}
@@ -322,101 +332,14 @@ public abstract class Cache : DisposeBase, ICache
/// <summary>已重载。</summary>
/// <returns></returns>
public override String ToString() => Name;
#endregion
#if NET6_0_OR_GREATER
#region
/// <inheritdoc/>
public virtual void HashAdd<T>(string key, string hashKey, T value)
{
lock (this)
{
//获取字典
var exist = GetDictionary<T>(key);
if (exist.ContainsKey(hashKey))//如果包含Key
exist[hashKey] = value;//重新赋值
else exist.TryAdd(hashKey, value);//加上新的值
Set(key, exist);
}
}
/// <inheritdoc/>
public virtual bool HashSet<T>(string key, Dictionary<string, T> dic)
{
lock (this)
{
//获取字典
var exist = GetDictionary<T>(key);
foreach (var it in dic)
{
if (exist.ContainsKey(it.Key))//如果包含Key
exist[it.Key] = it.Value;//重新赋值
else exist.Add(it.Key, it.Value);//加上新的值
}
return true;
}
}
/// <inheritdoc/>
public virtual int HashDel<T>(string key, params string[] fields)
{
var result = 0;
//获取字典
var exist = GetDictionary<T>(key);
foreach (var field in fields)
{
if (field != null && exist.ContainsKey(field))//如果包含Key
{
exist.Remove(field);//删除
result++;
}
}
return result;
}
/// <inheritdoc/>
public virtual List<T> HashGet<T>(string key, params string[] fields)
{
var list = new List<T>();
//获取字典
var exist = GetDictionary<T>(key);
foreach (var field in fields)
{
if (exist.TryGetValue(field, out var data))//如果包含Key
{
list.Add(data);
}
else { list.Add(default); }
}
return list;
}
/// <inheritdoc/>
public virtual T HashGetOne<T>(string key, string field)
{
//获取字典
var exist = GetDictionary<T>(key);
exist.TryGetValue(field, out var result);
return result;
}
/// <inheritdoc/>
public virtual IDictionary<string, T> HashGetAll<T>(string key)
{
var data = GetDictionary<T>(key);
return data;
}
/// <inheritdoc/>
public void DelByPattern(string pattern)
{
var keys = Keys;//获取所有key
foreach (var item in keys.ToList())
{
if (item.StartsWith(pattern))//如果匹配
Remove(item);
}
}
#endregion
#endif
}
public abstract void HashAdd<T>(string key, string hashKey, T value);
public abstract bool HashSet<T>(string key, Dictionary<string, T> dic);
public abstract int HashDel<T>(string key, params string[] fields);
public abstract List<T> HashGet<T>(string key, params string[] fields);
public abstract T HashGetOne<T>(string key, string field);
public abstract IDictionary<string, T> HashGetAll<T>(string key);
public abstract long DelByPattern(string v);
}

View File

@@ -8,7 +8,7 @@ public class CacheLock : DisposeBase
/// <summary>
/// 是否持有锁
/// </summary>
private Boolean _hasLock;
private Boolean _hasLock = false;
/// <summary>键</summary>
public String Key { get; set; }

View File

@@ -202,7 +202,6 @@ public interface ICache
IDisposable? AcquireLock(String key, Int32 msTimeout, Int32 msExpire, Boolean throwOnFailure);
#endregion
#if NET6_0_OR_GREATER
#region
/// <inheritdoc/>
public void HashAdd<T>(string key, string hashKey, T value);
@@ -226,9 +225,8 @@ public interface ICache
/// 按前缀删除
/// </summary>
/// <param name="v"></param>
void DelByPattern(string v);
long DelByPattern(string v);
#endregion
#endif
}

View File

@@ -2,8 +2,11 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using ThingsGateway.NewLife.Data;
using ThingsGateway.NewLife.Log;
using ThingsGateway.NewLife.Messaging;
using ThingsGateway.NewLife.Reflection;
using ThingsGateway.NewLife.Serialization;
using ThingsGateway.NewLife.Threading;
namespace ThingsGateway.NewLife.Caching;
@@ -34,7 +37,7 @@ public class MemoryCache : Cache
#region
/// <summary>默认缓存</summary>
public static ICache Instance { get; set; } = new MemoryCache();
public static MemoryCache Instance { get; set; } = new MemoryCache();
#endregion
#region
@@ -52,9 +55,10 @@ public class MemoryCache : Cache
{
base.Dispose(disposing);
_clearTimer.TryDispose();
_clearTimer?.TryDispose();
_clearTimer = null;
}
#endregion
#region
@@ -68,7 +72,6 @@ public class MemoryCache : Cache
#region
#if !NET452
/// <summary>返回全部</summary>
@@ -85,35 +88,6 @@ public class MemoryCache : Cache
}
}
/// <summary>获取或添加缓存项</summary>
/// <typeparam name="T">值类型</typeparam>
/// <param name="key">键</param>
/// <param name="value">值</param>
/// <param name="expire">过期时间,秒</param>
/// <returns></returns>
public virtual T? GetOrAdd<T>(String key, T value, Int32 expire = -1)
{
if (expire < 0) expire = Expire;
CacheItem? item = null;
do
{
if (_cache.TryGetValue(key, out item) && item != null)
{
if (!item.Expired) return item.Visit<T>();
item.Set(value, expire);
return value;
}
item ??= new CacheItem(value, expire);
} while (!_cache.TryAdd(key, item));
Interlocked.Increment(ref _count);
return item.Visit<T>();
}
#endregion
#region
@@ -444,6 +418,19 @@ public class MemoryCache : Cache
throw new InvalidCastException($"Unable to convert the value of [{key}] from {item.TypeCode} to {typeof(ICollection<T>)}");
}
/// <summary>获取事件总线,可发布消息或订阅消息</summary>
/// <typeparam name="T"></typeparam>
/// <param name="topic">事件主题</param>
/// <param name="clientId">客户标识/消息分组</param>
/// <returns></returns>
public override IEventBus<T> GetEventBus<T>(String topic, String clientId = "")
{
var key = $"eventbus:{topic}";
var item = GetOrAddItem(key, k => new QueueEventBus<T>(this, topic));
return item.Visit<IEventBus<T>>() ??
throw new InvalidCastException($"Unable to convert the value of [{topic}] from {item.TypeCode} to {typeof(IEventBus<T>)}");
}
/// <summary>获取 或 添加 缓存项</summary>
/// <param name="key"></param>
/// <param name="valueFactory"></param>
@@ -750,6 +737,224 @@ public class MemoryCache : Cache
}
#endregion
#region
private const String MAGIC = "NewLifeCache";
private const Byte _Ver = 1;
/// <summary>保存到数据流</summary>
/// <param name="stream"></param>
/// <returns></returns>
public void Save(Stream stream)
{
var bn = new Binary
{
Stream = stream,
EncodeInt = true,
};
// 头部,幻数、版本和标记
bn.Write(MAGIC.GetBytes(), 0, MAGIC.Length);
bn.Write(_Ver);
bn.Write(0);
bn.WriteSize(_cache.Count);
foreach (var item in _cache)
{
var ci = item.Value;
// Key+Expire+Empty
// Key+Expire+TypeCode+Value
// Key+Expire+TypeCode+Type+Length+Value
bn.Write(item.Key);
bn.Write((Int32)(ci.ExpiredTime / 1000));
var value = ci.Value;
var type = value?.GetType();
if (type == null)
{
bn.Write((Byte)TypeCode.Empty);
}
else
{
var code = type.GetTypeCode();
bn.Write((Byte)code);
if (code != TypeCode.Object)
bn.Write(value);
else
{
bn.Write(type.FullName);
if (value != null) bn.Write(Binary.FastWrite(value));
}
}
}
}
/// <summary>从数据流加载</summary>
/// <param name="stream"></param>
/// <returns></returns>
public void Load(Stream stream)
{
var bn = new Binary
{
Stream = stream,
EncodeInt = true,
};
// 头部,幻数、版本和标记
var magic = bn.ReadBytes(MAGIC.Length).ToStr();
if (magic != MAGIC) throw new InvalidDataException();
var ver = bn.Read<Byte>();
_ = bn.Read<Byte>();
// 版本兼容
if (ver > _Ver) throw new InvalidDataException($"MemoryCache[ver={_Ver}] Unable to support newer versions [{ver}]");
var count = bn.ReadSize();
while (count-- > 0)
{
// Key+Expire+Empty
// Key+Expire+TypeCode+Value
// Key+Expire+TypeCode+Type+Length+Value
var key = bn.Read<String>();
var exp = bn.Read<Int32>();
var code = (TypeCode)bn.ReadByte();
Object? value = null;
if (code == TypeCode.Empty)
{
}
else if (code != TypeCode.Object)
{
var type = Type.GetType("System." + code);
if (type != null) value = bn.Read(type);
}
else
{
var typeName = bn.Read<String>();
//var type = Type.GetType(typeName);
var type = typeName?.GetTypeEx();
var pk = bn.Read<IPacket>();
value = pk;
if (type != null && pk != null)
{
var bn2 = new Binary() { Stream = pk.GetStream(), EncodeInt = true };
value = bn2.Read(type);
}
}
if (key != null) Set(key, value, exp - (Int32)(Runtime.TickCount64 / 1000));
}
}
/// <summary>保存到文件</summary>
/// <param name="file"></param>
/// <param name="compressed"></param>
/// <returns></returns>
public Int64 Save(String file, Boolean compressed) => file.AsFile().OpenWrite(compressed, s => Save(s));
/// <summary>从文件加载</summary>
/// <param name="file"></param>
/// <param name="compressed"></param>
/// <returns></returns>
public Int64 Load(String file, Boolean compressed) => file.AsFile().OpenRead(compressed, s => Load(s));
#endregion
#region
/// <inheritdoc/>
public override void HashAdd<T>(string key, string hashKey, T value)
{
lock (this)
{
//获取字典
var exist = GetDictionary<T>(key);
if (exist.ContainsKey(hashKey))//如果包含Key
exist[hashKey] = value;//重新赋值
else exist.TryAdd(hashKey, value);//加上新的值
Set(key, exist);
}
}
/// <inheritdoc/>
public override bool HashSet<T>(string key, Dictionary<string, T> dic)
{
lock (this)
{
//获取字典
var exist = GetDictionary<T>(key);
foreach (var it in dic)
{
if (exist.ContainsKey(it.Key))//如果包含Key
exist[it.Key] = it.Value;//重新赋值
else exist.Add(it.Key, it.Value);//加上新的值
}
return true;
}
}
/// <inheritdoc/>
public override int HashDel<T>(string key, params string[] fields)
{
var result = 0;
//获取字典
var exist = GetDictionary<T>(key);
foreach (var field in fields)
{
if (field != null && exist.ContainsKey(field))//如果包含Key
{
exist.Remove(field);//删除
result++;
}
}
return result;
}
/// <inheritdoc/>
public override List<T> HashGet<T>(string key, params string[] fields)
{
var list = new List<T>();
//获取字典
var exist = GetDictionary<T>(key);
foreach (var field in fields)
{
if (exist.TryGetValue(field, out var data))//如果包含Key
{
list.Add(data);
}
else { list.Add(default); }
}
return list;
}
/// <inheritdoc/>
public override T HashGetOne<T>(string key, string field)
{
//获取字典
var exist = GetDictionary<T>(key);
exist.TryGetValue(field, out var result);
return result;
}
/// <inheritdoc/>
public override IDictionary<string, T> HashGetAll<T>(string key)
{
var data = GetDictionary<T>(key);
return data;
}
/// <inheritdoc/>
public override long DelByPattern(string pattern)
{
var keys = Keys;//获取所有key
long count = 0;
foreach (var item in keys.ToList())
{
if (item.StartsWith(pattern))//如果匹配
count += Remove(item);
}
return count;
}
#endregion
}
/// <summary>生产者消费者</summary>
@@ -881,4 +1086,4 @@ public class MemoryQueue<T> : DisposeBase, IProducerConsumer<T>
/// <param name="keys"></param>
/// <returns></returns>
public Int32 Acknowledge(params String[] keys) => 0;
}
}

View File

@@ -0,0 +1,97 @@
using System.Diagnostics.CodeAnalysis;
using ThingsGateway.NewLife.Log;
using ThingsGateway.NewLife.Messaging;
namespace ThingsGateway.NewLife.Caching;
/// <summary>消息队列事件总线。通过消息队列来发布和订阅消息</summary>
/// <remarks>实例化消息队列事件总线</remarks>
public class QueueEventBus<TEvent>(ICache cache, String topic) : EventBus<TEvent>
{
private IProducerConsumer<TEvent>? _queue;
private CancellationTokenSource? _source;
/// <summary>销毁</summary>
/// <param name="disposing"></param>
protected override void Dispose(Boolean disposing)
{
base.Dispose(disposing);
_source?.TryDispose();
}
/// <summary>初始化</summary>
[MemberNotNull(nameof(_queue))]
protected virtual void Init()
{
if (_queue != null) return;
_queue = cache.GetQueue<TEvent>(topic);
}
/// <summary>发布消息到消息队列</summary>
/// <param name="event">事件</param>
/// <param name="context">上下文</param>
/// <param name="cancellationToken">取消令牌</param>
public override Task<Int32> PublishAsync(TEvent @event, IEventContext<TEvent>? context = null, CancellationToken cancellationToken = default)
{
Init();
var rs = _queue.Add(@event);
return Task.FromResult(rs);
}
/// <summary>订阅消息。启动大循环,从消息队列订阅消息,再分发到本地订阅者</summary>
/// <param name="handler">处理器</param>
/// <param name="clientId">客户标识。每个客户只能订阅一次,重复订阅将会挤掉前一次订阅</param>
public override Boolean Subscribe(IEventHandler<TEvent> handler, String clientId = "")
{
if (_source == null)
{
var source = new CancellationTokenSource();
if (Interlocked.CompareExchange(ref _source, source, null) == null)
{
Init();
_ = Task.Run(() => ConsumeMessage(_source));
}
}
return base.Subscribe(handler, clientId);
}
/// <summary>从队列中消费消息,经事件总线送给设备会话</summary>
/// <param name="source"></param>
/// <returns></returns>
protected virtual async Task ConsumeMessage(CancellationTokenSource source)
{
DefaultSpan.Current = null;
var cancellationToken = source.Token;
try
{
while (!cancellationToken.IsCancellationRequested)
{
var msg = await _queue!.TakeOneAsync(15, cancellationToken).ConfigureAwait(false);
if (msg != null)
{
// 发布到事件总线
await DispatchAsync(msg, null, cancellationToken).ConfigureAwait(false);
}
else
{
await Task.Delay(1_000, cancellationToken).ConfigureAwait(false);
}
}
}
catch (TaskCanceledException) { }
catch (OperationCanceledException) { }
catch (Exception ex)
{
XTrace.WriteException(ex);
}
finally
{
source.Cancel();
}
}
}

View File

@@ -0,0 +1,150 @@
缓存架构以ICache接口为核心包括MemoryCache、Redis和DbCache实现
后续例程与使用说明均以Redis为例各缓存实现类似。
### 内存缓存 MemoryCache
MemoryCache核心是并发字典ConcurrentDictionary由于省去了序列化和网络通信使得它具有千万级超高性能。
MemoryCache支持过期时间默认容量10万个未过期key超过该值后每60秒根据LRU清理溢出部分。
常用于进程内千万级以下数据缓存场景。
```csharp
// 缓存默认实现Cache.Default是MemoryCache可修改
//var ic = Cache.Default;
//var ic = new MemoryCache();
```
### 基础 Redis
Redis实现标准协议以及基础字符串操作完整实现由独立开源项目[NewLife.Redis](https://github.com/NewLifeX/NewLife.Redis)提供。
采取连接池加同步阻塞架构具有超低延迟200~600us以及超高吞吐量的特点。
在物流行业大数据实时计算中广泛应有经过日均100亿次调用量验证。
```csharp
// 实例化Redis默认端口6379可以省略密码有两种写法
//var ic = new Redis("127.0.0.1", 7);
var ic = new Redis("pass@127.0.0.1:6379", 7);
//var ic = new Redis("server=127.0.0.1:6379;password=pass", 7);
ic.Log = XTrace.Log; // 调试日志。正式使用时注释
```
### 数据库 DbCache
DbCache属于实验性质采用数据库存储数据默认SQLite。
### 基本操作
在基本操作之前,我们先做一些准备工作:
+ 新建控制台项目,并在入口函数开头加上 `XTrace.UseConsole();` ,这是为了方便查看调试日志
+ 具体测试代码之前需要加上前面MemoryCache或Redis的实例化代码
+ 准备一个模型类User
```csharp
class User
{
public String Name { get; set; }
public DateTime CreateTime { get; set; }
}
```
添删改查:
```csharp
var user = new User { Name = "NewLife", CreateTime = DateTime.Now };
ic.Set("user", user, 3600);
var user2 = ic.Get<User>("user");
XTrace.WriteLine("Json: {0}", ic.Get<String>("user"));
if (ic.ContainsKey("user")) XTrace.WriteLine("存在!");
ic.Remove("user");
```
执行结果:
```csharp
14:14:25.990 1 N - SELECT 7
14:14:25.992 1 N - => OK
14:14:26.008 1 N - SETEX user 3600 [53]
14:14:26.021 1 N - => OK
14:14:26.042 1 N - GET user
14:14:26.048 1 N - => [53]
14:14:26.064 1 N - GET user
14:14:26.065 1 N - => [53]
14:14:26.066 1 N - Json: {"Name":"NewLife","CreateTime":"2018-09-25 14:14:25"}
14:14:26.067 1 N - EXISTS user
14:14:26.068 1 N - => 1
14:14:26.068 1 N - 存在!
14:14:26.069 1 N - DEL user
14:14:26.070 1 N - => 1
```
保存复杂对象时默认采用Json序列化所以上面可以按字符串把结果取回来发现正是Json字符串。
Redis的strings实质上就是带有长度前缀的二进制数据[53]表示一段53字节长度的二进制数据。
### 集合操作
GetAll/SetAll 在Redis上是很常用的批量操作同时获取或设置多个key一般有10倍以上吞吐量。
批量操作:
```csharp
var dic = new Dictionary<String, Object>
{
["name"] = "NewLife",
["time"] = DateTime.Now,
["count"] = 1234
};
ic.SetAll(dic, 120);
var vs = ic.GetAll<String>(dic.Keys);
XTrace.WriteLine(vs.Join(",", e => $"{e.Key}={e.Value}"));
```
执行结果:
```csharp
MSET name NewLife time 2018-09-25 15:56:26 count 1234
=> OK
EXPIRE name 120
EXPIRE time 120
EXPIRE count 120
MGET name time count
name=NewLife,time=2018-09-25 15:56:26,count=1234
```
集合操作里面还有 `GetList/GetDictionary/GetQueue/GetSet` 四个类型集合分别代表Redis的列表、哈希、队列、Set集合等。
基础版Redis不支持这四个集合完整版[NewLife.Redis](https://github.com/NewLifeX/NewLife.Redis)支持MemoryCache则直接支持。
### 高级操作
+ Add 添加当key不存在时添加已存在时返回false。
+ Replace 替换,替换已有值为新值,返回旧值。
+ Increment 累加,原子操作
+ Decrement 递减,原子操作
高级操作:
```csharp
var flag = ic.Add("count", 5678);
XTrace.WriteLine(flag ? "Add成功" : "Add失败");
var ori = ic.Replace("count", 777);
var count = ic.Get<Int32>("count");
XTrace.WriteLine("count由{0}替换为{1}", ori, count);
ic.Increment("count", 11);
var count2 = ic.Decrement("count", 10);
XTrace.WriteLine("count={0}", count2);
```
执行结果:
```csharp
SETNX count 5678
=> 0
Add失败
GETSET count 777
=> 1234
GET count
=> 777
count由1234替换为777
INCRBY count 11
=> 788
DECRBY count 10
=> 778
count=778
```
### 性能测试
Bench 会分根据线程数分多组进行添删改压力测试。
rand 参数是否随机产生key/value。
batch 批大小分批执行读写操作借助GetAll/SetAll进行优化。
Redis默认设置AutoPipeline=100无分批时打开管道操作对添删改优化。
### 容器化部署
支持从环境变量 `Redis_{Name}` 中加载配置,用于容器化部署。

View File

@@ -0,0 +1,248 @@
using System.Collections.Concurrent;
using ThingsGateway.NewLife.Collections;
using ThingsGateway.NewLife.Data;
using ThingsGateway.NewLife.Reflection;
using ThingsGateway.NewLife.Serialization;
#if NETCOREAPP
using System.Text.Json;
#endif
namespace System.Collections.Generic;
/// <summary>集合扩展</summary>
public static class CollectionHelper
{
/// <summary>集合转为数组,加锁确保安全</summary>
/// <typeparam name="T"></typeparam>
/// <param name="collection"></param>
/// <returns></returns>
public static T[] ToArray<T>(this ICollection<T> collection)
{
//if (collection == null) return null;
lock (collection)
{
var count = collection.Count;
if (count == 0) return [];
var arr = new T[count];
collection.CopyTo(arr, 0);
return arr;
}
}
/// <summary>集合转为数组</summary>
/// <typeparam name="TKey"></typeparam>
/// <typeparam name="TValue"></typeparam>
/// <param name="collection"></param>
/// <param name="index"></param>
/// <returns></returns>
public static IList<TKey> ToKeyArray<TKey, TValue>(this IDictionary<TKey, TValue> collection, Int32 index = 0) where TKey : notnull
{
//if (collection == null) return null;
if (collection is ConcurrentDictionary<TKey, TValue> cdiv && cdiv.Keys is IList<TKey> list) return list;
if (collection.Count == 0) return [];
lock (collection)
{
var arr = new TKey[collection.Count - index];
collection.Keys.CopyTo(arr, index);
return arr;
}
}
/// <summary>集合转为数组</summary>
/// <typeparam name="TKey"></typeparam>
/// <typeparam name="TValue"></typeparam>
/// <param name="collection"></param>
/// <param name="index"></param>
/// <returns></returns>
public static IList<TValue> ToValueArray<TKey, TValue>(this IDictionary<TKey, TValue> collection, Int32 index = 0) where TKey : notnull
{
//if (collection == null) return null;
//if (collection is ConcurrentDictionary<TKey, TValue> cdiv) return cdiv.Values as IList<TValue>;
if (collection is ConcurrentDictionary<TKey, TValue> cdiv && cdiv.Values is IList<TValue> list) return list;
if (collection.Count == 0) return [];
lock (collection)
{
var arr = new TValue[collection.Count - index];
collection.Values.CopyTo(arr, index);
return arr;
}
}
/// <summary>目标匿名参数对象转为名值字典</summary>
/// <param name="source"></param>
/// <returns></returns>
public static IDictionary<String, Object?> ToDictionary(this Object source)
{
//!! 即使传入为空也返回字典而不是null避免业务层需要大量判空
//if (target == null) return null;
#pragma warning disable CA1859 // 尽可能使用具体类型以提高性能
if (source is IDictionary<String, Object?> dic) return dic;
#pragma warning restore CA1859 // 尽可能使用具体类型以提高性能
var type = source?.GetType();
if (type?.IsBaseType() == true)
throw new InvalidDataException("source is not Object");
dic = new NullableDictionary<String, Object?>(StringComparer.OrdinalIgnoreCase);
if (source != null)
{
// 修正字符串字典的支持问题
if (source is IDictionary dic2)
{
foreach (var item in dic2)
{
if (item is DictionaryEntry de)
dic[de.Key + ""] = de.Value;
}
}
#if NETCOREAPP
else if (source is JsonElement element && element.ValueKind == JsonValueKind.Object)
{
foreach (var item in element.EnumerateObject())
{
Object? v = item.Value.ValueKind switch
{
JsonValueKind.Object => item.Value.ToDictionary(),
JsonValueKind.Array => ToArray(item.Value),
JsonValueKind.String => item.Value.GetString(),
JsonValueKind.Number when item.Value.GetRawText().Contains('.') => item.Value.GetDouble(),
JsonValueKind.Number => item.Value.GetInt64(),
JsonValueKind.True or JsonValueKind.False => item.Value.GetBoolean(),
_ => item.Value.GetString(),
};
if (v is Int64 n && n < Int32.MaxValue) v = (Int32)n;
dic[item.Name] = v;
}
}
#endif
else
{
foreach (var pi in source.GetType().GetProperties(true))
{
var name = SerialHelper.GetName(pi);
if (source is IModel src)
dic[name] = src[name];
else
dic[name] = source.GetValue(pi);
}
// 增加扩展属性
if (source is IExtend ext && ext.Items != null)
{
foreach (var item in ext.Items)
{
dic[item.Key] = item.Value;
}
}
}
}
return dic;
}
#if NETCOREAPP
/// <summary>Json对象转为数组</summary>
/// <param name="element"></param>
/// <returns></returns>
public static IList<Object?> ToArray(this JsonElement element)
{
var list = new List<Object?>();
foreach (var item in element.EnumerateArray())
{
Object? v = item.ValueKind switch
{
JsonValueKind.Object => item.ToDictionary(),
JsonValueKind.Array => ToArray(item),
JsonValueKind.String => item.GetString(),
JsonValueKind.Number when item.GetRawText().Contains('.') => item.GetDouble(),
JsonValueKind.Number => item.GetInt64(),
JsonValueKind.True or JsonValueKind.False => item.GetBoolean(),
_ => item.GetString(),
};
if (v is Int64 n && n < Int32.MaxValue) v = (Int32)n;
list.Add(v);
}
return list;
}
#endif
/// <summary>合并字典参数</summary>
/// <param name="dic">字典</param>
/// <param name="target">目标对象</param>
/// <param name="overwrite">是否覆盖同名参数</param>
/// <param name="excludes">排除项</param>
/// <returns></returns>
public static IDictionary<String, Object?> Merge(this IDictionary<String, Object?> dic, Object target, Boolean overwrite = true, String[]? excludes = null)
{
if (target?.GetType().IsBaseType() != false) return dic;
var exs = excludes != null ? new HashSet<String>(excludes, StringComparer.OrdinalIgnoreCase) : null;
foreach (var item in target.ToDictionary())
{
if (exs?.Contains(item.Key) != true)
{
if (overwrite || !dic.ContainsKey(item.Key)) dic[item.Key] = item.Value;
}
}
return dic;
}
/// <summary>转为可空字典</summary>
/// <typeparam name="TKey"></typeparam>
/// <typeparam name="TValue"></typeparam>
/// <param name="collection"></param>
/// <param name="comparer"></param>
/// <returns></returns>
public static IDictionary<TKey, TValue> ToNullable<TKey, TValue>(this IDictionary<TKey, TValue> collection, IEqualityComparer<TKey>? comparer = null) where TKey : notnull
{
//if (collection == null) return null;
if (collection is NullableDictionary<TKey, TValue> dic && (comparer == null || dic.Comparer == comparer)) return dic;
if (comparer == null)
return new NullableDictionary<TKey, TValue>(collection);
else
return new NullableDictionary<TKey, TValue>(collection, comparer);
}
/// <summary>从队列里面获取指定个数元素</summary>
/// <typeparam name="T"></typeparam>
/// <param name="collection">消费集合</param>
/// <param name="count">元素个数</param>
/// <returns></returns>
public static IEnumerable<T> Take<T>(this Queue<T> collection, Int32 count)
{
if (collection == null) yield break;
while (count-- > 0 && collection.Count > 0)
{
yield return collection.Dequeue();
}
}
/// <summary>从消费集合里面获取指定个数元素</summary>
/// <typeparam name="T"></typeparam>
/// <param name="collection">消费集合</param>
/// <param name="count">元素个数</param>
/// <returns></returns>
public static IEnumerable<T> Take<T>(this IProducerConsumerCollection<T> collection, Int32 count)
{
if (collection == null) yield break;
while (count-- > 0 && collection.TryTake(out var item))
{
yield return item;
}
}
}

View File

@@ -0,0 +1,76 @@
namespace ThingsGateway.NewLife.Collections
{
/// <summary>集群管理</summary>
/// <typeparam name="TKey"></typeparam>
/// <typeparam name="TValue"></typeparam>
public interface ICluster<TKey, TValue>
{
/// <summary>最后使用资源</summary>
KeyValuePair<TKey, TValue> Current { get; }
/// <summary>资源列表</summary>
Func<IEnumerable<TKey>> GetItems { get; set; }
/// <summary>打开</summary>
Boolean Open();
/// <summary>关闭</summary>
/// <param name="reason">关闭原因。便于日志分析</param>
/// <returns>是否成功</returns>
Boolean Close(String reason);
/// <summary>从集群中获取资源</summary>
/// <returns></returns>
TValue Get();
/// <summary>归还</summary>
/// <param name="value"></param>
Boolean Put(TValue value);
}
/// <summary>集群助手</summary>
public static class ClusterHelper
{
/// <summary>借助集群资源处理事务</summary>
/// <typeparam name="TKey"></typeparam>
/// <typeparam name="TValue"></typeparam>
/// <typeparam name="TResult"></typeparam>
/// <param name="cluster"></param>
/// <param name="func"></param>
/// <returns></returns>
public static TResult Invoke<TKey, TValue, TResult>(this ICluster<TKey, TValue> cluster, Func<TValue, TResult> func)
{
var item = default(TValue);
try
{
item = cluster.Get();
return func(item);
}
finally
{
cluster.Put(item);
}
}
/// <summary>借助集群资源处理事务</summary>
/// <typeparam name="TKey"></typeparam>
/// <typeparam name="TValue"></typeparam>
/// <typeparam name="TResult"></typeparam>
/// <param name="cluster"></param>
/// <param name="func"></param>
/// <returns></returns>
public static async Task<TResult> InvokeAsync<TKey, TValue, TResult>(this ICluster<TKey, TValue> cluster, Func<TValue, Task<TResult>> func)
{
var item = default(TValue);
try
{
item = cluster.Get();
return await func(item).ConfigureAwait(false);
}
finally
{
cluster.Put(item);
}
}
}
}

View File

@@ -0,0 +1,13 @@
namespace ThingsGateway.NewLife.Collections;
/// <summary>
/// 字典数据源接口。定义该模型类支持输出名值字典,便于序列化传输
/// </summary>
public interface IDictionarySource
{
/// <summary>
/// 把对象转为名值字典,便于序列化传输
/// </summary>
/// <returns></returns>
IDictionary<String, Object?> ToDictionary();
}

View File

@@ -94,7 +94,7 @@ public static class Pool
{
//if (ms == null) return null;
var buf = returnResult ? ms.ToArray() : Array.Empty<byte>();
var buf = returnResult ? ms.ToArray() : Empty;
Pool.MemoryStream.Return(ms);
@@ -132,5 +132,11 @@ public static class Pool
}
#endregion
#region ByteArray
/// <summary>字节数组共享存储</summary>
public static ArrayPool<Byte> Shared { get; set; } = ArrayPool<Byte>.Shared;
/// <summary>空数组</summary>
public static Byte[] Empty { get; } = [];
#endregion
}

View File

@@ -26,8 +26,8 @@ public class ObjectPool<T> : DisposeBase, IPool<T> where T : notnull
/// <summary>繁忙个数</summary>
public Int32 BusyCount => _BusyCount;
/// <summary>最大个数。默认1000表示无上限</summary>
public Int32 Max { get; set; } = 100;
/// <summary>最大个数。默认00表示无上限</summary>
public Int32 Max { get; set; } = 0;
/// <summary>最小个数。默认1</summary>
public Int32 Min { get; set; } = 1;

View File

@@ -151,4 +151,4 @@ public class Pool<T> : IPool<T> where T : class
/// <returns></returns>
protected virtual T? OnCreate() => typeof(T).CreateInstance() as T;
#endregion
}
}

View File

@@ -1,113 +0,0 @@
using System.Collections.Concurrent;
using ThingsGateway.NewLife.Caching;
namespace ThingsGateway.NewLife.Collections
{
/// <summary>主动式消息服务</summary>
/// <typeparam name="T">数据类型</typeparam>
public interface IQueueService<T>
{
/// <summary>发布消息</summary>
/// <param name="topic">主题</param>
/// <param name="value">消息</param>
/// <returns></returns>
Int32 Public(String topic, T value);
/// <summary>订阅</summary>
/// <param name="clientId">客户标识</param>
/// <param name="topic">主题</param>
Boolean Subscribe(String clientId, String topic);
/// <summary>取消订阅</summary>
/// <param name="clientId">客户标识</param>
/// <param name="topic">主题</param>
Boolean UnSubscribe(String clientId, String topic);
/// <summary>消费消息</summary>
/// <param name="clientId">客户标识</param>
/// <param name="topic">主题</param>
/// <param name="count">要拉取的消息数</param>
/// <returns></returns>
T[] Consume(String clientId, String topic, Int32 count);
}
/// <summary>轻量级主动式消息服务</summary>
/// <typeparam name="T">数据类型</typeparam>
public class QueueService<T> : IQueueService<T>
{
#region
/// <summary>数据存储</summary>
public ICache Cache { get; set; } = MemoryCache.Instance;
/// <summary>每个主题的所有订阅者</summary>
private readonly ConcurrentDictionary<String, ConcurrentDictionary<String, IProducerConsumer<T>>> _topics = new();
#endregion
#region
/// <summary>发布消息</summary>
/// <param name="topic">主题</param>
/// <param name="value">消息</param>
/// <returns></returns>
public Int32 Public(String topic, T value)
{
var rs = 0;
if (_topics.TryGetValue(topic, out var clients))
{
// 向每个订阅者推送
foreach (var item in clients)
{
var queue = item.Value;
rs += queue.Add(new[] { value });
}
}
return rs;
}
/// <summary>订阅</summary>
/// <param name="clientId">客户标识</param>
/// <param name="topic">主题</param>
public Boolean Subscribe(String clientId, String topic)
{
var dic = _topics.GetOrAdd(topic, k => new ConcurrentDictionary<String, IProducerConsumer<T>>());
if (dic.ContainsKey(clientId)) return false;
// 创建队列
var queue = Cache.GetQueue<T>($"{topic}_{clientId}");
return dic.TryAdd(clientId, queue);
}
/// <summary>取消订阅</summary>
/// <param name="clientId">客户标识</param>
/// <param name="topic">主题</param>
public Boolean UnSubscribe(String clientId, String topic)
{
if (_topics.TryGetValue(topic, out var clients))
{
return clients.TryRemove(clientId, out _);
}
return false;
}
/// <summary>消费消息</summary>
/// <param name="clientId">客户标识</param>
/// <param name="topic">主题</param>
/// <param name="count"></param>
/// <returns></returns>
public T[] Consume(String clientId, String topic, Int32 count)
{
if (_topics.TryGetValue(topic, out var clients))
{
if (clients.TryGetValue(clientId, out var queue))
{
return queue.Take(count).ToArray();
}
}
return [];
}
#endregion
}
}

View File

@@ -2,6 +2,8 @@
using System.Globalization;
using System.Reflection;
using ThingsGateway.NewLife.Collections;
namespace ThingsGateway.NewLife.Extension;
/// <summary>工具类</summary>
@@ -175,7 +177,7 @@ public static class ConvertUtility
public class DefaultConvert
{
private static readonly DateTime _dt1970 = new(1970, 1, 1);
private static readonly DateTimeOffset _dto1970 = new(new DateTime(1970, 1, 1));
private static readonly DateTimeOffset _dto1970 = new(new DateTime(1970, 1, 1), TimeSpan.Zero);
private static readonly Int64 _maxSeconds = (Int64)(DateTime.MaxValue - DateTime.MinValue).TotalSeconds;
private static readonly Int64 _maxMilliseconds = (Int64)(DateTime.MaxValue - DateTime.MinValue).TotalMilliseconds;
@@ -338,7 +340,6 @@ public class DefaultConvert
}
}
//暂时不做处理 先处理异常转换
try
{
// 转换接口
@@ -450,12 +451,12 @@ public class DefaultConvert
// 凑够8字节
if (buf.Length < 8)
{
var bts = ArrayPool<Byte>.Shared.Rent(8);
var bts = Pool.Shared.Rent(8);
Buffer.BlockCopy(buf, 0, bts, 0, buf.Length);
var dec = BitConverter.ToDouble(bts, 0).ToDecimal();
ArrayPool<Byte>.Shared.Return(bts);
Pool.Shared.Return(bts);
return dec;
}
@@ -500,16 +501,35 @@ public class DefaultConvert
}
// 特殊处理字符串,也是最常见的
var str = value.ToString().Trim();
if (str.IsNullOrEmpty()) return defaultValue;
if (value is String str)
{
str = str.Trim();
if (str.IsNullOrEmpty()) return defaultValue;
if (Boolean.TryParse(str, out var b)) return b;
if (Boolean.TryParse(str, out var b)) return b;
if (String.Equals(str, Boolean.TrueString, StringComparison.OrdinalIgnoreCase)) return true;
if (String.Equals(str, Boolean.FalseString, StringComparison.OrdinalIgnoreCase)) return false;
if (String.Equals(str, Boolean.TrueString, StringComparison.OrdinalIgnoreCase)) return true;
if (String.Equals(str, Boolean.FalseString, StringComparison.OrdinalIgnoreCase)) return false;
if (Int32.TryParse(str, out var n)) return n != 0;
return Int32.TryParse(str, out var n) ? n != 0 : defaultValue;
}
else
{
var str2 = value.ToString()?.Trim();
if (!str2.IsNullOrEmpty() && Boolean.TryParse(str2, out var n2))
{
return n2;
}
if (String.Equals(str2, Boolean.TrueString, StringComparison.OrdinalIgnoreCase)) return true;
if (String.Equals(str2, Boolean.FalseString, StringComparison.OrdinalIgnoreCase)) return false;
if (!str2.IsNullOrEmpty() && Int32.TryParse(str2, out var n3))
{
return n3 != 0;
}
}
try
{
// 转换接口
@@ -519,9 +539,7 @@ public class DefaultConvert
}
catch { }
// 转字符串再转整数,作为兜底方案
var str2 = value.ToString();
return !str2.IsNullOrEmpty() && Boolean.TryParse(str2.Trim(), out var n2) ? n2 : defaultValue;
return defaultValue;
}
/// <summary>转为时间日期转换失败时返回最小时间。支持字符串、整数Unix秒</summary>
@@ -672,12 +690,14 @@ public class DefaultConvert
// 去掉逗号分隔符
var ch = input[i];
if (ch == ',' || ch == '_' || ch == ' ') continue;
// 支持前缀正号。Redis响应中就会返回带正号的整数
if (ch == '+')
{
if (idx == 0) continue;
return 0;
}
// 支持负数
if (ch == '-' && idx > 0) return 0;
@@ -696,7 +716,7 @@ public class DefaultConvert
return idx;
}
/// <summary>去掉时间日期指定位置后面部分可指定毫秒ms、秒s、分m、小时h</summary>
/// <summary>去掉时间日期指定位置后面部分可指定毫秒ms、秒s、分m、小时h、纳秒ns</summary>
/// <param name="value">时间日期</param>
/// <param name="format">格式字符串默认s格式化到秒ms格式化到毫秒</param>
/// <returns></returns>
@@ -706,6 +726,7 @@ public class DefaultConvert
{
#if NET8_0_OR_GREATER
"us" => new DateTime(value.Year, value.Month, value.Day, value.Hour, value.Minute, value.Second, value.Millisecond, value.Microsecond, value.Kind),
"ns" => new DateTime(value.Year, value.Month, value.Day, value.Hour, value.Minute, value.Second, value.Millisecond, value.Microsecond / 100 * 100, value.Kind),
#endif
"ms" => new DateTime(value.Year, value.Month, value.Day, value.Hour, value.Minute, value.Second, value.Millisecond, value.Kind),
"s" => new DateTime(value.Year, value.Month, value.Day, value.Hour, value.Minute, value.Second, value.Kind),
@@ -907,9 +928,10 @@ public class DefaultConvert
/// <returns></returns>
public virtual String GetMessage(Exception ex)
{
// 部分异常ToString可能报错例如System.Data.SqlClient.SqlException
try
{
var msg = ex + "";
var msg = ex + string.Empty;
if (msg.IsNullOrEmpty()) return ex.Message;
var ss = msg.Split(Environment.NewLine);

View File

@@ -3,7 +3,8 @@ using System.ComponentModel;
using System.Runtime.Serialization;
using System.Xml.Serialization;
#nullable enable
using ThingsGateway.NewLife.Log;
namespace ThingsGateway.NewLife;
/// <summary>具有是否已释放和释放后事件的接口</summary>
@@ -94,7 +95,7 @@ public abstract class DisposeBase : IDisposable2
}
catch (Exception ex)
{
NewLife.Log.XTrace.WriteException(ex);
XTrace.WriteException(ex);
}
}
#endregion
@@ -150,5 +151,4 @@ public static class DisposeHelper
return obj;
}
}
#nullable restore
}

View File

@@ -69,4 +69,4 @@ public class Gen2GcCallback : CriticalFinalizerObject
}
GC.ReRegisterForFinalize(this);
}
}
}

View File

@@ -1,6 +1,4 @@
using Newtonsoft.Json.Linq;
using System.ComponentModel;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Net.NetworkInformation;
using System.Reflection;
@@ -9,9 +7,11 @@ using System.Runtime.Versioning;
using System.Security;
using ThingsGateway.NewLife.Collections;
using ThingsGateway.NewLife.Json.Extension;
using ThingsGateway.NewLife.Data;
using ThingsGateway.NewLife.Log;
using ThingsGateway.NewLife.Model;
using ThingsGateway.NewLife.Reflection;
using ThingsGateway.NewLife.Serialization;
using ThingsGateway.NewLife.Windows;
#if NETFRAMEWORK
@@ -42,7 +42,7 @@ public interface IMachineInfo
///
/// 刷新信息成本较高,建议采用单例模式
/// </remarks>
public class MachineInfo
public class MachineInfo : IExtend
{
#region
/// <summary>系统名称</summary>
@@ -116,6 +116,13 @@ public class MachineInfo
[DisplayName("电池剩余")]
public Double Battery { get; set; }
private readonly Dictionary<String, Object?> _items = [];
IDictionary<String, Object?> IExtend.Items => _items;
/// <summary>获取 或 设置 扩展属性数据</summary>
/// <param name="key"></param>
/// <returns></returns>
public Object? this[String key] { get => _items.TryGetValue(key, out var obj) ? obj : null; set => _items[key] = value; }
#endregion
#region
@@ -127,28 +134,44 @@ public class MachineInfo
//static MachineInfo() => RegisterAsync().Wait(100);
private static Task<MachineInfo>? _task;
/// <summary>异步注册一个初始化后的机器信息实例</summary>
/// <returns></returns>
public static MachineInfo Register()
public static Task<MachineInfo> RegisterAsync()
{
if (Current != null) return Current;
if (_task != null) return _task;
return _task = Task.Factory.StartNew(() =>
{
return Register();
});
}
private static MachineInfo Register()
{
var set = Setting.Current;
var dataPath = set.DataPath;
if (dataPath.IsNullOrEmpty()) dataPath = "Data";
// 文件缓存加快机器信息获取。在Linux下可能StarAgent以root权限写入缓存文件其它应用以普通用户访问
var file = Path.GetTempPath().CombinePath("machine_info.json");
var json = "";
var file2 = dataPath.CombinePath("machine_info.json").GetBasePath();
var json = string.Empty;
if (Current == null)
{
var f = file;
if (!File.Exists(f)) f = file2;
if (File.Exists(f))
{
try
{
//XTrace.WriteLine("Load MachineInfo {0}", f);
json = File.ReadAllText(f);
Current = json.FromJsonNetString<MachineInfo>();
Current = json.ToJsonEntity<MachineInfo>();
}
catch (Exception ex)
{
if (XTrace.Log.Level <= LogLevel.Debug) NewLife.Log.XTrace.WriteException(ex);
if (XTrace.Log.Level <= LogLevel.Debug) XTrace.WriteException(ex);
}
}
}
@@ -158,27 +181,33 @@ public class MachineInfo
mi.Init();
Current = mi;
// 注册到对象容器
ObjectContainer.Current.AddSingleton(mi);
try
{
var json2 = mi.ToJsonNetString();
var json2 = mi.ToJson(true);
if (json != json2)
{
File.WriteAllText(file2.EnsureDirectory(true), json2);
File.WriteAllText(file.EnsureDirectory(true), json2);
}
}
catch (Exception ex)
{
if (XTrace.Log.Level <= LogLevel.Debug) NewLife.Log.XTrace.WriteException(ex);
if (XTrace.Log.Level <= LogLevel.Debug) XTrace.WriteException(ex);
}
return mi;
}
/// <summary>获取当前信息,如果未设置则等待异步注册结果</summary>
/// <returns></returns>
public static MachineInfo GetCurrent() => Current ?? Register();
/// <summary>从对象容器中获取一个已注册机器信息实例</summary>
/// <returns></returns>
public static MachineInfo? Resolve() => ObjectContainer.Current.Resolve<MachineInfo>();
#endregion
#region
@@ -186,9 +215,9 @@ public class MachineInfo
public void Init()
{
var osv = Environment.OSVersion;
if (OSVersion.IsNullOrEmpty()) OSVersion = osv.Version + "";
if (OSVersion.IsNullOrEmpty()) OSVersion = osv.Version + string.Empty;
if (OSName.IsNullOrEmpty()) OSName = (osv + "").TrimStart("Microsoft").TrimEnd(OSVersion).Trim();
if (Guid.IsNullOrEmpty()) Guid = "";
if (Guid.IsNullOrEmpty()) Guid = string.Empty;
try
{
@@ -210,7 +239,7 @@ public class MachineInfo
}
catch (Exception ex)
{
if (XTrace.Log.Level <= LogLevel.Debug) NewLife.Log.XTrace.WriteException(ex);
if (XTrace.Log.Level <= LogLevel.Debug) XTrace.WriteException(ex);
}
// 裁剪不可见字符,顺带去掉两头空白
@@ -234,7 +263,7 @@ public class MachineInfo
}
catch (Exception ex)
{
if (XTrace.Log.Level <= LogLevel.Debug) NewLife.Log.XTrace.WriteException(ex);
if (XTrace.Log.Level <= LogLevel.Debug) XTrace.WriteException(ex);
}
}
@@ -243,43 +272,43 @@ public class MachineInfo
#endif
private void LoadWindowsInfo()
{
var str = "";
var str = string.Empty;
// 从注册表读取 MachineGuid
#if NETFRAMEWORK || NET6_0_OR_GREATER
using var Cryptography = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Cryptography");
if (Cryptography != null) str = Cryptography.GetValue("MachineGuid") + "";
var reg = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Cryptography");
if (reg != null) str = reg.GetValue("MachineGuid") + string.Empty;
if (str.IsNullOrEmpty())
{
using var Registry64 = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64);
using var Registry64Cryptography = Registry64?.OpenSubKey(@"SOFTWARE\Microsoft\Cryptography");
if (Registry64Cryptography != null) str = Registry64Cryptography.GetValue("MachineGuid") + "";
reg = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64);
reg = reg?.OpenSubKey(@"SOFTWARE\Microsoft\Cryptography");
if (reg != null) str = reg.GetValue("MachineGuid") + string.Empty;
}
if (!str.IsNullOrEmpty()) Guid = str;
using var HardwareConfig = Registry.LocalMachine.OpenSubKey(@"SYSTEM\HardwareConfig");
if (HardwareConfig != null)
reg = Registry.LocalMachine.OpenSubKey(@"SYSTEM\HardwareConfig");
if (reg != null)
{
str = (HardwareConfig.GetValue("LastConfig") + "")?.Trim('{', '}').ToUpper();
str = (reg.GetValue("LastConfig") + "")?.Trim('{', '}').ToUpper();
// UUID取不到时返回 FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF
if (!str.IsNullOrEmpty() && !str.EqualIgnoreCase("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF")) UUID = str;
}
using var BIOS = Registry.LocalMachine.OpenSubKey(@"HARDWARE\DESCRIPTION\System\BIOS") ?? Registry.LocalMachine.OpenSubKey(@"SYSTEM\HardwareConfig\Current");
if (BIOS != null)
reg = Registry.LocalMachine.OpenSubKey(@"HARDWARE\DESCRIPTION\System\BIOS");
reg ??= Registry.LocalMachine.OpenSubKey(@"SYSTEM\HardwareConfig\Current");
if (reg != null)
{
Product = (BIOS.GetValue("SystemProductName") + "").Replace("System Product Name", null);
if (Product.IsNullOrEmpty()) Product = BIOS.GetValue("BaseBoardProduct") + "";
Product = (reg.GetValue("SystemProductName") + "").Replace("System Product Name", null);
if (Product.IsNullOrEmpty()) Product = reg.GetValue("BaseBoardProduct") + string.Empty;
Vendor = BIOS.GetValue("SystemManufacturer") + "";
if (Vendor.IsNullOrEmpty()) Vendor = BIOS.GetValue("ASUSTeK COMPUTER INC.") + "";
Vendor = reg.GetValue("SystemManufacturer") + string.Empty;
if (Vendor.IsNullOrEmpty()) Vendor = reg.GetValue("ASUSTeK COMPUTER INC.") + string.Empty;
}
using var CentralProcessor = Registry.LocalMachine.OpenSubKey(@"HARDWARE\DESCRIPTION\System\CentralProcessor\0");
if (CentralProcessor != null) Processor = CentralProcessor.GetValue("ProcessorNameString") + "";
reg = Registry.LocalMachine.OpenSubKey(@"HARDWARE\DESCRIPTION\System\CentralProcessor\0");
if (reg != null) Processor = reg.GetValue("ProcessorNameString") + string.Empty;
// 旧版系统如win2008没有UUID的注册表项需要用wmic查询。也可能因为过去的某个BUG导致GUID跟UUID相等
if (UUID.IsNullOrEmpty() || UUID == Guid || Vendor.IsNullOrEmpty())
@@ -329,13 +358,13 @@ public class MachineInfo
var reg2 = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion");
if (reg2 != null)
{
OSName = reg2.GetValue("ProductName") + "";
OSVersion = reg2.GetValue("ReleaseId") + "";
OSName = reg2.GetValue("ProductName") + string.Empty;
OSVersion = reg2.GetValue("ReleaseId") + string.Empty;
}
}
catch (Exception ex)
{
if (XTrace.Log.Level <= LogLevel.Debug) NewLife.Log.XTrace.WriteException(ex);
if (XTrace.Log.Level <= LogLevel.Debug) XTrace.WriteException(ex);
}
}
//#elif NET6_0_OR_GREATER
@@ -446,7 +475,7 @@ public class MachineInfo
// Guid = str;
// DMI信息位于 /sys/class/dmi/id/ 目录可以直接读取不需要执行dmidecode命令
var uuid = "";
var uuid = string.Empty;
var file = "/sys/class/dmi/id/product_uuid";
if (!File.Exists(file)) file = "/etc/uuid";
if (!File.Exists(file)) file = "/proc/serial_num"; // miui12支持/proc/serial_num
@@ -906,7 +935,7 @@ public class MachineInfo
var str = "powershell.exe".Execute(args, 3_000) ?? String.Empty;
if (!String.IsNullOrWhiteSpace(str))
{
foreach (var item in JObject.Parse(str)!)
foreach (var item in str.DecodeJson()!)
{
dic[item.Key] = item.Value?.ToString() ?? String.Empty;
}
@@ -971,7 +1000,7 @@ public class MachineInfo
{
try
{
dic[item.Name] = item.GetValue(null) + "";
dic[item.Name] = item.GetValue(null) + string.Empty;
}
catch { }
}
@@ -985,7 +1014,7 @@ public class MachineInfo
{
try
{
dic[item.Name] = item.GetValue(null) + "";
dic[item.Name] = item.GetValue(null) + string.Empty;
}
catch { }
}
@@ -1137,7 +1166,7 @@ public class MachineInfo
public Int64 ToLong() => (Int64)(((UInt64)High << 32) | Low);
}
private sealed class SystemTime
private class SystemTime
{
public Int64 IdleTime;
public Int64 TotalTime;

View File

@@ -4,6 +4,7 @@ using System.Runtime;
using System.Runtime.InteropServices;
using ThingsGateway.NewLife.Log;
using ThingsGateway.NewLife.Threading;
namespace ThingsGateway.NewLife;
@@ -65,9 +66,11 @@ public static class Runtime
#region
/// <summary>是否Mono环境</summary>
public static Boolean Mono { get; } = Type.GetType("Mono.Runtime") != null;
public static Boolean Mono { get; }
/// <summary>是否Unity环境</summary>
public static Boolean Unity { get; }
#if !NETFRAMEWORK
private static Boolean? _IsWeb;
/// <summary>是否Web环境</summary>
@@ -132,13 +135,17 @@ public static class Runtime
}
#endif
/// <summary>获取当前UTC时间。基于全局时间提供者在星尘应用中会屏蔽服务器时间差</summary>
/// <returns></returns>
public static DateTimeOffset UtcNow => TimerScheduler.GlobalTimeProvider.GetUtcNow();
private static Int32 _ProcessId;
#if NET6_0_OR_GREATER
/// <summary>当前进程Id</summary>
public static Int32 ProcessId => _ProcessId > 0 ? _ProcessId : _ProcessId = Environment.ProcessId;
#else
/// <summary>当前进程Id</summary>
public static Int32 ProcessId => _ProcessId > 0 ? _ProcessId : _ProcessId = Process.GetCurrentProcess().Id;
public static Int32 ProcessId => _ProcessId > 0 ? _ProcessId : _ProcessId = ProcessHelper.GetProcessId();
#endif
/// <summary>

View File

@@ -0,0 +1,150 @@
using System.ComponentModel;
using System.Reflection;
using ThingsGateway.NewLife.Configuration;
using ThingsGateway.NewLife.Reflection;
using ThingsGateway.NewLife.Security;
namespace ThingsGateway.NewLife.Common;
/// <summary>系统设置。提供系统名称、版本等基本设置</summary>
/// <remarks>
/// 文档 https://newlifex.com/core/sysconfig
/// </remarks>
[DisplayName("系统设置")]
public class SysConfig : Config<SysConfig>
{
#region
/// <summary>系统名称</summary>
[DisplayName("系统名称")]
[Description("用于标识系统的英文名")]
public String Name { get; set; } = string.Empty;
/// <summary>系统版本</summary>
[DisplayName("系统版本")]
public String Version { get; set; } = string.Empty;
/// <summary>显示名称</summary>
[DisplayName("显示名称")]
[Description("用户可见的名称")]
public String DisplayName { get; set; } = string.Empty;
/// <summary>公司</summary>
[DisplayName("公司")]
public String Company { get; set; } = string.Empty;
/// <summary>应用实例。单应用多实例部署时用于唯一标识实例节点</summary>
[DisplayName("应用实例。单应用多实例部署时用于唯一标识实例节点")]
public Int32 Instance { get; set; }
/// <summary>开发者模式</summary>
[DisplayName("开发者模式")]
public Boolean Develop { get; set; } = true;
/// <summary>启用</summary>
[DisplayName("启用")]
public Boolean Enable { get; set; } = true;
/// <summary>安装时间</summary>
[DisplayName("安装时间")]
public DateTime InstallTime { get; set; } = DateTime.Now;
#endregion
#region
/// <summary>加载后触发</summary>
protected override void OnLoaded()
{
if (IsNew)
{
var asmx = SysAssembly;
Name = asmx?.Name ?? "NewLife.Cube";
Version = asmx?.Version ?? "0.1";
DisplayName = (asmx?.Title ?? asmx?.Name) ?? "魔方平台";
Company = asmx?.Company ?? "新生命开发团队";
//Address = "新生命开发团队";
if (DisplayName.IsNullOrEmpty()) DisplayName = "系统设置";
}
// 强制设置
var name = GetSysName();
if (!name.IsNullOrEmpty()) Name = name;
// 本地实例取IPv4地址后两段
if (Instance <= 0)
{
try
{
var ip = NetHelper.MyIP();
if (ip != null)
{
var buf = ip.GetAddressBytes();
Instance = (buf[2] << 8) | buf[3];
}
else
{
Instance = Rand.Next(1, 1024);
}
}
catch
{
// 异常时随机
Instance = Rand.Next(1, 1024);
}
}
base.OnLoaded();
}
/// <summary>获取系统名</summary>
/// <returns></returns>
public static String? GetSysName()
{
// 从命令参数或环境变量获取系统名称强制覆盖SysConfig方便星尘发布根据命令行控制系统名称
var name = string.Empty;
// 命令参数
var args = Environment.GetCommandLineArgs();
for (var i = 0; i < args.Length; i++)
{
if (args[i].EqualIgnoreCase("-Name") && i + 1 < args.Length)
{
name = args[i + 1];
break;
}
}
// 环境变量
if (name.IsNullOrEmpty()) name = Runtime.GetEnvironmentVariable("Name");
return name;
}
/// <summary>系统主程序集</summary>
public static AssemblyX? SysAssembly
{
get
{
try
{
var asm = AssemblyX.Entry;
if (asm != null) return asm;
var sm = Assembly.GetCallingAssembly();
if (sm != null) return AssemblyX.Create(sm);
var list = AssemblyX.GetMyAssemblies();
// 最后编译那一个
list = list.OrderByDescending(e => e.Compile)
.ThenByDescending(e => e.Name.EndsWithIgnoreCase("Web"))
.ToList();
return list.FirstOrDefault();
}
catch { return null; }
}
}
#endregion
}

View File

@@ -66,4 +66,4 @@ public abstract class TimeProvider
/// <returns></returns>
public TimeSpan GetElapsedTime(Int64 startingTimestamp) => GetElapsedTime(startingTimestamp, GetTimestamp());
}
#endif
#endif

View File

@@ -0,0 +1,64 @@
using ThingsGateway.NewLife.Log;
namespace ThingsGateway.NewLife.Compression;
/// <summary>7Zip</summary>
public class SevenZip
{
#region
private static readonly String _7z = null!;
static SevenZip()
{
var p = string.Empty;
var set = Setting.Current;
// 附近文件
if (p.IsNullOrEmpty())
{
p = "7z.exe".GetFullPath();
if (!File.Exists(p)) p = set.PluginPath.CombinePath("7z.exe").GetFullPath();
if (!File.Exists(p)) p = "7z/7z.exe".GetFullPath();
if (!File.Exists(p)) p = "../7z/7z.exe".GetFullPath();
if (!File.Exists(p)) p = string.Empty;
}
if (!p.IsNullOrEmpty()) _7z = p.GetFullPath();
XTrace.WriteLine("7Z目录 {0}", _7z);
}
#endregion
#region /
/// <summary>压缩文件</summary>
/// <param name="path"></param>
/// <param name="destFile"></param>
/// <returns></returns>
public void Compress(String path, String destFile)
{
if (Directory.Exists(path)) path = path.GetFullPath().EnsureEnd("\\") + "*";
Run($"a \"{destFile}\" \"{path}\" -mx9 -ssw");
}
/// <summary>解压缩文件</summary>
/// <param name="file"></param>
/// <param name="destDir"></param>
/// <param name="overwrite">是否覆盖目标同名文件</param>
/// <returns></returns>
public void Extract(String file, String destDir, Boolean overwrite = false)
{
destDir.EnsureDirectory(false);
var args = $"x \"{file}\" -o\"{destDir}\" -y -r";
if (overwrite)
args += " -aoa";
else
args += " -aos";
Run(args);
}
private Int32 Run(String args) => _7z.Run(args, 5000);
#endregion
}

View File

@@ -0,0 +1,300 @@
using System.Collections.Concurrent;
namespace ThingsGateway.NewLife.Configuration;
/// <summary>复合配置提供者。常用于本地配置与网络配置的混合</summary>
public class CompositeConfigProvider : IConfigProvider
{
#region
/// <summary>日志提供者集合</summary>
/// <remarks>为了线程安全,使用数组</remarks>
public IConfigProvider[] Configs { get; set; } //= new IConfigProvider[0];
/// <summary>名称</summary>
public String Name { get; set; }
/// <summary>根元素</summary>
public IConfigSection Root { get => Configs[0].Root; set => throw new NotImplementedException(); }
/// <summary>所有键</summary>
public ICollection<String> Keys
{
get
{
var ks = new List<String>();
foreach (var cfg in Configs)
{
if (cfg.Keys != null)
{
foreach (var item in cfg.Keys)
{
if (!ks.Contains(item)) ks.Add(item);
}
}
}
return ks;
}
}
/// <summary>是否新的配置文件</summary>
public Boolean IsNew { get => Configs[0].IsNew; set => Configs[0].IsNew = value; }
/// <summary>返回获取配置的委托</summary>
public GetConfigCallback GetConfig => key => GetSection(key)?.Value;
#endregion
#region
///// <summary>实例化</summary>
//public CompositeConfigProvider() => Name = GetType().Name.TrimEnd("ConfigProvider");
/// <summary>实例化</summary>
/// <param name="configProvider1"></param>
/// <param name="configProvider2"></param>
public CompositeConfigProvider(IConfigProvider configProvider1, IConfigProvider configProvider2)
{
Name = GetType().Name.TrimEnd("ConfigProvider");
Configs = [configProvider1, configProvider2];
}
/// <summary>添加</summary>
/// <param name="configProviders"></param>
public void Add(params IConfigProvider[] configProviders)
{
var list = new List<IConfigProvider>(Configs);
list.AddRange(configProviders);
Configs = list.ToArray();
}
#endregion
#region
/// <summary>获取 或 设置 配置值</summary>
/// <param name="key">键</param>
/// <returns></returns>
public String? this[String key]
{
get
{
foreach (var cfg in Configs)
{
var value = cfg[key];
if (value != null) return value;
}
return null;
}
set
{
foreach (var cfg in Configs)
{
//cfg[key] = value;
var section = cfg.GetSection(key);
if (section != null) section.Value = value;
}
}
}
/// <summary>查找配置项。可得到子级和配置</summary>
/// <param name="key">配置名</param>
/// <returns></returns>
public IConfigSection? GetSection(String key)
{
foreach (var cfg in Configs)
{
var section = cfg.GetSection(key);
if (section != null) return section;
}
return null;
}
#endregion
#region
/// <summary>从数据源加载数据到配置树</summary>
public Boolean LoadAll()
{
var rs = false;
foreach (var cfg in Configs)
{
rs |= cfg.LoadAll();
}
return rs;
}
/// <summary>保存配置树到数据源</summary>
public Boolean SaveAll()
{
var rs = false;
foreach (var cfg in Configs)
{
rs |= cfg.SaveAll();
}
return rs;
}
/// <summary>加载配置到模型</summary>
/// <typeparam name="T">模型。可通过实现IConfigMapping接口来自定义映射配置到模型实例</typeparam>
/// <param name="path">路径。配置树位置,配置中心等多对象混合使用时</param>
/// <returns></returns>
public T? Load<T>(String? path = null) where T : new()
{
foreach (var cfg in Configs)
{
var model = cfg.Load<T>(path);
if (model != null) return model;
}
return default;
}
/// <summary>保存模型实例</summary>
/// <typeparam name="T">模型</typeparam>
/// <param name="model">模型实例</param>
/// <param name="path">路径。配置树位置</param>
public Boolean Save<T>(T model, String? path = null)
{
foreach (var cfg in Configs)
{
var rs = cfg.Save(model, path);
if (rs) return true;
}
return false;
}
#endregion
#region
private readonly ConcurrentDictionary<Object, String> _models = [];
private readonly ConcurrentDictionary<Object, ModelWrap> _models2 = [];
/// <summary>绑定模型,使能热更新,配置存储数据改变时同步修改模型属性</summary>
/// <typeparam name="T">模型。可通过实现IConfigMapping接口来自定义映射配置到模型实例</typeparam>
/// <param name="model">模型实例</param>
/// <param name="autoReload">是否自动更新。默认true</param>
/// <param name="path">命名空间。配置树位置,配置中心等多对象混合使用时</param>
public virtual void Bind<T>(T model, Boolean autoReload = true, String? path = null)
{
if (model == null) return;
// 如果有命名空间则使用指定层级数据源
path ??= String.Empty;
var source = GetSection(path);
if (source != null)
{
if (model is IConfigMapping map)
map.MapConfig(this, source);
else
source.MapTo(model, this);
}
if (autoReload)
{
_models.TryAdd(model, path);
}
AddChanged();
}
/// <summary>绑定模型,使能热更新,配置存储数据改变时同步修改模型属性</summary>
/// <typeparam name="T">模型。可通过实现IConfigMapping接口来自定义映射配置到模型实例</typeparam>
/// <param name="model">模型实例</param>
/// <param name="path">命名空间。配置树位置,配置中心等多对象混合使用时</param>
/// <param name="onChange">配置改变时执行的委托</param>
public virtual void Bind<T>(T model, String? path, Action<IConfigSection> onChange)
{
if (model == null) return;
// 如果有命名空间则使用指定层级数据源
path ??= String.Empty;
var source = GetSection(path);
if (source != null)
{
if (model is IConfigMapping map)
map.MapConfig(this, source);
else
source.MapTo(model, this);
}
if (onChange != null)
{
_models2.TryAdd(model, new ModelWrap(path, onChange));
}
AddChanged();
}
private record ModelWrap(String Path, Action<IConfigSection> OnChange);
/// <summary>通知绑定对象,配置数据有改变</summary>
protected virtual void NotifyChange()
{
foreach (var item in _models)
{
var model = item.Key;
var source = GetSection(item.Value);
if (source != null)
{
if (model is IConfigMapping map)
map.MapConfig(this, source);
else
source.MapTo(model, this);
}
}
foreach (var item in _models2)
{
var model = item.Key;
var source = GetSection(item.Value.Path);
if (source != null) item.Value.OnChange(source);
}
// 通过事件通知外部
_Changed?.Invoke(this, EventArgs.Empty);
}
#endregion
#region
private Int32 _count;
private event EventHandler? _Changed;
/// <summary>配置改变事件。执行了某些动作,可能导致配置数据发生改变时触发</summary>
public event EventHandler Changed
{
add
{
_Changed += value;
// 首次注册事件时,向内部提供者注册事件
AddChanged();
}
remove
{
// 最后一次取消注册时,向内部提供者取消注册
if (Interlocked.Decrement(ref _count) == 0)
{
foreach (var cfg in Configs)
{
cfg.Changed -= OnChange;
}
}
_Changed -= value;
}
}
private void AddChanged()
{
if (Interlocked.Increment(ref _count) == 1)
{
foreach (var cfg in Configs)
{
cfg.Changed += OnChange;
}
}
}
private void OnChange(Object? sender, EventArgs e) => NotifyChange();
#endregion
}

View File

@@ -6,7 +6,7 @@
/// 如未指定提供者,则使用全局默认,此时将根据全局代码配置或环境变量配置使用不同提供者,实现配置信息整体转移。
/// </remarks>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public sealed class ConfigAttribute : Attribute
public class ConfigAttribute : Attribute
{
/// <summary>提供者。内置ini/xml/json/http一般不指定使用全局默认</summary>
public String? Provider { get; set; }
@@ -23,3 +23,48 @@ public sealed class ConfigAttribute : Attribute
Name = name;
}
}
/// <summary>http配置特性</summary>
/// <remarks>
/// 声明配置模型使用哪一种配置提供者,以及所需要的文件名和分类名。
/// 如未指定提供者,则使用全局默认,此时将根据全局代码配置或环境变量配置使用不同提供者,实现配置信息整体转移。
/// </remarks>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class HttpConfigAttribute : ConfigAttribute
{
/// <summary>应用标识</summary>
public String Server { get; set; }
/// <summary>服务操作</summary>
public String Action { get; set; }
/// <summary>应用标识</summary>
public String AppId { get; set; }
/// <summary>应用密钥</summary>
public String? Secret { get; set; }
/// <summary>作用域。获取指定作用域下的配置值,生产、开发、测试 等</summary>
public String? Scope { get; set; }
/// <summary>本地缓存配置数据。即使网络断开仍然能够加载使用本地数据默认Encrypted</summary>
public ConfigCacheLevel CacheLevel { get; set; }
/// <summary>指定配置名</summary>
/// <param name="server">服务器地址</param>
/// <param name="action">服务操作</param>
/// <param name="name">配置名。可以是文件名或分类名</param>
/// <param name="appId">应用标识</param>
/// <param name="secret">应用密钥</param>
/// <param name="scope">作用域。获取指定作用域下的配置值,生产、开发、测试 等</param>
/// <param name="cacheLevel">本地缓存配置数据。即使网络断开仍然能够加载使用本地数据默认Encrypted</param>
public HttpConfigAttribute(String name, String server, String action, String appId, String? secret = null, String? scope = null, ConfigCacheLevel cacheLevel = ConfigCacheLevel.Encrypted) : base(name, "http")
{
Server = server;
Action = action;
AppId = appId;
Secret = secret;
Scope = scope;
CacheLevel = cacheLevel;
}
}

View File

@@ -0,0 +1,15 @@
namespace ThingsGateway.NewLife.Configuration
{
/// <summary>配置数据缓存等级</summary>
public enum ConfigCacheLevel
{
/// <summary>不缓存</summary>
NoCache,
/// <summary>Json格式缓存</summary>
Json,
/// <summary>加密缓存</summary>
Encrypted,
}
}

View File

@@ -1,5 +1,4 @@

using ThingsGateway.NewLife.Log;
using ThingsGateway.NewLife.Log;
using ThingsGateway.NewLife.Threading;
namespace ThingsGateway.NewLife.Configuration;
@@ -13,7 +12,7 @@ public abstract class FileConfigProvider : ConfigProvider
#region
/// <summary>文件名。最高优先级,优先于模型特性指定的文件名</summary>
public String? FileName { get; set; }
public static String DataPath { get; set; } = "Configuration";
/// <summary>更新周期。默认5秒</summary>
public Int32 Period { get; set; } = 5;
#endregion
@@ -34,6 +33,8 @@ public abstract class FileConfigProvider : ConfigProvider
#endregion
#region
/// <summary>初始化</summary>
/// <param name="value"></param>
public override void Init(String value)
@@ -45,7 +46,7 @@ public abstract class FileConfigProvider : ConfigProvider
{
// 加上配置目录
var str = value;
if (!str.StartsWithIgnoreCase("Config/", "Config\\")) str = "Config".CombinePath(str);
if (!str.StartsWithIgnoreCase($"{DataPath}/", $"{DataPath}\\")) str = $"{DataPath}".CombinePath(str);
FileName = str;
}
@@ -126,8 +127,8 @@ public abstract class FileConfigProvider : ConfigProvider
protected virtual void OnWrite(String fileName, IConfigSection section)
{
var str = GetString(section);
var old = "";
if (File.Exists(fileName)) old = File.ReadAllText(fileName)?.Trim() ?? "";
var old = string.Empty;
if (File.Exists(fileName)) old = File.ReadAllText(fileName)?.Trim() ?? string.Empty;
if (str != null && str != old)
{
@@ -206,7 +207,7 @@ public abstract class FileConfigProvider : ConfigProvider
}
catch (Exception ex)
{
NewLife.Log.XTrace.WriteException(ex);
XTrace.WriteException(ex);
}
finally
{

View File

@@ -304,7 +304,7 @@ public abstract class ConfigProvider : DisposeBase, IConfigProvider
}
}
private sealed record ModelWrap(String Path, Action<IConfigSection> OnChange);
private record ModelWrap(String Path, Action<IConfigSection> OnChange);
/// <summary>通知绑定对象,配置数据有改变</summary>
protected virtual void NotifyChange()
@@ -340,7 +340,7 @@ public abstract class ConfigProvider : DisposeBase, IConfigProvider
static ConfigProvider()
{
// 支持从命令行参数和环境变量设定默认配置提供者
var str = "";
var str = string.Empty;
var args = Environment.GetCommandLineArgs();
for (var i = 0; i < args.Length; i++)
{
@@ -350,11 +350,12 @@ public abstract class ConfigProvider : DisposeBase, IConfigProvider
break;
}
}
if (str.IsNullOrEmpty()) str = ThingsGateway.NewLife.Runtime.GetEnvironmentVariable("DefaultConfig");
if (str.IsNullOrEmpty()) str = NewLife.Runtime.GetEnvironmentVariable("DefaultConfig");
if (!str.IsNullOrEmpty()) DefaultProvider = str;
Register<InIConfigProvider>("ini");
Register<XmlConfigProvider>("xml");
Register<JsonConfigProvider>("json");
Register<XmlConfigProvider>("config");
}

View File

@@ -26,7 +26,7 @@ public class InIConfigProvider : FileConfigProvider
var lines = File.ReadAllLines(fileName);
var currentSection = section;
var remark = "";
var remark = string.Empty;
foreach (var item in lines)
{
var str = item.Trim();

View File

@@ -0,0 +1,223 @@
using ThingsGateway.NewLife.Serialization;
namespace ThingsGateway.NewLife.Configuration;
/// <summary>Json文件配置提供者</summary>
/// <remarks>
/// 支持从不同配置文件加载到不同配置模型
/// </remarks>
public class JsonConfigProvider : FileConfigProvider
{
#region
/// <summary>加载本地配置文件得到配置提供者</summary>
/// <param name="fileName">配置文件名默认appsettings.json</param>
/// <returns></returns>
public static JsonConfigProvider LoadAppSettings(String? fileName = null)
{
if (fileName.IsNullOrEmpty()) fileName = "appsettings.json";
// 读取本地配置
var jsonConfig = new JsonConfigProvider { FileName = fileName };
return jsonConfig;
}
#endregion
/// <summary>初始化</summary>
/// <param name="value"></param>
public override void Init(String value)
{
// 加上默认后缀
if (!value.IsNullOrEmpty() && Path.GetExtension(value).IsNullOrEmpty()) value += ".json";
base.Init(value);
}
/// <summary>读取配置文件</summary>
/// <param name="fileName">文件名</param>
/// <param name="section">配置段</param>
protected override void OnRead(String fileName, IConfigSection section)
{
var txt = File.ReadAllText(fileName);
// 预处理注释
txt = TrimComment(txt);
var src = txt.DecodeJson();
if (src != null) Map(src, section);
}
/// <summary>获取字符串形式</summary>
/// <param name="section">配置段</param>
/// <returns></returns>
public override String GetString(IConfigSection? section = null)
{
section ??= Root;
var rs = new Dictionary<String, Object?>();
Map(section, rs);
var jw = new JsonWriter
{
//IgnoreNullValues = false,
//IgnoreComment = false,
//Indented = true,
//SmartIndented = true,
};
jw.Options.IgnoreNullValues = false;
jw.Options.WriteIndented = true;
jw.Write(rs);
return jw.GetString();
//var js = new Json();
//js.Write(rs);
//return js.GetBytes().ToStr();
}
#region
/// <summary>字典映射到配置树</summary>
/// <param name="src"></param>
/// <param name="section"></param>
protected virtual void Map(IDictionary<String, Object?> src, IConfigSection section)
{
foreach (var item in src)
{
var name = item.Key;
if (name[0] == '#') continue;
var cfg = section.GetOrAddChild(name);
var cname = "#" + name;
if (src.TryGetValue(cname, out var comment) && comment != null) cfg.Comment = comment + string.Empty;
// 支持字典
if (item.Value is IDictionary<String, Object?> dic)
Map(dic, cfg);
else if (item.Value is IList<Object> list)
{
cfg.Childs = new List<IConfigSection>();
foreach (var elm in list)
{
// 复杂对象
if (elm is IDictionary<String, Object?> dic2)
{
var cfg2 = new ConfigSection();
Map(dic2, cfg2);
cfg.Childs.Add(cfg2);
}
// 简单基元类型
else
{
var key = elm?.GetType()?.Name;
if (!key.IsNullOrEmpty())
{
var cfg2 = new ConfigSection
{
Key = key,
Value = elm + "",
};
cfg.Childs.Add(cfg2);
}
}
}
}
else
cfg.SetValue(item.Value);
}
}
/// <summary>配置树映射到字典</summary>
/// <param name="section"></param>
/// <param name="dst"></param>
protected virtual void Map(IConfigSection section, IDictionary<String, Object?> dst)
{
if (section.Childs == null) return;
foreach (var item in section.Childs.ToArray())
{
//// 注释
//if (!item.Comment.IsNullOrEmpty()) dst["#" + item.Key] = item.Comment;
var key = item.Key + string.Empty;
var cs = item.Childs;
if (cs != null)
{
// 数组
if (cs.Count == 0 || cs.Count > 0 && cs[0].Key == null || cs.Count >= 2 && cs[0].Key == cs[1].Key)
{
Object? val = null;
// 普通基元类型数组
if (cs.Count > 0)
{
var childs = cs[0].Childs;
if (childs == null || childs.Count == 0) val = cs.Select(e => e.Value).ToArray();
}
if (val == null)
{
var list = new List<Object>();
foreach (var elm in cs)
{
var rs = new Dictionary<String, Object?>();
Map(elm, rs);
list.Add(rs);
}
val = list;
}
dst[key] = val;
}
else
{
var rs = new Dictionary<String, Object?>();
Map(item, rs);
dst[key] = rs;
}
}
else
{
dst[key] = item.Value;
}
}
}
/// <summary>
/// 清理json字符串中的注释避免json解析错误
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public static String TrimComment(String text)
{
while (true)
{
// 以下处理多行注释 “/**/” 放在一行的情况
var p = text.IndexOf("/*");
if (p < 0) break;
var p2 = text.IndexOf("*/", p + 2);
if (p2 < 0) break;
text = text[..p] + text[(p2 + 2)..];
}
// 增加 \r以及\n的处理 处理类似如下json转换时的错误==>{"key":"http://*:5000" \n /*注释*/}<==
var lines = text.Split("\r\n", "\n", "\r");
text = lines
.Where(e => !e.IsNullOrEmpty() && !e.TrimStart().StartsWith("//"))
// 没考虑到链接中带双斜杠的,以下导致链接的内容被干掉
//.Select(e =>
//{
// // 单行注释 “//” 放在最后的情况
// var p0 = e.IndexOf("//");
// if (p0 > 0) return e.Substring(0, p0);
// return e;
//})
.Join(Environment.NewLine);
return text;
}
#endregion
}

View File

@@ -45,11 +45,11 @@ public class XmlConfigProvider : FileConfigProvider
if (reader.NodeType == XmlNodeType.EndElement) reader.ReadEndElement();
}
private static void ReadNode(XmlReader reader, IConfigSection section)
private void ReadNode(XmlReader reader, IConfigSection section)
{
while (true)
{
var remark = "";
var remark = string.Empty;
if (reader.NodeType == XmlNodeType.Comment) remark = reader.Value;
while (reader.NodeType is XmlNodeType.Comment or XmlNodeType.Whitespace) reader.Skip();
if (reader.NodeType != XmlNodeType.Element) break;
@@ -126,7 +126,7 @@ public class XmlConfigProvider : FileConfigProvider
return ms.ToStr();
}
private static void WriteNode(XmlWriter writer, String name, IConfigSection section)
private void WriteNode(XmlWriter writer, String name, IConfigSection section)
{
if (section.Childs == null) return;
@@ -169,7 +169,7 @@ public class XmlConfigProvider : FileConfigProvider
writer.WriteEndElement();
}
private static void WriteAttributeNode(XmlWriter writer, String name, IConfigSection section)
private void WriteAttributeNode(XmlWriter writer, String name, IConfigSection section)
{
writer.WriteStartElement(name);
//writer.WriteStartAttribute(name);

View File

@@ -0,0 +1,52 @@
using System.Runtime.Serialization;
using System.Xml.Serialization;
namespace ThingsGateway.NewLife.Data;
/// <summary>数据行</summary>
/// <remarks>
/// 文档 https://newlifex.com/core/dbtable
/// </remarks>
/// <remarks>构造数据行</remarks>
/// <param name="table"></param>
/// <param name="index"></param>
public readonly struct DbRow(DbTable table, Int32 index) : IModel
{
#region
/// <summary>数据表</summary>
[XmlIgnore, IgnoreDataMember]
public readonly DbTable Table { get; } = table;
/// <summary>行索引</summary>
public readonly Int32 Index { get; } = index;
#endregion
#region
/// <summary>基于列索引访问</summary>
/// <param name="column"></param>
/// <returns></returns>
public readonly Object? this[Int32 column]
{
get => Table.Rows?[Index][column];
set
{
var rows = Table.Rows;
if (rows != null) rows[Index][column] = value;
}
}
/// <summary>基于列名访问</summary>
/// <param name="name"></param>
/// <returns></returns>
public Object? this[String name] { get => this[Table.GetColumn(name)]; set => this[Table.GetColumn(name)] = value; }
#endregion
#region
/// <summary>读取指定行的字段值</summary>
/// <typeparam name="T"></typeparam>
/// <param name="name"></param>
/// <returns></returns>
public readonly T? Get<T>(String name) => Table.Get<T>(Index, name);
#endregion
}

View File

@@ -0,0 +1,845 @@
using System.Collections;
using System.Data;
using System.Data.Common;
using System.Reflection;
using System.Runtime.Serialization;
using System.Xml;
using System.Xml.Serialization;
using ThingsGateway.NewLife.IO;
using ThingsGateway.NewLife.Reflection;
using ThingsGateway.NewLife.Serialization;
namespace ThingsGateway.NewLife.Data;
/// <summary>数据表</summary>
/// <remarks>
/// 文档 https://newlifex.com/core/dbtable
/// </remarks>
public class DbTable : IEnumerable<DbRow>, ICloneable, IAccessor
{
#region
/// <summary>数据列</summary>
public String[] Columns { get; set; } = [];
/// <summary>数据列类型</summary>
[XmlIgnore, IgnoreDataMember]
public Type[] Types { get; set; } = [];
/// <summary>数据行</summary>
public IList<Object?[]> Rows { get; set; } = [];
/// <summary>总行数</summary>
public Int32 Total { get; set; }
#endregion
#region
#endregion
#region
/// <summary>读取数据</summary>
/// <param name="dr"></param>
public void Read(IDataReader dr)
{
ReadHeader(dr);
ReadData(dr);
}
/// <summary>读取头部</summary>
/// <param name="dr"></param>
public void ReadHeader(IDataReader dr)
{
var count = dr.FieldCount;
// 字段
var cs = new String[count];
var ts = new Type[count];
for (var i = 0; i < count; i++)
{
cs[i] = dr.GetName(i);
ts[i] = dr.GetFieldType(i);
}
Columns = cs;
Types = ts;
}
/// <summary>读取数据</summary>
/// <param name="dr">数据读取器</param>
/// <param name="fields">要读取的字段序列</param>
public void ReadData(IDataReader dr, Int32[]? fields = null)
{
// 字段
var cs = Columns ?? throw new ArgumentNullException(nameof(Columns));
var ts = Types ?? throw new ArgumentNullException(nameof(Types));
fields ??= Enumerable.Range(0, cs.Length).ToArray();
// 数据
var rs = new List<Object?[]>();
while (dr.Read())
{
var row = new Object?[fields.Length];
for (var i = 0; i < fields.Length; i++)
{
// MySql在读取0000时间数据时会报错
try
{
var val = dr[fields[i]];
if (val == DBNull.Value) val = GetDefault(ts[i].GetTypeCode());
row[i] = val;
}
catch { }
}
rs.Add(row);
}
Rows = rs;
Total = rs.Count;
}
/// <summary>读取数据</summary>
/// <param name="dr"></param>
/// <param name="cancellationToken">取消通知</param>
public async Task ReadAsync(DbDataReader dr, CancellationToken cancellationToken = default)
{
ReadHeader(dr);
await ReadDataAsync(dr, null, cancellationToken).ConfigureAwait(false);
}
/// <summary>读取数据</summary>
/// <param name="dr">数据读取器</param>
/// <param name="fields">要读取的字段序列</param>
/// <param name="cancellationToken">取消通知</param>
public async Task ReadDataAsync(DbDataReader dr, Int32[]? fields = null, CancellationToken cancellationToken = default)
{
// 字段
var cs = Columns ?? throw new ArgumentNullException(nameof(Columns));
var ts = Types ?? throw new ArgumentNullException(nameof(Types));
fields ??= Enumerable.Range(0, cs.Length).ToArray();
// 数据
var rs = new List<Object?[]>();
while (await dr.ReadAsync(cancellationToken).ConfigureAwait(false))
{
var row = new Object?[fields.Length];
for (var i = 0; i < fields.Length; i++)
{
// MySql在读取0000时间数据时会报错
try
{
var val = dr[fields[i]];
if (val == DBNull.Value) val = GetDefault(ts[i].GetTypeCode());
row[i] = val;
}
catch { }
}
rs.Add(row);
}
Rows = rs;
Total = rs.Count;
}
#endregion
#region DataTable互转
/// <summary>从DataTable读取数据</summary>
/// <param name="dataTable">数据表</param>
public Int32 Read(DataTable dataTable)
{
if (dataTable == null) throw new ArgumentNullException(nameof(dataTable));
var cs = new List<String>();
var ts = new List<Type>();
foreach (var item in dataTable.Columns)
{
if (item is DataColumn dc)
{
cs.Add(dc.ColumnName);
ts.Add(dc.DataType);
}
}
Columns = cs.ToArray();
Types = ts.ToArray();
var rs = new List<Object?[]>();
foreach (var item in dataTable.Rows)
{
if (item is DataRow dr)
rs.Add(dr.ItemArray);
}
Rows = rs;
return rs.Count;
}
/// <summary>转换为DataTable</summary>
/// <returns></returns>
public DataTable ToDataTable() => Write(new DataTable());
/// <summary>转换为DataTable</summary>
/// <param name="dataTable">数据表</param>
/// <returns></returns>
public DataTable Write(DataTable dataTable)
{
if (dataTable == null) throw new ArgumentNullException(nameof(dataTable));
var cs = Columns ?? throw new ArgumentNullException(nameof(Columns));
var ts = Types ?? throw new ArgumentNullException(nameof(Types));
for (var i = 0; i < cs.Length; i++)
{
var dc = new DataColumn(cs[i], ts[i]);
dataTable.Columns.Add(dc);
}
var rs = Rows;
if (rs != null)
{
for (var i = 0; i < rs.Count; i++)
{
var dr = dataTable.NewRow();
dr.ItemArray = rs[i];
dataTable.Rows.Add(dr);
}
}
return dataTable;
}
#endregion
#region
private const Byte _Ver = 3;
private const String MAGIC = "NewLifeDbTable";
/// <summary>从数据流读取</summary>
/// <param name="stream"></param>
public void Read(Stream stream)
{
var bn = new Binary
{
FullTime = true,
EncodeInt = true,
Stream = stream,
};
// 读取头部
ReadHeader(bn);
// 读取全部数据
ReadData(bn, Total);
}
/// <summary>读取头部</summary>
/// <param name="bn"></param>
public void ReadHeader(Binary bn)
{
// 头部,幻数、版本和标记
var magic = bn.ReadBytes(MAGIC.Length).ToStr();
if (magic != MAGIC) throw new InvalidDataException();
var ver = bn.Read<Byte>();
_ = bn.Read<Byte>();
// 版本兼容
if (ver > _Ver) throw new InvalidDataException($"DbTable[ver={_Ver}] Unable to support newer versions [{ver}]");
// v3开始支持FullTime
if (ver < 3) bn.FullTime = false;
// 读取头部
var count = bn.Read<Int32>();
var cs = new String[count];
var ts = new Type[count];
for (var i = 0; i < count; i++)
{
cs[i] = bn.Read<String>() + string.Empty;
// 复杂类型写入类型字符串
var tc = (TypeCode)bn.Read<Byte>();
if (tc != TypeCode.Object)
ts[i] = Type.GetType("System." + tc) ?? typeof(Object);
else if (ver >= 2)
ts[i] = Type.GetType(bn.Read<String>() + "") ?? typeof(Object);
}
Columns = cs;
Types = ts;
Total = bn.ReadBytes(4).ToInt();
}
/// <summary>读取数据</summary>
/// <param name="bn"></param>
/// <param name="rows"></param>
/// <returns></returns>
public void ReadData(Binary bn, Int32 rows)
{
if (rows <= 0) return;
var ts = Types ?? throw new ArgumentNullException(nameof(Types));
var rs = new List<Object?[]>(rows);
for (var k = 0; k < rows; k++)
{
var row = new Object?[ts.Length];
for (var i = 0; i < ts.Length; i++)
{
row[i] = bn.Read(ts[i]);
}
rs.Add(row);
}
Rows = rs;
}
/// <summary>读取</summary>
/// <param name="pk"></param>
/// <returns></returns>
public Boolean Read(IPacket pk)
{
if (pk == null || pk.Length == 0) return false;
Read(pk.GetStream());
return true;
}
/// <summary>读取</summary>
/// <param name="buffer"></param>
/// <param name="offset"></param>
/// <param name="count"></param>
/// <returns></returns>
public Boolean Read(Byte[] buffer, Int32 offset = 0, Int32 count = -1)
{
if (count < 0) count = buffer.Length - offset;
var ms = new MemoryStream(buffer, offset, count);
Read(ms);
return true;
}
/// <summary>从文件加载</summary>
/// <param name="file"></param>
/// <param name="compressed">是否压缩</param>
/// <returns></returns>
public Int64 LoadFile(String file, Boolean compressed = false) => file.AsFile().OpenRead(compressed, s => Read(s));
Boolean IAccessor.Read(Stream stream, Object? context)
{
Read(stream);
return true;
}
#endregion
#region
/// <summary>写入数据流</summary>
/// <param name="stream"></param>
public void Write(Stream stream)
{
var bn = new Binary
{
FullTime = true,
EncodeInt = true,
Stream = stream,
};
// 写入数据体
var rs = Rows;
if (Total == 0 && rs != null) Total = rs.Count;
// 写入头部
WriteHeader(bn);
// 写入数据行
WriteData(bn);
}
/// <summary>写入头部到数据流</summary>
/// <param name="bn"></param>
public void WriteHeader(Binary bn)
{
var cs = Columns ?? throw new ArgumentNullException(nameof(Columns));
var ts = Types ?? throw new ArgumentNullException(nameof(Types));
// 头部,幻数、版本和标记
var buf = MAGIC.GetBytes();
bn.Write(buf, 0, buf.Length);
bn.Write(_Ver);
bn.Write(0);
// 写入头部
var count = cs.Length;
bn.Write(count);
for (var i = 0; i < count; i++)
{
bn.Write(cs[i]);
// 复杂类型写入类型字符串
var code = ts[i].GetTypeCode();
bn.Write((Byte)code);
if (code == TypeCode.Object) bn.Write(ts[i].FullName);
}
// 数据行数
bn.Write(Total.GetBytes(), 0, 4);
}
/// <summary>写入数据部分到数据流</summary>
/// <param name="bn"></param>
public void WriteData(Binary bn)
{
var ts = Types ?? throw new ArgumentNullException(nameof(Types));
var rs = Rows;
if (rs == null) return;
// 写入数据
foreach (var row in rs)
{
for (var i = 0; i < row.Length; i++)
{
bn.Write(row[i], ts[i]);
}
}
}
/// <summary>写入数据部分到数据流</summary>
/// <param name="bn"></param>
/// <param name="fields">要写入的字段序列</param>
public void WriteData(Binary bn, Int32[] fields)
{
var ts = Types ?? throw new ArgumentNullException(nameof(Types));
var rs = Rows;
if (rs == null) return;
// 写入数据,按照指定的顺序
foreach (var row in rs)
{
for (var i = 0; i < fields.Length; i++)
{
// 找到目标顺序,实际上几乎不可能出现-1
var idx = fields[i];
if (idx >= 0)
bn.Write(row[idx], ts[idx]);
else
bn.Write(null, ts[idx]);
}
}
}
/// <summary>转数据包</summary>
/// <returns></returns>
public IPacket ToPacket()
{
// 不确定所需大小,只能使用内存流,再包装为数据包。
// 头部预留8个字节方便网络传输时添加协议头。
var ms = new MemoryStream
{
Position = 8
};
Write(ms);
ms.Position = 8;
// 包装为数据包,直接窃取内存流内部的缓冲区
return new ArrayPacket(ms);
}
/// <summary>保存到文件</summary>
/// <param name="file"></param>
/// <param name="compressed">是否压缩</param>
/// <returns></returns>
public void SaveFile(String file, Boolean compressed = false) => file.AsFile().OpenWrite(compressed, s => Write(s));
Boolean IAccessor.Write(Stream stream, Object? context)
{
Write(stream);
return true;
}
#endregion
#region Json序列化
/// <summary>转Json字符串</summary>
/// <param name="indented">是否缩进。默认false</param>
/// <param name="nullValue">是否写空值。默认true</param>
/// <param name="camelCase">是否驼峰命名。默认false</param>
/// <returns></returns>
public String ToJson(Boolean indented = false, Boolean nullValue = true, Boolean camelCase = false)
{
// 先转为名值对象的数组,再进行序列化
var list = ToDictionary();
return list.ToJson(indented, nullValue, camelCase);
}
/// <summary>转为字典数组形式</summary>
/// <returns></returns>
public IList<IDictionary<String, Object?>> ToDictionary()
{
var list = new List<IDictionary<String, Object?>>();
var cs = Columns ?? throw new ArgumentNullException(nameof(Columns));
var rows = Rows;
if (rows != null)
{
foreach (var row in rows)
{
var dic = new Dictionary<String, Object?>();
for (var i = 0; i < cs.Length; i++)
{
dic[cs[i]] = row[i];
}
list.Add(dic);
}
}
return list;
}
#endregion
#region Xml序列化
/// <summary>转Xml字符串</summary>
/// <returns></returns>
public String GetXml()
{
//var doc = new XmlDocument();
//var root = doc.CreateElement("DbTable");
//doc.AppendChild(root);
//foreach (var row in Rows)
//{
// var dr = doc.CreateElement("Table");
// for (var i = 0; i < Columns.Length; i++)
// {
// var elm = doc.CreateElement(Columns[i]);
// elm.InnerText = row[i] + string.Empty;
// dr.AppendChild(elm);
// }
// root.AppendChild(dr);
//}
//return doc.OuterXml;
var ms = new MemoryStream();
WriteXml(ms).Wait(15_000);
return ms.ToArray().ToStr();
}
/// <summary>以Xml格式写入数据流中</summary>
/// <param name="stream"></param>
public async Task WriteXml(Stream stream)
{
var set = new XmlWriterSettings
{
OmitXmlDeclaration = true,
ConformanceLevel = ConformanceLevel.Auto,
Indent = true,
Async = true,
};
using var writer = XmlWriter.Create(stream, set);
await writer.WriteStartDocumentAsync().ConfigureAwait(false);
await writer.WriteStartElementAsync(null, "DbTable", null).ConfigureAwait(false);
var cs = Columns ?? throw new ArgumentNullException(nameof(Columns));
var ts = Types ?? throw new ArgumentNullException(nameof(Types));
var rows = Rows;
if (rows != null)
{
foreach (var row in rows)
{
await writer.WriteStartElementAsync(null, "Table", null).ConfigureAwait(false);
for (var i = 0; i < cs.Length; i++)
{
await writer.WriteStartElementAsync(null, cs[i], null).ConfigureAwait(false);
if (ts[i] == typeof(Boolean))
writer.WriteValue(row[i].ToBoolean());
else if (ts[i] == typeof(DateTime))
writer.WriteValue(new DateTimeOffset(row[i].ChangeType<DateTime>()));
else if (ts[i] == typeof(DateTimeOffset))
writer.WriteValue(row[i].ChangeType<DateTimeOffset>());
else if (row[i] is IFormattable ft)
await writer.WriteStringAsync(ft + "").ConfigureAwait(false);
else
await writer.WriteStringAsync(row[i] + "").ConfigureAwait(false);
await writer.WriteEndElementAsync().ConfigureAwait(false);
}
await writer.WriteEndElementAsync().ConfigureAwait(false);
}
}
await writer.WriteEndElementAsync().ConfigureAwait(false);
await writer.WriteEndDocumentAsync().ConfigureAwait(false);
}
#endregion
#region Csv序列化
/// <summary>保存到Csv文件</summary>
/// <param name="file"></param>
public void SaveCsv(String file)
{
var cs = Columns ?? throw new ArgumentNullException(nameof(Columns));
var rows = Rows;
using var csv = new CsvFile(file, true);
csv.WriteLine(cs);
if (rows != null) csv.WriteAll(rows);
}
/// <summary>从Csv文件加载</summary>
/// <param name="file"></param>
public void LoadCsv(String file)
{
using var csv = new CsvFile(file, false);
var cs = csv.ReadLine();
if (cs != null) Columns = cs;
Rows = csv.ReadAll().Cast<Object?[]>().ToList();
}
#endregion
#region
/// <summary>写入模型列表</summary>
/// <typeparam name="T"></typeparam>
/// <param name="models"></param>
public void WriteModels<T>(IEnumerable<T> models)
{
// 可用属性
var pis = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
pis = pis.Where(e => e.PropertyType.IsBaseType()).ToArray();
Rows = [];
foreach (var item in models)
{
// 头部
if (Columns == null || Columns.Length == 0)
{
Columns = pis.Select(e => SerialHelper.GetName(e)).ToArray();
Types = pis.Select(e => e.PropertyType).ToArray();
}
var row = new Object?[Columns.Length];
for (var i = 0; i < row.Length; i++)
{
// 反射取值
if (pis[i].CanRead)
{
if (item is IModel ext)
row[i] = ext[pis[i].Name];
else if (item != null)
row[i] = item.GetValue(pis[i]);
}
}
Rows.Add(row);
}
}
/// <summary>数据表转模型列表。普通反射便于DAL查询后转任意模型列表</summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public IEnumerable<T> ReadModels<T>()
{
foreach (var model in ReadModels(typeof(T)))
{
yield return (T)model;
}
}
/// <summary>数据表转模型列表。普通反射便于DAL查询后转任意模型列表</summary>
/// <param name="type"></param>
/// <returns></returns>
public IEnumerable<Object> ReadModels(Type type)
{
var cs = Columns ?? throw new ArgumentNullException(nameof(Columns));
var rows = Rows;
if (rows == null) yield break;
// 可用属性
var pis = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
var dic = pis.ToDictionary(e => SerialHelper.GetName(e), e => e, StringComparer.OrdinalIgnoreCase);
foreach (var row in rows)
{
var model = type.CreateInstance();
if (model == null) continue;
for (var i = 0; i < row.Length; i++)
{
// 扩展赋值,或 反射赋值
if (dic.TryGetValue(cs[i], out var pi) && pi.CanWrite)
{
var val = row[i].ChangeType(pi.PropertyType);
if (model is IModel ext)
ext[pi.Name] = val;
else
model.SetValue(pi, val);
}
}
yield return model;
}
}
#endregion
#region
/// <summary>读取指定行的字段值</summary>
/// <typeparam name="T"></typeparam>
/// <param name="row"></param>
/// <param name="name"></param>
/// <returns></returns>
public T? Get<T>(Int32 row, String name) => !TryGet<T>(row, name, out var value) ? default : value;
/// <summary>尝试读取指定行的字段值</summary>
/// <typeparam name="T"></typeparam>
/// <param name="row"></param>
/// <param name="name"></param>
/// <param name="value"></param>
/// <returns></returns>
public Boolean TryGet<T>(Int32 row, String name, out T? value)
{
value = default;
var rs = Rows;
if (rs == null) return false;
if (row < 0 || row >= rs.Count || name.IsNullOrEmpty()) return false;
var col = GetColumn(name);
if (col < 0) return false;
value = rs[row][col].ChangeType<T>();
return true;
}
/// <summary>根据名称找字段序号</summary>
/// <param name="name"></param>
/// <returns></returns>
public Int32 GetColumn(String name)
{
var cs = Columns;
if (cs == null) return -1;
for (var i = 0; i < cs.Length; i++)
{
if (cs[i].EqualIgnoreCase(name)) return i;
}
return -1;
}
#endregion
#region
/// <summary>数据集</summary>
/// <returns></returns>
public override String ToString() => $"DbTable[{Columns?.Length}][{Rows?.Count}]";
private static Dictionary<TypeCode, Object?>? _Defs;
private static Object? GetDefault(TypeCode tc)
{
if (_Defs == null)
{
var dic = new Dictionary<TypeCode, Object?>();
foreach (var item in Enum.GetValues(typeof(TypeCode)))
{
if (item is not TypeCode tc2) continue;
Object? val = null;
val = tc2 switch
{
TypeCode.Boolean => false,
TypeCode.Char => (Char)0,
TypeCode.SByte => (SByte)0,
TypeCode.Byte => (Byte)0,
TypeCode.Int16 => (Int16)0,
TypeCode.UInt16 => (UInt16)0,
TypeCode.Int32 => 0,
TypeCode.UInt32 => (UInt32)0,
TypeCode.Int64 => (Int64)0,
TypeCode.UInt64 => (UInt64)0,
TypeCode.Single => (Single)0,
TypeCode.Double => (Double)0,
TypeCode.Decimal => (Decimal)0,
TypeCode.DateTime => DateTime.MinValue,
_ => null,
};
dic[tc2] = val;
}
_Defs = dic;
}
return _Defs.TryGetValue(tc, out var obj) ? obj : null;
}
Object ICloneable.Clone() => Clone();
/// <summary>克隆</summary>
/// <returns></returns>
public DbTable Clone()
{
var dt = new DbTable
{
Columns = Columns,
Types = Types,
Rows = Rows,
Total = Total
};
return dt;
}
/// <summary>获取数据行</summary>
/// <param name="index"></param>
/// <returns></returns>
public DbRow GetRow(Int32 index) => new(this, index);
#endregion
#region
/// <summary>获取枚举</summary>
/// <returns></returns>
public IEnumerator<DbRow> GetEnumerator() => new DbEnumerator { Table = this };
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
private struct DbEnumerator : IEnumerator<DbRow>
{
public DbTable Table { get; set; }
private Int32 _row;
private DbRow _Current;
public readonly DbRow Current => _Current;
readonly Object IEnumerator.Current => _Current;
public Boolean MoveNext()
{
var rs = Table.Rows;
if (rs == null || rs.Count == 0) return false;
if (_row < 0 || _row >= rs.Count)
{
_Current = default;
return false;
}
_Current = new DbRow(Table, _row);
_row++;
return true;
}
public void Reset()
{
_Current = default;
_row = -1;
}
public readonly void Dispose() { }
}
#endregion
}

View File

@@ -0,0 +1,121 @@
using System.Text;
namespace ThingsGateway.NewLife.Data;
/// <summary>经纬坐标的一维编码表示</summary>
/// <remarks>
/// 文档 https://newlifex.com/core/geo_hash
///
/// 一维编码表示一个矩形区域前缀表示更大区域例如北京wx4fbzdvs80包含在wx4fbzdvs里面。
/// 这个特性可以用于附近地点搜索。
/// GeoHash编码位数及距离关系
/// 1位+-2500km
/// 2位+-630km
/// 3位+-78km
/// 4位+-20km
/// 5位+-2.4km
/// 6位+-610m
/// 7位+-76m
/// 8位+-19m
/// 9位+-2m
/// </remarks>
public static class GeoHash
{
#region
private static readonly Int32[] BITS = [16, 8, 4, 2, 1];
private const String _base32 = "0123456789bcdefghjkmnpqrstuvwxyz";
private static readonly Dictionary<Char, Int32> _decode = new();
#endregion
#region
static GeoHash()
{
for (var i = 0; i < _base32.Length; i++)
{
_decode[_base32[i]] = i;
}
}
#endregion
#region
/// <summary>编码坐标点为GeoHash字符串</summary>
/// <param name="longitude">经度</param>
/// <param name="latitude">纬度</param>
/// <param name="charCount">字符个数。默认9位字符编码精度2米</param>
/// <returns></returns>
public static String Encode(Double longitude, Double latitude, Int32 charCount = 9)
{
Double[] longitudeRange = [-180, 180];
Double[] latitudeRange = [-90, 90];
var isEvenBit = true;
UInt64 bits = 0;
var len = charCount * 5;
for (var i = 0; i < len; i++)
{
bits <<= 1;
// 轮流占用信息位
var value = isEvenBit ? longitude : latitude;
var rang = isEvenBit ? longitudeRange : latitudeRange;
var mid = (rang[0] + rang[1]) / 2;
if (value >= mid)
{
bits |= 0x1;
rang[0] = mid;
}
else
{
rang[1] = mid;
}
isEvenBit = !isEvenBit;
}
bits <<= (64 - len);
// base32编码
var sb = new StringBuilder();
for (var i = 0; i < charCount; i++)
{
var pointer = (Int32)((bits & 0xf800000000000000L) >> 59);
sb.Append(_base32[pointer]);
bits <<= 5;
}
return sb.ToString();
}
/// <summary>解码GeoHash字符串为坐标点</summary>
/// <param name="geohash"></param>
/// <returns></returns>
public static (Double Longitude, Double Latitude) Decode(String geohash)
{
Double[] latitudeRange = [-90, 90];
Double[] longitudeRange = [-180, 180];
var isEvenBit = true;
for (var i = 0; i < geohash.Length; i++)
{
var ch = _decode[geohash[i]];
for (var j = 0; j < 5; j++)
{
// 轮流解码信息位
var rang = isEvenBit ? longitudeRange : latitudeRange;
var mid = (rang[0] + rang[1]) / 2;
if ((ch & BITS[j]) != 0)
rang[0] = mid;
else
rang[1] = mid;
isEvenBit = !isEvenBit;
}
}
var longitude = (longitudeRange[0] + longitudeRange[1]) / 2;
var latitude = (latitudeRange[0] + latitudeRange[1]) / 2;
return (longitude, latitude);
}
#endregion
}

View File

@@ -0,0 +1,21 @@
using System.Net;
namespace ThingsGateway.NewLife.Data;
/// <summary>数据帧接口。用于网络通信领域,定义数据帧的必要字段</summary>
public interface IData
{
#region
/// <summary>原始数据包</summary>
IPacket? Packet { get; set; }
/// <summary>远程地址</summary>
IPEndPoint? Remote { get; set; }
/// <summary>解码后的消息</summary>
Object? Message { get; set; }
/// <summary>用户自定义数据</summary>
Object? UserState { get; set; }
#endregion
}

View File

@@ -0,0 +1,16 @@
namespace ThingsGateway.NewLife.Data;
/// <summary>具有可读写的扩展数据</summary>
/// <remarks>
/// 仅限于扩展属性,不包括基本属性,区别于 IModel
/// </remarks>
public interface IExtend
{
/// <summary>数据项</summary>
IDictionary<String, Object?> Items { get; }
/// <summary>设置 或 获取 数据项</summary>
/// <param name="key"></param>
/// <returns></returns>
Object? this[String key] { get; set; }
}

View File

@@ -0,0 +1,74 @@
namespace ThingsGateway.NewLife.Data;
/// <summary>数据过滤器</summary>
public interface IFilter
{
/// <summary>下一个过滤器</summary>
IFilter? Next { get; }
/// <summary>对封包执行过滤器</summary>
/// <param name="context"></param>
void Execute(FilterContext context);
}
/// <summary>过滤器上下文</summary>
public class FilterContext
{
/// <summary>封包</summary>
public virtual IPacket? Packet { get; set; }
}
/// <summary>过滤器助手</summary>
public static class FilterHelper
{
/// <summary>在链条里面查找指定类型的过滤器</summary>
/// <param name="filter"></param>
/// <param name="filterType"></param>
/// <returns></returns>
public static IFilter? Find(this IFilter filter, Type filterType)
{
if (filter == null || filterType == null) return null;
if (filter.GetType() == filterType) return filter;
return filter.Next?.Find(filterType);
}
///// <summary>在开头插入过滤器</summary>
///// <param name="filter"></param>
///// <param name="newFilter"></param>
///// <returns></returns>
//public static IFilter Add(this IFilter filter, IFilter newFilter)
//{
// if (filter == null || newFilter == null) return filter;
// newFilter.Next = filter;
// return newFilter;
//}
}
/// <summary>数据过滤器基类</summary>
public abstract class FilterBase : IFilter
{
/// <summary>下一个过滤器</summary>
public IFilter? Next { get; set; }
///// <summary>实例化过滤器</summary>
///// <param name="next"></param>
//public FilterBase(IFilter next) { Next = next; }
/// <summary>对封包执行过滤器</summary>
/// <param name="context"></param>
public virtual void Execute(FilterContext context)
{
if (!OnExecute(context) || context.Packet == null) return;
Next?.Execute(context);
}
/// <summary>执行过滤</summary>
/// <param name="context"></param>
/// <returns>返回是否执行下一个过滤器</returns>
protected abstract Boolean OnExecute(FilterContext context);
}

View File

@@ -0,0 +1,16 @@
namespace ThingsGateway.NewLife.Data;
/// <summary>模型数据接口,支持索引器读写属性</summary>
/// <remarks>
/// 可借助反射取得属性列表成员,从而对实体模型属性进行读写操作,避免反射带来的负担。
/// 常用于WebApi模型类以及XCode数据实体类也用于魔方接口拷贝。
///
/// 逐步替代 IExtend 的大部分使用场景
/// </remarks>
public interface IModel
{
/// <summary>设置 或 获取 数据项</summary>
/// <param name="key"></param>
/// <returns></returns>
Object? this[String key] { get; set; }
}

View File

@@ -0,0 +1,900 @@
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Text;
using ThingsGateway.NewLife.Collections;
namespace ThingsGateway.NewLife.Data;
/// <summary>数据包接口。几乎内存共享理念统一提供数据包内部可能是内存池、数组和旧版Packet等多种实现</summary>
/// <remarks>
/// 常用于网络编程和协议解析,为了避免大量内存分配和拷贝,采用数据包对象池,复用内存。
/// 数据包接口一般由结构体实现提升GC性能。
///
/// 特别需要注意内存管理权转移问题,一般由调用栈的上部负责释放内存。
/// Socket非阻塞事件接收时负责申请与释放内存数据处理是调用栈下游
/// Socket阻塞接收时接收函数内部申请内存外部使用方释放内存管理权甚至在此次传递给消息层
///
/// 作为过渡期旧版Packet也会实现该接口以便逐步替换。
/// </remarks>
public interface IPacket
{
/// <summary>数据长度。仅当前数据包不包括Next</summary>
Int32 Length { get; }
/// <summary>下一个链式包</summary>
[EditorBrowsable(EditorBrowsableState.Never)]
IPacket? Next { get; set; }
/// <summary>总长度。包括Next链的长度</summary>
Int32 Total { get; }
/// <summary>获取/设置 指定位置的字节</summary>
/// <param name="index"></param>
/// <returns></returns>
Byte this[Int32 index] { get; set; }
/// <summary>获取分片包。在管理权生命周期内短暂使用</summary>
/// <returns></returns>
Span<Byte> GetSpan();
/// <summary>获取内存包。在管理权生命周期内短暂使用</summary>
/// <returns></returns>
Memory<Byte> GetMemory();
/// <summary>切片得到新数据包</summary>
/// <remarks>引用相同内存块或缓冲区,减少内存分配</remarks>
/// <param name="offset">偏移</param>
/// <param name="count">个数。默认-1表示到末尾</param>
/// <returns></returns>
IPacket Slice(Int32 offset, Int32 count = -1);
/// <summary>切片得到新数据包,同时转移内存管理权</summary>
/// <remarks>
/// 引用相同内存块或缓冲区,减少内存分配。
/// 如果原数据包只切一次给新包,可以转移内存管理权,由新数据包负责释放;
/// 如果原数据包需要切多次,不要转移内存管理权,由原数据包负责释放。
/// </remarks>
/// <param name="offset">偏移</param>
/// <param name="count">个数。默认-1表示到末尾</param>
/// <param name="transferOwner">转移所有权。若为true则由新数据包负责归还缓冲区只能转移一次。并非所有数据包都支持</param>
/// <returns></returns>
IPacket Slice(Int32 offset, Int32 count, Boolean transferOwner);
/// <summary>尝试获取缓冲区。仅本片段不包括Next</summary>
/// <param name="segment"></param>
/// <returns></returns>
Boolean TryGetArray(out ArraySegment<Byte> segment);
}
/// <summary>拥有管理权的数据包。使用完以后需要释放</summary>
public interface IOwnerPacket : IPacket, IDisposable { }
/// <summary>内存包辅助类</summary>
public static class PacketHelper
{
/// <summary>附加一个包到当前包链的末尾</summary>
/// <param name="pk"></param>
/// <param name="next"></param>
public static IPacket Append(this IPacket pk, IPacket next)
{
if (next == null) return pk;
var p = pk;
while (p.Next != null) p = p.Next;
p.Next = next;
return pk;
}
/// <summary>附加一个包到当前包链的末尾</summary>
/// <param name="pk"></param>
/// <param name="next"></param>
public static IPacket Append(this IPacket pk, Byte[] next) => Append(pk, new ArrayPacket(next));
/// <summary>转字符串</summary>
/// <param name="pk"></param>
/// <param name="encoding"></param>
/// <param name="offset"></param>
/// <param name="count"></param>
/// <returns></returns>
public static String ToStr(this IPacket pk, Encoding? encoding = null, Int32 offset = 0, Int32 count = -1)
{
// 总是有异常数据,这里屏蔽异常
if (pk == null) return null!;
if (pk.Next == null)
{
if (count < 0) count = pk.Length - offset;
var span = pk.GetSpan();
if (span.Length > count) span = span[..count];
return span.ToStr(encoding);
}
if (count < 0) count = pk.Total - offset;
var sb = Pool.StringBuilder.Get();
for (var p = pk; p != null; p = p.Next)
{
var span = p.GetSpan();
if (span.Length > count) span = span[..count];
sb.Append(span.ToStr(encoding));
count -= span.Length;
if (count <= 0) break;
}
return sb.Return(true);
}
/// <summary>以十六进制编码表示</summary>
/// <param name="pk"></param>
/// <param name="maxLength">最大显示多少个字节。默认-1显示全部</param>
/// <param name="separate">分隔符</param>
/// <param name="groupSize">分组大小为0时对每个字节应用分隔符否则对每个分组使用</param>
/// <returns></returns>
public static String ToHex(this IPacket pk, Int32 maxLength = 32, String? separate = null, Int32 groupSize = 0)
{
if (pk.Length == 0) return String.Empty;
if (pk.Next == null)
return pk.GetSpan().ToHex(separate, groupSize, maxLength);
var sb = Pool.StringBuilder.Get();
for (var p = pk; p != null; p = p.Next)
{
sb.Append(p.GetSpan().ToHex(separate, groupSize, maxLength));
maxLength -= p.Length;
if (maxLength <= 0) break;
}
return sb.Return(true);
}
/// <summary>写入数据流netfx中可能有二次拷贝</summary>
/// <param name="pk"></param>
/// <param name="stream"></param>
public static void CopyTo(this IPacket pk, Stream stream)
{
for (var p = pk; p != null; p = p.Next)
{
if (p.TryGetArray(out var segment))
stream.Write(segment.Array!, segment.Offset, segment.Count);
else
stream.Write(p.GetMemory());
}
}
/// <summary>异步拷贝</summary>
/// <param name="pk"></param>
/// <param name="stream"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public static async Task CopyToAsync(this IPacket pk, Stream stream, CancellationToken cancellationToken = default)
{
for (var p = pk; p != null; p = p.Next)
{
if (p.TryGetArray(out var segment))
await stream.WriteAsync(segment.Array!, segment.Offset, segment.Count, cancellationToken).ConfigureAwait(false);
else
await stream.WriteAsync(p.GetMemory(), cancellationToken).ConfigureAwait(false);
}
}
/// <summary>获取数据流</summary>
/// <param name="pk"></param>
/// <returns></returns>
public static Stream GetStream(this IPacket pk)
{
var ms = new MemoryStream(pk.Total);
pk.CopyTo(ms);
ms.Position = 0;
return ms;
}
/// <summary>返回数据段,可能有拷贝</summary>
/// <returns></returns>
public static ArraySegment<Byte> ToSegment(this IPacket pk)
{
if (pk.Next == null && pk.TryGetArray(out var segment)) return segment;
var ms = Pool.MemoryStream.Get();
pk.CopyTo(ms);
ms.Position = 0;
return new ArraySegment<Byte>(ms.Return(true));
}
/// <summary>返回数据段集合,可能有拷贝</summary>
/// <returns></returns>
public static IList<ArraySegment<Byte>> ToSegments(this IPacket pk)
{
// 初始4元素优化扩容
var list = new List<ArraySegment<Byte>>(4);
for (var p = pk; p != null; p = p.Next)
{
if (p.TryGetArray(out var seg))
list.Add(seg);
else
list.Add(new ArraySegment<Byte>(p.GetSpan().ToArray(), 0, p.Length));
}
return list;
}
/// <summary>返回字节数组。无差别复制,一定返回新数组</summary>
/// <returns></returns>
public static Byte[] ToArray(this IPacket pk)
{
if (pk.Next == null) return pk.GetSpan().ToArray();
// 链式包输出
var ms = Pool.MemoryStream.Get();
pk.CopyTo(ms);
return ms.Return(true);
}
/// <summary>从封包中读取指定数据区,读取全部时直接返回缓冲区,以提升性能</summary>
/// <param name="pk"></param>
/// <param name="offset">相对于数据包的起始位置实际上是数组的Offset+offset</param>
/// <param name="count">字节个数</param>
/// <returns></returns>
public static Byte[] ReadBytes(this IPacket pk, Int32 offset = 0, Int32 count = -1)
{
if (pk.Next == null)
{
if (count < 0) count = pk.Length - offset;
if (pk.TryGetArray(out var seg))
{
// 读取全部
if (offset == 0 && count == pk.Length)
{
if (seg.Offset == 0 && seg.Count == seg.Array!.Length) return seg.Array;
}
return seg.Array!.ReadBytes(seg.Offset + offset, count);
}
var span = pk.GetSpan();
return span.Slice(offset, count).ToArray();
}
return pk.ToArray().ReadBytes(offset, count);
}
/// <summary>深度克隆一份数据包,拷贝数据区</summary>
/// <returns></returns>
public static IPacket Clone(this IPacket pk)
{
if (pk.Next == null)
{
// 需要深度拷贝,避免重用缓冲区
return new ArrayPacket(pk.GetSpan().ToArray());
}
// 链式包输出
var ms = new MemoryStream();
pk.CopyTo(ms);
ms.Position = 0;
return new ArrayPacket(ms);
}
/// <summary>尝试获取内存片段。非链式数据包时直接返回</summary>
/// <param name="pk"></param>
/// <param name="span"></param>
/// <returns></returns>
public static Boolean TryGetSpan(this IPacket pk, out Span<Byte> span)
{
if (pk.Next == null)
{
span = pk.GetSpan();
return true;
}
span = default;
return false;
}
/// <summary>扩展头部,用于填充包头,减少内存分配</summary>
/// <param name="pk">数据包</param>
/// <param name="size">要扩大的头部大小,不包括负载数据</param>
/// <returns>扩展后的数据包</returns>
public static IPacket ExpandHeader(this IPacket? pk, Int32 size)
{
if (pk is ArrayPacket ap && ap.Offset >= size)
{
return new ArrayPacket(ap.Buffer, ap.Offset - size, ap.Length + size) { Next = ap.Next };
}
else if (pk is OwnerPacket owner && owner.Offset >= size)
{
return new OwnerPacket(owner, size);
}
return new OwnerPacket(size) { Next = pk };
}
}
/// <summary>所有权内存包。具有所有权管理,不再使用时释放</summary>
/// <remarks>
/// 使用时务必明确所有权归属,用完后及时释放。
/// </remarks>
public class OwnerPacket : MemoryManager<Byte>, IPacket, IOwnerPacket
{
#region
private Byte[] _buffer;
/// <summary>缓冲区</summary>
public Byte[] Buffer => _buffer;
private Int32 _offset;
/// <summary>数据偏移</summary>
public Int32 Offset => _offset;
private Int32 _length;
/// <summary>数据长度</summary>
public Int32 Length => _length;
/// <summary>获取/设置 指定位置的字节</summary>
/// <param name="index"></param>
/// <returns></returns>
public Byte this[Int32 index]
{
get
{
var p = index - _length;
if (p >= 0)
{
if (Next == null) throw new IndexOutOfRangeException(nameof(index));
return Next[p];
}
return _buffer[_offset + index];
}
set
{
var p = index - _length;
if (p >= 0)
{
if (Next == null) throw new IndexOutOfRangeException(nameof(index));
Next[p] = value;
}
else
{
_buffer[_offset + index] = value;
}
}
}
/// <summary>下一个链式包</summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public IPacket? Next { get; set; }
/// <summary>总长度</summary>
public Int32 Total => Length + (Next?.Total ?? 0);
private Boolean _hasOwner;
#endregion
#region
/// <summary>实例化指定长度的内存包,从共享内存池中借出</summary>
/// <param name="length">长度</param>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public OwnerPacket(Int32 length)
{
if (length < 0)
throw new ArgumentOutOfRangeException(nameof(length), "Length must be non-negative and less than or equal to the memory owner's length.");
_buffer = ArrayPool<Byte>.Shared.Rent(length);
_offset = 0;
_length = length;
_hasOwner = true;
}
/// <summary>实例化内存包,指定内存所有者和长度</summary>
/// <param name="buffer">缓冲区</param>
/// <param name="offset"></param>
/// <param name="length">长度</param>
/// <param name="hasOwner">是否转移所有权</param>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public OwnerPacket(Byte[] buffer, Int32 offset, Int32 length, Boolean hasOwner)
{
if (offset < 0 || length < 0 || offset + length > buffer.Length)
throw new ArgumentOutOfRangeException(nameof(length), "Length must be non-negative and less than or equal to the memory owner's length.");
_buffer = buffer;
_offset = offset;
_length = length;
_hasOwner = hasOwner;
}
/// <summary>从另一个内存包创建新内存包,共用缓冲区</summary>
/// <param name="owner"></param>
/// <param name="expandSize"></param>
public OwnerPacket(OwnerPacket owner, Int32 expandSize)
{
_buffer = owner.Buffer;
_offset = owner.Offset - expandSize;
_length = owner.Length + expandSize;
Next = owner.Next;
// 转移所有权
_hasOwner = owner._hasOwner;
owner._hasOwner = false;
}
/// <summary>销毁释放</summary>
/// <param name="disposing"></param>
protected override void Dispose(Boolean disposing)
{
if (!_hasOwner) return;
_hasOwner = true;
var buffer = _buffer;
if (buffer != null)
{
// 释放内存所有者以后,直接置空,避免重复使用
_buffer = null!;
ArrayPool<Byte>.Shared.Return(buffer);
}
Next.TryDispose();
}
#endregion
/// <summary>获取分片包。在管理权生命周期内短暂使用</summary>
/// <returns></returns>
public override Span<Byte> GetSpan() => new(_buffer, _offset, _length);
/// <summary>获取内存包。在管理权生命周期内短暂使用</summary>
/// <returns></returns>
public Memory<Byte> GetMemory() => new(_buffer, _offset, _length);
/// <summary>重新设置数据包大小。一般用于申请缓冲区并读取数据后设置为实际大小</summary>
/// <param name="size"></param>
/// <exception cref="ArgumentNullException"></exception>
public OwnerPacket Resize(Int32 size)
{
if (size < 0) throw new ArgumentNullException(nameof(size));
if (Next == null)
{
if (size > _buffer.Length) throw new ArgumentOutOfRangeException(nameof(size));
_length = size;
}
else
{
if (size >= _length) throw new NotSupportedException();
_length = size;
}
return this;
}
/// <summary>切片得到新数据包</summary>
/// <remarks>引用相同内存块,减少内存分配</remarks>
/// <param name="offset">偏移</param>
/// <param name="count">个数。默认-1表示到末尾</param>
public IPacket Slice(Int32 offset, Int32 count) => Slice(offset, count, true);
/// <summary>切片得到新数据包,同时转移内存管理权</summary>
/// <remarks>引用相同内存块,减少内存分配</remarks>
/// <param name="offset">偏移</param>
/// <param name="count">个数。默认-1表示到末尾</param>
/// <param name="transferOwner">转移所有权。若为true则由新数据包负责归还缓冲区只能转移一次</param>
public IPacket Slice(Int32 offset, Int32 count, Boolean transferOwner)
{
// 释放后无法再次使用
if (_buffer == null) throw new InvalidDataException();
var buffer = _buffer;
var start = _offset + offset;
var remain = _length - offset;
var hasOwner = _hasOwner && transferOwner;
// 超出范围
if (count > Total - offset) throw new ArgumentOutOfRangeException(nameof(count), "count must be non-negative and less than or equal to the memory owner's length.");
// 单个数据包
if (Next == null)
{
// 转移管理权
if (transferOwner) _hasOwner = false;
if (count < 0 || count > remain) count = remain;
return new OwnerPacket(buffer, start, count, hasOwner);
}
else
{
// 如果当前段用完,则取下一段。当前包自己负责释放
if (remain <= 0) return Next.Slice(offset - _length, count, transferOwner);
// 转移管理权
if (transferOwner) _hasOwner = false;
// 当前包用一截剩下的全部。转移管理权后Next随新包一起释放
if (count < 0) return new OwnerPacket(buffer, start, remain, hasOwner) { Next = Next };
// 当前包可以读完。转移管理权后Next失去释放机会
if (count <= remain) return new OwnerPacket(buffer, start, count, hasOwner);
// 当前包用一截剩下的再截取。转移管理权后Next再次转移管理权随新包一起释放
return new OwnerPacket(buffer, start, remain, hasOwner) { Next = Next.Slice(0, count - remain, transferOwner) };
}
}
/// <summary>尝试获取缓冲区。仅本片段不包括Next</summary>
/// <param name="segment"></param>
/// <returns></returns>
protected override Boolean TryGetArray(out ArraySegment<Byte> segment)
{
segment = new ArraySegment<Byte>(_buffer, _offset, _length);
return true;
}
/// <summary>尝试获取数据段</summary>
/// <param name="segment"></param>
/// <returns></returns>
Boolean IPacket.TryGetArray(out ArraySegment<Byte> segment) => TryGetArray(out segment);
/// <summary>释放所有权,不再使用</summary>
public void Free()
{
_buffer = null!;
Next = null;
}
/// <summary>钉住内存</summary>
/// <param name="elementIndex"></param>
/// <returns></returns>
/// <exception cref="NotSupportedException"></exception>
public override MemoryHandle Pin(Int32 elementIndex = 0) => throw new NotSupportedException();
/// <summary>取消钉内存</summary>
/// <exception cref="NotImplementedException"></exception>
public override void Unpin() => throw new NotImplementedException();
#region
/// <summary>已重载</summary>
/// <returns></returns>
public override String ToString() => $"[{_buffer.Length}]({_offset}, {_length})<{Total}>";
#endregion
}
/// <summary>内存包</summary>
/// <remarks>
/// 内存包可能来自内存池,失去所有权时已被释放,因此不应该长期持有。
/// </remarks>
public struct MemoryPacket : IPacket
{
#region
private readonly Memory<Byte> _memory;
/// <summary>内存</summary>
public readonly Memory<Byte> Memory => _memory;
private readonly Int32 _length;
/// <summary>数据长度</summary>
public readonly Int32 Length => _length;
/// <summary>获取/设置 指定位置的字节</summary>
/// <param name="index"></param>
/// <returns></returns>
public Byte this[Int32 index]
{
get
{
var p = index - _length;
if (p >= 0)
{
if (Next == null) throw new IndexOutOfRangeException(nameof(index));
return Next[p];
}
return _memory.Span[index];
}
set
{
var p = index - _length;
if (p >= 0)
{
if (Next == null) throw new IndexOutOfRangeException(nameof(index));
Next[p] = value;
}
else
{
_memory.Span[index] = value;
}
}
}
/// <summary>下一个链式包</summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public IPacket? Next { get; set; }
/// <summary>总长度</summary>
public readonly Int32 Total => Length + (Next?.Total ?? 0);
#endregion
/// <summary>实例化内存包,指定内存和长度</summary>
/// <param name="memory">内存</param>
/// <param name="length">长度</param>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public MemoryPacket(Memory<Byte> memory, Int32 length)
{
if (length < 0 || length > memory.Length)
throw new ArgumentOutOfRangeException(nameof(length), "Length must be non-negative and less than or equal to the memory owner's length.");
_memory = memory;
_length = length;
}
/// <summary>获取分片包。在管理权生命周期内短暂使用</summary>
/// <returns></returns>
public readonly Span<Byte> GetSpan() => _memory.Span[.._length];
/// <summary>获取内存包。在管理权生命周期内短暂使用</summary>
/// <returns></returns>
public readonly Memory<Byte> GetMemory() => _memory[.._length];
/// <summary>切片得到新数据包,共用内存块</summary>
/// <param name="offset">偏移</param>
/// <param name="count">个数。默认-1表示到末尾</param>
public IPacket Slice(Int32 offset, Int32 count) => Slice(offset, count, true);
/// <summary>切片得到新数据包,共用内存块</summary>
/// <param name="offset">偏移</param>
/// <param name="count">个数。默认-1表示到末尾</param>
/// <param name="transferOwner">转移所有权。不支持</param>
public IPacket Slice(Int32 offset, Int32 count, Boolean transferOwner)
{
// 带有Next时不支持Slice
if (Next != null) throw new NotSupportedException("Slice with Next");
var remain = _length - offset;
if (count < 0 || count > remain) count = remain;
if (offset == 0 && count == _length) return this;
if (offset == 0)
return new MemoryPacket(_memory, count);
return new MemoryPacket(_memory[offset..], count);
}
/// <summary>尝试获取缓冲区。仅本片段不包括Next</summary>
/// <param name="segment"></param>
/// <returns></returns>
public readonly Boolean TryGetArray(out ArraySegment<Byte> segment) => MemoryMarshal.TryGetArray(GetMemory(), out segment);
/// <summary>已重载</summary>
/// <returns></returns>
public override readonly String ToString() => $"[{_memory.Length}](0, {_length})<{Total}>";
}
/// <summary>字节数组包</summary>
public struct ArrayPacket : IPacket
{
#region
private Byte[] _buffer;
/// <summary>缓冲区</summary>
public readonly Byte[] Buffer => _buffer;
private readonly Int32 _offset;
/// <summary>数据偏移</summary>
public readonly Int32 Offset => _offset;
private readonly Int32 _length;
/// <summary>数据长度</summary>
public readonly Int32 Length => _length;
/// <summary>下一个链式包</summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public IPacket? Next { get; set; }
/// <summary>总长度</summary>
public readonly Int32 Total => Length + (Next?.Total ?? 0);
/// <summary>空数组</summary>
public static ArrayPacket Empty = new([]);
#endregion
#region
/// <summary>获取/设置 指定位置的字节</summary>
/// <param name="index"></param>
/// <returns></returns>
public Byte this[Int32 index]
{
get
{
var p = index - _length;
if (p >= 0)
{
if (Next == null) throw new IndexOutOfRangeException(nameof(index));
return Next[p];
}
return _buffer[_offset + index];
}
set
{
var p = index - _length;
if (p >= 0)
{
if (Next == null) throw new IndexOutOfRangeException(nameof(index));
Next[p] = value;
}
else
{
_buffer[_offset + index] = value;
}
}
}
#endregion
#region
/// <summary>通过指定字节数组来实例化数据包</summary>
/// <param name="buf"></param>
/// <param name="offset"></param>
/// <param name="count"></param>
public ArrayPacket(Byte[] buf, Int32 offset = 0, Int32 count = -1)
{
if (count < 0) count = buf.Length - offset;
_buffer = buf;
_offset = offset;
_length = count;
}
/// <summary>从可扩展内存流实例化,尝试窃取内存流内部的字节数组,失败后拷贝</summary>
/// <remarks>因数据包内数组窃取自内存流,需要特别小心,避免多线程共用。常用于内存流转数据包,而内存流不再使用</remarks>
/// <param name="stream"></param>
public ArrayPacket(Stream stream)
{
if (stream is MemoryStream ms)
{
#if !NET45
// 尝试抠了内部存储区,下面代码需要.Net 4.6支持
if (ms.TryGetBuffer(out var seg))
{
if (seg.Array == null) throw new InvalidDataException();
_buffer = seg.Array;
_offset = seg.Offset + (Int32)ms.Position;
_length = seg.Count - (Int32)ms.Position;
return;
}
// GetBuffer窃取内部缓冲区后无法得知真正的起始位置index可能导致错误取数
// public MemoryStream(byte[] buffer, int index, int count, bool writable, bool publiclyVisible)
//try
//{
// Set(ms.GetBuffer(), (Int32)ms.Position, (Int32)(ms.Length - ms.Position));
//}
//catch (UnauthorizedAccessException) { }
#endif
}
var buf = new Byte[stream.Length - stream.Position];
var count = stream.Read(buf, 0, buf.Length);
_buffer = buf;
_offset = 0;
_length = count;
// 必须确保数据流位置不变
if (count > 0) stream.Seek(-count, SeekOrigin.Current);
}
/// <summary>从数据段实例化数据包</summary>
/// <param name="segment"></param>
public ArrayPacket(ArraySegment<Byte> segment) : this(segment.Array!, segment.Offset, segment.Count) { }
#endregion
/// <summary>获取分片包。在管理权生命周期内短暂使用</summary>
/// <returns></returns>
public readonly Span<Byte> GetSpan() => new(_buffer, _offset, _length);
/// <summary>获取内存包。在管理权生命周期内短暂使用</summary>
/// <returns></returns>
public readonly Memory<Byte> GetMemory() => new(_buffer, _offset, _length);
/// <summary>切片得到新数据包,共用缓冲区</summary>
/// <param name="offset">偏移</param>
/// <param name="count">个数。默认-1表示到末尾</param>
IPacket IPacket.Slice(Int32 offset, Int32 count) => (this as IPacket).Slice(offset, count, true);
/// <summary>切片得到新数据包,共用缓冲区</summary>
/// <param name="offset">偏移</param>
/// <param name="count">个数。默认-1表示到末尾</param>
/// <param name="transferOwner">转移所有权。仅对Next有效</param>
IPacket IPacket.Slice(Int32 offset, Int32 count, Boolean transferOwner)
{
if (count == 0) return Empty;
var remain = _length - offset;
var next = Next;
if (next != null && remain <= 0) return next.Slice(offset - _length, count, transferOwner);
return Slice(offset, count, transferOwner);
}
/// <summary>切片得到新数据包,共用缓冲区,无内存分配</summary>
/// <param name="offset">偏移</param>
/// <param name="count">个数。默认-1表示到末尾</param>
/// <param name="transferOwner">转移所有权。仅对Next有效</param>
public ArrayPacket Slice(Int32 offset, Int32 count = -1, Boolean transferOwner = false)
{
if (count == 0) return Empty;
var start = Offset + offset;
var remain = _length - offset;
var next = Next;
if (next == null)
{
// count 是 offset 之后的个数
if (count < 0 || count > remain) count = remain;
if (count < 0) count = 0;
return new ArrayPacket(_buffer, start, count);
}
else
{
// 如果当前段用完则取下一段。强转ArrayPacket如果不是则抛出异常
if (remain <= 0)
return (ArrayPacket)next.Slice(offset - _length, count, transferOwner);
// 当前包用一截,剩下的全部
if (count < 0)
return new ArrayPacket(_buffer, start, remain) { Next = next };
// 当前包可以读完
if (count <= remain)
return new ArrayPacket(_buffer, start, count);
// 当前包用一截,剩下的再截取
return new ArrayPacket(_buffer, start, remain) { Next = next.Slice(0, count - remain, transferOwner) };
}
}
/// <summary>尝试获取缓冲区。仅本片段不包括Next</summary>
/// <param name="segment"></param>
/// <returns></returns>
public readonly Boolean TryGetArray(out ArraySegment<Byte> segment)
{
segment = new ArraySegment<Byte>(_buffer, _offset, _length);
return true;
}
#region
/// <summary>重载类型转换字节数组直接转为Packet对象</summary>
/// <param name="value"></param>
/// <returns></returns>
public static implicit operator ArrayPacket(Byte[] value) => new(value);
/// <summary>重载类型转换一维数组直接转为Packet对象</summary>
/// <param name="value"></param>
/// <returns></returns>
public static implicit operator ArrayPacket(ArraySegment<Byte> value) => new(value.Array!, value.Offset, value.Count);
/// <summary>重载类型转换字符串直接转为Packet对象</summary>
/// <param name="value"></param>
/// <returns></returns>
public static implicit operator ArrayPacket(String value) => new(value.GetBytes());
/// <summary>已重载</summary>
/// <returns></returns>
public override String ToString() => $"[{_buffer.Length}]({_offset}, {_length})<{Total}>";
#endregion
}

View File

@@ -0,0 +1,112 @@
using ThingsGateway.NewLife.Reflection;
using ThingsGateway.NewLife.Serialization;
namespace ThingsGateway.NewLife.Data;
/// <summary>数据包编码器接口</summary>
public interface IPacketEncoder
{
/// <summary>数值转数据包</summary>
/// <param name="value">数值对象</param>
/// <returns></returns>
IPacket? Encode(Object value);
/// <summary>数据包转对象</summary>
/// <param name="data">数据包</param>
/// <param name="type">目标类型</param>
/// <returns></returns>
Object? Decode(IPacket data, Type type);
}
/// <summary>编码器扩展</summary>
public static class PackerEncoderExtensions
{
/// <summary>数据包转对象</summary>
/// <typeparam name="T">目标类型</typeparam>
/// <param name="encoder"></param>
/// <param name="data">数据包</param>
/// <returns></returns>
public static T? Decode<T>(this IPacketEncoder encoder, IPacket data) => (T?)encoder.Decode(data, typeof(T));
}
/// <summary>默认数据包编码器。基础类型直接转复杂类型Json序列化</summary>
public class DefaultPacketEncoder : IPacketEncoder
{
#region
/// <summary>Json序列化主机</summary>
public IJsonHost JsonHost { get; set; } = JsonHelper.Default;
/// <summary>解码出错时抛出异常。默认false不抛出异常仅返回默认值</summary>
public Boolean ThrowOnError { get; set; }
#endregion
/// <summary>数值转数据包</summary>
/// <param name="value"></param>
/// <returns></returns>
public virtual IPacket? Encode(Object value)
{
if (value == null) return null!;
if (value is IPacket pk) return pk;
if (value is Byte[] buf) return (ArrayPacket)buf;
if (value is IAccessor acc) return acc.ToPacket();
var str = OnEncode(value);
return (ArrayPacket)str.GetBytes();
}
/// <summary>编码为字符串。复杂类型采用Json序列化</summary>
/// <param name="value"></param>
/// <returns></returns>
protected virtual String? OnEncode(Object value)
{
var type = value.GetType();
return type.GetTypeCode() switch
{
TypeCode.Object => JsonHost.Write(value),
TypeCode.String => value as String,
TypeCode.DateTime => ((DateTime)value).ToString("yyyy-MM-dd HH:mm:ss.fff"),
_ => value + "",
};
}
/// <summary>数据包转对象</summary>
/// <param name="data"></param>
/// <param name="type"></param>
/// <returns></returns>
public virtual Object? Decode(IPacket data, Type type)
{
try
{
if (type == typeof(IPacket)) return data;
if (type == typeof(Packet)) return data is Packet pk ? pk : data.ReadBytes();
if (type == typeof(Byte[])) return data.ReadBytes();
if (type.As<IAccessor>()) return type.AccessorRead(data);
// 可空类型
if (data.Length == 0 && type.IsNullable()) return null;
var str = data.ToStr();
return OnDecode(str, type);
}
catch
{
if (ThrowOnError) throw;
return null;
}
}
/// <summary>字符串解码为对象。复杂类型采用Json反序列化</summary>
/// <param name="value"></param>
/// <param name="type"></param>
/// <returns></returns>
protected virtual Object? OnDecode(String value, Type type)
{
if (type.GetTypeCode() == TypeCode.String) return value;
if (type.IsBaseType()) return value.ChangeType(type);
return JsonHost.Read(value, type);
}
}

View File

@@ -0,0 +1,24 @@
namespace ThingsGateway.NewLife.Data
{
/// <summary>
/// 范围
/// </summary>
public struct IndexRange
{
/// <summary>
/// 开始,包含
/// </summary>
public Int32 Start;
/// <summary>
/// 结束,不包含
/// </summary>
public Int32 End;
/// <summary>
/// 已重载
/// </summary>
/// <returns></returns>
public override String ToString() => $"({Start}, {End})";
}
}

View File

@@ -0,0 +1,544 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using System.Text;
using ThingsGateway.NewLife.Collections;
namespace ThingsGateway.NewLife.Data;
/// <summary>数据包。表示数据区Data的指定范围Offset, Count。</summary>
/// <remarks>
/// 文档 https://newlifex.com/core/packet
/// 设计于.NET2.0时代功能上类似于NETCore的Span/Memory。
/// Packet的设计目标就是网络库零拷贝所以Slice切片是其最重要功能。
/// </remarks>
public class Packet : IPacket
{
#region
/// <summary>数据</summary>
public Byte[] Data { get; private set; }
/// <summary>偏移</summary>
public Int32 Offset { get; private set; }
/// <summary>长度</summary>
public Int32 Count { get; private set; }
Int32 IPacket.Length => Count;
/// <summary>下一个链式包</summary>
public Packet? Next { get; set; }
/// <summary>总长度</summary>
public Int32 Total => Count + (Next != null ? Next.Total : 0);
IPacket? IPacket.Next { get => Next; set => Next = (value as Packet) ?? throw new InvalidDataException(); }
#endregion
#region
/// <summary>根据数据区实例化</summary>
/// <param name="data"></param>
/// <param name="offset"></param>
/// <param name="count"></param>
public Packet(Byte[] data, Int32 offset = 0, Int32 count = -1) => Set(data, offset, count);
/// <summary>根据数组段实例化</summary>
/// <param name="seg"></param>
public Packet(ArraySegment<Byte> seg)
{
if (seg.Array == null) throw new ArgumentNullException(nameof(seg));
Set(seg.Array, seg.Offset, seg.Count);
}
/// <summary>从可扩展内存流实例化,尝试窃取内存流内部的字节数组,失败后拷贝</summary>
/// <remarks>因数据包内数组窃取自内存流,需要特别小心,避免多线程共用。常用于内存流转数据包,而内存流不再使用</remarks>
/// <param name="stream"></param>
public Packet(Stream stream)
{
if (stream is MemoryStream ms)
{
#if !NET45
// 尝试抠了内部存储区,下面代码需要.Net 4.6支持
if (ms.TryGetBuffer(out var seg))
{
if (seg.Array == null) throw new ArgumentNullException(nameof(seg));
Set(seg.Array, seg.Offset + (Int32)ms.Position, seg.Count - (Int32)ms.Position);
return;
}
// GetBuffer窃取内部缓冲区后无法得知真正的起始位置index可能导致错误取数
// public MemoryStream(byte[] buffer, int index, int count, bool writable, bool publiclyVisible)
//try
//{
// Set(ms.GetBuffer(), (Int32)ms.Position, (Int32)(ms.Length - ms.Position));
//}
//catch (UnauthorizedAccessException) { }
#endif
}
//Set(stream.ToArray());
var buf = new Byte[stream.Length - stream.Position];
var count = stream.Read(buf, 0, buf.Length);
Set(buf, 0, count);
// 必须确保数据流位置不变
if (count > 0) stream.Seek(-count, SeekOrigin.Current);
}
/// <summary>从Span实例化</summary>
/// <param name="span"></param>
public Packet(Span<Byte> span) => Set(span.ToArray());
/// <summary>从Memory实例化</summary>
/// <param name="memory"></param>
public Packet(Memory<Byte> memory)
{
if (MemoryMarshal.TryGetArray<Byte>(memory, out var segment))
{
Set(segment.Array!, segment.Offset, segment.Count);
}
else
{
Set(memory.ToArray());
}
}
#endregion
#region
/// <summary>获取/设置 指定位置的字节</summary>
/// <param name="index"></param>
/// <returns></returns>
public Byte this[Int32 index]
{
get
{
var p = index - Count;
if (p >= 0)
{
if (Next == null) throw new IndexOutOfRangeException(nameof(index));
return Next[p];
}
return Data[Offset + index];
}
set
{
var p = index - Count;
if (p >= 0)
{
if (Next == null) throw new IndexOutOfRangeException(nameof(index));
Next[p] = value;
}
else
{
Data[Offset + index] = value;
}
}
}
#endregion
#region
/// <summary>设置新的数据区</summary>
/// <param name="data">数据区</param>
/// <param name="offset">偏移</param>
/// <param name="count">字节个数</param>
[MemberNotNull(nameof(Data))]
public virtual void Set(Byte[] data, Int32 offset = 0, Int32 count = -1)
{
Data = data;
if (data == null)
{
Offset = 0;
Count = 0;
}
else
{
Offset = offset;
if (count < 0) count = data.Length - offset;
Count = count;
}
}
/// <summary>截取子数据区</summary>
/// <param name="offset">相对偏移</param>
/// <param name="count">字节个数</param>
/// <returns></returns>
public Packet Slice(Int32 offset, Int32 count = -1)
{
var start = Offset + offset;
var remain = Count - offset;
if (Next == null)
{
// count 是 offset 之后的个数
if (count < 0 || count > remain) count = remain;
if (count < 0) count = 0;
return new Packet(Data, start, count);
}
else
{
// 如果当前段用完,则取下一段
if (remain <= 0) return Next.Slice(offset - Count, count);
// 当前包用一截,剩下的全部
if (count < 0) return new Packet(Data, start, remain) { Next = Next };
// 当前包可以读完
if (count <= remain) return new Packet(Data, start, count);
// 当前包用一截,剩下的再截取
return new Packet(Data, start, remain) { Next = Next.Slice(0, count - remain) };
}
}
IPacket IPacket.Slice(Int32 offset, Int32 count) => Slice(offset, count);
IPacket IPacket.Slice(Int32 offset, Int32 count, Boolean transferOwner) => Slice(offset, count);
/// <summary>查找目标数组</summary>
/// <param name="data">目标数组</param>
/// <param name="offset">本数组起始偏移</param>
/// <param name="count">本数组搜索个数</param>
/// <returns></returns>
public Int32 IndexOf(Byte[] data, Int32 offset = 0, Int32 count = -1)
{
var start = offset;
var length = data.Length;
if (count < 0 || count > Total - offset) count = Total - offset;
// 快速查找
if (Next == null)
{
if (start >= Count) return -1;
//#if NETCOREAPP3_1_OR_GREATER
// var s1 = new Span<Byte>(Data, Offset + offset, count);
// var p = s1.IndexOf(data);
// return p >= 0 ? (p + offset) : -1;
//#endif
var p = Data.IndexOf(data, Offset + start, count);
return p >= 0 ? (p - Offset) : -1;
}
// 已匹配字节数
var win = 0;
// 索引加上data剩余字节数必须小于count否则就是已匹配
for (var i = 0; i + length - win <= count; i++)
{
if (this[start + i] == data[win])
{
win++;
// 全部匹配,退出
if (win >= length) return (start + i) - length + 1;
}
else
{
//win = 0; // 只要有一个不匹配,马上清零
// 不能直接清零,那样会导致数据丢失,需要逐位探测,窗口一个个字节滑动
i -= win;
win = 0;
// 本段分析未匹配,递归下一段
if (start + i == Count && Next != null)
{
var p = Next.IndexOf(data, 0, count - i);
if (p >= 0) return (start + i) + p;
break;
}
}
}
return -1;
}
/// <summary>附加一个包到当前包链的末尾</summary>
/// <param name="pk"></param>
public Packet Append(Packet pk)
{
if (pk == null) return this;
var p = this;
while (p.Next != null) p = p.Next;
p.Next = pk;
return this;
}
/// <summary>返回字节数组。无差别复制,一定返回新数组</summary>
/// <returns></returns>
public virtual Byte[] ToArray()
{
//if (Offset == 0 && (Count < 0 || Offset + Count == Data.Length) && Next == null) return Data;
if (Next == null) return Data.ReadBytes(Offset, Count);
// 链式包输出
var ms = Pool.MemoryStream.Get();
CopyTo(ms);
return ms.Return(true);
}
/// <summary>从封包中读取指定数据区,读取全部时直接返回缓冲区,以提升性能</summary>
/// <param name="offset">相对于数据包的起始位置实际上是数组的Offset+offset</param>
/// <param name="count">字节个数</param>
/// <returns></returns>
public Byte[] ReadBytes(Int32 offset = 0, Int32 count = -1)
{
// 读取全部
if (offset == 0 && count < 0)
{
if (Offset == 0 && (Count < 0 || Offset + Count == Data.Length) && Next == null) return Data;
return ToArray();
}
if (Next == null) return Data.ReadBytes(Offset + offset, count < 0 || count > Count ? Count : count);
// 当前包足够长
if (count >= 0 && offset + count <= Count) return Data.ReadBytes(Offset + offset, count);
// 链式包输出
if (count < 0) count = Total - offset;
var ms = Pool.MemoryStream.Get();
// 遍历
var cur = this;
while (cur != null && count > 0)
{
var len = cur.Count;
// 当前包不够用
if (len < offset)
offset -= len;
else if (cur.Data != null)
{
len -= offset;
if (len > count) len = count;
ms.Write(cur.Data, cur.Offset + offset, len);
offset = 0;
count -= len;
}
cur = cur.Next;
}
return ms.Return(true);
//// 以上算法太复杂,直接来
//return ToArray().ReadBytes(offset, count);
}
/// <summary>返回数据段</summary>
/// <returns></returns>
public ArraySegment<Byte> ToSegment()
{
if (Next == null) return new ArraySegment<Byte>(Data, Offset, Count);
return new ArraySegment<Byte>(ToArray());
}
/// <summary>返回数据段集合</summary>
/// <returns></returns>
public IList<ArraySegment<Byte>> ToSegments()
{
// 初始4元素优化扩容
var list = new List<ArraySegment<Byte>>(4);
for (var pk = this; pk != null; pk = pk.Next)
{
list.Add(new ArraySegment<Byte>(pk.Data, pk.Offset, pk.Count));
}
return list;
}
/// <summary>转为Span</summary>
/// <returns></returns>
public Span<Byte> AsSpan()
{
if (Next == null) return new Span<Byte>(Data, Offset, Count);
return new Span<Byte>(ToArray());
}
/// <summary>转为Memory</summary>
/// <returns></returns>
public Memory<Byte> AsMemory()
{
if (Next == null) return new Memory<Byte>(Data, Offset, Count);
return new Memory<Byte>(ToArray());
}
Span<Byte> IPacket.GetSpan() => AsSpan();
Memory<Byte> IPacket.GetMemory() => AsMemory();
/// <summary>获取封包的数据流形式</summary>
/// <returns></returns>
public virtual MemoryStream GetStream()
{
if (Next == null) return new MemoryStream(Data, Offset, Count, false, true);
var ms = new MemoryStream();
CopyTo(ms);
ms.Position = 0;
return ms;
}
/// <summary>把封包写入到数据流</summary>
/// <param name="stream"></param>
public void CopyTo(Stream stream)
{
stream.Write(Data, Offset, Count);
Next?.CopyTo(stream);
}
/// <summary>把封包写入到目标数组</summary>
/// <param name="buffer">目标数组</param>
/// <param name="offset">目标数组的偏移量</param>
/// <param name="count">目标数组的字节数</param>
public void WriteTo(Byte[] buffer, Int32 offset = 0, Int32 count = -1)
{
if (count < 0) count = Total;
var len = count;
if (len > Count) len = Count;
Buffer.BlockCopy(Data, Offset, buffer, offset, len);
offset += len;
count -= len;
if (count > 0) Next?.WriteTo(buffer, offset, count);
}
/// <summary>异步复制到目标数据流</summary>
/// <param name="stream"></param>
/// <returns></returns>
public async Task CopyToAsync(Stream stream)
{
await stream.WriteAsync(Data, Offset, Count).ConfigureAwait(false);
if (Next != null) await Next.CopyToAsync(stream).ConfigureAwait(false);
}
/// <summary>异步复制到目标数据流</summary>
/// <param name="stream"></param>
/// <param name="cancellationToken">取消通知</param>
/// <returns></returns>
public async Task CopyToAsync(Stream stream, CancellationToken cancellationToken)
{
await stream.WriteAsync(Data, Offset, Count, cancellationToken).ConfigureAwait(false);
if (Next != null) await Next.CopyToAsync(stream, cancellationToken).ConfigureAwait(false);
}
/// <summary>深度克隆一份数据包,拷贝数据区</summary>
/// <returns></returns>
public Packet Clone()
{
if (Next == null) return new Packet(Data.ReadBytes(Offset, Count));
// 链式包输出
var ms = Pool.MemoryStream.Get();
CopyTo(ms);
return new Packet(ms.Return(true));
}
/// <summary>尝试获取缓冲区</summary>
/// <param name="segment"></param>
/// <returns></returns>
public Boolean TryGetArray(out ArraySegment<Byte> segment)
{
if (Next == null)
{
segment = new ArraySegment<Byte>(Data, Offset, Count);
return true;
}
segment = default;
return false;
}
/// <summary>以字符串表示</summary>
/// <param name="encoding">字符串编码默认URF-8</param>
/// <param name="offset"></param>
/// <param name="count"></param>
/// <returns></returns>
public String ToStr(Encoding? encoding = null, Int32 offset = 0, Int32 count = -1)
{
if (Data == null) return String.Empty;
encoding ??= Encoding.UTF8;
if (count < 0) count = Total - offset;
if (Next == null) return Data.ToStr(encoding, Offset + offset, count);
return ReadBytes(offset, count).ToStr(encoding);
}
/// <summary>以十六进制编码表示</summary>
/// <param name="maxLength">最大显示多少个字节。默认-1显示全部</param>
/// <param name="separate">分隔符</param>
/// <param name="groupSize">分组大小为0时对每个字节应用分隔符否则对每个分组使用</param>
/// <returns></returns>
public String ToHex(Int32 maxLength = 32, String? separate = null, Int32 groupSize = 0)
{
if (Data == null) return String.Empty;
var hex = ReadBytes(0, maxLength).ToHex(separate, groupSize);
return (maxLength == -1 || Count <= maxLength) ? hex : String.Concat(hex, "...");
}
/// <summary>转为Base64编码</summary>
/// <returns></returns>
public String ToBase64()
{
if (Data == null) return String.Empty;
if (Next == null) Data.ToBase64(Offset, Count);
return ToArray().ToBase64();
}
/// <summary>读取无符号短整数</summary>
/// <param name="isLittleEndian"></param>
/// <returns></returns>
public UInt16 ReadUInt16(Boolean isLittleEndian = true) => Data.ToUInt16(Offset, isLittleEndian);
/// <summary>读取无符号整数</summary>
/// <param name="isLittleEndian"></param>
/// <returns></returns>
public UInt32 ReadUInt32(Boolean isLittleEndian = true) => Data.ToUInt32(Offset, isLittleEndian);
#endregion
#region
/// <summary>重载类型转换字节数组直接转为Packet对象</summary>
/// <param name="value"></param>
/// <returns></returns>
public static implicit operator Packet(Byte[] value) => value == null ? null! : new(value);
/// <summary>重载类型转换一维数组直接转为Packet对象</summary>
/// <param name="value"></param>
/// <returns></returns>
public static implicit operator Packet(ArraySegment<Byte> value) => new(value);
/// <summary>重载类型转换字符串直接转为Packet对象</summary>
/// <param name="value"></param>
/// <returns></returns>
public static implicit operator Packet(String value) => new(value.GetBytes());
/// <summary>已重载</summary>
/// <returns></returns>
public override String ToString() => $"[{Data.Length}]({Offset}, {Count})" + (Next == null ? "" : $"<{Total}>");
#endregion
}

View File

@@ -0,0 +1,148 @@
using System.Runtime.Serialization;
using System.Web.Script.Serialization;
using System.Xml.Serialization;
namespace ThingsGateway.NewLife.Data;
/// <summary>分页参数信息。可携带统计和数据权限扩展查询等信息</summary>
/// <remarks>
/// 文档 https://newlifex.com/core/page_parameter
/// </remarks>
public class PageParameter
{
#region
private String? _Sort;
/// <summary>获取 或 设置 排序字段前台接收便于做SQL安全性校验</summary>
/// <remarks>
/// 一般用于接收单个排序字段可以带上Asc/Desc这里会自动拆分。
/// 极少数情况下前端需要传递多个字段排序这时候可以使用OrderBy。
///
/// OrderBy优先级更高且支持手写复杂排序语句不做SQL安全性校验
/// 如果设置SortOrderBy将被清空。
/// </remarks>
[XmlIgnore, ScriptIgnore, IgnoreDataMember]
public virtual String? Sort
{
get => _Sort;
set
{
_Sort = value;
// 自动识别带有Asc/Desc的排序
if (!_Sort.IsNullOrEmpty() && !_Sort.Contains(','))
{
_Sort = _Sort.Trim();
var p = _Sort.LastIndexOf(' ');
if (p > 0)
{
var dir = _Sort[(p + 1)..];
if (dir.EqualIgnoreCase("asc"))
{
Desc = false;
_Sort = _Sort[..p].Trim();
}
else if (dir.EqualIgnoreCase("desc"))
{
Desc = true;
_Sort = _Sort[..p].Trim();
}
}
}
OrderBy = null;
}
}
/// <summary>获取 或 设置 是否降序</summary>
[XmlIgnore, ScriptIgnore, IgnoreDataMember]
public virtual Boolean Desc { get; set; }
/// <summary>获取 或 设置 页面索引。从1开始默认1</summary>
/// <remarks>如果设定了开始行分页时将不再使用PageIndex</remarks>
public virtual Int32 PageIndex { get; set; } = 1;
/// <summary>获取 或 设置 页面大小。默认20若为0表示不分页</summary>
public virtual Int32 PageSize { get; set; } = 20;
#endregion
#region
/// <summary>获取 或 设置 总记录数</summary>
public virtual Int64 TotalCount { get; set; }
/// <summary>获取 页数</summary>
public virtual Int64 PageCount
{
get
{
// 如果PageSize小于等于0则直接返回1
if (PageSize <= 0) return 1;
var count = TotalCount / PageSize;
if ((TotalCount % PageSize) != 0) count++;
return count;
}
}
/// <summary>获取 或 设置 自定义排序字句。常用于用户自定义排序不经过SQL安全性校验</summary>
/// <remarks>
/// OrderBy优先级更高且支持手写复杂排序语句不做SQL安全性校验
/// 如果设置SortOrderBy将被清空。
/// </remarks>
public virtual String? OrderBy { get; set; }
/// <summary>获取 或 设置 开始行</summary>
/// <remarks>如果设定了开始行分页时将不再使用PageIndex</remarks>
[XmlIgnore, ScriptIgnore, IgnoreDataMember]
public virtual Int64 StartRow { get; set; } = -1;
/// <summary>获取 或 设置 是否获取总记录数默认false</summary>
[XmlIgnore, ScriptIgnore, IgnoreDataMember]
public Boolean RetrieveTotalCount { get; set; }
/// <summary>获取 或 设置 状态。用于传递统计、扩展查询等用户数据</summary>
[XmlIgnore, ScriptIgnore, IgnoreDataMember]
public virtual Object? State { get; set; }
/// <summary>获取 或 设置 是否获取统计默认false</summary>
[XmlIgnore, ScriptIgnore, IgnoreDataMember]
public Boolean RetrieveState { get; set; }
#endregion
#region
/// <summary>实例化分页参数</summary>
public PageParameter() { }
/// <summary>通过另一个分页参数来实例化当前分页参数</summary>
/// <param name="pm"></param>
public PageParameter(PageParameter pm) => CopyFrom(pm);
#endregion
#region
/// <summary>从另一个分页参数拷贝到当前分页参数</summary>
/// <param name="pm"></param>
/// <returns></returns>
public virtual PageParameter CopyFrom(PageParameter pm)
{
if (pm == null) return this;
OrderBy = pm.OrderBy;
Sort = pm.Sort;
Desc = pm.Desc;
PageIndex = pm.PageIndex;
PageSize = pm.PageSize;
StartRow = pm.StartRow;
TotalCount = pm.TotalCount;
RetrieveTotalCount = pm.RetrieveTotalCount;
State = pm.State;
RetrieveState = pm.RetrieveState;
return this;
}
/// <summary>获取表示分页参数唯一性的键值,可用作缓存键</summary>
/// <returns></returns>
public virtual String GetKey() => $"{PageIndex}-{PageCount}-{OrderBy}";
#endregion
}

View File

@@ -0,0 +1,120 @@
namespace ThingsGateway.NewLife.Data;
/// <summary>环形缓冲区。用于协议组包设计</summary>
public class RingBuffer
{
#region
/// <summary>容量</summary>
public Int32 Capacity => _data.Length;
/// <summary>头指针。写入位置</summary>
public Int32 Head { get; set; }
/// <summary>尾指针。读取位置</summary>
public Int32 Tail { get; set; }
/// <summary>数据长度</summary>
public Int32 Length => Head >= Tail ? (Head - Tail) : (Head + _data.Length - Tail);
private Byte[] _data;
#endregion
#region
/// <summary>使用默认容量1024来初始化</summary>
public RingBuffer() : this(1024) { }
/// <summary>实例化环形缓冲区</summary>
/// <param name="capacity">容量。合理的容量能够减少扩容</param>
public RingBuffer(Int32 capacity) => _data = new Byte[capacity];
#endregion
#region
/// <summary>扩容,确保容量</summary>
/// <param name="capacity"></param>
public void EnsureCapacity(Int32 capacity)
{
if (capacity <= Capacity) return;
// 分配新空间,全量拷贝。比分块拷贝要低效一些,但是代码简单直接
var data = new Byte[capacity];
if (Length > 0)
Buffer.BlockCopy(_data, 0, data, 0, _data.Length);
_data = data;
}
private void CheckCapacity(Int32 capacity)
{
var len = _data.Length;
// 两倍增长
while (len < capacity) len *= 2;
EnsureCapacity(len);
}
/// <summary>写入数据</summary>
/// <param name="data">数据</param>
/// <param name="offset">偏移量</param>
/// <param name="count">个数</param>
public void Write(Byte[] data, Int32 offset = 0, Int32 count = -1)
{
if (count < 0) count = data.Length - offset;
CheckCapacity(Length + count);
var len = _data.Length - Head;
if (len > count) len = count;
Buffer.BlockCopy(data, offset, _data, Head, len);
count -= len;
Head += len;
if (Head == _data.Length) Head = 0;
// 还有数据,移到开头
if (count > 0)
{
Buffer.BlockCopy(data, offset, _data, Head, len);
Head = count;
}
}
/// <summary>读取数据</summary>
/// <param name="data">数据</param>
/// <param name="offset">偏移量</param>
/// <param name="count">个数</param>
/// <returns></returns>
public Int32 Read(Byte[] data, Int32 offset = 0, Int32 count = -1)
{
if (count < 0) count = data.Length - offset;
var len = Length;
if (len > count) len = count;
if (Tail + len > _data.Length) len = _data.Length - Tail;
Buffer.BlockCopy(_data, Tail, data, offset, len);
var rs = len;
count -= len;
Tail += len;
if (Tail == _data.Length) Tail = 0;
// 还有数据,移到开头
if (count > 0)
{
offset += len;
len = Length;
if (len > count) len = count;
Buffer.BlockCopy(_data, 0, data, offset, len);
rs += len;
count -= len;
Tail += len;
}
return rs;
}
#endregion
}

View File

@@ -0,0 +1,372 @@
using ThingsGateway.NewLife.Caching;
using ThingsGateway.NewLife.Log;
using ThingsGateway.NewLife.Model;
using ThingsGateway.NewLife.Security;
namespace ThingsGateway.NewLife.Data;
/// <summary>雪花算法。分布式Id业务内必须确保单例</summary>
/// <remarks>
/// 文档 https://newlifex.com/core/snow_flake
///
/// 使用一个 64 bit 的 long 型的数字作为全局唯一 id。在分布式系统中的应用十分广泛且ID 引入了时间戳,基本上保持自增。
/// 1bit保留 + 41bit时间戳 + 10bit机器 + 12bit序列号
///
/// 内置自动选择机器workerIdIP+进程+线程无法绝对保证唯一从而导致整体生成的雪花Id有一定几率重复。
/// 如果想要绝对唯一建议在外部设置唯一的workerId再结合单例使用此时确保最终生成的Id绝对不重复
/// 高要求场合推荐使用Redis自增序数作为workerId在大型分布式系统中亦能保证绝对唯一。
/// 已提供JoinCluster方法用于把当前对象加入集群确保workerId唯一。
///
/// 务必请保证Snowflake对象的唯一性Snowflake确保本对象生成的Id绝对唯一但如果有多个Snowflake对象可能会生成重复Id。
/// 特别在使用XCode等数据中间件时要确保每张表只有一个Snowflake实例。
/// </remarks>
public class Snowflake
{
#region
/// <summary>开始时间戳。首次使用前设置否则无效默认1970-1-1</summary>
/// <remarks>
/// 该时间戳默认已带有时区偏移不管是为本地时间还是UTC时间生成雪花Id都是一样的时间大小。
/// 默认值本质上就是UTC 1970-1-1转本地时间是为了方便解析雪花Id时得到的时间就是本地时间最大兼容已有业务。
/// 在星尘和IoT的自动分表场景中一般需要用本地时间来作为分表依据所以默认值是本地时间。
/// </remarks>
public DateTime StartTimestamp { get; set; } = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).ToLocalTime();
/// <summary>机器Id取10位</summary>
/// <remarks>
/// 内置默认取IP+进程+线程不能保证绝对唯一要求高的场合建议外部保证workerId唯一。
/// 一般借助Redis自增序数作为workerId确保绝对唯一。
/// 如果应用接入星尘将自动从星尘配置中心获取workerId确保全局唯一。
/// </remarks>
public Int32 WorkerId { get; set; }
private Int32 _Sequence;
/// <summary>序列号取12位。进程内静态避免多个实例生成重复Id</summary>
public Int32 Sequence => _Sequence;
/// <summary>全局机器Id。若设置所有雪花实例都将使用该Id可以由星尘配置中心提供本应用全局唯一机器码且跨多环境唯一</summary>
public static Int32 GlobalWorkerId { get; set; }
/// <summary>workerId分配集群。配置后可确保所有实例化的雪花对象得到唯一workerId建议使用Redis</summary>
public static ICache? Cluster { get; set; }
//private Int64 _msStart;
//private Stopwatch _watch = null!;
private Int64 _lastTime;
#endregion
#region
private static Int32 _gid;
private static readonly Int32 _instance;
static Snowflake()
{
try
{
// 从容器中获取缓存提供者查找Redis作为集群WorkerId分配器
var provider = ObjectContainer.Provider?.GetService<ICacheProvider>();
if (provider != null && provider.Cache != provider.InnerCache && provider.Cache is not MemoryCache)
Cluster = provider.Cache;
var ip = NetHelper.MyIP();
if (ip != null)
{
var buf = ip.GetAddressBytes();
_instance = (buf[2] << 8) | buf[3];
}
else
{
_instance = Rand.Next(1, 1024);
}
}
catch
{
// 异常时随机
_instance = Rand.Next(1, 1024);
}
}
#endregion
#region
private Boolean _inited;
private void Init()
{
if (_inited) return;
lock (this)
{
if (_inited) return;
// 记录雪花算法初始化埋点,及时发现算法使用错误
using var span = DefaultTracer.Instance?.NewSpan("Snowflake-Init", new { id = Interlocked.Increment(ref _gid) });
if (WorkerId <= 0 && GlobalWorkerId > 0) WorkerId = GlobalWorkerId & 0x3FF;
if (WorkerId <= 0 && Cluster != null) JoinCluster(Cluster);
// 初始化WorkerId取5位实例加上5位进程确保同一台机器的WorkerId不同
if (WorkerId <= 0)
{
var nodeId = _instance;
var pid = ProcessHelper.GetProcessId();
var tid = Environment.CurrentManagedThreadId;
//WorkerId = ((nodeId & 0x1F) << 5) | (pid & 0x1F);
//WorkerId = (nodeId ^ pid ^ tid) & 0x3FF;
WorkerId = ((nodeId & 0x1F) << 5) | ((pid ^ tid) & 0x1F);
}
//// 记录此时距离起点的毫秒数以及开机嘀嗒数
//if (_watch == null)
//{
// var now = ConvertKind(DateTime.Now);
// _msStart = (Int64)(now - StartTimestamp).TotalMilliseconds;
// _watch = Stopwatch.StartNew();
//}
//span?.AppendTag($"WorkerId={WorkerId} StartTimestamp={StartTimestamp.ToFullString()} _msStart={_msStart}");
span?.AppendTag($"WorkerId={WorkerId} StartTimestamp={StartTimestamp.ToFullString()}");
_inited = true;
}
}
/// <summary>获取下一个Id</summary>
/// <remarks>基于当前时间转StartTimestamp所属时区后生成Id</remarks>
/// <returns></returns>
public virtual Int64 NewId()
{
Init();
// 此时嘀嗒数减去起点嘀嗒数,加上起点毫秒数
var ms = (Int64)(ConvertKind(DateTime.Now) - StartTimestamp).TotalMilliseconds;
//var ms = _watch.ElapsedMilliseconds + _msStart;
var wid = WorkerId & (-1 ^ (-1 << 10));
var origin = Volatile.Read(ref _lastTime);
//!!! 避免时间倒退
if (ms < origin)
{
var t = origin - ms;
// 在夏令时地区时间可能回拨1个小时
if (t > 3600_000 + 10_000) throw new InvalidOperationException($"Time reversal too large ({t}ms). To ensure uniqueness, Snowflake refuses to generate a new Id");
// 暂时使用上次时间,即未来时间
ms = origin;
}
// 核心理念:时间不同时序号置零,时间相同时序号递增
var seq = 0;
lock (this)
{
while (true)
{
if (ms > _lastTime)
{
_Sequence = 0;
_lastTime = ms;
seq = 0;
break;
}
else
{
ms = _lastTime;
seq = Interlocked.Increment(ref _Sequence);
if (seq < 4096) break;
ms++;
_Sequence = 0;
}
}
}
//var seq = 0;
//while (true)
//{
// if (ms > origin)
// {
// lock (this)
// {
// origin = Volatile.Read(ref _lastTime);
// if (ms > origin)
// {
// Volatile.Write(ref _Sequence, 0);
// // 1空闲时走这里。跟上次时间不同抢夺当前坑位序号0。每毫秒只有1次机会
// if (Interlocked.CompareExchange(ref _lastTime, ms, origin) == origin)
// {
// seq = 0;
// break;
// }
// // 抢夺失败,必须用新的时间,原来时间已经错过,无法得到唯一序号
// origin = Volatile.Read(ref _lastTime);
// //ms = origin;
// }
// ms = origin;
// }
// }
// // 2繁忙时走这里。时间相同递增序列号较小序列号直接采用。每毫秒有4095次机会
// seq = Interlocked.Increment(ref _Sequence);
// if (seq < 4096) break;
// // 3极度繁忙时走这里。4096之外的“幸运儿”集体加锁重置序列号和时间准备再来抢一次。很少业务会走到这里只可能是积压数据冲击
// origin = Volatile.Read(ref _lastTime);
// if (ms == origin)
// {
// lock (this)
// {
// origin = Volatile.Read(ref _lastTime);
// if (ms == origin)
// {
// // 时间不允许后退否则可能生成重复Id。算法在每毫秒上生成4096个Id等待被回拨的时间追上
// //origin = Volatile.Read(ref _lastTime);
// ms++;
// }
// else
// {
// XTrace.WriteLine("ms.notEqual2 ms={0}, origin={1}", ms, origin);
// ms = origin;
// }
// }
// }
// else
// {
// XTrace.WriteLine("ms.notEqual1 ms={0}, origin={1}", ms, origin);
// ms = origin;
// }
//}
seq &= (-1 ^ (-1 << 12));
return (ms << (10 + 12)) | (Int64)(wid << 12) | (Int64)seq;
}
/// <summary>获取指定时间的Id带上节点和序列号。可用于根据业务时间构造插入Id</summary>
/// <remarks>
/// 基于指定时间转StartTimestamp所属时区后生成Id。
///
/// 如果为指定毫秒时间生成多个Id超过4096则可能重复。
/// </remarks>
/// <param name="time">时间</param>
/// <returns></returns>
public virtual Int64 NewId(DateTime time)
{
Init();
time = ConvertKind(time);
var ms = (Int64)(time - StartTimestamp).TotalMilliseconds;
var wid = WorkerId & (-1 ^ (-1 << 10));
var seq = Interlocked.Increment(ref _Sequence) & (-1 ^ (-1 << 12));
return (ms << (10 + 12)) | (Int64)(wid << 12) | (Int64)seq;
}
/// <summary>获取指定时间的Id传入唯一业务id取模为10位。可用于物联网数据采集每1024个传感器为一组每组每毫秒多个Id</summary>
/// <remarks>
/// 基于指定时间转StartTimestamp所属时区后生成Id。
///
/// 在物联网数据采集中,数据分析需要,更多希望能够按照采集时间去存储。
/// 为了避免主键重复可以使用传感器id作为workerId。
/// uid需要取模为10位即按1024分组每组每毫秒最多生成4096个Id。
///
/// 如果为指定分组在特定毫秒时间生成多个Id超过4096则可能重复。
/// </remarks>
/// <param name="time">时间</param>
/// <param name="uid">唯一业务id。例如传感器id</param>
/// <returns></returns>
public virtual Int64 NewId(DateTime time, Int32 uid)
{
Init();
time = ConvertKind(time);
// 业务id作为workerId保留12位序列号。即传感器按1024分组每组每毫秒最多生成4096个Id
var ms = (Int64)(time - StartTimestamp).TotalMilliseconds;
var wid = uid & (-1 ^ (-1 << 10));
var seq = Interlocked.Increment(ref _Sequence) & (-1 ^ (-1 << 12));
return (ms << (10 + 12)) | (Int64)(wid << 12) | (Int64)seq;
}
/// <summary>获取指定时间的Id传入唯一业务id22位。可用于物联网数据采集每4194304个传感器一组每组每毫秒1个Id</summary>
/// <remarks>
/// 基于指定时间转StartTimestamp所属时区后生成Id。
///
/// 在物联网数据采集中,数据分析需要,更多希望能够按照采集时间去存储。
/// 为了避免主键重复可以使用传感器id作为workerId。
/// 再配合upsert写入数据如果同一个毫秒内传感器有多行数据则只会插入一行。
///
/// 如果为指定业务id在特定毫秒时间生成多个Id超过1个则可能重复。
/// </remarks>
/// <param name="time">时间</param>
/// <param name="uid">唯一业务id。例如传感器id</param>
/// <returns></returns>
public virtual Int64 NewId22(DateTime time, Int32 uid)
{
Init();
time = ConvertKind(time);
// 业务id作为workerId不保留序列号。即传感器按41943041<<22分组每组每毫秒最多生成1个Id
var ms = (Int64)(time - StartTimestamp).TotalMilliseconds;
var wid = uid & (-1 ^ (-1 << 22));
return (ms << (10 + 12)) | (Int64)wid;
}
/// <summary>时间转为Id不带节点和序列号。可用于构建时间片段查询</summary>
/// <remarks>
/// 基于指定时间转StartTimestamp所属时区后生成不带WorkerId和序列号的Id。
/// 一般用于构建时间片段查询例如查询某个时间段内的数据把时间片段转为雪花Id片段。
/// </remarks>
/// <param name="time">时间</param>
/// <returns></returns>
public virtual Int64 GetId(DateTime time)
{
time = ConvertKind(time);
var t = (Int64)(time - StartTimestamp).TotalMilliseconds;
return t << (10 + 12);
}
/// <summary>解析雪花Id得到时间、WorkerId和序列号</summary>
/// <remarks>
/// 其中的时间是StartTimestamp所属时区的时间。
/// </remarks>
/// <param name="id"></param>
/// <param name="time">时间</param>
/// <param name="workerId">节点</param>
/// <param name="sequence">序列号</param>
/// <returns></returns>
public virtual Boolean TryParse(Int64 id, out DateTime time, out Int32 workerId, out Int32 sequence)
{
time = StartTimestamp.AddMilliseconds(id >> (10 + 12));
workerId = (Int32)((id >> 12) & 0x3FF);
sequence = (Int32)(id & 0x0FFF);
return true;
}
/// <summary>把输入时间转为开始时间戳的类型,便于相减</summary>
/// <param name="time"></param>
/// <returns></returns>
public DateTime ConvertKind(DateTime time)
{
// 如果待转换时间未指定时区,则直接返回
if (time.Kind == DateTimeKind.Unspecified) return time;
return StartTimestamp.Kind switch
{
DateTimeKind.Utc => time.ToUniversalTime(),
DateTimeKind.Local => time.ToLocalTime(),
_ => time,
};
}
#endregion
#region
/// <summary>加入集群。由集群统一分配WorkerId确保唯一从而保证生成的雪花Id绝对唯一</summary>
/// <param name="cache"></param>
/// <param name="key"></param>
public virtual void JoinCluster(ICache cache, String key = "SnowflakeWorkerId")
{
var wid = (Int32)cache.Increment(key, 1);
WorkerId = wid & 0x3FF;
}
#endregion
}

View File

@@ -0,0 +1,39 @@
namespace ThingsGateway.NewLife.Data;
/// <summary>
/// 时序点,用于时序数据计算
/// </summary>
public struct TimePoint
{
/// <summary>
/// 时间
/// </summary>
public Int64 Time;
/// <summary>
/// 数值
/// </summary>
public Double Value;
/// <summary>
/// 已重载
/// </summary>
/// <returns></returns>
public override String ToString() => $"({Time}, {Value})";
}
///// <summary>
///// 时序点,用于时序数据计算
///// </summary>
//public struct LongTimePoint
//{
// /// <summary>
// /// 时间
// /// </summary>
// public Int64 Time;
// /// <summary>
// /// 数值
// /// </summary>
// public Double Value;
//}

View File

@@ -17,19 +17,19 @@ public class WeakAction<TArgs>
{
#region
/// <summary>目标对象。弱引用使得调用方对象可以被GC回收</summary>
private readonly WeakReference? Target;
readonly WeakReference? Target;
/// <summary>委托方法</summary>
private readonly MethodBase Method;
readonly MethodBase Method;
/// <summary>经过包装的新的委托</summary>
private readonly Action<TArgs> Handler;
readonly Action<TArgs> Handler;
/// <summary>取消注册的委托</summary>
private Action<Action<TArgs>>? UnHandler;
Action<Action<TArgs>>? UnHandler;
/// <summary>是否只使用一次,如果只使用一次,执行委托后马上取消注册</summary>
private readonly Boolean Once;
readonly Boolean Once;
#endregion
#region

View File

@@ -1,86 +1,87 @@
namespace ThingsGateway.NewLife;
/// <summary>数据位助手</summary>
public static class BitHelper
namespace ThingsGateway.NewLife
{
/// <summary>设置数据位</summary>
/// <param name="value">数值</param>
/// <param name="position"></param>
/// <param name="flag"></param>
/// <returns></returns>
public static UInt16 SetBit(this UInt16 value, Int32 position, Boolean flag)
/// <summary>数据位助手</summary>
public static class BitHelper
{
return SetBits(value, position, 1, (flag ? (Byte)1 : (Byte)0));
/// <summary>设置数据位</summary>
/// <param name="value">数值</param>
/// <param name="position"></param>
/// <param name="flag"></param>
/// <returns></returns>
public static UInt16 SetBit(this UInt16 value, Int32 position, Boolean flag)
{
return SetBits(value, position, 1, (flag ? (Byte)1 : (Byte)0));
}
/// <summary>设置数据位</summary>
/// <param name="value">数值</param>
/// <param name="position"></param>
/// <param name="length"></param>
/// <param name="bits"></param>
/// <returns></returns>
public static UInt16 SetBits(this UInt16 value, Int32 position, Int32 length, UInt16 bits)
{
if (length <= 0 || position >= 16) return value;
var mask = (2 << (length - 1)) - 1;
value &= (UInt16)~(mask << position);
value |= (UInt16)((bits & mask) << position);
return value;
}
/// <summary>设置数据位</summary>
/// <param name="value">数值</param>
/// <param name="position"></param>
/// <param name="flag"></param>
/// <returns></returns>
public static Byte SetBit(this Byte value, Int32 position, Boolean flag)
{
if (position >= 8) return value;
var mask = (2 << (1 - 1)) - 1;
value &= (Byte)~(mask << position);
value |= (Byte)(((flag ? 1 : 0) & mask) << position);
return value;
}
/// <summary>获取数据位</summary>
/// <param name="value">数值</param>
/// <param name="position"></param>
/// <returns></returns>
public static Boolean GetBit(this UInt16 value, Int32 position)
{
return GetBits(value, position, 1) == 1;
}
/// <summary>获取数据位</summary>
/// <param name="value">数值</param>
/// <param name="position"></param>
/// <param name="length"></param>
/// <returns></returns>
public static UInt16 GetBits(this UInt16 value, Int32 position, Int32 length)
{
if (length <= 0 || position >= 16) return 0;
var mask = (2 << (length - 1)) - 1;
return (UInt16)((value >> position) & mask);
}
/// <summary>获取数据位</summary>
/// <param name="value">数值</param>
/// <param name="position"></param>
/// <returns></returns>
public static Boolean GetBit(this Byte value, Int32 position)
{
if (position >= 8) return false;
var mask = (2 << (1 - 1)) - 1;
return ((Byte)((value >> position) & mask)) == 1;
}
}
/// <summary>设置数据位</summary>
/// <param name="value">数值</param>
/// <param name="position"></param>
/// <param name="length"></param>
/// <param name="bits"></param>
/// <returns></returns>
public static UInt16 SetBits(this UInt16 value, Int32 position, Int32 length, UInt16 bits)
{
if (length <= 0 || position >= 16) return value;
var mask = (2 << (length - 1)) - 1;
value &= (UInt16)~(mask << position);
value |= (UInt16)((bits & mask) << position);
return value;
}
/// <summary>设置数据位</summary>
/// <param name="value">数值</param>
/// <param name="position"></param>
/// <param name="flag"></param>
/// <returns></returns>
public static Byte SetBit(this Byte value, Int32 position, Boolean flag)
{
if (position >= 8) return value;
var mask = (2 << (1 - 1)) - 1;
value &= (Byte)~(mask << position);
value |= (Byte)(((flag ? 1 : 0) & mask) << position);
return value;
}
/// <summary>获取数据位</summary>
/// <param name="value">数值</param>
/// <param name="position"></param>
/// <returns></returns>
public static Boolean GetBit(this UInt16 value, Int32 position)
{
return GetBits(value, position, 1) == 1;
}
/// <summary>获取数据位</summary>
/// <param name="value">数值</param>
/// <param name="position"></param>
/// <param name="length"></param>
/// <returns></returns>
public static UInt16 GetBits(this UInt16 value, Int32 position, Int32 length)
{
if (length <= 0 || position >= 16) return 0;
var mask = (2 << (length - 1)) - 1;
return (UInt16)((value >> position) & mask);
}
/// <summary>获取数据位</summary>
/// <param name="value">数值</param>
/// <param name="position"></param>
/// <returns></returns>
public static Boolean GetBit(this Byte value, Int32 position)
{
if (position >= 8) return false;
var mask = (2 << (1 - 1)) - 1;
return ((Byte)((value >> position) & mask)) == 1;
}
}
}

View File

@@ -1,9 +1,9 @@
using System.Collections.Concurrent;
namespace ThingsGateway.NewLife.Extension;
namespace ThingsGateway.NewLife.DictionaryExtensions;
/// <summary>并发字典扩展</summary>
public static class ConcurrentDictionaryExtensions
public static class DictionaryExtensions
{
/// <summary>从并发字典中删除</summary>
/// <typeparam name="TKey"></typeparam>
@@ -13,6 +13,18 @@ public static class ConcurrentDictionaryExtensions
/// <returns></returns>
public static Boolean Remove<TKey, TValue>(this ConcurrentDictionary<TKey, TValue> dict, TKey key) where TKey : notnull => dict.TryRemove(key, out _);
#if !NET6_0_OR_GREATER
public static bool TryAdd<TKey, TValue>(this IDictionary<TKey, TValue> pairs, TKey key, TValue value)
{
if (!pairs.ContainsKey(key))
{
pairs.Add(key, value);
return true;
}
return false;
}
#endif
/// <inheritdoc/>
public static int RemoveWhere<TKey, TValue>(this IDictionary<TKey, TValue> pairs, Func<KeyValuePair<TKey, TValue>, bool> func)
{
@@ -97,4 +109,4 @@ public static class ConcurrentDictionaryExtensions
return dict;
}
}
}

View File

@@ -1,45 +0,0 @@
using System.Net;
namespace ThingsGateway.NewLife.Extension;
/// <summary>网络结点扩展</summary>
public static class EndPointExtensions
{
public static String ToAddress(this EndPoint endpoint)
{
return ((IPEndPoint)endpoint).ToAddress();
}
public static String ToAddress(this IPEndPoint endpoint)
{
return String.Format("{0}:{1}", endpoint.Address, endpoint.Port);
}
private static readonly String[] SplitColon = new String[] { ":" };
public static IPEndPoint ToEndPoint(this String address)
{
var array = address.Split(SplitColon, StringSplitOptions.RemoveEmptyEntries);
if (array.Length != 2)
{
throw new Exception("Invalid endpoint address: " + address);
}
var ip = IPAddress.Parse(array[0]);
var port = Int32.Parse(array[1]);
return new IPEndPoint(ip, port);
}
private static readonly String[] SplitComma = new String[] { "," };
public static IEnumerable<IPEndPoint> ToEndPoints(this String addresses)
{
var array = addresses.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
var list = new List<IPEndPoint>();
foreach (var item in array)
{
list.Add(item.ToEndPoint());
}
return list;
}
}

View File

@@ -1,4 +1,4 @@
namespace ThingsGateway.NewLife.Extension;
namespace System.Collections.Generic;
/// <summary>扩展List支持遍历中修改元素</summary>
public static class ListExtension
@@ -24,4 +24,4 @@ public static class ListExtension
return list.ToArray().Where(e => match(e)).ToList();
}
}
}

View File

@@ -14,6 +14,17 @@ namespace ThingsGateway.NewLife;
/// </remarks>
public static class ProcessHelper
{
public static int GetProcessId()
{
#if NET6_0_OR_GREATER
return Environment.ProcessId;
#else
return ProcessHelper.GetProcessId();
#endif
}
#region
/// <summary>获取二级进程名。默认一级如果是dotnet/java则取二级</summary>
/// <param name="process"></param>
@@ -187,6 +198,10 @@ public static class ProcessHelper
{
if (process?.GetHasExited() != false) return process;
var span = DefaultSpan.Current;
//XTrace.WriteLine("安全温柔一刀PID={0}/{1}", process.Id, process.ProcessName);
span?.AppendTag($"SafetyKill温柔一刀PID={process.Id}/{process.ProcessName}");
// 杀进程,如果命令未成功则马上退出(后续强杀),否则循环检测并等待
try
{
@@ -211,8 +226,9 @@ public static class ProcessHelper
}
}
}
catch
catch (Exception ex)
{
span?.AppendTag(ex.Message);
}
//if (!process.GetHasExited()) process.Kill();
@@ -228,6 +244,9 @@ public static class ProcessHelper
{
if (process?.GetHasExited() != false) return process;
var span = DefaultSpan.Current;
//XTrace.WriteLine("强杀大力出奇迹PID={0}/{1}", process.Id, process.ProcessName);
span?.AppendTag($"ForceKill大力出奇迹PID={process.Id}/{process.ProcessName}");
// 终止指定的进程及启动的子进程,如nginx等
// 在Core 3.0, Core 3.1, 5, 6, 7, 8, 9 中支持此重载
@@ -240,8 +259,9 @@ public static class ProcessHelper
process.Kill();
#endif
}
catch
catch (Exception ex)
{
span?.AppendTag(ex.Message);
}
if (process.GetHasExited()) return process;
@@ -260,9 +280,9 @@ public static class ProcessHelper
Process.Start("taskkill", $"/t /f /pid {process.Id}").WaitForExit(msWait);
}
}
catch
catch (Exception ex)
{
span?.AppendTag(ex.Message);
}
// 兜底再来一次
@@ -276,8 +296,9 @@ public static class ProcessHelper
process.Kill();
#endif
}
catch
catch (Exception ex)
{
span?.AppendTag(ex.Message);
}
}
@@ -333,7 +354,7 @@ public static class ProcessHelper
encoding ??= Encoding.UTF8;
using var p = new Process();
var p = new Process();
var si = p.StartInfo;
si.FileName = fileName;
if (arguments != null) si.Arguments = arguments;

View File

@@ -5,7 +5,7 @@ using ThingsGateway.NewLife.Reflection;
namespace ThingsGateway.NewLife.Extension;
internal sealed class SpeakProvider
class SpeakProvider
{
private const String typeName = "System.Speech.Synthesis.SpeechSynthesizer";
private Type? _type;
@@ -38,15 +38,14 @@ internal sealed class SpeakProvider
}
catch (Exception ex)
{
NewLife.Log.XTrace.WriteException(ex);
XTrace.WriteException(ex);
}
if (_type == null) XTrace.WriteLine("找不到语音库System.Speech需要从nuget引用");
}
private Object? synth;
private void EnsureSynth()
void EnsureSynth()
{
if (synth == null && _type != null)
{
@@ -57,7 +56,7 @@ internal sealed class SpeakProvider
}
catch (Exception ex)
{
NewLife.Log.XTrace.WriteException(ex);
XTrace.WriteException(ex);
_type = null;
}
}

View File

@@ -321,8 +321,8 @@ public static class StringHelper
/// <returns></returns>
public static String EnsureStart(this String? str, String start)
{
if (String.IsNullOrEmpty(start)) return str + "";
if (String.IsNullOrEmpty(str) || str == null) return start + "";
if (String.IsNullOrEmpty(start)) return str + string.Empty;
if (String.IsNullOrEmpty(str) || str == null) return start + string.Empty;
if (str.StartsWith(start, StringComparison.OrdinalIgnoreCase)) return str;
@@ -335,8 +335,8 @@ public static class StringHelper
/// <returns></returns>
public static String EnsureEnd(this String? str, String end)
{
if (String.IsNullOrEmpty(end)) return str + "";
if (String.IsNullOrEmpty(str) || str == null) return end + "";
if (String.IsNullOrEmpty(end)) return str + string.Empty;
if (String.IsNullOrEmpty(str) || str == null) return end + string.Empty;
if (str.EndsWith(end, StringComparison.OrdinalIgnoreCase)) return str;
@@ -846,14 +846,14 @@ public static class StringHelper
#endregion
#region
private static ThingsGateway.NewLife.Extension.SpeakProvider? _provider;
private static NewLife.Extension.SpeakProvider? _provider;
//private static System.Speech.Synthesis.SpeechSynthesizer _provider;
[MemberNotNull(nameof(_provider))]
private static void Init()
static void Init()
{
//_provider = new Speech.Synthesis.SpeechSynthesizer();
//_provider.SetOutputToDefaultAudioDevice();
_provider ??= new ThingsGateway.NewLife.Extension.SpeakProvider();
_provider ??= new NewLife.Extension.SpeakProvider();
}
/// <summary>调用语音引擎说出指定话</summary>
@@ -903,4 +903,4 @@ public static class StringHelper
return value;
}
#endregion
}
}

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