Compare commits
11 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b06405717d | ||
![]() |
298a1f2ed4 | ||
![]() |
74a47a1983 | ||
![]() |
a48a42abe4 | ||
![]() |
feb1d0a3c5 | ||
![]() |
92bca824e6 | ||
![]() |
025c699517 | ||
![]() |
53f8fbe4b1 | ||
![]() |
77bfabc41d | ||
![]() |
6427ee6ee0 | ||
![]() |
4c95997d62 |
@@ -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>
|
||||
|
@@ -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
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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));
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@@ -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 登录错误次数
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -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));
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@@ -10,6 +10,8 @@
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using ThingsGateway.NewLife.DictionaryExtensions;
|
||||
|
||||
namespace ThingsGateway.Admin.Application;
|
||||
|
||||
/// <summary>
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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;//令牌数量
|
||||
|
@@ -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();//获取主管信息
|
||||
}
|
||||
|
||||
//获取按钮码
|
||||
|
@@ -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);
|
||||
|
@@ -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' ">
|
||||
|
@@ -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.
|
||||
|
@@ -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)
|
||||
{
|
||||
|
@@ -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();
|
||||
}
|
||||
|
||||
|
@@ -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; }
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
22
src/Admin/ThingsGateway.AdminServer/Configuration/Cache.json
Normal file
22
src/Admin/ThingsGateway.AdminServer/Configuration/Cache.json
Normal 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:"
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
||||
{
|
||||
|
@@ -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
|
||||
{
|
||||
|
@@ -45,9 +45,7 @@
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="$(NET9Version)" />
|
||||
</ItemGroup>
|
||||
|
||||
<!--安装服务守护-->
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
|
||||
|
||||
|
@@ -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);
|
||||
//数据源增加
|
||||
|
@@ -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();
|
||||
|
||||
// 默认内置 GBK,Windows-1252, Shift-JIS, GB2312 编码支持
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
|
@@ -151,6 +151,8 @@ internal static class InternalApp
|
||||
// 存储服务提供器
|
||||
InternalServices = hostApplicationBuilder.Services;
|
||||
|
||||
|
||||
|
||||
// 存储根服务
|
||||
hostApplicationBuilder.Services.AddHostedService<GenericHostLifetimeEventsHostedService>();
|
||||
|
||||
|
@@ -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));
|
||||
|
@@ -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>
|
||||
|
@@ -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;
|
||||
// }
|
||||
//}
|
||||
|
66
src/Admin/ThingsGateway.Furion/Redis/Cache/CacheOptions.cs
Normal file
66
src/Admin/ThingsGateway.Furion/Redis/Cache/CacheOptions.cs
Normal 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
|
||||
{
|
||||
|
||||
}
|
@@ -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
|
116
src/Admin/ThingsGateway.Furion/Redis/Extension/RedisCache.cs
Normal file
116
src/Admin/ThingsGateway.Furion/Redis/Extension/RedisCache.cs
Normal 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
|
@@ -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
|
@@ -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
|
@@ -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
|
@@ -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>
|
||||
|
27
src/Admin/ThingsGateway.NewLife.X/Algorithms/AlignModes.cs
Normal file
27
src/Admin/ThingsGateway.NewLife.X/Algorithms/AlignModes.cs
Normal 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,
|
||||
}
|
116
src/Admin/ThingsGateway.NewLife.X/Algorithms/AverageSampling.cs
Normal file
116
src/Admin/ThingsGateway.NewLife.X/Algorithms/AverageSampling.cs
Normal 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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
138
src/Admin/ThingsGateway.NewLife.X/Algorithms/ISampling.cs
Normal file
138
src/Admin/ThingsGateway.NewLife.X/Algorithms/ISampling.cs
Normal 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();
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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))
|
||||
|
377
src/Admin/ThingsGateway.NewLife.X/Buffers/SpanReader.cs
Normal file
377
src/Admin/ThingsGateway.NewLife.X/Buffers/SpanReader.cs
Normal 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
|
||||
}
|
338
src/Admin/ThingsGateway.NewLife.X/Buffers/SpanWriter.cs
Normal file
338
src/Admin/ThingsGateway.NewLife.X/Buffers/SpanWriter.cs
Normal 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
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -8,7 +8,7 @@ public class CacheLock : DisposeBase
|
||||
/// <summary>
|
||||
/// 是否持有锁
|
||||
/// </summary>
|
||||
private Boolean _hasLock;
|
||||
private Boolean _hasLock = false;
|
||||
|
||||
/// <summary>键</summary>
|
||||
public String Key { get; set; }
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
97
src/Admin/ThingsGateway.NewLife.X/Caching/QueueEventBus.cs
Normal file
97
src/Admin/ThingsGateway.NewLife.X/Caching/QueueEventBus.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
150
src/Admin/ThingsGateway.NewLife.X/Caching/Readme.MD
Normal file
150
src/Admin/ThingsGateway.NewLife.X/Caching/Readme.MD
Normal 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}` 中加载配置,用于容器化部署。
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
76
src/Admin/ThingsGateway.NewLife.X/Collections/ICluster.cs
Normal file
76
src/Admin/ThingsGateway.NewLife.X/Collections/ICluster.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
namespace ThingsGateway.NewLife.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// 字典数据源接口。定义该模型类支持输出名值字典,便于序列化传输
|
||||
/// </summary>
|
||||
public interface IDictionarySource
|
||||
{
|
||||
/// <summary>
|
||||
/// 把对象转为名值字典,便于序列化传输
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IDictionary<String, Object?> ToDictionary();
|
||||
}
|
@@ -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
|
||||
}
|
||||
|
@@ -26,8 +26,8 @@ public class ObjectPool<T> : DisposeBase, IPool<T> where T : notnull
|
||||
/// <summary>繁忙个数</summary>
|
||||
public Int32 BusyCount => _BusyCount;
|
||||
|
||||
/// <summary>最大个数。默认100,0表示无上限</summary>
|
||||
public Int32 Max { get; set; } = 100;
|
||||
/// <summary>最大个数。默认0,0表示无上限</summary>
|
||||
public Int32 Max { get; set; } = 0;
|
||||
|
||||
/// <summary>最小个数。默认1</summary>
|
||||
public Int32 Min { get; set; } = 1;
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
@@ -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);
|
||||
|
@@ -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
|
||||
}
|
@@ -69,4 +69,4 @@ public class Gen2GcCallback : CriticalFinalizerObject
|
||||
}
|
||||
GC.ReRegisterForFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
|
@@ -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>
|
||||
|
150
src/Admin/ThingsGateway.NewLife.X/Common/SysConfig.cs
Normal file
150
src/Admin/ThingsGateway.NewLife.X/Common/SysConfig.cs
Normal 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
|
||||
}
|
@@ -66,4 +66,4 @@ public abstract class TimeProvider
|
||||
/// <returns></returns>
|
||||
public TimeSpan GetElapsedTime(Int64 startingTimestamp) => GetElapsedTime(startingTimestamp, GetTimestamp());
|
||||
}
|
||||
#endif
|
||||
#endif
|
64
src/Admin/ThingsGateway.NewLife.X/Compression/SevenZip.cs
Normal file
64
src/Admin/ThingsGateway.NewLife.X/Compression/SevenZip.cs
Normal 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
|
||||
}
|
@@ -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
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
namespace ThingsGateway.NewLife.Configuration
|
||||
{
|
||||
/// <summary>配置数据缓存等级</summary>
|
||||
public enum ConfigCacheLevel
|
||||
{
|
||||
/// <summary>不缓存</summary>
|
||||
NoCache,
|
||||
|
||||
/// <summary>Json格式缓存</summary>
|
||||
Json,
|
||||
|
||||
/// <summary>加密缓存</summary>
|
||||
Encrypted,
|
||||
}
|
||||
}
|
@@ -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
|
||||
{
|
||||
|
@@ -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");
|
||||
}
|
||||
|
||||
|
@@ -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();
|
||||
|
@@ -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
|
||||
}
|
@@ -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);
|
||||
|
52
src/Admin/ThingsGateway.NewLife.X/Data/DbRow.cs
Normal file
52
src/Admin/ThingsGateway.NewLife.X/Data/DbRow.cs
Normal 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
|
||||
}
|
845
src/Admin/ThingsGateway.NewLife.X/Data/DbTable.cs
Normal file
845
src/Admin/ThingsGateway.NewLife.X/Data/DbTable.cs
Normal 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
|
||||
}
|
121
src/Admin/ThingsGateway.NewLife.X/Data/GeoHash.cs
Normal file
121
src/Admin/ThingsGateway.NewLife.X/Data/GeoHash.cs
Normal 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
|
||||
}
|
21
src/Admin/ThingsGateway.NewLife.X/Data/IData.cs
Normal file
21
src/Admin/ThingsGateway.NewLife.X/Data/IData.cs
Normal 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
|
||||
}
|
16
src/Admin/ThingsGateway.NewLife.X/Data/IExtend.cs
Normal file
16
src/Admin/ThingsGateway.NewLife.X/Data/IExtend.cs
Normal 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; }
|
||||
}
|
74
src/Admin/ThingsGateway.NewLife.X/Data/IFilter.cs
Normal file
74
src/Admin/ThingsGateway.NewLife.X/Data/IFilter.cs
Normal 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);
|
||||
}
|
16
src/Admin/ThingsGateway.NewLife.X/Data/IModel.cs
Normal file
16
src/Admin/ThingsGateway.NewLife.X/Data/IModel.cs
Normal 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; }
|
||||
}
|
900
src/Admin/ThingsGateway.NewLife.X/Data/IPacket.cs
Normal file
900
src/Admin/ThingsGateway.NewLife.X/Data/IPacket.cs
Normal 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
|
||||
}
|
112
src/Admin/ThingsGateway.NewLife.X/Data/IPacketEncoder.cs
Normal file
112
src/Admin/ThingsGateway.NewLife.X/Data/IPacketEncoder.cs
Normal 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);
|
||||
}
|
||||
}
|
24
src/Admin/ThingsGateway.NewLife.X/Data/IndexRange.cs
Normal file
24
src/Admin/ThingsGateway.NewLife.X/Data/IndexRange.cs
Normal 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})";
|
||||
}
|
||||
}
|
544
src/Admin/ThingsGateway.NewLife.X/Data/Packet.cs
Normal file
544
src/Admin/ThingsGateway.NewLife.X/Data/Packet.cs
Normal 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
|
||||
}
|
148
src/Admin/ThingsGateway.NewLife.X/Data/PageParameter.cs
Normal file
148
src/Admin/ThingsGateway.NewLife.X/Data/PageParameter.cs
Normal 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安全性校验)。
|
||||
/// 如果设置Sort,OrderBy将被清空。
|
||||
/// </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安全性校验)。
|
||||
/// 如果设置Sort,OrderBy将被清空。
|
||||
/// </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
|
||||
}
|
120
src/Admin/ThingsGateway.NewLife.X/Data/RingBuffer.cs
Normal file
120
src/Admin/ThingsGateway.NewLife.X/Data/RingBuffer.cs
Normal 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
|
||||
}
|
372
src/Admin/ThingsGateway.NewLife.X/Data/Snowflake.cs
Normal file
372
src/Admin/ThingsGateway.NewLife.X/Data/Snowflake.cs
Normal 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序列号
|
||||
///
|
||||
/// 内置自动选择机器workerId,IP+进程+线程,无法绝对保证唯一,从而导致整体生成的雪花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,传入唯一业务id(22位)。可用于物联网数据采集,每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,不保留序列号。即传感器按4194304(1<<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
|
||||
}
|
39
src/Admin/ThingsGateway.NewLife.X/Data/TimePoint.cs
Normal file
39
src/Admin/ThingsGateway.NewLife.X/Data/TimePoint.cs
Normal 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;
|
||||
//}
|
@@ -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 扩展属性
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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
Reference in New Issue
Block a user