mirror of
https://gitee.com/ThingsGateway/ThingsGateway.git
synced 2025-10-20 10:50:48 +08:00
更新依赖
This commit is contained in:
@@ -1 +1 @@
|
||||
https://gitee.com/dotnetchina/Furion/commit/ef5310cbf2618584c1bfc812253309299ec620d7
|
||||
https://gitee.com/dotnetchina/Furion/commit/8bf85f6908c1630268e45eeec607267a03947d2b
|
@@ -486,20 +486,21 @@ public static class App
|
||||
try
|
||||
{
|
||||
if (BakImagePaths.Contains(assemblyFileFullPath)) continue;
|
||||
// 根据路径加载程序集
|
||||
//var loadedAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyFileFullPath);
|
||||
var runtimeAssembliesPath = Path.GetDirectoryName(typeof(object).Assembly.Location);
|
||||
// 将目标程序集和运行时核心程序集一起提供给 PathAssemblyResolver
|
||||
var assemblies = Directory.GetFiles(runtimeAssembliesPath, "*.dll");
|
||||
var resolver = new PathAssemblyResolver(new[] { assemblyFileFullPath }.Concat(assemblies));
|
||||
// 使用 MetadataLoadContext
|
||||
using var metadataContext = new MetadataLoadContext(resolver);
|
||||
//// 根据路径加载程序集
|
||||
////var loadedAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyFileFullPath);
|
||||
//var runtimeAssembliesPath = Path.GetDirectoryName(typeof(object).Assembly.Location);
|
||||
//// 将目标程序集和运行时核心程序集一起提供给 PathAssemblyResolver
|
||||
//var assemblies = Directory.GetFiles(runtimeAssembliesPath, "*.dll");
|
||||
//var resolver = new PathAssemblyResolver(new[] { assemblyFileFullPath }.Concat(assemblies));
|
||||
//// 使用 MetadataLoadContext
|
||||
//using var metadataContext = new MetadataLoadContext(resolver);
|
||||
|
||||
var referencedAssemblies = metadataContext.LoadFromAssemblyPath(assemblyFileFullPath)?.GetReferencedAssemblies();
|
||||
if ((referencedAssemblies?.Any(a => a.Name.StartsWith("ThingsGateway"))) != true)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
//var referencedAssemblies = metadataContext.LoadFromAssemblyPath(assemblyFileFullPath)?.GetReferencedAssemblies();
|
||||
//if ((referencedAssemblies?.Any(a => a.Name.StartsWith("ThingsGateway"))) != true)
|
||||
//{
|
||||
// continue;
|
||||
//}
|
||||
|
||||
var loadedAssembly = Reflect.LoadAssembly(assemblyFileFullPath);
|
||||
if (loadedAssembly == default) continue;
|
||||
|
||||
|
@@ -177,7 +177,10 @@ public static class AppServiceCollectionExtensions
|
||||
public static IServiceCollection AddAppHostedService(this IServiceCollection services)
|
||||
{
|
||||
// 获取所有 BackgroundService 类型,排除泛型主机
|
||||
var backgroundServiceTypes = App.EffectiveTypes.Where(u => !u.IsAbstract && !u.IsInterface && typeof(IHostedService).IsAssignableFrom(u) && u.Name != "GenericWebHostService");
|
||||
var backgroundServiceTypes = App.EffectiveTypes.Where(u => !u.IsAbstract && !u.IsInterface && !u.IsGenericType
|
||||
&& typeof(IHostedService).IsAssignableFrom(u) && u.Name != "GenericWebHostService"
|
||||
&& !services.Any(c => c.ServiceType == typeof(IHostedService) && c.ImplementationType != u));
|
||||
|
||||
var addHostServiceMethod = typeof(ServiceCollectionHostedServiceExtensions).GetMethods(BindingFlags.Static | BindingFlags.Public)
|
||||
.Where(u => u.Name.Equals("AddHostedService") && u.IsGenericMethod && u.GetParameters().Length == 1)
|
||||
.FirstOrDefault();
|
||||
|
@@ -16,7 +16,7 @@ namespace ThingsGateway.AspNetCore;
|
||||
/// <summary>
|
||||
/// 数组 URL 地址参数模型绑定特性
|
||||
/// </summary>
|
||||
[SuppressSniffer]
|
||||
[SuppressSniffer, AttributeUsage(AttributeTargets.Parameter)]
|
||||
public sealed class FlexibleArrayAttribute<T> : ModelBinderAttribute
|
||||
{
|
||||
/// <summary>
|
||||
|
@@ -170,7 +170,7 @@ public sealed class ScheduleOptionsBuilder
|
||||
public ScheduleOptionsBuilder AddJob(params SchedulerBuilder[] schedulerBuilders)
|
||||
{
|
||||
// 空检查
|
||||
if (schedulerBuilders == null || schedulerBuilders.Length == 0) throw new ArgumentNullException(nameof(schedulerBuilders));
|
||||
if (schedulerBuilders == null) throw new ArgumentNullException(nameof(schedulerBuilders));
|
||||
|
||||
// 逐条将作业计划构建器添加到集合中
|
||||
foreach (var schedulerBuilder in schedulerBuilders)
|
||||
|
@@ -93,8 +93,11 @@ internal sealed class JobCancellationToken : IJobCancellationToken
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException) { }
|
||||
catch (AggregateException ex) when (ex.InnerExceptions.Count == 1 && ex.InnerExceptions[0] is TaskCanceledException) { }
|
||||
catch (OperationCanceledException) { }
|
||||
catch (Exception ex) when (!(ex is OperationCanceledException ||
|
||||
ex is ObjectDisposedException ||
|
||||
(ex is AggregateException aggEx && aggEx.InnerExceptions.All(e => e is OperationCanceledException || e is ObjectDisposedException))))
|
||||
{ }
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
@@ -82,7 +82,7 @@ public abstract class JobExecutionContext
|
||||
/// <summary>
|
||||
/// 触发模式
|
||||
/// </summary>
|
||||
/// <remarks>默认为定时触发,0:定时,1:手动</remarks>
|
||||
/// <remarks>默认为定时触发,0:定时,1:手动</remarks>
|
||||
public int Mode { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
|
@@ -41,6 +41,6 @@ public sealed class JobFactoryContext
|
||||
/// <summary>
|
||||
/// 触发模式
|
||||
/// </summary>
|
||||
/// <remarks>默认为定时触发,0:定时,1:手动</remarks>
|
||||
/// <remarks>默认为定时触发,0:定时,1:手动</remarks>
|
||||
public int Mode { get; internal set; }
|
||||
}
|
@@ -61,7 +61,7 @@ public sealed class PersistenceExecutionRecordContext
|
||||
/// <summary>
|
||||
/// 触发模式
|
||||
/// </summary>
|
||||
/// <remarks>默认为定时触发,0:定时,1:手动</remarks>
|
||||
/// <remarks>默认为定时触发,0:定时,1:手动</remarks>
|
||||
public int Mode { get; }
|
||||
|
||||
/// <summary>
|
||||
|
@@ -45,7 +45,7 @@ public sealed class PersistenceTriggerContext : PersistenceContext
|
||||
/// <summary>
|
||||
/// 触发模式
|
||||
/// </summary>
|
||||
/// <remarks>默认为定时触发,0:定时,1:手动</remarks>
|
||||
/// <remarks>默认为定时触发,0:定时,1:手动</remarks>
|
||||
public int Mode { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
|
@@ -334,7 +334,7 @@ internal sealed partial class SchedulerFactory
|
||||
public void SaveJob(params SchedulerBuilder[] schedulerBuilders)
|
||||
{
|
||||
// 空检查
|
||||
if (schedulerBuilders == null || schedulerBuilders.Length == 0) throw new ArgumentNullException(nameof(schedulerBuilders));
|
||||
if (schedulerBuilders == null) throw new ArgumentNullException(nameof(schedulerBuilders));
|
||||
|
||||
// 逐条将作业计划构建器保存到作业计划中
|
||||
foreach (var schedulerBuilder in schedulerBuilders)
|
||||
@@ -387,7 +387,7 @@ internal sealed partial class SchedulerFactory
|
||||
public void AddJob(params SchedulerBuilder[] schedulerBuilders)
|
||||
{
|
||||
// 空检查
|
||||
if (schedulerBuilders == null || schedulerBuilders.Length == 0) throw new ArgumentNullException(nameof(schedulerBuilders));
|
||||
if (schedulerBuilders == null) throw new ArgumentNullException(nameof(schedulerBuilders));
|
||||
|
||||
// 逐条将作业计划构建器保存到作业计划中
|
||||
foreach (var schedulerBuilder in schedulerBuilders)
|
||||
@@ -1026,7 +1026,7 @@ internal sealed partial class SchedulerFactory
|
||||
public void UpdateJob(params SchedulerBuilder[] schedulerBuilders)
|
||||
{
|
||||
// 空检查
|
||||
if (schedulerBuilders == null || schedulerBuilders.Length == 0) throw new ArgumentNullException(nameof(schedulerBuilders));
|
||||
if (schedulerBuilders == null) throw new ArgumentNullException(nameof(schedulerBuilders));
|
||||
|
||||
// 逐条将作业计划构建器保存到作业计划中
|
||||
foreach (var schedulerBuilder in schedulerBuilders)
|
||||
|
@@ -20,6 +20,11 @@ namespace ThingsGateway.Schedule;
|
||||
/// </summary>
|
||||
internal sealed partial class SchedulerFactory : ISchedulerFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// 取消作业调度器休眠状态并发锁
|
||||
/// </summary>
|
||||
private readonly object _lock = new();
|
||||
|
||||
/// <summary>
|
||||
/// 作业计划变更通知
|
||||
/// </summary>
|
||||
@@ -297,21 +302,25 @@ internal sealed partial class SchedulerFactory : ISchedulerFactory
|
||||
/// </summary>
|
||||
public void CancelSleep()
|
||||
{
|
||||
try
|
||||
lock (_lock)
|
||||
{
|
||||
// 取消休眠,如果存在错误立即抛出
|
||||
_sleepCancellationTokenSource.Cancel(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 输出非任务取消异常日志
|
||||
if (!(ex is TaskCanceledException || (ex is AggregateException aggEx && aggEx.InnerExceptions.Count == 1 && aggEx.InnerExceptions[0] is TaskCanceledException)))
|
||||
try
|
||||
{
|
||||
_logger.LogError(ex, ex.Message);
|
||||
// 取消休眠,如果存在错误立即抛出
|
||||
_sleepCancellationTokenSource.Cancel(true);
|
||||
}
|
||||
// 非任务取消异常日志
|
||||
catch (Exception ex) when (!(ex is OperationCanceledException ||
|
||||
ex is ObjectDisposedException ||
|
||||
(ex is AggregateException aggEx && aggEx.InnerExceptions.All(e => e is OperationCanceledException || e is ObjectDisposedException))))
|
||||
{
|
||||
_logger.LogError(ex, $"Error canceling sleep. {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 重新初始化作业调度器取消休眠 Token
|
||||
CreateCancellationTokenSource();
|
||||
}
|
||||
|
||||
// 重新初始化作业调度器取消休眠 Token
|
||||
CreateCancellationTokenSource();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -380,7 +380,7 @@ internal sealed class ScheduleHostedService : BackgroundService
|
||||
// 记录作业触发器运行信息
|
||||
await trigger.RecordTimelineAsync(_schedulerFactory, jobId, executionException?.ToString()).ConfigureAwait(false);
|
||||
|
||||
// 重置触发模式:0:定时,1:手动
|
||||
// 重置触发模式:0:定时,1:手动
|
||||
trigger.Mode = 0;
|
||||
|
||||
// 处理临时作业,执行完成后移除
|
||||
|
@@ -194,6 +194,6 @@ public partial class Trigger
|
||||
/// <summary>
|
||||
/// 触发模式
|
||||
/// </summary>
|
||||
/// <remarks>默认为定时触发,0:定时,1:手动</remarks>
|
||||
/// <remarks>默认为定时触发,0:定时,1:手动</remarks>
|
||||
internal int Mode { get; set; }
|
||||
}
|
@@ -76,7 +76,7 @@ public sealed class TriggerTimeline : IDisposable
|
||||
/// <summary>
|
||||
/// 触发模式
|
||||
/// </summary>
|
||||
/// <remarks>默认为定时触发,0:定时,1:手动</remarks>
|
||||
/// <remarks>默认为定时触发,0:定时,1:手动</remarks>
|
||||
[JsonInclude]
|
||||
public int Mode { get; internal set; }
|
||||
|
||||
|
@@ -0,0 +1,93 @@
|
||||
// Copyright (c) Ben A Adams. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Reflection;
|
||||
|
||||
namespace System.Diagnostics
|
||||
{
|
||||
public class EnhancedStackFrame : StackFrame
|
||||
{
|
||||
private readonly string? _fileName;
|
||||
private readonly int _lineNumber;
|
||||
private readonly int _colNumber;
|
||||
|
||||
public StackFrame StackFrame { get; }
|
||||
|
||||
public bool IsRecursive
|
||||
{
|
||||
get => MethodInfo.RecurseCount > 0;
|
||||
internal set => MethodInfo.RecurseCount++;
|
||||
}
|
||||
|
||||
public ResolvedMethod MethodInfo { get; }
|
||||
|
||||
internal EnhancedStackFrame(StackFrame stackFrame, ResolvedMethod methodInfo, string? fileName, int lineNumber, int colNumber)
|
||||
: base(fileName, lineNumber, colNumber)
|
||||
{
|
||||
StackFrame = stackFrame;
|
||||
MethodInfo = methodInfo;
|
||||
|
||||
_fileName = fileName;
|
||||
_lineNumber = lineNumber;
|
||||
_colNumber = colNumber;
|
||||
}
|
||||
|
||||
internal bool IsEquivalent(ResolvedMethod methodInfo, string? fileName, int lineNumber, int colNumber)
|
||||
{
|
||||
return _lineNumber == lineNumber &&
|
||||
_colNumber == colNumber &&
|
||||
_fileName == fileName &&
|
||||
MethodInfo.IsSequentialEquivalent(methodInfo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the column number in the file that contains the code that is executing.
|
||||
/// This information is typically extracted from the debugging symbols for the executable.
|
||||
/// </summary>
|
||||
/// <returns>The file column number, or 0 (zero) if the file column number cannot be determined.</returns>
|
||||
public override int GetFileColumnNumber() => _colNumber;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the line number in the file that contains the code that is executing.
|
||||
/// This information is typically extracted from the debugging symbols for the executable.
|
||||
/// </summary>
|
||||
/// <returns>The file line number, or 0 (zero) if the file line number cannot be determined.</returns>
|
||||
public override int GetFileLineNumber() => _lineNumber;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file name that contains the code that is executing.
|
||||
/// This information is typically extracted from the debugging symbols for the executable.
|
||||
/// </summary>
|
||||
/// <returns>The file name, or null if the file name cannot be determined.</returns>
|
||||
public override string? GetFileName() => _fileName;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the offset from the start of the Microsoft intermediate language (MSIL)
|
||||
/// code for the method that is executing. This offset might be an approximation
|
||||
/// depending on whether or not the just-in-time (JIT) compiler is generating debugging
|
||||
/// code. The generation of this debugging information is controlled by the System.Diagnostics.DebuggableAttribute.
|
||||
/// </summary>
|
||||
/// <returns>The offset from the start of the MSIL code for the method that is executing.</returns>
|
||||
public override int GetILOffset() => StackFrame.GetILOffset();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the method in which the frame is executing.
|
||||
/// </summary>
|
||||
/// <returns>The method in which the frame is executing.</returns>
|
||||
public override MethodBase? GetMethod() => StackFrame.GetMethod();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the offset from the start of the native just-in-time (JIT)-compiled code
|
||||
/// for the method that is being executed. The generation of this debugging information
|
||||
/// is controlled by the System.Diagnostics.DebuggableAttribute class.
|
||||
/// </summary>
|
||||
/// <returns>The offset from the start of the JIT-compiled code for the method that is being executed.</returns>
|
||||
public override int GetNativeOffset() => StackFrame.GetNativeOffset();
|
||||
|
||||
/// <summary>
|
||||
/// Builds a readable representation of the stack trace.
|
||||
/// </summary>
|
||||
/// <returns>A readable representation of the stack trace.</returns>
|
||||
public override string ToString() => MethodInfo.ToString();
|
||||
}
|
||||
}
|
@@ -0,0 +1,944 @@
|
||||
// Copyright (c) Ben A Adams. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic.Enumerable;
|
||||
using System.Diagnostics.Internal;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace System.Diagnostics
|
||||
{
|
||||
public partial class EnhancedStackTrace
|
||||
{
|
||||
private static readonly Type? StackTraceHiddenAttributeType = Type.GetType("System.Diagnostics.StackTraceHiddenAttribute", false);
|
||||
private static readonly Type? AsyncIteratorStateMachineAttributeType = Type.GetType("System.Runtime.CompilerServices.AsyncIteratorStateMachineAttribute", false);
|
||||
|
||||
static EnhancedStackTrace()
|
||||
{
|
||||
if (AsyncIteratorStateMachineAttributeType != null) return;
|
||||
|
||||
Assembly mba;
|
||||
try
|
||||
{
|
||||
mba = Assembly.Load("Microsoft.Bcl.AsyncInterfaces");
|
||||
}
|
||||
catch
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncIteratorStateMachineAttributeType = mba.GetType("System.Runtime.CompilerServices.AsyncIteratorStateMachineAttribute", false);
|
||||
}
|
||||
|
||||
private static List<EnhancedStackFrame> GetFrames(Exception exception)
|
||||
{
|
||||
if (exception == null)
|
||||
{
|
||||
return new List<EnhancedStackFrame>();
|
||||
}
|
||||
|
||||
var needFileInfo = true;
|
||||
var stackTrace = new StackTrace(exception, needFileInfo);
|
||||
|
||||
return GetFrames(stackTrace);
|
||||
}
|
||||
|
||||
public static List<EnhancedStackFrame> GetFrames(StackTrace stackTrace)
|
||||
{
|
||||
var frames = new List<EnhancedStackFrame>();
|
||||
var stackFrames = stackTrace.GetFrames();
|
||||
|
||||
if (stackFrames == null)
|
||||
{
|
||||
return frames;
|
||||
}
|
||||
|
||||
EnhancedStackFrame? lastFrame = null;
|
||||
PortablePdbReader? portablePdbReader = null;
|
||||
try
|
||||
{
|
||||
for (var i = 0; i < stackFrames.Length; i++)
|
||||
{
|
||||
var frame = stackFrames[i];
|
||||
if (frame is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var method = frame.GetMethod();
|
||||
|
||||
// Always show last stackFrame
|
||||
if (method != null && !ShowInStackTrace(method) && i < stackFrames.Length - 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var fileName = frame.GetFileName();
|
||||
var row = frame.GetFileLineNumber();
|
||||
var column = frame.GetFileColumnNumber();
|
||||
var ilOffset = frame.GetILOffset();
|
||||
if (method != null && string.IsNullOrEmpty(fileName) && ilOffset >= 0)
|
||||
{
|
||||
// .NET Framework and older versions of mono don't support portable PDBs
|
||||
// so we read it manually to get file name and line information
|
||||
(portablePdbReader ??= new PortablePdbReader()).PopulateStackFrame(frame, method, frame.GetILOffset(), out fileName, out row, out column);
|
||||
}
|
||||
|
||||
if (method is null)
|
||||
{
|
||||
// Method can't be null
|
||||
continue;
|
||||
}
|
||||
|
||||
var resolvedMethod = GetMethodDisplayString(method);
|
||||
if (lastFrame?.IsEquivalent(resolvedMethod, fileName, row, column) ?? false)
|
||||
{
|
||||
lastFrame.IsRecursive = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
var stackFrame = new EnhancedStackFrame(frame, resolvedMethod, fileName, row, column);
|
||||
frames.Add(stackFrame);
|
||||
lastFrame = stackFrame;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
portablePdbReader?.Dispose();
|
||||
}
|
||||
|
||||
return frames;
|
||||
}
|
||||
|
||||
public static ResolvedMethod GetMethodDisplayString(MethodBase originMethod)
|
||||
{
|
||||
var method = originMethod;
|
||||
|
||||
var methodDisplayInfo = new ResolvedMethod
|
||||
{
|
||||
SubMethodBase = method
|
||||
};
|
||||
|
||||
// Type name
|
||||
var type = method.DeclaringType;
|
||||
|
||||
var subMethodName = method.Name;
|
||||
var methodName = method.Name;
|
||||
|
||||
var isAsyncStateMachine = typeof(IAsyncStateMachine).IsAssignableFrom(type);
|
||||
if (isAsyncStateMachine || typeof(IEnumerator).IsAssignableFrom(type))
|
||||
{
|
||||
methodDisplayInfo.IsAsync = isAsyncStateMachine;
|
||||
|
||||
// Convert StateMachine methods to correct overload +MoveNext()
|
||||
if (!TryResolveStateMachineMethod(ref method, out type))
|
||||
{
|
||||
methodDisplayInfo.SubMethodBase = null;
|
||||
subMethodName = null;
|
||||
}
|
||||
|
||||
methodName = method.Name;
|
||||
}
|
||||
else if (IsFSharpAsync(method))
|
||||
{
|
||||
methodDisplayInfo.IsAsync = true;
|
||||
methodDisplayInfo.SubMethodBase = null;
|
||||
subMethodName = null;
|
||||
methodName = null;
|
||||
}
|
||||
|
||||
// Method name
|
||||
methodDisplayInfo.MethodBase = method;
|
||||
methodDisplayInfo.Name = methodName;
|
||||
if (method.Name.IndexOf('<') >= 0)
|
||||
{
|
||||
if (TryResolveGeneratedName(ref method, out type, out methodName, out subMethodName, out var kind, out var ordinal))
|
||||
{
|
||||
methodName = method.Name;
|
||||
methodDisplayInfo.MethodBase = method;
|
||||
methodDisplayInfo.Name = methodName;
|
||||
methodDisplayInfo.Ordinal = ordinal;
|
||||
}
|
||||
else
|
||||
{
|
||||
methodDisplayInfo.MethodBase = null;
|
||||
}
|
||||
|
||||
methodDisplayInfo.IsLambda = (kind == GeneratedNameKind.LambdaMethod);
|
||||
|
||||
if (methodDisplayInfo.IsLambda && type != null)
|
||||
{
|
||||
if (methodName == ".cctor")
|
||||
{
|
||||
if (type.IsGenericTypeDefinition && !type.IsConstructedGenericType)
|
||||
{
|
||||
// TODO: diagnose type's generic type arguments from frame's "this" or something
|
||||
}
|
||||
else
|
||||
{
|
||||
var fields = type.GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
foreach (var field in fields)
|
||||
{
|
||||
var value = field.GetValue(field);
|
||||
if (value is Delegate d && d.Target is not null)
|
||||
{
|
||||
if (ReferenceEquals(d.Method, originMethod) &&
|
||||
d.Target.ToString() == originMethod.DeclaringType?.ToString())
|
||||
{
|
||||
methodDisplayInfo.Name = field.Name;
|
||||
methodDisplayInfo.IsLambda = false;
|
||||
method = originMethod;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (subMethodName != methodName)
|
||||
{
|
||||
methodDisplayInfo.SubMethod = subMethodName;
|
||||
}
|
||||
|
||||
// ResolveStateMachineMethod may have set declaringType to null
|
||||
if (type != null)
|
||||
{
|
||||
methodDisplayInfo.DeclaringType = type;
|
||||
}
|
||||
|
||||
if (method is MethodInfo mi)
|
||||
{
|
||||
var returnParameter = mi.ReturnParameter;
|
||||
if (returnParameter != null)
|
||||
{
|
||||
methodDisplayInfo.ReturnParameter = GetParameter(mi.ReturnParameter);
|
||||
}
|
||||
else if (mi.ReturnType != null)
|
||||
{
|
||||
methodDisplayInfo.ReturnParameter = new ResolvedParameter(mi.ReturnType)
|
||||
{
|
||||
Prefix = "",
|
||||
Name = "",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (method.IsGenericMethod)
|
||||
{
|
||||
var genericArguments = method.GetGenericArguments();
|
||||
var genericArgumentsString = string.Join(", ", genericArguments
|
||||
.Select(arg => TypeNameHelper.GetTypeDisplayName(arg, fullName: false, includeGenericParameterNames: true)));
|
||||
methodDisplayInfo.GenericArguments += "<" + genericArgumentsString + ">";
|
||||
methodDisplayInfo.ResolvedGenericArguments = genericArguments;
|
||||
}
|
||||
|
||||
// Method parameters
|
||||
var parameters = method.GetParameters();
|
||||
if (parameters.Length > 0)
|
||||
{
|
||||
var parameterList = new List<ResolvedParameter>(parameters.Length);
|
||||
foreach (var parameter in parameters)
|
||||
{
|
||||
parameterList.Add(GetParameter(parameter));
|
||||
}
|
||||
|
||||
methodDisplayInfo.Parameters = parameterList;
|
||||
}
|
||||
|
||||
if (methodDisplayInfo.SubMethodBase == methodDisplayInfo.MethodBase)
|
||||
{
|
||||
methodDisplayInfo.SubMethodBase = null;
|
||||
}
|
||||
else if (methodDisplayInfo.SubMethodBase != null)
|
||||
{
|
||||
parameters = methodDisplayInfo.SubMethodBase.GetParameters();
|
||||
if (parameters.Length > 0)
|
||||
{
|
||||
var parameterList = new List<ResolvedParameter>(parameters.Length);
|
||||
foreach (var parameter in parameters)
|
||||
{
|
||||
var param = GetParameter(parameter);
|
||||
if (param.Name?.StartsWith('<') ?? true) continue;
|
||||
|
||||
parameterList.Add(param);
|
||||
}
|
||||
|
||||
methodDisplayInfo.SubMethodParameters = parameterList;
|
||||
}
|
||||
}
|
||||
|
||||
return methodDisplayInfo;
|
||||
}
|
||||
|
||||
private static bool IsFSharpAsync(MethodBase method)
|
||||
{
|
||||
if (method is MethodInfo minfo)
|
||||
{
|
||||
var returnType = minfo.ReturnType;
|
||||
if (returnType.Namespace == "Microsoft.FSharp.Control" && returnType.Name == "FSharpAsync`1")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryResolveGeneratedName(ref MethodBase method, out Type? type, out string methodName, out string? subMethodName, out GeneratedNameKind kind, out int? ordinal)
|
||||
{
|
||||
kind = GeneratedNameKind.None;
|
||||
type = method.DeclaringType;
|
||||
subMethodName = null;
|
||||
ordinal = null;
|
||||
methodName = method.Name;
|
||||
|
||||
var generatedName = methodName;
|
||||
|
||||
if (!TryParseGeneratedName(generatedName, out kind, out var openBracketOffset, out var closeBracketOffset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
methodName = generatedName.Substring(openBracketOffset + 1, closeBracketOffset - openBracketOffset - 1);
|
||||
|
||||
switch (kind)
|
||||
{
|
||||
case GeneratedNameKind.LocalFunction:
|
||||
{
|
||||
var localNameStart = generatedName.IndexOf((char)kind, closeBracketOffset + 1);
|
||||
if (localNameStart < 0) break;
|
||||
localNameStart += 3;
|
||||
|
||||
if (localNameStart < generatedName.Length)
|
||||
{
|
||||
var localNameEnd = generatedName.IndexOf('|', localNameStart);
|
||||
if (localNameEnd > 0)
|
||||
{
|
||||
subMethodName = generatedName.Substring(localNameStart, localNameEnd - localNameStart);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case GeneratedNameKind.LambdaMethod:
|
||||
subMethodName = "";
|
||||
break;
|
||||
}
|
||||
|
||||
var dt = method.DeclaringType;
|
||||
if (dt == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var matchHint = GetMatchHint(kind, method);
|
||||
|
||||
var matchName = methodName;
|
||||
|
||||
var candidateMethods = dt.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly).Where(m => m.Name == matchName);
|
||||
if (TryResolveSourceMethod(candidateMethods, kind, matchHint, ref method, ref type, out ordinal)) return true;
|
||||
|
||||
var candidateConstructors = dt.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly).Where(m => m.Name == matchName);
|
||||
if (TryResolveSourceMethod(candidateConstructors, kind, matchHint, ref method, ref type, out ordinal)) return true;
|
||||
|
||||
const int MaxResolveDepth = 10;
|
||||
for (var i = 0; i < MaxResolveDepth; i++)
|
||||
{
|
||||
dt = dt.DeclaringType;
|
||||
if (dt == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
candidateMethods = dt.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly).Where(m => m.Name == matchName);
|
||||
if (TryResolveSourceMethod(candidateMethods, kind, matchHint, ref method, ref type, out ordinal)) return true;
|
||||
|
||||
candidateConstructors = dt.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly).Where(m => m.Name == matchName);
|
||||
if (TryResolveSourceMethod(candidateConstructors, kind, matchHint, ref method, ref type, out ordinal)) return true;
|
||||
|
||||
if (methodName == ".cctor")
|
||||
{
|
||||
candidateConstructors = dt.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.DeclaredOnly).Where(m => m.Name == matchName);
|
||||
foreach (var cctor in candidateConstructors)
|
||||
{
|
||||
method = cctor;
|
||||
type = dt;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryResolveSourceMethod(IEnumerable<MethodBase> candidateMethods, GeneratedNameKind kind, string? matchHint, ref MethodBase method, ref Type? type, out int? ordinal)
|
||||
{
|
||||
ordinal = null;
|
||||
foreach (var candidateMethod in candidateMethods)
|
||||
{
|
||||
if (candidateMethod.GetMethodBody() is not { } methodBody)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (kind == GeneratedNameKind.LambdaMethod)
|
||||
{
|
||||
foreach (var v in EnumerableIList.Create(methodBody.LocalVariables))
|
||||
{
|
||||
if (v.LocalType == type)
|
||||
{
|
||||
GetOrdinal(method, ref ordinal);
|
||||
|
||||
}
|
||||
method = candidateMethod;
|
||||
type = method.DeclaringType;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var rawIl = methodBody.GetILAsByteArray();
|
||||
if (rawIl is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var reader = new ILReader(rawIl);
|
||||
while (reader.Read(candidateMethod))
|
||||
{
|
||||
if (reader.Operand is MethodBase mb)
|
||||
{
|
||||
if (method == mb || matchHint != null && method.Name.Contains(matchHint))
|
||||
{
|
||||
if (kind == GeneratedNameKind.LambdaMethod)
|
||||
{
|
||||
GetOrdinal(method, ref ordinal);
|
||||
}
|
||||
|
||||
method = candidateMethod;
|
||||
type = method.DeclaringType;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// https://github.com/benaadams/Ben.Demystifier/issues/32
|
||||
// Skip methods where il can't be interpreted
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void GetOrdinal(MethodBase method, ref int? ordinal)
|
||||
{
|
||||
var lamdaStart = method.Name.IndexOf((char)GeneratedNameKind.LambdaMethod + "__") + 3;
|
||||
if (lamdaStart > 3)
|
||||
{
|
||||
var secondStart = method.Name.IndexOf('_', lamdaStart) + 1;
|
||||
if (secondStart > 0)
|
||||
{
|
||||
lamdaStart = secondStart;
|
||||
}
|
||||
|
||||
if (!int.TryParse(method.Name.AsSpan(lamdaStart), out var foundOrdinal))
|
||||
{
|
||||
ordinal = null;
|
||||
return;
|
||||
}
|
||||
|
||||
ordinal = foundOrdinal;
|
||||
|
||||
var methods = method.DeclaringType?.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly);
|
||||
|
||||
var count = 0;
|
||||
if (methods != null)
|
||||
{
|
||||
var startName = method.Name.Substring(0, lamdaStart);
|
||||
foreach (var m in methods)
|
||||
{
|
||||
if (m.Name.Length > lamdaStart && m.Name.StartsWith(startName))
|
||||
{
|
||||
count++;
|
||||
|
||||
if (count > 1)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count <= 1)
|
||||
{
|
||||
ordinal = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static string? GetMatchHint(GeneratedNameKind kind, MethodBase method)
|
||||
{
|
||||
var methodName = method.Name;
|
||||
|
||||
switch (kind)
|
||||
{
|
||||
case GeneratedNameKind.LocalFunction:
|
||||
var start = methodName.IndexOf('|');
|
||||
if (start < 1) return null;
|
||||
var end = methodName.IndexOf('_', start) + 1;
|
||||
if (end <= start) return null;
|
||||
|
||||
return methodName.Substring(start, end - start);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parse the generated name. Returns true for names of the form
|
||||
// [CS$]<[middle]>c[__[suffix]] where [CS$] is included for certain
|
||||
// generated names, where [middle] and [__[suffix]] are optional,
|
||||
// and where c is a single character in [1-9a-z]
|
||||
// (csharp\LanguageAnalysis\LIB\SpecialName.cpp).
|
||||
internal static bool TryParseGeneratedName(
|
||||
string name,
|
||||
out GeneratedNameKind kind,
|
||||
out int openBracketOffset,
|
||||
out int closeBracketOffset)
|
||||
{
|
||||
openBracketOffset = -1;
|
||||
if (name.StartsWith("CS$<", StringComparison.Ordinal))
|
||||
{
|
||||
openBracketOffset = 3;
|
||||
}
|
||||
else if (name.StartsWith('<'))
|
||||
{
|
||||
openBracketOffset = 0;
|
||||
}
|
||||
|
||||
if (openBracketOffset >= 0)
|
||||
{
|
||||
closeBracketOffset = IndexOfBalancedParenthesis(name, openBracketOffset, '>');
|
||||
if (closeBracketOffset >= 0 && closeBracketOffset + 1 < name.Length)
|
||||
{
|
||||
int c = name[closeBracketOffset + 1];
|
||||
if ((c >= '1' && c <= '9') || (c >= 'a' && c <= 'z')) // Note '0' is not special.
|
||||
{
|
||||
kind = (GeneratedNameKind)c;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kind = GeneratedNameKind.None;
|
||||
openBracketOffset = -1;
|
||||
closeBracketOffset = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private static int IndexOfBalancedParenthesis(string str, int openingOffset, char closing)
|
||||
{
|
||||
var opening = str[openingOffset];
|
||||
|
||||
var depth = 1;
|
||||
for (var i = openingOffset + 1; i < str.Length; i++)
|
||||
{
|
||||
var c = str[i];
|
||||
if (c == opening)
|
||||
{
|
||||
depth++;
|
||||
}
|
||||
else if (c == closing)
|
||||
{
|
||||
depth--;
|
||||
if (depth == 0)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static string GetPrefix(ParameterInfo parameter)
|
||||
{
|
||||
if (Attribute.IsDefined(parameter, typeof(ParamArrayAttribute), false))
|
||||
{
|
||||
return "params";
|
||||
}
|
||||
|
||||
if (parameter.IsOut)
|
||||
{
|
||||
return "out";
|
||||
}
|
||||
|
||||
if (parameter.IsIn)
|
||||
{
|
||||
return "in";
|
||||
}
|
||||
|
||||
if (parameter.ParameterType.IsByRef)
|
||||
{
|
||||
return "ref";
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static ResolvedParameter GetParameter(ParameterInfo parameter)
|
||||
{
|
||||
var prefix = GetPrefix(parameter);
|
||||
var parameterType = parameter.ParameterType;
|
||||
|
||||
if (parameterType.IsGenericType)
|
||||
{
|
||||
var customAttribs = parameter.GetCustomAttributes(inherit: false);
|
||||
|
||||
var tupleNameAttribute = customAttribs.OfType<Attribute>().FirstOrDefault(a => a.IsTupleElementNameAttribute());
|
||||
|
||||
var tupleNames = tupleNameAttribute?.GetTransformerNames();
|
||||
|
||||
if (tupleNames?.Count > 0)
|
||||
{
|
||||
return GetValueTupleParameter(tupleNames, prefix, parameter.Name, parameterType);
|
||||
}
|
||||
}
|
||||
|
||||
if (parameterType.IsByRef && parameterType.GetElementType() is {} elementType)
|
||||
{
|
||||
parameterType = elementType;
|
||||
}
|
||||
|
||||
return new ResolvedParameter(parameterType)
|
||||
{
|
||||
Prefix = prefix,
|
||||
Name = parameter.Name,
|
||||
IsDynamicType = parameter.IsDefined(typeof(DynamicAttribute), false)
|
||||
};
|
||||
}
|
||||
|
||||
private static ValueTupleResolvedParameter GetValueTupleParameter(IList<string> tupleNames, string prefix, string? name, Type parameterType)
|
||||
{
|
||||
return new ValueTupleResolvedParameter(parameterType, tupleNames)
|
||||
{
|
||||
Prefix = prefix,
|
||||
Name = name,
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetValueTupleParameterName(IList<string> tupleNames, Type parameterType)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append('(');
|
||||
var args = parameterType.GetGenericArguments();
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
sb.Append(", ");
|
||||
}
|
||||
|
||||
sb.Append(TypeNameHelper.GetTypeDisplayName(args[i], fullName: false, includeGenericParameterNames: true));
|
||||
|
||||
if (i >= tupleNames.Count)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var argName = tupleNames[i];
|
||||
if (argName == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
sb.Append(' ');
|
||||
sb.Append(argName);
|
||||
}
|
||||
|
||||
sb.Append(')');
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static bool ShowInStackTrace(MethodBase method)
|
||||
{
|
||||
// Since .NET 5:
|
||||
// https://github.com/dotnet/runtime/blob/7c18d4d6488dab82124d475d1199def01d1d252c/src/libraries/System.Private.CoreLib/src/System/Diagnostics/StackTrace.cs#L348-L361
|
||||
if ((method.MethodImplementationFlags & MethodImplAttributes.AggressiveInlining) != 0)
|
||||
{
|
||||
// Aggressive Inlines won't normally show in the StackTrace; however for Tier0 Jit and
|
||||
// cross-assembly AoT/R2R these inlines will be blocked until Tier1 Jit re-Jits
|
||||
// them when they will inline. We don't show them in the StackTrace to bring consistency
|
||||
// between this first-pass asm and fully optimized asm.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Since .NET Core 2:
|
||||
if (StackTraceHiddenAttributeType != null)
|
||||
{
|
||||
// Don't show any methods marked with the StackTraceHiddenAttribute
|
||||
// https://github.com/dotnet/coreclr/pull/14652
|
||||
if (IsStackTraceHidden(method))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var type = method.DeclaringType;
|
||||
|
||||
if (type == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Since .NET Core 2:
|
||||
if (StackTraceHiddenAttributeType != null)
|
||||
{
|
||||
// Don't show any methods marked with the StackTraceHiddenAttribute
|
||||
// https://github.com/dotnet/coreclr/pull/14652
|
||||
if (IsStackTraceHidden(type))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (type == typeof(Task<>) && method.Name == "InnerInvoke")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (type == typeof(ValueTask<>) && method.Name == "get_Result")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (method.Name.StartsWith("System.Threading.Tasks.Sources.IValueTaskSource") && method.Name.EndsWith(".GetResult"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (method.Name == "GetResult" && method.DeclaringType?.FullName == "System.Threading.Tasks.Sources.ManualResetValueTaskSourceCore`1")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (type == typeof(Task) || type.DeclaringType == typeof(Task))
|
||||
{
|
||||
if (method.Name.Contains(".cctor"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (method.Name)
|
||||
{
|
||||
case "ExecuteWithThreadLocal":
|
||||
case "Execute":
|
||||
case "ExecutionContextCallback":
|
||||
case "ExecuteEntry":
|
||||
case "InnerInvoke":
|
||||
case "ExecuteEntryUnsafe":
|
||||
case "ExecuteFromThreadPool":
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (type == typeof(ExecutionContext))
|
||||
{
|
||||
if (method.Name.Contains(".cctor"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (method.Name)
|
||||
{
|
||||
case "RunInternal":
|
||||
case "Run":
|
||||
case "RunFromThreadPoolDispatchLoop":
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (type.Namespace == "Microsoft.FSharp.Control")
|
||||
{
|
||||
switch (type.Name)
|
||||
{
|
||||
case "AsyncPrimitives":
|
||||
case "Trampoline":
|
||||
return false;
|
||||
case var typeName when type.IsGenericType:
|
||||
{
|
||||
if (typeName == "AsyncResult`1") return false;
|
||||
else break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (type.Namespace == "Ply")
|
||||
{
|
||||
if (type.DeclaringType?.Name == "TplPrimitives")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallbacks for runtime pre-StackTraceHiddenAttribute
|
||||
if (type == typeof(ExceptionDispatchInfo) && method.Name == "Throw")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (type == typeof(TaskAwaiter) ||
|
||||
type == typeof(TaskAwaiter<>) ||
|
||||
type == typeof(ValueTaskAwaiter) ||
|
||||
type == typeof(ValueTaskAwaiter<>) ||
|
||||
type == typeof(ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter) ||
|
||||
type == typeof(ConfiguredValueTaskAwaitable<>.ConfiguredValueTaskAwaiter) ||
|
||||
type == typeof(ConfiguredTaskAwaitable.ConfiguredTaskAwaiter) ||
|
||||
type == typeof(ConfiguredTaskAwaitable<>.ConfiguredTaskAwaiter))
|
||||
{
|
||||
switch (method.Name)
|
||||
{
|
||||
case "HandleNonSuccessAndDebuggerNotification":
|
||||
case "ThrowForNonSuccess":
|
||||
case "ValidateEnd":
|
||||
case "GetResult":
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (type.FullName == "System.ThrowHelper")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsStackTraceHidden(MemberInfo memberInfo)
|
||||
{
|
||||
if (StackTraceHiddenAttributeType is not null && !memberInfo.Module.Assembly.ReflectionOnly)
|
||||
{
|
||||
return memberInfo.GetCustomAttributes(StackTraceHiddenAttributeType, false).Length != 0;
|
||||
}
|
||||
|
||||
EnumerableIList<CustomAttributeData> attributes;
|
||||
try
|
||||
{
|
||||
attributes = EnumerableIList.Create(memberInfo.GetCustomAttributesData());
|
||||
}
|
||||
catch (NotImplementedException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var attribute in attributes)
|
||||
{
|
||||
// reflection-only attribute, match on name
|
||||
if (attribute.AttributeType.FullName == StackTraceHiddenAttributeType?.FullName)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// https://github.com/dotnet/runtime/blob/c985bdcec2a9190e733bcada413a193d5ff60c0d/src/libraries/System.Private.CoreLib/src/System/Diagnostics/StackTrace.cs#L375-L430
|
||||
private static bool TryResolveStateMachineMethod(ref MethodBase method, out Type declaringType)
|
||||
{
|
||||
if (method.DeclaringType is null)
|
||||
{
|
||||
declaringType = null!;
|
||||
return false;
|
||||
}
|
||||
declaringType = method.DeclaringType;
|
||||
|
||||
var parentType = declaringType.DeclaringType;
|
||||
if (parentType is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
static MethodInfo[] GetDeclaredMethods(Type type) =>
|
||||
type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly);
|
||||
|
||||
var methods = GetDeclaredMethods(parentType);
|
||||
if (methods == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var candidateMethod in methods)
|
||||
{
|
||||
var attributes = candidateMethod.GetCustomAttributes<StateMachineAttribute>(inherit: false);
|
||||
// ReSharper disable once ConditionIsAlwaysTrueOrFalse - Taken from CoreFX
|
||||
if (attributes is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bool foundAttribute = false, foundIteratorAttribute = false;
|
||||
foreach (var asma in attributes)
|
||||
{
|
||||
if (asma.StateMachineType == declaringType)
|
||||
{
|
||||
foundAttribute = true;
|
||||
foundIteratorAttribute |= asma is IteratorStateMachineAttribute
|
||||
|| AsyncIteratorStateMachineAttributeType?.IsInstanceOfType(asma) == true;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundAttribute)
|
||||
{
|
||||
// If this is an iterator (sync or async), mark the iterator as changed, so it gets the + annotation
|
||||
// of the original method. Non-iterator async state machines resolve directly to their builder methods
|
||||
// so aren't marked as changed.
|
||||
method = candidateMethod;
|
||||
declaringType = candidateMethod.DeclaringType!;
|
||||
return foundIteratorAttribute;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
internal enum GeneratedNameKind
|
||||
{
|
||||
None = 0,
|
||||
|
||||
// Used by EE:
|
||||
ThisProxyField = '4',
|
||||
HoistedLocalField = '5',
|
||||
DisplayClassLocalOrField = '8',
|
||||
LambdaMethod = 'b',
|
||||
LambdaDisplayClass = 'c',
|
||||
StateMachineType = 'd',
|
||||
LocalFunction = 'g', // note collision with Deprecated_InitializerLocal, however this one is only used for method names
|
||||
|
||||
// Used by EnC:
|
||||
AwaiterField = 'u',
|
||||
HoistedSynthesizedLocalField = 's',
|
||||
|
||||
// Currently not parsed:
|
||||
StateMachineStateField = '1',
|
||||
IteratorCurrentBackingField = '2',
|
||||
StateMachineParameterProxyField = '3',
|
||||
ReusableHoistedLocalField = '7',
|
||||
LambdaCacheField = '9',
|
||||
FixedBufferField = 'e',
|
||||
AnonymousType = 'f',
|
||||
TransparentIdentifier = 'h',
|
||||
AnonymousTypeField = 'i',
|
||||
AutoPropertyBackingField = 'k',
|
||||
IteratorCurrentThreadIdField = 'l',
|
||||
IteratorFinallyMethod = 'm',
|
||||
BaseMethodWrapper = 'n',
|
||||
AsyncBuilderField = 't',
|
||||
DynamicCallSiteContainerType = 'o',
|
||||
DynamicCallSiteField = 'p'
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,141 @@
|
||||
// Copyright (c) Ben A Adams. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic.Enumerable;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace System.Diagnostics
|
||||
{
|
||||
public partial class EnhancedStackTrace : StackTrace, IEnumerable<EnhancedStackFrame>
|
||||
{
|
||||
public static EnhancedStackTrace Current() => new EnhancedStackTrace(new StackTrace(1 /* skip this one frame */, true));
|
||||
|
||||
private readonly List<EnhancedStackFrame> _frames;
|
||||
|
||||
// Summary:
|
||||
// Initializes a new instance of the System.Diagnostics.StackTrace class using the
|
||||
// provided exception object.
|
||||
//
|
||||
// Parameters:
|
||||
// e:
|
||||
// The exception object from which to construct the stack trace.
|
||||
//
|
||||
// Exceptions:
|
||||
// T:System.ArgumentNullException:
|
||||
// The parameter e is null.
|
||||
public EnhancedStackTrace(Exception e)
|
||||
{
|
||||
if (e == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(e));
|
||||
}
|
||||
|
||||
_frames = GetFrames(e);
|
||||
}
|
||||
|
||||
|
||||
public EnhancedStackTrace(StackTrace stackTrace)
|
||||
{
|
||||
if (stackTrace == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(stackTrace));
|
||||
}
|
||||
|
||||
_frames = GetFrames(stackTrace);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of frames in the stack trace.
|
||||
/// </summary>
|
||||
/// <returns>The number of frames in the stack trace.</returns>
|
||||
public override int FrameCount => _frames.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the specified stack frame.
|
||||
/// </summary>
|
||||
/// <param name="index">The index of the stack frame requested.</param>
|
||||
/// <returns>The specified stack frame.</returns>
|
||||
public override StackFrame GetFrame(int index) => _frames[index];
|
||||
|
||||
/// <summary>
|
||||
/// Returns a copy of all stack frames in the current stack trace.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// An array of type System.Diagnostics.StackFrame representing the function calls
|
||||
/// in the stack trace.
|
||||
/// </returns>
|
||||
public override StackFrame[] GetFrames() => _frames.ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// Builds a readable representation of the stack trace.
|
||||
/// </summary>
|
||||
/// <returns>A readable representation of the stack trace.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
if (_frames == null || _frames.Count == 0) return "";
|
||||
|
||||
var sb = new StringBuilder();
|
||||
|
||||
Append(sb);
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
|
||||
internal void Append(StringBuilder sb)
|
||||
{
|
||||
var frames = _frames;
|
||||
var count = frames.Count;
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
sb.Append(Environment.NewLine);
|
||||
}
|
||||
|
||||
var frame = frames[i];
|
||||
|
||||
sb.Append(" at ");
|
||||
frame.MethodInfo.Append(sb);
|
||||
|
||||
if (frame.GetFileName() is {} fileName
|
||||
// IsNullOrEmpty alone wasn't enough to disable the null warning
|
||||
&& !string.IsNullOrEmpty(fileName))
|
||||
{
|
||||
sb.Append(" in ");
|
||||
sb.Append(TryGetFullPath(fileName));
|
||||
|
||||
}
|
||||
|
||||
var lineNo = frame.GetFileLineNumber();
|
||||
if (lineNo != 0)
|
||||
{
|
||||
sb.Append(":line ");
|
||||
sb.Append(lineNo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EnumerableIList<EnhancedStackFrame> GetEnumerator() => EnumerableIList.Create(_frames);
|
||||
IEnumerator<EnhancedStackFrame> IEnumerable<EnhancedStackFrame>.GetEnumerator() => _frames.GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => _frames.GetEnumerator();
|
||||
|
||||
/// <summary>
|
||||
/// Tries to convert a given <paramref name="filePath"/> to a full path.
|
||||
/// Returns original value if the conversion isn't possible or a given path is relative.
|
||||
/// </summary>
|
||||
public static string TryGetFullPath(string filePath)
|
||||
{
|
||||
if (Uri.TryCreate(filePath, UriKind.Absolute, out var uri) && uri.IsFile)
|
||||
{
|
||||
return Uri.UnescapeDataString(uri.AbsolutePath);
|
||||
}
|
||||
|
||||
return filePath;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) Ben A Adams. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
namespace System.Collections.Generic.Enumerable
|
||||
{
|
||||
public static class EnumerableIList
|
||||
{
|
||||
public static EnumerableIList<T> Create<T>(IList<T> list) => new EnumerableIList<T>(list);
|
||||
}
|
||||
|
||||
public struct EnumerableIList<T> : IEnumerableIList<T>, IList<T>
|
||||
{
|
||||
private readonly IList<T> _list;
|
||||
|
||||
public EnumerableIList(IList<T> list) => _list = list;
|
||||
|
||||
public EnumeratorIList<T> GetEnumerator() => new EnumeratorIList<T>(_list);
|
||||
|
||||
public static implicit operator EnumerableIList<T>(List<T> list) => new EnumerableIList<T>(list);
|
||||
|
||||
public static implicit operator EnumerableIList<T>(T[] array) => new EnumerableIList<T>(array);
|
||||
|
||||
public static EnumerableIList<T> Empty = default;
|
||||
|
||||
|
||||
// IList pass through
|
||||
|
||||
/// <inheritdoc />
|
||||
public T this[int index] { get => _list[index]; set => _list[index] = value; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Count => _list.Count;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsReadOnly => _list.IsReadOnly;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Add(T item) => _list.Add(item);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Clear() => _list.Clear();
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Contains(T item) => _list.Contains(item);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CopyTo(T[] array, int arrayIndex) => _list.CopyTo(array, arrayIndex);
|
||||
|
||||
/// <inheritdoc />
|
||||
public int IndexOf(T item) => _list.IndexOf(item);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Insert(int index, T item) => _list.Insert(index, item);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Remove(T item) => _list.Remove(item);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RemoveAt(int index) => _list.RemoveAt(index);
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) Ben A Adams. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
namespace System.Collections.Generic.Enumerable
|
||||
{
|
||||
public struct EnumeratorIList<T> : IEnumerator<T>
|
||||
{
|
||||
private readonly IList<T> _list;
|
||||
private int _index;
|
||||
|
||||
public EnumeratorIList(IList<T> list)
|
||||
{
|
||||
_index = -1;
|
||||
_list = list;
|
||||
}
|
||||
|
||||
public T Current => _list[_index];
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
_index++;
|
||||
|
||||
return _index < (_list?.Count ?? 0);
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
object? IEnumerator.Current => Current;
|
||||
public void Reset() => _index = -1;
|
||||
}
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) Ben A Adams. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
namespace System.Collections.Generic.Enumerable
|
||||
{
|
||||
interface IEnumerableIList<T> : IEnumerable<T>
|
||||
{
|
||||
new EnumeratorIList<T> GetEnumerator();
|
||||
}
|
||||
}
|
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) Ben A Adams. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic.Enumerable;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace System.Diagnostics
|
||||
{
|
||||
public static class ExceptionExtensions
|
||||
{
|
||||
private static readonly FieldInfo? stackTraceString = typeof(Exception).GetField("_stackTraceString", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
|
||||
private static void SetStackTracesString(this Exception exception, string value)
|
||||
=> stackTraceString?.SetValue(exception, value);
|
||||
|
||||
/// <summary>
|
||||
/// Demystifies the given <paramref name="exception"/> and tracks the original stack traces for the whole exception tree.
|
||||
/// </summary>
|
||||
public static T Demystify<T>(this T exception) where T : Exception
|
||||
{
|
||||
try
|
||||
{
|
||||
var stackTrace = new EnhancedStackTrace(exception);
|
||||
|
||||
if (stackTrace.FrameCount > 0)
|
||||
{
|
||||
exception.SetStackTracesString(stackTrace.ToString());
|
||||
}
|
||||
|
||||
if (exception is AggregateException aggEx)
|
||||
{
|
||||
foreach (var ex in EnumerableIList.Create(aggEx.InnerExceptions))
|
||||
{
|
||||
ex.Demystify();
|
||||
}
|
||||
}
|
||||
|
||||
exception.InnerException?.Demystify();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Processing exceptions shouldn't throw exceptions; if it fails
|
||||
}
|
||||
|
||||
return exception;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets demystified string representation of the <paramref name="exception"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="Demystify{T}"/> method mutates the exception instance that can cause
|
||||
/// issues if a system relies on the stack trace be in the specific form.
|
||||
/// Unlike <see cref="Demystify{T}"/> this method is pure. It calls <see cref="Demystify{T}"/> first,
|
||||
/// computes a demystified string representation and then restores the original state of the exception back.
|
||||
/// </remarks>
|
||||
[Contracts.Pure]
|
||||
public static string ToStringDemystified(this Exception exception)
|
||||
=> new StringBuilder().AppendDemystified(exception).ToString();
|
||||
}
|
||||
}
|
@@ -0,0 +1,145 @@
|
||||
using System.Reflection;
|
||||
using System.Reflection.Emit;
|
||||
|
||||
namespace System.Diagnostics.Internal
|
||||
{
|
||||
internal class ILReader
|
||||
{
|
||||
private static OpCode[] singleByteOpCode;
|
||||
private static OpCode[] doubleByteOpCode;
|
||||
|
||||
private readonly byte[] _cil;
|
||||
private int ptr;
|
||||
|
||||
|
||||
public ILReader(byte[] cil) => _cil = cil;
|
||||
|
||||
public OpCode OpCode { get; private set; }
|
||||
public int MetadataToken { get; private set; }
|
||||
public MemberInfo? Operand { get; private set; }
|
||||
|
||||
public bool Read(MethodBase methodInfo)
|
||||
{
|
||||
if (ptr < _cil.Length)
|
||||
{
|
||||
OpCode = ReadOpCode();
|
||||
Operand = ReadOperand(OpCode, methodInfo);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
OpCode ReadOpCode()
|
||||
{
|
||||
var instruction = ReadByte();
|
||||
if (instruction < 254)
|
||||
return singleByteOpCode[instruction];
|
||||
else
|
||||
return doubleByteOpCode[ReadByte()];
|
||||
}
|
||||
|
||||
MemberInfo? ReadOperand(OpCode code, MethodBase methodInfo)
|
||||
{
|
||||
MetadataToken = 0;
|
||||
int inlineLength;
|
||||
switch (code.OperandType)
|
||||
{
|
||||
case OperandType.InlineMethod:
|
||||
MetadataToken = ReadInt();
|
||||
Type[]? methodArgs = null;
|
||||
if (methodInfo.GetType() != typeof(ConstructorInfo) && !methodInfo.GetType().IsSubclassOf(typeof(ConstructorInfo)))
|
||||
{
|
||||
methodArgs = methodInfo.GetGenericArguments();
|
||||
}
|
||||
Type[]? typeArgs = null;
|
||||
if (methodInfo.DeclaringType != null)
|
||||
{
|
||||
typeArgs = methodInfo.DeclaringType.GetGenericArguments();
|
||||
}
|
||||
try
|
||||
{
|
||||
return methodInfo.Module.ResolveMember(MetadataToken, typeArgs, methodArgs);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Can return System.ArgumentException : Token xxx is not a valid MemberInfo token in the scope of module xxx.dll
|
||||
return null;
|
||||
}
|
||||
|
||||
case OperandType.InlineNone:
|
||||
inlineLength = 0;
|
||||
break;
|
||||
|
||||
case OperandType.ShortInlineBrTarget:
|
||||
case OperandType.ShortInlineVar:
|
||||
case OperandType.ShortInlineI:
|
||||
inlineLength = 1;
|
||||
break;
|
||||
|
||||
case OperandType.InlineVar:
|
||||
inlineLength = 2;
|
||||
break;
|
||||
|
||||
case OperandType.InlineBrTarget:
|
||||
case OperandType.InlineField:
|
||||
case OperandType.InlineI:
|
||||
case OperandType.InlineString:
|
||||
case OperandType.InlineSig:
|
||||
case OperandType.InlineSwitch:
|
||||
case OperandType.InlineTok:
|
||||
case OperandType.InlineType:
|
||||
case OperandType.ShortInlineR:
|
||||
inlineLength = 4;
|
||||
break;
|
||||
|
||||
case OperandType.InlineI8:
|
||||
case OperandType.InlineR:
|
||||
inlineLength = 8;
|
||||
break;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
for (var i = 0; i < inlineLength; i++)
|
||||
{
|
||||
ReadByte();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
byte ReadByte() => _cil[ptr++];
|
||||
|
||||
int ReadInt()
|
||||
{
|
||||
var b1 = ReadByte();
|
||||
var b2 = ReadByte();
|
||||
var b3 = ReadByte();
|
||||
var b4 = ReadByte();
|
||||
return b1 | b2 << 8 | b3 << 16 | b4 << 24;
|
||||
}
|
||||
|
||||
static ILReader()
|
||||
{
|
||||
singleByteOpCode = new OpCode[225];
|
||||
doubleByteOpCode = new OpCode[31];
|
||||
|
||||
var fields = GetOpCodeFields();
|
||||
|
||||
for (var i = 0; i < fields.Length; i++)
|
||||
{
|
||||
var code = (OpCode)fields[i].GetValue(null)!;
|
||||
if (code.OpCodeType == OpCodeType.Nternal)
|
||||
continue;
|
||||
|
||||
if (code.Size == 1)
|
||||
singleByteOpCode[code.Value] = code;
|
||||
else
|
||||
doubleByteOpCode[code.Value & 0xff] = code;
|
||||
}
|
||||
}
|
||||
|
||||
static FieldInfo[] GetOpCodeFields() => typeof(OpCodes).GetFields(BindingFlags.Public | BindingFlags.Static);
|
||||
}
|
||||
}
|
@@ -0,0 +1,136 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Reflection.Metadata.Ecma335;
|
||||
using System.Reflection.PortableExecutable;
|
||||
|
||||
namespace System.Diagnostics.Internal
|
||||
{
|
||||
// Adapted from https://github.com/aspnet/Common/blob/dev/shared/Microsoft.Extensions.StackTrace.Sources/StackFrame/PortablePdbReader.cs
|
||||
internal class PortablePdbReader : IDisposable
|
||||
{
|
||||
private readonly Dictionary<string, MetadataReaderProvider> _cache =
|
||||
new Dictionary<string, MetadataReaderProvider>(StringComparer.Ordinal);
|
||||
|
||||
public void PopulateStackFrame(StackFrame frameInfo, MethodBase method, int IlOffset, out string fileName, out int row, out int column)
|
||||
{
|
||||
fileName = "";
|
||||
row = 0;
|
||||
column = 0;
|
||||
|
||||
if (method.Module.Assembly.IsDynamic)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var metadataReader = GetMetadataReader(method.Module.Assembly.Location);
|
||||
|
||||
if (metadataReader == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var methodToken = MetadataTokens.Handle(method.MetadataToken);
|
||||
|
||||
Debug.Assert(methodToken.Kind == HandleKind.MethodDefinition);
|
||||
|
||||
var handle = ((MethodDefinitionHandle)methodToken).ToDebugInformationHandle();
|
||||
|
||||
if (!handle.IsNil)
|
||||
{
|
||||
var methodDebugInfo = metadataReader.GetMethodDebugInformation(handle);
|
||||
var sequencePoints = methodDebugInfo.GetSequencePoints();
|
||||
SequencePoint? bestPointSoFar = null;
|
||||
|
||||
foreach (var point in sequencePoints)
|
||||
{
|
||||
if (point.Offset > IlOffset)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (point.StartLine != SequencePoint.HiddenLine)
|
||||
{
|
||||
bestPointSoFar = point;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestPointSoFar.HasValue)
|
||||
{
|
||||
row = bestPointSoFar.Value.StartLine;
|
||||
column = bestPointSoFar.Value.StartColumn;
|
||||
fileName = metadataReader.GetString(metadataReader.GetDocument(bestPointSoFar.Value.Document).Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private MetadataReader? GetMetadataReader(string assemblyPath)
|
||||
{
|
||||
if (!_cache.TryGetValue(assemblyPath, out var provider) && provider is not null)
|
||||
{
|
||||
var pdbPath = GetPdbPath(assemblyPath);
|
||||
|
||||
if (!string.IsNullOrEmpty(pdbPath) && File.Exists(pdbPath) && IsPortable(pdbPath!))
|
||||
{
|
||||
var pdbStream = File.OpenRead(pdbPath);
|
||||
provider = MetadataReaderProvider.FromPortablePdbStream(pdbStream);
|
||||
}
|
||||
|
||||
_cache[assemblyPath] = provider;
|
||||
}
|
||||
|
||||
return provider?.GetMetadataReader();
|
||||
}
|
||||
|
||||
private static string? GetPdbPath(string assemblyPath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(assemblyPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (File.Exists(assemblyPath))
|
||||
{
|
||||
var peStream = File.OpenRead(assemblyPath);
|
||||
|
||||
using var peReader = new PEReader(peStream);
|
||||
foreach (var entry in peReader.ReadDebugDirectory())
|
||||
{
|
||||
if (entry.Type == DebugDirectoryEntryType.CodeView)
|
||||
{
|
||||
var codeViewData = peReader.ReadCodeViewDebugDirectoryData(entry);
|
||||
var peDirectory = Path.GetDirectoryName(assemblyPath);
|
||||
return peDirectory is null
|
||||
? null
|
||||
: Path.Combine(peDirectory, Path.GetFileName(codeViewData.Path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsPortable(string pdbPath)
|
||||
{
|
||||
using var pdbStream = File.OpenRead(pdbPath);
|
||||
return pdbStream.ReadByte() == 'B' &&
|
||||
pdbStream.ReadByte() == 'S' &&
|
||||
pdbStream.ReadByte() == 'J' &&
|
||||
pdbStream.ReadByte() == 'B';
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var entry in _cache)
|
||||
{
|
||||
entry.Value?.Dispose();
|
||||
}
|
||||
|
||||
_cache.Clear();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) Ben A Adams. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
|
||||
namespace System.Diagnostics.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// A helper class that contains utilities methods for dealing with reflection.
|
||||
/// </summary>
|
||||
public static class ReflectionHelper
|
||||
{
|
||||
private static PropertyInfo? transformerNamesLazyPropertyInfo;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the <paramref name="type"/> is a value tuple type.
|
||||
/// </summary>
|
||||
public static bool IsValueTuple(this Type type)
|
||||
{
|
||||
return type.Namespace == "System" && type.Name.Contains("ValueTuple`");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the given <paramref name="attribute"/> is of type <code>TupleElementNameAttribute</code>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// To avoid compile-time dependency hell with System.ValueTuple, this method uses reflection and not checks statically that
|
||||
/// the given <paramref name="attribute"/> is <code>TupleElementNameAttribute</code>.
|
||||
/// </remarks>
|
||||
public static bool IsTupleElementNameAttribute(this Attribute attribute)
|
||||
{
|
||||
var attributeType = attribute.GetType();
|
||||
return attributeType.Namespace == "System.Runtime.CompilerServices" &&
|
||||
attributeType.Name == "TupleElementNamesAttribute";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns 'TransformNames' property value from a given <paramref name="attribute"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// To avoid compile-time dependency hell with System.ValueTuple, this method uses reflection
|
||||
/// instead of casting the attribute to a specific type.
|
||||
/// </remarks>
|
||||
public static IList<string>? GetTransformerNames(this Attribute attribute)
|
||||
{
|
||||
Debug.Assert(attribute.IsTupleElementNameAttribute());
|
||||
|
||||
var propertyInfo = GetTransformNamesPropertyInfo(attribute.GetType());
|
||||
return propertyInfo?.GetValue(attribute) as IList<string>;
|
||||
}
|
||||
|
||||
private static PropertyInfo? GetTransformNamesPropertyInfo(Type attributeType)
|
||||
{
|
||||
#pragma warning disable 8634
|
||||
return LazyInitializer.EnsureInitialized(ref transformerNamesLazyPropertyInfo,
|
||||
#pragma warning restore 8634
|
||||
() => attributeType.GetProperty("TransformNames", BindingFlags.Instance | BindingFlags.Public)!);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,172 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic.Enumerable;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace System.Diagnostics
|
||||
{
|
||||
public class ResolvedMethod
|
||||
{
|
||||
public MethodBase? MethodBase { get; set; }
|
||||
|
||||
public Type? DeclaringType { get; set; }
|
||||
|
||||
public bool IsAsync { get; set; }
|
||||
|
||||
public bool IsLambda { get; set; }
|
||||
|
||||
public ResolvedParameter? ReturnParameter { get; set; }
|
||||
|
||||
public string? Name { get; set; }
|
||||
|
||||
public int? Ordinal { get; set; }
|
||||
|
||||
public string? GenericArguments { get; set; }
|
||||
|
||||
public Type[]? ResolvedGenericArguments { get; set; }
|
||||
|
||||
public MethodBase? SubMethodBase { get; set; }
|
||||
|
||||
public string? SubMethod { get; set; }
|
||||
|
||||
public EnumerableIList<ResolvedParameter> Parameters { get; set; }
|
||||
|
||||
public EnumerableIList<ResolvedParameter> SubMethodParameters { get; set; }
|
||||
public int RecurseCount { get; internal set; }
|
||||
|
||||
internal bool IsSequentialEquivalent(ResolvedMethod obj)
|
||||
{
|
||||
return
|
||||
IsAsync == obj.IsAsync &&
|
||||
DeclaringType == obj.DeclaringType &&
|
||||
Name == obj.Name &&
|
||||
IsLambda == obj.IsLambda &&
|
||||
Ordinal == obj.Ordinal &&
|
||||
GenericArguments == obj.GenericArguments &&
|
||||
SubMethod == obj.SubMethod;
|
||||
}
|
||||
|
||||
public override string ToString() => Append(new StringBuilder()).ToString();
|
||||
|
||||
public StringBuilder Append(StringBuilder builder)
|
||||
=> Append(builder, true);
|
||||
|
||||
public StringBuilder Append(StringBuilder builder, bool fullName)
|
||||
{
|
||||
if (IsAsync)
|
||||
{
|
||||
builder.Append("async ");
|
||||
}
|
||||
|
||||
if (ReturnParameter != null)
|
||||
{
|
||||
ReturnParameter.Append(builder);
|
||||
builder.Append(' ');
|
||||
}
|
||||
|
||||
if (DeclaringType != null)
|
||||
{
|
||||
|
||||
if (Name == ".ctor")
|
||||
{
|
||||
if (string.IsNullOrEmpty(SubMethod) && !IsLambda)
|
||||
builder.Append("new ");
|
||||
|
||||
AppendDeclaringTypeName(builder, fullName);
|
||||
}
|
||||
else if (Name == ".cctor")
|
||||
{
|
||||
builder.Append("static ");
|
||||
AppendDeclaringTypeName(builder, fullName);
|
||||
}
|
||||
else
|
||||
{
|
||||
AppendDeclaringTypeName(builder, fullName)
|
||||
.Append('.')
|
||||
.Append(Name);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append(Name);
|
||||
}
|
||||
builder.Append(GenericArguments);
|
||||
|
||||
builder.Append('(');
|
||||
if (MethodBase != null)
|
||||
{
|
||||
var isFirst = true;
|
||||
foreach(var param in Parameters)
|
||||
{
|
||||
if (isFirst)
|
||||
{
|
||||
isFirst = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append(", ");
|
||||
}
|
||||
param.Append(builder);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append('?');
|
||||
}
|
||||
builder.Append(')');
|
||||
|
||||
if (!string.IsNullOrEmpty(SubMethod) || IsLambda)
|
||||
{
|
||||
builder.Append('+');
|
||||
builder.Append(SubMethod);
|
||||
builder.Append('(');
|
||||
if (SubMethodBase != null)
|
||||
{
|
||||
var isFirst = true;
|
||||
foreach (var param in SubMethodParameters)
|
||||
{
|
||||
if (isFirst)
|
||||
{
|
||||
isFirst = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append(", ");
|
||||
}
|
||||
param.Append(builder);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append('?');
|
||||
}
|
||||
builder.Append(')');
|
||||
if (IsLambda)
|
||||
{
|
||||
builder.Append(" => { }");
|
||||
|
||||
if (Ordinal.HasValue)
|
||||
{
|
||||
builder.Append(" [");
|
||||
builder.Append(Ordinal);
|
||||
builder.Append(']');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (RecurseCount > 0)
|
||||
{
|
||||
builder.Append($" x {RecurseCount + 1:0}");
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private StringBuilder AppendDeclaringTypeName(StringBuilder builder, bool fullName = true)
|
||||
{
|
||||
return DeclaringType != null ? builder.AppendTypeDisplayName(DeclaringType, fullName: fullName, includeGenericParameterNames: true) : builder;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) Ben A Adams. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Text;
|
||||
|
||||
namespace System.Diagnostics
|
||||
{
|
||||
public class ResolvedParameter
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
|
||||
public Type ResolvedType { get; set; }
|
||||
|
||||
public string? Prefix { get; set; }
|
||||
public bool IsDynamicType { get; set; }
|
||||
|
||||
public ResolvedParameter(Type resolvedType) => ResolvedType = resolvedType;
|
||||
|
||||
public override string ToString() => Append(new StringBuilder()).ToString();
|
||||
|
||||
public StringBuilder Append(StringBuilder sb)
|
||||
{
|
||||
if (ResolvedType.Assembly.ManifestModule.Name == "FSharp.Core.dll" && ResolvedType.Name == "Unit")
|
||||
return sb;
|
||||
|
||||
if (!string.IsNullOrEmpty(Prefix))
|
||||
{
|
||||
sb.Append(Prefix)
|
||||
.Append(' ');
|
||||
}
|
||||
|
||||
if (IsDynamicType)
|
||||
{
|
||||
sb.Append("dynamic");
|
||||
}
|
||||
else if (ResolvedType != null)
|
||||
{
|
||||
AppendTypeName(sb);
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append('?');
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(Name))
|
||||
{
|
||||
sb.Append(' ')
|
||||
.Append(Name);
|
||||
}
|
||||
|
||||
return sb;
|
||||
}
|
||||
|
||||
protected virtual void AppendTypeName(StringBuilder sb)
|
||||
{
|
||||
sb.AppendTypeDisplayName(ResolvedType, fullName: false, includeGenericParameterNames: true);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) Ben A Adams. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic.Enumerable;
|
||||
using System.Text;
|
||||
|
||||
namespace System.Diagnostics
|
||||
{
|
||||
public static class StringBuilderExtentions
|
||||
{
|
||||
public static StringBuilder AppendDemystified(this StringBuilder builder, Exception exception)
|
||||
{
|
||||
try
|
||||
{
|
||||
var stackTrace = new EnhancedStackTrace(exception);
|
||||
|
||||
builder.Append(exception.GetType());
|
||||
if (!string.IsNullOrEmpty(exception.Message))
|
||||
{
|
||||
builder.Append(": ").Append(exception.Message);
|
||||
}
|
||||
builder.Append(Environment.NewLine);
|
||||
|
||||
if (stackTrace.FrameCount > 0)
|
||||
{
|
||||
stackTrace.Append(builder);
|
||||
}
|
||||
|
||||
if (exception is AggregateException aggEx)
|
||||
{
|
||||
foreach (var ex in EnumerableIList.Create(aggEx.InnerExceptions))
|
||||
{
|
||||
builder.AppendInnerException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (exception.InnerException != null)
|
||||
{
|
||||
builder.AppendInnerException(exception.InnerException);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Processing exceptions shouldn't throw exceptions; if it fails
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static void AppendInnerException(this StringBuilder builder, Exception exception)
|
||||
=> builder.Append(" ---> ")
|
||||
.AppendDemystified(exception)
|
||||
.AppendLine()
|
||||
.Append(" --- End of inner exception stack trace ---");
|
||||
}
|
||||
}
|
@@ -0,0 +1,218 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace System.Diagnostics
|
||||
{
|
||||
// Adapted from https://github.com/aspnet/Common/blob/dev/shared/Microsoft.Extensions.TypeNameHelper.Sources/TypeNameHelper.cs
|
||||
public static class TypeNameHelper
|
||||
{
|
||||
public static readonly Dictionary<Type, string> BuiltInTypeNames = new Dictionary<Type, string>
|
||||
{
|
||||
{ typeof(void), "void" },
|
||||
{ typeof(bool), "bool" },
|
||||
{ typeof(byte), "byte" },
|
||||
{ typeof(char), "char" },
|
||||
{ typeof(decimal), "decimal" },
|
||||
{ typeof(double), "double" },
|
||||
{ typeof(float), "float" },
|
||||
{ typeof(int), "int" },
|
||||
{ typeof(long), "long" },
|
||||
{ typeof(object), "object" },
|
||||
{ typeof(sbyte), "sbyte" },
|
||||
{ typeof(short), "short" },
|
||||
{ typeof(string), "string" },
|
||||
{ typeof(uint), "uint" },
|
||||
{ typeof(ulong), "ulong" },
|
||||
{ typeof(ushort), "ushort" }
|
||||
};
|
||||
|
||||
public static readonly Dictionary<string, string> FSharpTypeNames = new Dictionary<string, string>
|
||||
{
|
||||
{ "Unit", "void" },
|
||||
{ "FSharpOption", "Option" },
|
||||
{ "FSharpAsync", "Async" },
|
||||
{ "FSharpOption`1", "Option" },
|
||||
{ "FSharpAsync`1", "Async" }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Pretty print a type name.
|
||||
/// </summary>
|
||||
/// <param name="type">The <see cref="Type"/>.</param>
|
||||
/// <param name="fullName"><c>true</c> to print a fully qualified name.</param>
|
||||
/// <param name="includeGenericParameterNames"><c>true</c> to include generic parameter names.</param>
|
||||
/// <returns>The pretty printed type name.</returns>
|
||||
public static string GetTypeDisplayName(Type type, bool fullName = true, bool includeGenericParameterNames = false)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
ProcessType(builder, type, new DisplayNameOptions(fullName, includeGenericParameterNames));
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
public static StringBuilder AppendTypeDisplayName(this StringBuilder builder, Type type, bool fullName = true, bool includeGenericParameterNames = false)
|
||||
{
|
||||
ProcessType(builder, type, new DisplayNameOptions(fullName, includeGenericParameterNames));
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a name of given generic type without '`'.
|
||||
/// </summary>
|
||||
public static string GetTypeNameForGenericType(Type type)
|
||||
{
|
||||
if (!type.IsGenericType)
|
||||
{
|
||||
throw new ArgumentException("The given type should be generic", nameof(type));
|
||||
}
|
||||
|
||||
var genericPartIndex = type.Name.IndexOf('`');
|
||||
|
||||
return (genericPartIndex >= 0) ? type.Name.Substring(0, genericPartIndex) : type.Name;
|
||||
}
|
||||
|
||||
private static void ProcessType(StringBuilder builder, Type type, DisplayNameOptions options)
|
||||
{
|
||||
if (type.IsGenericType)
|
||||
{
|
||||
var underlyingType = Nullable.GetUnderlyingType(type);
|
||||
if (underlyingType != null)
|
||||
{
|
||||
ProcessType(builder, underlyingType, options);
|
||||
builder.Append('?');
|
||||
}
|
||||
else
|
||||
{
|
||||
var genericArguments = type.GetGenericArguments();
|
||||
ProcessGenericType(builder, type, genericArguments, genericArguments.Length, options);
|
||||
}
|
||||
}
|
||||
else if (type.IsArray)
|
||||
{
|
||||
ProcessArrayType(builder, type, options);
|
||||
}
|
||||
else if (BuiltInTypeNames.TryGetValue(type, out var builtInName))
|
||||
{
|
||||
builder.Append(builtInName);
|
||||
}
|
||||
else if (type.Namespace == nameof(System))
|
||||
{
|
||||
builder.Append(type.Name);
|
||||
}
|
||||
else if (type.Assembly.ManifestModule.Name == "FSharp.Core.dll"
|
||||
&& FSharpTypeNames.TryGetValue(type.Name, out builtInName))
|
||||
{
|
||||
builder.Append(builtInName);
|
||||
}
|
||||
else if (type.IsGenericParameter)
|
||||
{
|
||||
if (options.IncludeGenericParameterNames)
|
||||
{
|
||||
builder.Append(type.Name);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append(options.FullName ? type.FullName ?? type.Name : type.Name);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ProcessArrayType(StringBuilder builder, Type type, DisplayNameOptions options)
|
||||
{
|
||||
var innerType = type;
|
||||
while (innerType.IsArray)
|
||||
{
|
||||
if (innerType.GetElementType() is { } inner)
|
||||
{
|
||||
innerType = inner;
|
||||
}
|
||||
}
|
||||
|
||||
ProcessType(builder, innerType, options);
|
||||
|
||||
while (type.IsArray)
|
||||
{
|
||||
builder.Append('[');
|
||||
builder.Append(',', type.GetArrayRank() - 1);
|
||||
builder.Append(']');
|
||||
if (type.GetElementType() is not { } elementType)
|
||||
{
|
||||
break;
|
||||
}
|
||||
type = elementType;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ProcessGenericType(StringBuilder builder, Type type, Type[] genericArguments, int length, DisplayNameOptions options)
|
||||
{
|
||||
var offset = 0;
|
||||
if (type.IsNested && type.DeclaringType is not null)
|
||||
{
|
||||
offset = type.DeclaringType.GetGenericArguments().Length;
|
||||
}
|
||||
|
||||
if (options.FullName)
|
||||
{
|
||||
if (type.IsNested && type.DeclaringType is not null)
|
||||
{
|
||||
ProcessGenericType(builder, type.DeclaringType, genericArguments, offset, options);
|
||||
builder.Append('+');
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(type.Namespace))
|
||||
{
|
||||
builder.Append(type.Namespace);
|
||||
builder.Append('.');
|
||||
}
|
||||
}
|
||||
|
||||
var genericPartIndex = type.Name.IndexOf('`');
|
||||
if (genericPartIndex <= 0)
|
||||
{
|
||||
builder.Append(type.Name);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type.Assembly.ManifestModule.Name == "FSharp.Core.dll"
|
||||
&& FSharpTypeNames.TryGetValue(type.Name, out var builtInName))
|
||||
{
|
||||
builder.Append(builtInName);
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append(type.Name, 0, genericPartIndex);
|
||||
}
|
||||
|
||||
builder.Append('<');
|
||||
for (var i = offset; i < length; i++)
|
||||
{
|
||||
ProcessType(builder, genericArguments[i], options);
|
||||
if (i + 1 == length)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
builder.Append(',');
|
||||
if (options.IncludeGenericParameterNames || !genericArguments[i + 1].IsGenericParameter)
|
||||
{
|
||||
builder.Append(' ');
|
||||
}
|
||||
}
|
||||
builder.Append('>');
|
||||
}
|
||||
|
||||
private struct DisplayNameOptions
|
||||
{
|
||||
public DisplayNameOptions(bool fullName, bool includeGenericParameterNames)
|
||||
{
|
||||
FullName = fullName;
|
||||
IncludeGenericParameterNames = includeGenericParameterNames;
|
||||
}
|
||||
|
||||
public bool FullName { get; }
|
||||
|
||||
public bool IncludeGenericParameterNames { get; }
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) Ben A Adams. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Internal;
|
||||
using System.Text;
|
||||
|
||||
namespace System.Diagnostics
|
||||
{
|
||||
public class ValueTupleResolvedParameter : ResolvedParameter
|
||||
{
|
||||
public IList<string> TupleNames { get; }
|
||||
|
||||
public ValueTupleResolvedParameter(Type resolvedType, IList<string> tupleNames)
|
||||
: base(resolvedType)
|
||||
=> TupleNames = tupleNames;
|
||||
|
||||
protected override void AppendTypeName(StringBuilder sb)
|
||||
{
|
||||
if (ResolvedType is not null)
|
||||
{
|
||||
if (ResolvedType.IsValueTuple())
|
||||
{
|
||||
AppendValueTupleParameterName(sb, ResolvedType);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Need to unwrap the first generic argument first.
|
||||
sb.Append(TypeNameHelper.GetTypeNameForGenericType(ResolvedType));
|
||||
sb.Append('<');
|
||||
AppendValueTupleParameterName(sb, ResolvedType.GetGenericArguments()[0]);
|
||||
sb.Append('>');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AppendValueTupleParameterName(StringBuilder sb, Type parameterType)
|
||||
{
|
||||
sb.Append('(');
|
||||
var args = parameterType.GetGenericArguments();
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
sb.Append(", ");
|
||||
}
|
||||
|
||||
sb.AppendTypeDisplayName(args[i], fullName: false, includeGenericParameterNames: true);
|
||||
|
||||
if (i >= TupleNames.Count)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var argName = TupleNames[i];
|
||||
if (argName == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
sb.Append(' ');
|
||||
sb.Append(argName);
|
||||
}
|
||||
|
||||
sb.Append(')');
|
||||
}
|
||||
}
|
||||
}
|
@@ -23,15 +23,13 @@
|
||||
<ItemGroup>
|
||||
<None Remove="FriendlyException\Assets\error.html" />
|
||||
<None Remove="SpecificationDocument\Assets\index.html" />
|
||||
<None Remove="Schedule\Dashboard\frontend\**\*" />
|
||||
<EmbeddedResource Include="FriendlyException\Assets\error.html" />
|
||||
<EmbeddedResource Include="SpecificationDocument\Assets\index.html" />
|
||||
<EmbeddedResource Include="Schedule\Dashboard\frontend\**\*" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<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="Swashbuckle.AspNetCore" Version="9.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -39,15 +37,12 @@
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="$(NET8Version)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="$(NET8Version)" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="8.0.2" />
|
||||
<PackageReference Include="System.Reflection.MetadataLoadContext" Version="8.0.1" />
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net9.0' ">
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="$(NET9Version)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="$(NET9Version)" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="$(NET9Version)" />
|
||||
<PackageReference Include="System.Reflection.MetadataLoadContext" Version="$(NET9Version)" />
|
||||
<PackageReference Include="System.Text.Json" Version="$(NET9Version)" />
|
||||
|
||||
</ItemGroup>
|
||||
|
||||
|
@@ -94,6 +94,11 @@ public sealed class HttpFileDownloadBuilder
|
||||
/// </summary>
|
||||
internal Type? FileTransferEventHandlerType { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="HttpRequestBuilder" /> 配置委托
|
||||
/// </summary>
|
||||
internal Action<HttpRequestBuilder>? RequestConfigure { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 设置用于传输操作的缓冲区大小
|
||||
/// </summary>
|
||||
@@ -302,17 +307,50 @@ public sealed class HttpFileDownloadBuilder
|
||||
where TFileTransferEventHandler : IHttpFileTransferEventHandler =>
|
||||
SetEventHandler(typeof(TFileTransferEventHandler));
|
||||
|
||||
/// <summary>
|
||||
/// 配置 <see cref="HttpRequestBuilder" /> 实例
|
||||
/// </summary>
|
||||
/// <remarks>支持多次调用。</remarks>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
/// <returns>
|
||||
/// <see cref="HttpFileDownloadBuilder" />
|
||||
/// </returns>
|
||||
public HttpFileDownloadBuilder WithRequest(Action<HttpRequestBuilder> configure)
|
||||
{
|
||||
// 空检查
|
||||
ArgumentNullException.ThrowIfNull(configure);
|
||||
|
||||
// 如果 RequestConfigure 未设置则直接赋值
|
||||
if (RequestConfigure is null)
|
||||
{
|
||||
RequestConfigure = configure;
|
||||
}
|
||||
// 否则创建级联调用委托
|
||||
else
|
||||
{
|
||||
// 复制一个新的委托避免死循环
|
||||
var originalRequestConfigure = RequestConfigure;
|
||||
|
||||
RequestConfigure = httpRequestBuilder =>
|
||||
{
|
||||
originalRequestConfigure.Invoke(httpRequestBuilder);
|
||||
configure.Invoke(httpRequestBuilder);
|
||||
};
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建 <see cref="HttpRequestBuilder" /> 实例
|
||||
/// </summary>
|
||||
/// <param name="httpRemoteOptions">
|
||||
/// <see cref="HttpRemoteOptions" />
|
||||
/// </param>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
/// <returns>
|
||||
/// <see cref="HttpRequestBuilder" />
|
||||
/// </returns>
|
||||
internal HttpRequestBuilder Build(HttpRemoteOptions httpRemoteOptions, Action<HttpRequestBuilder>? configure = null)
|
||||
internal HttpRequestBuilder Build(HttpRemoteOptions httpRemoteOptions)
|
||||
{
|
||||
// 空检查
|
||||
ArgumentNullException.ThrowIfNull(httpRemoteOptions);
|
||||
@@ -335,7 +373,7 @@ public sealed class HttpFileDownloadBuilder
|
||||
}
|
||||
|
||||
// 调用自定义配置委托
|
||||
configure?.Invoke(httpRequestBuilder);
|
||||
RequestConfigure?.Invoke(httpRequestBuilder);
|
||||
|
||||
return httpRequestBuilder;
|
||||
}
|
||||
|
@@ -118,6 +118,11 @@ public sealed class HttpFileUploadBuilder
|
||||
/// </summary>
|
||||
internal Type? FileTransferEventHandlerType { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="HttpRequestBuilder" /> 配置委托
|
||||
/// </summary>
|
||||
internal Action<HttpRequestBuilder>? RequestConfigure { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 设置内容类型(文件类型)
|
||||
/// </summary>
|
||||
@@ -321,6 +326,40 @@ public sealed class HttpFileUploadBuilder
|
||||
where TFileTransferEventHandler : IHttpFileTransferEventHandler =>
|
||||
SetEventHandler(typeof(TFileTransferEventHandler));
|
||||
|
||||
/// <summary>
|
||||
/// 配置 <see cref="HttpRequestBuilder" /> 实例
|
||||
/// </summary>
|
||||
/// <remarks>支持多次调用。</remarks>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
/// <returns>
|
||||
/// <see cref="HttpFileUploadBuilder" />
|
||||
/// </returns>
|
||||
public HttpFileUploadBuilder WithRequest(Action<HttpRequestBuilder> configure)
|
||||
{
|
||||
// 空检查
|
||||
ArgumentNullException.ThrowIfNull(configure);
|
||||
|
||||
// 如果 RequestConfigure 未设置则直接赋值
|
||||
if (RequestConfigure is null)
|
||||
{
|
||||
RequestConfigure = configure;
|
||||
}
|
||||
// 否则创建级联调用委托
|
||||
else
|
||||
{
|
||||
// 复制一个新的委托避免死循环
|
||||
var originalRequestConfigure = RequestConfigure;
|
||||
|
||||
RequestConfigure = httpRequestBuilder =>
|
||||
{
|
||||
originalRequestConfigure.Invoke(httpRequestBuilder);
|
||||
configure.Invoke(httpRequestBuilder);
|
||||
};
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建 <see cref="HttpRequestBuilder" /> 实例
|
||||
/// </summary>
|
||||
@@ -328,13 +367,11 @@ public sealed class HttpFileUploadBuilder
|
||||
/// <see cref="HttpRemoteOptions" />
|
||||
/// </param>
|
||||
/// <param name="progressChannel">文件传输进度信息的通道</param>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
/// <returns>
|
||||
/// <see cref="HttpRequestBuilder" />
|
||||
/// </returns>
|
||||
internal HttpRequestBuilder Build(HttpRemoteOptions httpRemoteOptions,
|
||||
Channel<FileTransferProgress> progressChannel,
|
||||
Action<HttpRequestBuilder>? configure = null)
|
||||
Channel<FileTransferProgress> progressChannel)
|
||||
{
|
||||
// 空检查
|
||||
ArgumentNullException.ThrowIfNull(httpRemoteOptions);
|
||||
@@ -355,7 +392,7 @@ public sealed class HttpFileUploadBuilder
|
||||
}
|
||||
|
||||
// 调用自定义配置委托
|
||||
configure?.Invoke(httpRequestBuilder);
|
||||
RequestConfigure?.Invoke(httpRequestBuilder);
|
||||
|
||||
return httpRequestBuilder;
|
||||
}
|
||||
|
@@ -79,6 +79,11 @@ public sealed class HttpLongPollingBuilder
|
||||
/// </summary>
|
||||
internal Type? LongPollingEventHandlerType { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="HttpRequestBuilder" /> 配置委托
|
||||
/// </summary>
|
||||
internal Action<HttpRequestBuilder>? RequestConfigure { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 设置轮询重试间隔
|
||||
/// </summary>
|
||||
@@ -245,17 +250,50 @@ public sealed class HttpLongPollingBuilder
|
||||
where TLongPollingEventHandler : IHttpLongPollingEventHandler =>
|
||||
SetEventHandler(typeof(TLongPollingEventHandler));
|
||||
|
||||
/// <summary>
|
||||
/// 配置 <see cref="HttpRequestBuilder" /> 实例
|
||||
/// </summary>
|
||||
/// <remarks>支持多次调用。</remarks>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
/// <returns>
|
||||
/// <see cref="HttpLongPollingBuilder" />
|
||||
/// </returns>
|
||||
public HttpLongPollingBuilder WithRequest(Action<HttpRequestBuilder> configure)
|
||||
{
|
||||
// 空检查
|
||||
ArgumentNullException.ThrowIfNull(configure);
|
||||
|
||||
// 如果 RequestConfigure 未设置则直接赋值
|
||||
if (RequestConfigure is null)
|
||||
{
|
||||
RequestConfigure = configure;
|
||||
}
|
||||
// 否则创建级联调用委托
|
||||
else
|
||||
{
|
||||
// 复制一个新的委托避免死循环
|
||||
var originalRequestConfigure = RequestConfigure;
|
||||
|
||||
RequestConfigure = httpRequestBuilder =>
|
||||
{
|
||||
originalRequestConfigure.Invoke(httpRequestBuilder);
|
||||
configure.Invoke(httpRequestBuilder);
|
||||
};
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建 <see cref="HttpRequestBuilder" /> 实例
|
||||
/// </summary>
|
||||
/// <param name="httpRemoteOptions">
|
||||
/// <see cref="HttpRemoteOptions" />
|
||||
/// </param>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
/// <returns>
|
||||
/// <see cref="HttpRequestBuilder" />
|
||||
/// </returns>
|
||||
internal HttpRequestBuilder Build(HttpRemoteOptions httpRemoteOptions, Action<HttpRequestBuilder>? configure = null)
|
||||
internal HttpRequestBuilder Build(HttpRemoteOptions httpRemoteOptions)
|
||||
{
|
||||
// 空检查
|
||||
ArgumentNullException.ThrowIfNull(httpRemoteOptions);
|
||||
@@ -277,7 +315,7 @@ public sealed class HttpLongPollingBuilder
|
||||
}
|
||||
|
||||
// 调用自定义配置委托
|
||||
configure?.Invoke(httpRequestBuilder);
|
||||
RequestConfigure?.Invoke(httpRequestBuilder);
|
||||
|
||||
return httpRequestBuilder;
|
||||
}
|
||||
|
@@ -239,9 +239,7 @@ public sealed class HttpMultipartFormDataBuilder
|
||||
{
|
||||
_partContents.Add(new MultipartFormDataItem(name)
|
||||
{
|
||||
ContentType = mediaType,
|
||||
RawContent = rawObject,
|
||||
ContentEncoding = encoding
|
||||
ContentType = mediaType, RawContent = rawObject, ContentEncoding = encoding
|
||||
});
|
||||
|
||||
return this;
|
||||
@@ -250,14 +248,27 @@ public sealed class HttpMultipartFormDataBuilder
|
||||
// 空检查
|
||||
ArgumentNullException.ThrowIfNull(rawObject);
|
||||
|
||||
// 将对象转换为 MultipartFormDataItem 集合再追加
|
||||
_partContents.AddRange(rawObject.ObjectToDictionary()!.Select(u =>
|
||||
new MultipartFormDataItem(u.Key.ToCultureString(CultureInfo.InvariantCulture)!)
|
||||
// 将对象转换为字典集合再追加
|
||||
var formDataItems = rawObject.ObjectToDictionary()!;
|
||||
|
||||
// 遍历字典集合并逐条追加
|
||||
foreach (var (key, rawContent) in formDataItems)
|
||||
{
|
||||
// 检查原始请求内容是否是 MultipartFile 类型
|
||||
if (rawContent is MultipartFile multipartFile)
|
||||
{
|
||||
ContentType = MediaTypeNames.Text.Plain,
|
||||
RawContent = u.Value,
|
||||
ContentEncoding = encoding
|
||||
}));
|
||||
AddFile(multipartFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
_partContents.Add(new MultipartFormDataItem(key.ToCultureString(CultureInfo.InvariantCulture)!)
|
||||
{
|
||||
ContentType = Helpers.GetContentTypeOrDefault(rawContent, MediaTypeNames.Text.Plain),
|
||||
RawContent = rawContent,
|
||||
ContentEncoding = encoding
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
@@ -515,10 +526,7 @@ public sealed class HttpMultipartFormDataBuilder
|
||||
|
||||
_partContents.Add(new MultipartFormDataItem(name)
|
||||
{
|
||||
ContentType = mimeType,
|
||||
RawContent = stream,
|
||||
ContentEncoding = encoding,
|
||||
FileName = fileName
|
||||
ContentType = mimeType, RawContent = stream, ContentEncoding = encoding, FileName = fileName
|
||||
});
|
||||
|
||||
// 是否在请求结束后自动释放流
|
||||
@@ -561,10 +569,7 @@ public sealed class HttpMultipartFormDataBuilder
|
||||
|
||||
_partContents.Add(new MultipartFormDataItem(name)
|
||||
{
|
||||
ContentType = mimeType,
|
||||
RawContent = byteArray,
|
||||
ContentEncoding = encoding,
|
||||
FileName = fileName
|
||||
ContentType = mimeType, RawContent = byteArray, ContentEncoding = encoding, FileName = fileName
|
||||
});
|
||||
|
||||
return this;
|
||||
@@ -687,9 +692,7 @@ public sealed class HttpMultipartFormDataBuilder
|
||||
|
||||
_partContents.Add(new MultipartFormDataItem(formName)
|
||||
{
|
||||
ContentType = mediaType,
|
||||
RawContent = httpContent,
|
||||
ContentEncoding = encoding
|
||||
ContentType = mediaType, RawContent = httpContent, ContentEncoding = encoding
|
||||
});
|
||||
|
||||
return this;
|
||||
@@ -815,7 +818,12 @@ public sealed class HttpMultipartFormDataBuilder
|
||||
new ContentDispositionHeaderValue(Constants.FORM_DATA_DISPOSITION_TYPE)
|
||||
{
|
||||
Name = multipartFormDataItem.Name.AddQuotes(),
|
||||
FileName = multipartFormDataItem.FileName.AddQuotes()
|
||||
FileName =
|
||||
(string.IsNullOrWhiteSpace(multipartFormDataItem.FileName) &&
|
||||
contentType.IsIn([MediaTypeNames.Application.Octet], StringComparer.OrdinalIgnoreCase)
|
||||
// 解决发送文件或二进制【未设置】文件名问题
|
||||
? $"Unnamed_{DateTime.Now:yyyyMMddHHmmss}_{Guid.NewGuid().ToString("N")[..8]}"
|
||||
: multipartFormDataItem.FileName).AddQuotes()
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -280,13 +280,28 @@ public sealed partial class HttpRequestBuilder
|
||||
/// <returns>
|
||||
/// <see cref="HttpRequestBuilder" />
|
||||
/// </returns>
|
||||
public HttpRequestBuilder SetMultipartContent(Action<HttpMultipartFormDataBuilder> configure)
|
||||
public HttpRequestBuilder SetMultipartContent(Action<HttpMultipartFormDataBuilder> configure) =>
|
||||
SetMultipartContent(configure, true);
|
||||
|
||||
/// <summary>
|
||||
/// 设置多部分表单内容,请求类型为 <c>multipart/form-data</c>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该操作将强制覆盖 <see cref="SetContent" />、<see cref="SetContentEncoding(System.Text.Encoding)" /> 和
|
||||
/// <see cref="SetContentType" /> 设置的内容。
|
||||
/// </remarks>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
/// <param name="omitContentType">是否移除默认的多部分内容的 <c>Content-Type</c></param>
|
||||
/// <returns>
|
||||
/// <see cref="HttpRequestBuilder" />
|
||||
/// </returns>
|
||||
public HttpRequestBuilder SetMultipartContent(Action<HttpMultipartFormDataBuilder> configure, bool omitContentType)
|
||||
{
|
||||
// 空检查
|
||||
ArgumentNullException.ThrowIfNull(configure);
|
||||
|
||||
// 初始化 HttpMultipartFormDataBuilder 实例
|
||||
var httpMultipartFormDataBuilder = new HttpMultipartFormDataBuilder(this);
|
||||
var httpMultipartFormDataBuilder = new HttpMultipartFormDataBuilder(this) { OmitContentType = omitContentType };
|
||||
|
||||
// 调用自定义配置委托
|
||||
configure.Invoke(httpMultipartFormDataBuilder);
|
||||
|
@@ -337,9 +337,7 @@ public sealed partial class HttpRequestBuilder
|
||||
{
|
||||
httpRequestMessage.Headers.CacheControl = new CacheControlHeaderValue
|
||||
{
|
||||
NoCache = true,
|
||||
NoStore = true,
|
||||
MustRevalidate = true
|
||||
NoCache = true, NoStore = true, MustRevalidate = true
|
||||
};
|
||||
}
|
||||
|
||||
@@ -604,15 +602,6 @@ public sealed partial class HttpRequestBuilder
|
||||
return;
|
||||
}
|
||||
|
||||
ContentType = RawContent switch
|
||||
{
|
||||
JsonContent => MediaTypeNames.Application.Json,
|
||||
FormUrlEncodedContent => MediaTypeNames.Application.FormUrlEncoded,
|
||||
(byte[] or Stream or ByteArrayContent or StreamContent or ReadOnlyMemoryContent or ReadOnlyMemory<byte>)
|
||||
and not StringContent => MediaTypeNames.Application
|
||||
.Octet,
|
||||
MultipartContent => MediaTypeNames.Multipart.FormData,
|
||||
_ => defaultContentType ?? Constants.TEXT_PLAIN_MIME_TYPE
|
||||
};
|
||||
ContentType = Helpers.GetContentTypeOrDefault(RawContent, defaultContentType);
|
||||
}
|
||||
}
|
@@ -88,6 +88,11 @@ public sealed class HttpServerSentEventsBuilder
|
||||
/// </summary>
|
||||
internal Type? ServerSentEventsEventHandlerType { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="HttpRequestBuilder" /> 配置委托
|
||||
/// </summary>
|
||||
internal Action<HttpRequestBuilder>? RequestConfigure { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 设置默认重新连接的间隔时间
|
||||
/// </summary>
|
||||
@@ -220,17 +225,50 @@ public sealed class HttpServerSentEventsBuilder
|
||||
where TServerSentEventsEventHandler : IHttpServerSentEventsEventHandler =>
|
||||
SetEventHandler(typeof(TServerSentEventsEventHandler));
|
||||
|
||||
/// <summary>
|
||||
/// 配置 <see cref="HttpRequestBuilder" /> 实例
|
||||
/// </summary>
|
||||
/// <remarks>支持多次调用。</remarks>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
/// <returns>
|
||||
/// <see cref="HttpServerSentEventsBuilder" />
|
||||
/// </returns>
|
||||
public HttpServerSentEventsBuilder WithRequest(Action<HttpRequestBuilder> configure)
|
||||
{
|
||||
// 空检查
|
||||
ArgumentNullException.ThrowIfNull(configure);
|
||||
|
||||
// 如果 RequestConfigure 未设置则直接赋值
|
||||
if (RequestConfigure is null)
|
||||
{
|
||||
RequestConfigure = configure;
|
||||
}
|
||||
// 否则创建级联调用委托
|
||||
else
|
||||
{
|
||||
// 复制一个新的委托避免死循环
|
||||
var originalRequestConfigure = RequestConfigure;
|
||||
|
||||
RequestConfigure = httpRequestBuilder =>
|
||||
{
|
||||
originalRequestConfigure.Invoke(httpRequestBuilder);
|
||||
configure.Invoke(httpRequestBuilder);
|
||||
};
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建 <see cref="HttpRequestBuilder" /> 实例
|
||||
/// </summary>
|
||||
/// <param name="httpRemoteOptions">
|
||||
/// <see cref="HttpRemoteOptions" />
|
||||
/// </param>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
/// <returns>
|
||||
/// <see cref="HttpRequestBuilder" />
|
||||
/// </returns>
|
||||
internal HttpRequestBuilder Build(HttpRemoteOptions httpRemoteOptions, Action<HttpRequestBuilder>? configure = null)
|
||||
internal HttpRequestBuilder Build(HttpRemoteOptions httpRemoteOptions)
|
||||
{
|
||||
// 空检查
|
||||
ArgumentNullException.ThrowIfNull(httpRemoteOptions);
|
||||
@@ -249,7 +287,7 @@ public sealed class HttpServerSentEventsBuilder
|
||||
}
|
||||
|
||||
// 调用自定义配置委托
|
||||
configure?.Invoke(httpRequestBuilder);
|
||||
RequestConfigure?.Invoke(httpRequestBuilder);
|
||||
|
||||
return httpRequestBuilder;
|
||||
}
|
||||
|
@@ -59,6 +59,11 @@ public sealed class HttpStressTestHarnessBuilder
|
||||
/// <remarks>默认值为:1。</remarks>
|
||||
public int NumberOfRounds { get; private set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="HttpRequestBuilder" /> 配置委托
|
||||
/// </summary>
|
||||
internal Action<HttpRequestBuilder>? RequestConfigure { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 设置并发请求数量
|
||||
/// </summary>
|
||||
@@ -124,17 +129,50 @@ public sealed class HttpStressTestHarnessBuilder
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置 <see cref="HttpRequestBuilder" /> 实例
|
||||
/// </summary>
|
||||
/// <remarks>支持多次调用。</remarks>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
/// <returns>
|
||||
/// <see cref="HttpStressTestHarnessBuilder" />
|
||||
/// </returns>
|
||||
public HttpStressTestHarnessBuilder WithRequest(Action<HttpRequestBuilder> configure)
|
||||
{
|
||||
// 空检查
|
||||
ArgumentNullException.ThrowIfNull(configure);
|
||||
|
||||
// 如果 RequestConfigure 未设置则直接赋值
|
||||
if (RequestConfigure is null)
|
||||
{
|
||||
RequestConfigure = configure;
|
||||
}
|
||||
// 否则创建级联调用委托
|
||||
else
|
||||
{
|
||||
// 复制一个新的委托避免死循环
|
||||
var originalRequestConfigure = RequestConfigure;
|
||||
|
||||
RequestConfigure = httpRequestBuilder =>
|
||||
{
|
||||
originalRequestConfigure.Invoke(httpRequestBuilder);
|
||||
configure.Invoke(httpRequestBuilder);
|
||||
};
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建 <see cref="HttpRequestBuilder" /> 实例
|
||||
/// </summary>
|
||||
/// <param name="httpRemoteOptions">
|
||||
/// <see cref="HttpRemoteOptions" />
|
||||
/// </param>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
/// <returns>
|
||||
/// <see cref="HttpRequestBuilder" />
|
||||
/// </returns>
|
||||
internal HttpRequestBuilder Build(HttpRemoteOptions httpRemoteOptions, Action<HttpRequestBuilder>? configure = null)
|
||||
internal HttpRequestBuilder Build(HttpRemoteOptions httpRemoteOptions)
|
||||
{
|
||||
// 空检查
|
||||
ArgumentNullException.ThrowIfNull(httpRemoteOptions);
|
||||
@@ -146,7 +184,7 @@ public sealed class HttpStressTestHarnessBuilder
|
||||
.PerformanceOptimization().UseHttpClientPool();
|
||||
|
||||
// 调用自定义配置委托
|
||||
configure?.Invoke(httpRequestBuilder);
|
||||
RequestConfigure?.Invoke(httpRequestBuilder);
|
||||
|
||||
return httpRequestBuilder;
|
||||
}
|
||||
|
@@ -15,7 +15,7 @@ namespace ThingsGateway.HttpRemote;
|
||||
/// HTTP 声明式多部分表单项内容特性
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Parameter)]
|
||||
public sealed class MultipartAttribute : Attribute
|
||||
public class MultipartAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// <inheritdoc cref="MultipartAttribute" />
|
||||
|
@@ -0,0 +1,24 @@
|
||||
// ------------------------------------------------------------------------
|
||||
// 版权信息
|
||||
// 版权归百小僧及百签科技(广东)有限公司所有。
|
||||
// 所有权利保留。
|
||||
// 官方网站:https://baiqian.com
|
||||
//
|
||||
// 许可证信息
|
||||
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
|
||||
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
namespace ThingsGateway.HttpRemote;
|
||||
|
||||
/// <summary>
|
||||
/// HTTP 声明式多部分表单对象内容特性
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Parameter)]
|
||||
public sealed class MultipartObjectAttribute : MultipartAttribute
|
||||
{
|
||||
/// <summary>
|
||||
/// <inheritdoc cref="MultipartObjectAttribute" />
|
||||
/// </summary>
|
||||
public MultipartObjectAttribute() => AsFormItem = false;
|
||||
}
|
@@ -11,11 +11,10 @@
|
||||
|
||||
// ReSharper disable ArrangeObjectCreationWhenTypeNotEvident
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
|
||||
namespace ThingsGateway.HttpRemote;
|
||||
|
||||
/// <summary>
|
||||
|
@@ -9,11 +9,10 @@
|
||||
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
using System.Net.Mime;
|
||||
using System.Reflection;
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
|
||||
namespace ThingsGateway.HttpRemote;
|
||||
|
||||
/// <summary>
|
||||
|
@@ -9,10 +9,9 @@
|
||||
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using System.Reflection;
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
using ThingsGateway.Utilities;
|
||||
using System.Reflection;
|
||||
|
||||
namespace ThingsGateway.HttpRemote;
|
||||
|
||||
|
@@ -9,10 +9,9 @@
|
||||
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using System.Reflection;
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
using ThingsGateway.Utilities;
|
||||
using System.Reflection;
|
||||
|
||||
namespace ThingsGateway.HttpRemote;
|
||||
|
||||
|
@@ -9,13 +9,12 @@
|
||||
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
using System.Globalization;
|
||||
using System.Net.Mime;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
|
||||
namespace ThingsGateway.HttpRemote;
|
||||
|
||||
/// <summary>
|
||||
|
@@ -9,10 +9,9 @@
|
||||
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using System.Reflection;
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
using ThingsGateway.Utilities;
|
||||
using System.Reflection;
|
||||
|
||||
namespace ThingsGateway.HttpRemote;
|
||||
|
||||
|
@@ -9,10 +9,9 @@
|
||||
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using System.Reflection;
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
using ThingsGateway.Utilities;
|
||||
using System.Reflection;
|
||||
|
||||
namespace ThingsGateway.HttpRemote;
|
||||
|
||||
|
@@ -9,11 +9,10 @@
|
||||
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Reflection;
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
|
||||
namespace ThingsGateway.HttpRemote;
|
||||
|
||||
/// <summary>
|
||||
|
@@ -9,11 +9,10 @@
|
||||
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
|
||||
namespace ThingsGateway.HttpRemote;
|
||||
|
||||
/// <summary>
|
||||
|
@@ -9,13 +9,11 @@
|
||||
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
|
||||
namespace ThingsGateway.HttpRemote.Extensions;
|
||||
|
||||
/// <summary>
|
||||
|
@@ -10,7 +10,6 @@
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
using System.Text;
|
||||
|
||||
namespace ThingsGateway.HttpRemote;
|
||||
|
@@ -9,18 +9,15 @@
|
||||
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
using ThingsGateway.Utilities;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
using ThingsGateway.Utilities;
|
||||
|
||||
using StringWithQualityHeaderValue = System.Net.Http.Headers.StringWithQualityHeaderValue;
|
||||
|
||||
namespace ThingsGateway.HttpRemote.Extensions;
|
||||
@@ -286,6 +283,11 @@ public static partial class HttpRemoteExtensions
|
||||
var charset = httpContent.Headers.ContentType?.CharSet ?? "utf-8";
|
||||
var partialContent = Encoding.GetEncoding(charset).GetString(buffer, 0, bytesToShow);
|
||||
|
||||
// 解决退格导致显示不全问题:保留 \n 和 \r,仅过滤其他 ASCII 控制字符(ASCII < 32 且不是 \n 或 \r)
|
||||
partialContent = new string(partialContent
|
||||
.Where(c => c >= 32 || c == '\n' || c == '\r')
|
||||
.ToArray());
|
||||
|
||||
// 检查是否是完整的 Unicode 转义字符串
|
||||
if (total == bytesToShow && UnicodeEscapeRegex().IsMatch(partialContent))
|
||||
{
|
||||
|
@@ -9,14 +9,14 @@
|
||||
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using ThingsGateway.Utilities;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Net.Mime;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using ThingsGateway.Utilities;
|
||||
|
||||
namespace ThingsGateway.HttpRemote;
|
||||
|
||||
/// <summary>
|
||||
@@ -213,6 +213,27 @@ internal static partial class Helpers
|
||||
$"{requestUri.Scheme}://{requestUri.Host}{(requestUri.IsDefaultPort ? string.Empty : $":{requestUri.Port}")}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据原始内容推断内容类型,失败时返回默认值
|
||||
/// </summary>
|
||||
/// <param name="rawContent">原始请求内容</param>
|
||||
/// <param name="defaultContentType">默认请求内容类型</param>
|
||||
/// <returns>
|
||||
/// <see cref="string" />
|
||||
/// </returns>
|
||||
internal static string GetContentTypeOrDefault(object? rawContent, string? defaultContentType) =>
|
||||
rawContent switch
|
||||
{
|
||||
JsonContent => MediaTypeNames.Application.Json,
|
||||
FormUrlEncodedContent => MediaTypeNames.Application.FormUrlEncoded,
|
||||
(byte[] or Stream or ByteArrayContent or StreamContent or ReadOnlyMemoryContent or ReadOnlyMemory<byte>)
|
||||
and not StringContent => MediaTypeNames.Application
|
||||
.Octet,
|
||||
MultipartContent => MediaTypeNames.Multipart.FormData,
|
||||
MultipartFile => MediaTypeNames.Application.Octet,
|
||||
_ => defaultContentType ?? Constants.TEXT_PLAIN_MIME_TYPE
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// <c>application/x-www-form-urlencoded</c> 格式正则表达式
|
||||
/// </summary>
|
||||
|
@@ -9,14 +9,12 @@
|
||||
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Channels;
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
|
||||
namespace ThingsGateway.HttpRemote;
|
||||
|
||||
/// <summary>
|
||||
@@ -44,9 +42,7 @@ internal sealed class FileDownloadManager
|
||||
/// <param name="httpFileDownloadBuilder">
|
||||
/// <see cref="HttpFileDownloadBuilder" />
|
||||
/// </param>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
internal FileDownloadManager(IHttpRemoteService httpRemoteService, HttpFileDownloadBuilder httpFileDownloadBuilder,
|
||||
Action<HttpRequestBuilder>? configure = null)
|
||||
internal FileDownloadManager(IHttpRemoteService httpRemoteService, HttpFileDownloadBuilder httpFileDownloadBuilder)
|
||||
{
|
||||
// 空检查
|
||||
ArgumentNullException.ThrowIfNull(httpRemoteService);
|
||||
@@ -65,7 +61,7 @@ internal sealed class FileDownloadManager
|
||||
|
||||
// 构建 HttpRequestBuilder 实例
|
||||
RequestBuilder = httpFileDownloadBuilder.Build(httpRemoteService.ServiceProvider
|
||||
.GetRequiredService<IOptions<HttpRemoteOptions>>().Value, configure);
|
||||
.GetRequiredService<IOptions<HttpRemoteOptions>>().Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@@ -9,14 +9,12 @@
|
||||
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Channels;
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
|
||||
namespace ThingsGateway.HttpRemote;
|
||||
|
||||
/// <summary>
|
||||
@@ -44,9 +42,7 @@ internal sealed class FileUploadManager
|
||||
/// <param name="httpFileUploadBuilder">
|
||||
/// <see cref="HttpFileUploadBuilder" />
|
||||
/// </param>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
internal FileUploadManager(IHttpRemoteService httpRemoteService, HttpFileUploadBuilder httpFileUploadBuilder,
|
||||
Action<HttpRequestBuilder>? configure = null)
|
||||
internal FileUploadManager(IHttpRemoteService httpRemoteService, HttpFileUploadBuilder httpFileUploadBuilder)
|
||||
{
|
||||
// 空检查
|
||||
ArgumentNullException.ThrowIfNull(httpRemoteService);
|
||||
@@ -65,7 +61,7 @@ internal sealed class FileUploadManager
|
||||
|
||||
// 构建 HttpRequestBuilder 实例
|
||||
RequestBuilder = httpFileUploadBuilder.Build(httpRemoteService.ServiceProvider
|
||||
.GetRequiredService<IOptions<HttpRemoteOptions>>().Value, _progressChannel, configure);
|
||||
.GetRequiredService<IOptions<HttpRemoteOptions>>().Value, _progressChannel);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@@ -9,13 +9,11 @@
|
||||
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using System.Threading.Channels;
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
|
||||
namespace ThingsGateway.HttpRemote;
|
||||
|
||||
/// <summary>
|
||||
@@ -38,9 +36,7 @@ internal sealed class LongPollingManager
|
||||
/// <param name="httpLongPollingBuilder">
|
||||
/// <see cref="HttpLongPollingBuilder" />
|
||||
/// </param>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
internal LongPollingManager(IHttpRemoteService httpRemoteService, HttpLongPollingBuilder httpLongPollingBuilder,
|
||||
Action<HttpRequestBuilder>? configure = null)
|
||||
internal LongPollingManager(IHttpRemoteService httpRemoteService, HttpLongPollingBuilder httpLongPollingBuilder)
|
||||
{
|
||||
// 空检查
|
||||
ArgumentNullException.ThrowIfNull(httpRemoteService);
|
||||
@@ -56,7 +52,7 @@ internal sealed class LongPollingManager
|
||||
|
||||
// 构建 HttpRequestBuilder 实例
|
||||
RequestBuilder = httpLongPollingBuilder.Build(httpRemoteService.ServiceProvider
|
||||
.GetRequiredService<IOptions<HttpRemoteOptions>>().Value, configure);
|
||||
.GetRequiredService<IOptions<HttpRemoteOptions>>().Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@@ -9,15 +9,13 @@
|
||||
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text;
|
||||
using System.Threading.Channels;
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
|
||||
namespace ThingsGateway.HttpRemote;
|
||||
|
||||
/// <summary>
|
||||
@@ -41,10 +39,8 @@ internal sealed class ServerSentEventsManager
|
||||
/// <param name="httpServerSentEventsBuilder">
|
||||
/// <see cref="HttpServerSentEventsBuilder" />
|
||||
/// </param>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
internal ServerSentEventsManager(IHttpRemoteService httpRemoteService,
|
||||
HttpServerSentEventsBuilder httpServerSentEventsBuilder,
|
||||
Action<HttpRequestBuilder>? configure = null)
|
||||
HttpServerSentEventsBuilder httpServerSentEventsBuilder)
|
||||
{
|
||||
// 空检查
|
||||
ArgumentNullException.ThrowIfNull(httpRemoteService);
|
||||
@@ -62,7 +58,7 @@ internal sealed class ServerSentEventsManager
|
||||
|
||||
// 构建 HttpRequestBuilder 实例
|
||||
RequestBuilder = httpServerSentEventsBuilder.Build(httpRemoteService.ServiceProvider
|
||||
.GetRequiredService<IOptions<HttpRemoteOptions>>().Value, configure);
|
||||
.GetRequiredService<IOptions<HttpRemoteOptions>>().Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -222,7 +218,7 @@ internal sealed class ServerSentEventsManager
|
||||
}
|
||||
|
||||
// 获取 HTTP 响应体中的内容流
|
||||
using var contentStream = (await httpResponseMessage.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false));
|
||||
using var contentStream = await httpResponseMessage.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// 初始化 StreamReader 实例
|
||||
using var streamReader = new StreamReader(contentStream, Encoding.UTF8);
|
||||
|
@@ -11,7 +11,6 @@
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace ThingsGateway.HttpRemote;
|
||||
@@ -36,10 +35,8 @@ internal sealed class StressTestHarnessManager
|
||||
/// <param name="httpStressTestHarnessBuilder">
|
||||
/// <see cref="HttpStressTestHarnessBuilder" />
|
||||
/// </param>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
internal StressTestHarnessManager(IHttpRemoteService httpRemoteService,
|
||||
HttpStressTestHarnessBuilder httpStressTestHarnessBuilder,
|
||||
Action<HttpRequestBuilder>? configure = null)
|
||||
HttpStressTestHarnessBuilder httpStressTestHarnessBuilder)
|
||||
{
|
||||
// 空检查
|
||||
ArgumentNullException.ThrowIfNull(httpRemoteService);
|
||||
@@ -50,7 +47,7 @@ internal sealed class StressTestHarnessManager
|
||||
|
||||
// 构建 HttpRequestBuilder 实例
|
||||
RequestBuilder = httpStressTestHarnessBuilder.Build(httpRemoteService.ServiceProvider
|
||||
.GetRequiredService<IOptions<HttpRemoteOptions>>().Value, configure);
|
||||
.GetRequiredService<IOptions<HttpRemoteOptions>>().Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@@ -22,141 +22,121 @@ internal sealed partial class HttpRemoteService
|
||||
public void DownloadFile(string? requestUri, string? destinationPath,
|
||||
Func<FileTransferProgress, Task>? onProgressChanged = null,
|
||||
FileExistsBehavior fileExistsBehavior = FileExistsBehavior.CreateNew,
|
||||
Action<HttpFileDownloadBuilder>? configure = null, Action<HttpRequestBuilder>? requestConfigure = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Action<HttpFileDownloadBuilder>? configure = null, CancellationToken cancellationToken = default) =>
|
||||
Send(
|
||||
HttpRequestBuilder.DownloadFile(requestUri, destinationPath, onProgressChanged, fileExistsBehavior,
|
||||
configure),
|
||||
requestConfigure, cancellationToken);
|
||||
configure), cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task DownloadFileAsync(string? requestUri, string? destinationPath,
|
||||
Func<FileTransferProgress, Task>? onProgressChanged = null,
|
||||
FileExistsBehavior fileExistsBehavior = FileExistsBehavior.CreateNew,
|
||||
Action<HttpFileDownloadBuilder>? configure = null, Action<HttpRequestBuilder>? requestConfigure = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Action<HttpFileDownloadBuilder>? configure = null, CancellationToken cancellationToken = default) =>
|
||||
SendAsync(
|
||||
HttpRequestBuilder.DownloadFile(requestUri, destinationPath, onProgressChanged, fileExistsBehavior,
|
||||
configure),
|
||||
requestConfigure, cancellationToken);
|
||||
configure), cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Send(HttpFileDownloadBuilder httpFileDownloadBuilder, Action<HttpRequestBuilder>? configure = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
new FileDownloadManager(this, httpFileDownloadBuilder, configure).Start(cancellationToken);
|
||||
public void Send(HttpFileDownloadBuilder httpFileDownloadBuilder, CancellationToken cancellationToken = default) =>
|
||||
new FileDownloadManager(this, httpFileDownloadBuilder).Start(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SendAsync(HttpFileDownloadBuilder httpFileDownloadBuilder, Action<HttpRequestBuilder>? configure = null,
|
||||
public Task SendAsync(HttpFileDownloadBuilder httpFileDownloadBuilder,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
new FileDownloadManager(this, httpFileDownloadBuilder, configure).StartAsync(cancellationToken);
|
||||
new FileDownloadManager(this, httpFileDownloadBuilder).StartAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public HttpResponseMessage? UploadFile(string? requestUri, string filePath, string name = "file",
|
||||
Func<FileTransferProgress, Task>? onProgressChanged = null, string? fileName = null,
|
||||
Action<HttpFileUploadBuilder>? configure = null, Action<HttpRequestBuilder>? requestConfigure = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Action<HttpFileUploadBuilder>? configure = null, CancellationToken cancellationToken = default) =>
|
||||
Send(HttpRequestBuilder.UploadFile(requestUri, filePath, name, onProgressChanged, fileName, configure),
|
||||
requestConfigure,
|
||||
cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<HttpResponseMessage?> UploadFileAsync(string? requestUri, string filePath, string name = "file",
|
||||
Func<FileTransferProgress, Task>? onProgressChanged = null, string? fileName = null,
|
||||
Action<HttpFileUploadBuilder>? configure = null, Action<HttpRequestBuilder>? requestConfigure = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Action<HttpFileUploadBuilder>? configure = null, CancellationToken cancellationToken = default) =>
|
||||
SendAsync(HttpRequestBuilder.UploadFile(requestUri, filePath, name, onProgressChanged, fileName, configure),
|
||||
requestConfigure,
|
||||
cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public HttpResponseMessage? Send(HttpFileUploadBuilder httpFileUploadBuilder,
|
||||
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) =>
|
||||
new FileUploadManager(this, httpFileUploadBuilder, configure).Start(cancellationToken);
|
||||
CancellationToken cancellationToken = default) =>
|
||||
new FileUploadManager(this, httpFileUploadBuilder).Start(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<HttpResponseMessage?> SendAsync(HttpFileUploadBuilder httpFileUploadBuilder,
|
||||
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) =>
|
||||
new FileUploadManager(this, httpFileUploadBuilder, configure).StartAsync(cancellationToken);
|
||||
CancellationToken cancellationToken = default) =>
|
||||
new FileUploadManager(this, httpFileUploadBuilder).StartAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ServerSentEvents(string? requestUri, Func<ServerSentEventsData, CancellationToken, Task> onMessage,
|
||||
Action<HttpServerSentEventsBuilder>? configure = null, Action<HttpRequestBuilder>? requestConfigure = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Send(HttpRequestBuilder.ServerSentEvents(requestUri, onMessage, configure), requestConfigure,
|
||||
cancellationToken);
|
||||
Action<HttpServerSentEventsBuilder>? configure = null, CancellationToken cancellationToken = default) =>
|
||||
Send(HttpRequestBuilder.ServerSentEvents(requestUri, onMessage, configure), cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task ServerSentEventsAsync(string? requestUri, Func<ServerSentEventsData, CancellationToken, Task> onMessage,
|
||||
Action<HttpServerSentEventsBuilder>? configure = null, Action<HttpRequestBuilder>? requestConfigure = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
SendAsync(HttpRequestBuilder.ServerSentEvents(requestUri, onMessage, configure), requestConfigure,
|
||||
cancellationToken);
|
||||
Action<HttpServerSentEventsBuilder>? configure = null, CancellationToken cancellationToken = default) =>
|
||||
SendAsync(HttpRequestBuilder.ServerSentEvents(requestUri, onMessage, configure), cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Send(HttpServerSentEventsBuilder httpServerSentEventsBuilder,
|
||||
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) =>
|
||||
new ServerSentEventsManager(this, httpServerSentEventsBuilder, configure).Start(cancellationToken);
|
||||
CancellationToken cancellationToken = default) =>
|
||||
new ServerSentEventsManager(this, httpServerSentEventsBuilder).Start(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SendAsync(HttpServerSentEventsBuilder httpServerSentEventsBuilder,
|
||||
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default) =>
|
||||
new ServerSentEventsManager(this, httpServerSentEventsBuilder, configure).StartAsync(cancellationToken);
|
||||
CancellationToken cancellationToken = default) =>
|
||||
new ServerSentEventsManager(this, httpServerSentEventsBuilder).StartAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public StressTestHarnessResult StressTestHarness(string? requestUri, int numberOfRequests = 100,
|
||||
Action<HttpStressTestHarnessBuilder>? configure = null, Action<HttpRequestBuilder>? requestConfigure = null,
|
||||
Action<HttpStressTestHarnessBuilder>? configure = null,
|
||||
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Send(HttpRequestBuilder.StressTestHarness(requestUri, numberOfRequests, configure), requestConfigure,
|
||||
completionOption, cancellationToken);
|
||||
Send(HttpRequestBuilder.StressTestHarness(requestUri, numberOfRequests, configure), completionOption,
|
||||
cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<StressTestHarnessResult> StressTestHarnessAsync(string? requestUri, int numberOfRequests = 100,
|
||||
Action<HttpStressTestHarnessBuilder>? configure = null, Action<HttpRequestBuilder>? requestConfigure = null,
|
||||
Action<HttpStressTestHarnessBuilder>? configure = null,
|
||||
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
SendAsync(HttpRequestBuilder.StressTestHarness(requestUri, numberOfRequests, configure), requestConfigure,
|
||||
completionOption, cancellationToken);
|
||||
SendAsync(HttpRequestBuilder.StressTestHarness(requestUri, numberOfRequests, configure), completionOption,
|
||||
cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public StressTestHarnessResult Send(HttpStressTestHarnessBuilder httpStressTestHarnessBuilder,
|
||||
Action<HttpRequestBuilder>? configure = null,
|
||||
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
new StressTestHarnessManager(this, httpStressTestHarnessBuilder, configure).Start(completionOption,
|
||||
new StressTestHarnessManager(this, httpStressTestHarnessBuilder).Start(completionOption,
|
||||
cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<StressTestHarnessResult> SendAsync(HttpStressTestHarnessBuilder httpStressTestHarnessBuilder,
|
||||
Action<HttpRequestBuilder>? configure = null,
|
||||
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
new StressTestHarnessManager(this, httpStressTestHarnessBuilder, configure).StartAsync(completionOption,
|
||||
new StressTestHarnessManager(this, httpStressTestHarnessBuilder).StartAsync(completionOption,
|
||||
cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void LongPolling(string? requestUri, Func<HttpResponseMessage, CancellationToken, Task> onDataReceived,
|
||||
Action<HttpLongPollingBuilder>? configure = null, Action<HttpRequestBuilder>? requestConfigure = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Send(HttpRequestBuilder.LongPolling(requestUri, onDataReceived, configure), requestConfigure,
|
||||
cancellationToken);
|
||||
Action<HttpLongPollingBuilder>? configure = null, CancellationToken cancellationToken = default) =>
|
||||
Send(HttpRequestBuilder.LongPolling(requestUri, onDataReceived, configure), cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task LongPollingAsync(string? requestUri, Func<HttpResponseMessage, CancellationToken, Task> onDataReceived,
|
||||
Action<HttpLongPollingBuilder>? configure = null, Action<HttpRequestBuilder>? requestConfigure = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
SendAsync(HttpRequestBuilder.LongPolling(requestUri, onDataReceived, configure), requestConfigure,
|
||||
cancellationToken);
|
||||
Action<HttpLongPollingBuilder>? configure = null, CancellationToken cancellationToken = default) =>
|
||||
SendAsync(HttpRequestBuilder.LongPolling(requestUri, onDataReceived, configure), cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Send(HttpLongPollingBuilder httpLongPollingBuilder, Action<HttpRequestBuilder>? configure = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
new LongPollingManager(this, httpLongPollingBuilder, configure).Start(cancellationToken);
|
||||
public void Send(HttpLongPollingBuilder httpLongPollingBuilder, CancellationToken cancellationToken = default) =>
|
||||
new LongPollingManager(this, httpLongPollingBuilder).Start(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SendAsync(HttpLongPollingBuilder httpLongPollingBuilder, Action<HttpRequestBuilder>? configure = null,
|
||||
public Task SendAsync(HttpLongPollingBuilder httpLongPollingBuilder,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
new LongPollingManager(this, httpLongPollingBuilder, configure).StartAsync(cancellationToken);
|
||||
new LongPollingManager(this, httpLongPollingBuilder).StartAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public object? Declarative(MethodInfo method, object[] args) =>
|
||||
|
@@ -9,17 +9,16 @@
|
||||
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
|
||||
namespace ThingsGateway.HttpRemote;
|
||||
|
||||
/// <summary>
|
||||
@@ -346,8 +345,7 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService
|
||||
// 初始化 HttpRemoteResult 实例
|
||||
var httpRemoteResult = new HttpRemoteResult<TResult>(httpResponseMessage)
|
||||
{
|
||||
Result = result,
|
||||
RequestDuration = requestDuration
|
||||
Result = result, RequestDuration = requestDuration
|
||||
};
|
||||
|
||||
return httpRemoteResult;
|
||||
@@ -380,8 +378,7 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService
|
||||
// 初始化 HttpRemoteResult 实例
|
||||
var httpRemoteResult = new HttpRemoteResult<TResult>(httpResponseMessage)
|
||||
{
|
||||
Result = result,
|
||||
RequestDuration = requestDuration
|
||||
Result = result, RequestDuration = requestDuration
|
||||
};
|
||||
|
||||
return httpRemoteResult;
|
||||
|
@@ -28,15 +28,13 @@ public partial interface IHttpRemoteService
|
||||
/// <see cref="FileExistsBehavior" />
|
||||
/// </param>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
/// <param name="requestConfigure">自定义配置委托</param>
|
||||
/// <param name="cancellationToken">
|
||||
/// <see cref="CancellationToken" />
|
||||
/// </param>
|
||||
void DownloadFile(string? requestUri, string? destinationPath,
|
||||
Func<FileTransferProgress, Task>? onProgressChanged = null,
|
||||
FileExistsBehavior fileExistsBehavior = FileExistsBehavior.CreateNew,
|
||||
Action<HttpFileDownloadBuilder>? configure = null, Action<HttpRequestBuilder>? requestConfigure = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
Action<HttpFileDownloadBuilder>? configure = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 下载文件
|
||||
@@ -48,7 +46,6 @@ public partial interface IHttpRemoteService
|
||||
/// <see cref="FileExistsBehavior" />
|
||||
/// </param>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
/// <param name="requestConfigure">自定义配置委托</param>
|
||||
/// <param name="cancellationToken">
|
||||
/// <see cref="CancellationToken" />
|
||||
/// </param>
|
||||
@@ -58,8 +55,7 @@ public partial interface IHttpRemoteService
|
||||
Task DownloadFileAsync(string? requestUri, string? destinationPath,
|
||||
Func<FileTransferProgress, Task>? onProgressChanged = null,
|
||||
FileExistsBehavior fileExistsBehavior = FileExistsBehavior.CreateNew,
|
||||
Action<HttpFileDownloadBuilder>? configure = null, Action<HttpRequestBuilder>? requestConfigure = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
Action<HttpFileDownloadBuilder>? configure = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 下载文件
|
||||
@@ -67,12 +63,10 @@ public partial interface IHttpRemoteService
|
||||
/// <param name="httpFileDownloadBuilder">
|
||||
/// <see cref="HttpFileDownloadBuilder" />
|
||||
/// </param>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
/// <param name="cancellationToken">
|
||||
/// <see cref="CancellationToken" />
|
||||
/// </param>
|
||||
void Send(HttpFileDownloadBuilder httpFileDownloadBuilder, Action<HttpRequestBuilder>? configure = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
void Send(HttpFileDownloadBuilder httpFileDownloadBuilder, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 下载文件
|
||||
@@ -80,15 +74,13 @@ public partial interface IHttpRemoteService
|
||||
/// <param name="httpFileDownloadBuilder">
|
||||
/// <see cref="HttpFileDownloadBuilder" />
|
||||
/// </param>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
/// <param name="cancellationToken">
|
||||
/// <see cref="CancellationToken" />
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// <see cref="Task" />
|
||||
/// </returns>
|
||||
Task SendAsync(HttpFileDownloadBuilder httpFileDownloadBuilder, Action<HttpRequestBuilder>? configure = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
Task SendAsync(HttpFileDownloadBuilder httpFileDownloadBuilder, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 上传文件
|
||||
@@ -99,7 +91,6 @@ public partial interface IHttpRemoteService
|
||||
/// <param name="onProgressChanged">用于传输进度发生变化时执行的委托</param>
|
||||
/// <param name="fileName">文件的名称</param>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
/// <param name="requestConfigure">自定义配置委托</param>
|
||||
/// <param name="cancellationToken">
|
||||
/// <see cref="CancellationToken" />
|
||||
/// </param>
|
||||
@@ -108,8 +99,7 @@ public partial interface IHttpRemoteService
|
||||
/// </returns>
|
||||
HttpResponseMessage? UploadFile(string? requestUri, string filePath, string name = "file",
|
||||
Func<FileTransferProgress, Task>? onProgressChanged = null, string? fileName = null,
|
||||
Action<HttpFileUploadBuilder>? configure = null, Action<HttpRequestBuilder>? requestConfigure = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
Action<HttpFileUploadBuilder>? configure = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 上传文件
|
||||
@@ -120,7 +110,6 @@ public partial interface IHttpRemoteService
|
||||
/// <param name="onProgressChanged">用于传输进度发生变化时执行的委托</param>
|
||||
/// <param name="fileName">文件的名称</param>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
/// <param name="requestConfigure">自定义配置委托</param>
|
||||
/// <param name="cancellationToken">
|
||||
/// <see cref="CancellationToken" />
|
||||
/// </param>
|
||||
@@ -129,8 +118,7 @@ public partial interface IHttpRemoteService
|
||||
/// </returns>
|
||||
Task<HttpResponseMessage?> UploadFileAsync(string? requestUri, string filePath, string name = "file",
|
||||
Func<FileTransferProgress, Task>? onProgressChanged = null, string? fileName = null,
|
||||
Action<HttpFileUploadBuilder>? configure = null, Action<HttpRequestBuilder>? requestConfigure = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
Action<HttpFileUploadBuilder>? configure = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 上传文件
|
||||
@@ -138,14 +126,13 @@ public partial interface IHttpRemoteService
|
||||
/// <param name="httpFileUploadBuilder">
|
||||
/// <see cref="HttpFileUploadBuilder" />
|
||||
/// </param>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
/// <param name="cancellationToken">
|
||||
/// <see cref="CancellationToken" />
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// <see cref="HttpResponseMessage" />
|
||||
/// </returns>
|
||||
HttpResponseMessage? Send(HttpFileUploadBuilder httpFileUploadBuilder, Action<HttpRequestBuilder>? configure = null,
|
||||
HttpResponseMessage? Send(HttpFileUploadBuilder httpFileUploadBuilder,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
@@ -154,7 +141,6 @@ public partial interface IHttpRemoteService
|
||||
/// <param name="httpFileUploadBuilder">
|
||||
/// <see cref="HttpFileUploadBuilder" />
|
||||
/// </param>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
/// <param name="cancellationToken">
|
||||
/// <see cref="CancellationToken" />
|
||||
/// </param>
|
||||
@@ -162,20 +148,6 @@ public partial interface IHttpRemoteService
|
||||
/// <see cref="Task{TResult}" />
|
||||
/// </returns>
|
||||
Task<HttpResponseMessage?> SendAsync(HttpFileUploadBuilder httpFileUploadBuilder,
|
||||
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 发送 Server-Sent Events 请求
|
||||
/// </summary>
|
||||
/// <param name="requestUri">请求地址</param>
|
||||
/// <param name="onMessage">用于在从事件源接收到数据时的操作</param>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
/// <param name="requestConfigure">自定义配置委托</param>
|
||||
/// <param name="cancellationToken">
|
||||
/// <see cref="CancellationToken" />
|
||||
/// </param>
|
||||
void ServerSentEvents(string? requestUri, Func<ServerSentEventsData, CancellationToken, Task> onMessage,
|
||||
Action<HttpServerSentEventsBuilder>? configure = null, Action<HttpRequestBuilder>? requestConfigure = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
@@ -184,7 +156,18 @@ public partial interface IHttpRemoteService
|
||||
/// <param name="requestUri">请求地址</param>
|
||||
/// <param name="onMessage">用于在从事件源接收到数据时的操作</param>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
/// <param name="requestConfigure">自定义配置委托</param>
|
||||
/// <param name="cancellationToken">
|
||||
/// <see cref="CancellationToken" />
|
||||
/// </param>
|
||||
void ServerSentEvents(string? requestUri, Func<ServerSentEventsData, CancellationToken, Task> onMessage,
|
||||
Action<HttpServerSentEventsBuilder>? configure = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 发送 Server-Sent Events 请求
|
||||
/// </summary>
|
||||
/// <param name="requestUri">请求地址</param>
|
||||
/// <param name="onMessage">用于在从事件源接收到数据时的操作</param>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
/// <param name="cancellationToken">
|
||||
/// <see cref="CancellationToken" />
|
||||
/// </param>
|
||||
@@ -192,8 +175,7 @@ public partial interface IHttpRemoteService
|
||||
/// <see cref="Task" />
|
||||
/// </returns>
|
||||
Task ServerSentEventsAsync(string? requestUri, Func<ServerSentEventsData, CancellationToken, Task> onMessage,
|
||||
Action<HttpServerSentEventsBuilder>? configure = null, Action<HttpRequestBuilder>? requestConfigure = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
Action<HttpServerSentEventsBuilder>? configure = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 发送 Server-Sent Events 请求
|
||||
@@ -201,12 +183,10 @@ public partial interface IHttpRemoteService
|
||||
/// <param name="httpServerSentEventsBuilder">
|
||||
/// <see cref="HttpServerSentEventsBuilder" />
|
||||
/// </param>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
/// <param name="cancellationToken">
|
||||
/// <see cref="CancellationToken" />
|
||||
/// </param>
|
||||
void Send(HttpServerSentEventsBuilder httpServerSentEventsBuilder, Action<HttpRequestBuilder>? configure = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
void Send(HttpServerSentEventsBuilder httpServerSentEventsBuilder, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 发送 Server-Sent Events 请求
|
||||
@@ -214,7 +194,6 @@ public partial interface IHttpRemoteService
|
||||
/// <param name="httpServerSentEventsBuilder">
|
||||
/// <see cref="HttpServerSentEventsBuilder" />
|
||||
/// </param>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
/// <param name="cancellationToken">
|
||||
/// <see cref="CancellationToken" />
|
||||
/// </param>
|
||||
@@ -222,7 +201,7 @@ public partial interface IHttpRemoteService
|
||||
/// <see cref="Task" />
|
||||
/// </returns>
|
||||
Task SendAsync(HttpServerSentEventsBuilder httpServerSentEventsBuilder,
|
||||
Action<HttpRequestBuilder>? configure = null, CancellationToken cancellationToken = default);
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 压力测试
|
||||
@@ -230,7 +209,6 @@ public partial interface IHttpRemoteService
|
||||
/// <param name="requestUri">请求地址</param>
|
||||
/// <param name="numberOfRequests">并发请求数量,默认值为:100。</param>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
/// <param name="requestConfigure">自定义配置委托</param>
|
||||
/// <param name="completionOption">
|
||||
/// <see cref="HttpCompletionOption" />
|
||||
/// </param>
|
||||
@@ -241,7 +219,7 @@ public partial interface IHttpRemoteService
|
||||
/// <see cref="StressTestHarnessResult" />
|
||||
/// </returns>
|
||||
StressTestHarnessResult StressTestHarness(string? requestUri, int numberOfRequests = 100,
|
||||
Action<HttpStressTestHarnessBuilder>? configure = null, Action<HttpRequestBuilder>? requestConfigure = null,
|
||||
Action<HttpStressTestHarnessBuilder>? configure = null,
|
||||
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
@@ -251,7 +229,6 @@ public partial interface IHttpRemoteService
|
||||
/// <param name="requestUri">请求地址</param>
|
||||
/// <param name="numberOfRequests">并发请求数量,默认值为:100。</param>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
/// <param name="requestConfigure">自定义配置委托</param>
|
||||
/// <param name="completionOption">
|
||||
/// <see cref="HttpCompletionOption" />
|
||||
/// </param>
|
||||
@@ -262,7 +239,7 @@ public partial interface IHttpRemoteService
|
||||
/// <see cref="Task{TResult}" />
|
||||
/// </returns>
|
||||
Task<StressTestHarnessResult> StressTestHarnessAsync(string? requestUri, int numberOfRequests = 100,
|
||||
Action<HttpStressTestHarnessBuilder>? configure = null, Action<HttpRequestBuilder>? requestConfigure = null,
|
||||
Action<HttpStressTestHarnessBuilder>? configure = null,
|
||||
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
@@ -272,7 +249,6 @@ public partial interface IHttpRemoteService
|
||||
/// <param name="httpStressTestHarnessBuilder">
|
||||
/// <see cref="HttpStressTestHarnessBuilder" />
|
||||
/// </param>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
/// <param name="completionOption">
|
||||
/// <see cref="HttpCompletionOption" />
|
||||
/// </param>
|
||||
@@ -283,7 +259,6 @@ public partial interface IHttpRemoteService
|
||||
/// <see cref="StressTestHarnessResult" />
|
||||
/// </returns>
|
||||
StressTestHarnessResult Send(HttpStressTestHarnessBuilder httpStressTestHarnessBuilder,
|
||||
Action<HttpRequestBuilder>? configure = null,
|
||||
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
@@ -293,7 +268,6 @@ public partial interface IHttpRemoteService
|
||||
/// <param name="httpStressTestHarnessBuilder">
|
||||
/// <see cref="HttpStressTestHarnessBuilder" />
|
||||
/// </param>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
/// <param name="completionOption">
|
||||
/// <see cref="HttpCompletionOption" />
|
||||
/// </param>
|
||||
@@ -304,7 +278,6 @@ public partial interface IHttpRemoteService
|
||||
/// <see cref="Task{TResult}" />
|
||||
/// </returns>
|
||||
Task<StressTestHarnessResult> SendAsync(HttpStressTestHarnessBuilder httpStressTestHarnessBuilder,
|
||||
Action<HttpRequestBuilder>? configure = null,
|
||||
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
@@ -314,13 +287,11 @@ public partial interface IHttpRemoteService
|
||||
/// <param name="requestUri">请求地址</param>
|
||||
/// <param name="onDataReceived">用于接收服务器返回 <c>200~299</c> 状态码的数据的操作</param>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
/// <param name="requestConfigure">自定义配置委托</param>
|
||||
/// <param name="cancellationToken">
|
||||
/// <see cref="CancellationToken" />
|
||||
/// </param>
|
||||
void LongPolling(string? requestUri, Func<HttpResponseMessage, CancellationToken, Task> onDataReceived,
|
||||
Action<HttpLongPollingBuilder>? configure = null, Action<HttpRequestBuilder>? requestConfigure = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
Action<HttpLongPollingBuilder>? configure = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 发送长轮询请求
|
||||
@@ -328,7 +299,6 @@ public partial interface IHttpRemoteService
|
||||
/// <param name="requestUri">请求地址</param>
|
||||
/// <param name="onDataReceived">用于接收服务器返回 <c>200~299</c> 状态码的数据的操作</param>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
/// <param name="requestConfigure">自定义配置委托</param>
|
||||
/// <param name="cancellationToken">
|
||||
/// <see cref="CancellationToken" />
|
||||
/// </param>
|
||||
@@ -336,8 +306,7 @@ public partial interface IHttpRemoteService
|
||||
/// <see cref="Task" />
|
||||
/// </returns>
|
||||
Task LongPollingAsync(string? requestUri, Func<HttpResponseMessage, CancellationToken, Task> onDataReceived,
|
||||
Action<HttpLongPollingBuilder>? configure = null, Action<HttpRequestBuilder>? requestConfigure = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
Action<HttpLongPollingBuilder>? configure = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 发送长轮询请求
|
||||
@@ -345,12 +314,10 @@ public partial interface IHttpRemoteService
|
||||
/// <param name="httpLongPollingBuilder">
|
||||
/// <see cref="HttpLongPollingBuilder" />
|
||||
/// </param>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
/// <param name="cancellationToken">
|
||||
/// <see cref="CancellationToken" />
|
||||
/// </param>
|
||||
void Send(HttpLongPollingBuilder httpLongPollingBuilder, Action<HttpRequestBuilder>? configure = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
void Send(HttpLongPollingBuilder httpLongPollingBuilder, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 发送长轮询请求
|
||||
@@ -358,15 +325,13 @@ public partial interface IHttpRemoteService
|
||||
/// <param name="httpLongPollingBuilder">
|
||||
/// <see cref="HttpLongPollingBuilder" />
|
||||
/// </param>
|
||||
/// <param name="configure">自定义配置委托</param>
|
||||
/// <param name="cancellationToken">
|
||||
/// <see cref="CancellationToken" />
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// <see cref="Task" />
|
||||
/// </returns>
|
||||
Task SendAsync(HttpLongPollingBuilder httpLongPollingBuilder, Action<HttpRequestBuilder>? configure = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
Task SendAsync(HttpLongPollingBuilder httpLongPollingBuilder, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 发送 HTTP 声明式请求
|
||||
|
@@ -556,7 +556,7 @@ public partial class Clay
|
||||
}
|
||||
|
||||
// 进行下一级查找
|
||||
currentNode = ((Clay?)currentValue)?.FindNode(identifiers[i]);
|
||||
currentNode = ((Clay)currentValue).FindNode(identifiers[i]);
|
||||
if (currentNode is null)
|
||||
{
|
||||
return null;
|
||||
@@ -934,6 +934,67 @@ public partial class Clay
|
||||
return Remove(new Range(start, end));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据标识符(路径)删除数据
|
||||
/// </summary>
|
||||
/// <param name="identifier">带路径的标识符</param>
|
||||
/// <param name="isPath">是否是带路径的标识符</param>
|
||||
/// <returns>
|
||||
/// <see cref="bool" />
|
||||
/// </returns>
|
||||
public bool Remove(string identifier, bool isPath) => isPath ? RemovePathValue(identifier) : Remove(identifier);
|
||||
|
||||
/// <summary>
|
||||
/// 根据路径删除值
|
||||
/// </summary>
|
||||
/// <param name="path">带路径的标识符</param>
|
||||
/// <returns>
|
||||
/// <see cref="bool" />
|
||||
/// </returns>
|
||||
public bool RemovePathValue(string path)
|
||||
{
|
||||
// 空检查
|
||||
ArgumentNullException.ThrowIfNull(path);
|
||||
|
||||
// 根据路径分隔符进行分割,并确保至少有一个标识符
|
||||
var identifiers = path.Split(Options.PathSeparator, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (identifiers is { Length: 0 })
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 根据标识符查找 JsonNode 节点
|
||||
var currentNode = FindNode(identifiers[0]);
|
||||
if (currentNode is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 遍历剩余的标识符
|
||||
for (var i = 1; i < identifiers.Length; i++)
|
||||
{
|
||||
// 将 currentNode 转换为对象实例
|
||||
var currentValue = DeserializeNode(currentNode, Options);
|
||||
|
||||
// 检查是否是 Clay 类型
|
||||
if (!IsClay(currentValue))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The identifier `{identifiers[i - 1]}` at path `{identifiers[i - 1]}:{identifiers[i]}` does not support further lookup.");
|
||||
}
|
||||
|
||||
// 进行下一级查找
|
||||
currentNode = ((Clay)currentValue).FindNode(identifiers[i]);
|
||||
if (currentNode is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 从父节点删除
|
||||
return ((Clay)DeserializeNode(currentNode.Parent, Options)!).Remove(identifiers[^1]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据标识符删除数据
|
||||
/// </summary>
|
||||
@@ -960,6 +1021,25 @@ public partial class Clay
|
||||
|
||||
return Remove(start, end);
|
||||
}
|
||||
/// <summary>
|
||||
/// 根据标识符(路径)删除数据
|
||||
/// </summary>
|
||||
/// <param name="identifier">带路径的标识符</param>
|
||||
/// <param name="isPath">是否是带路径的标识符</param>
|
||||
/// <returns>
|
||||
/// <see cref="bool" />
|
||||
/// </returns>
|
||||
public bool Delete(string identifier, bool isPath) => isPath ? RemovePathValue(identifier) : Remove(identifier);
|
||||
|
||||
/// <summary>
|
||||
/// 根据路径删除值
|
||||
/// </summary>
|
||||
/// <param name="path">带路径的标识符</param>
|
||||
/// <returns>
|
||||
/// <see cref="bool" />
|
||||
/// </returns>
|
||||
public bool DeletePathValue(string path) => RemovePathValue(path);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 将 <see cref="Clay" /> 转换为目标类型
|
||||
|
@@ -95,6 +95,7 @@ public static class FS
|
||||
fileExtensionProvider.Mappings[".properties"] = "application/octet-stream";
|
||||
fileExtensionProvider.Mappings[".m3u8"] = "application/x-mpegURL";
|
||||
fileExtensionProvider.Mappings[".ofd"] = "application/ofd";
|
||||
fileExtensionProvider.Mappings[".jsonl"] = "application/json-lines";
|
||||
return fileExtensionProvider;
|
||||
}
|
||||
}
|
@@ -1,3 +1,3 @@
|
||||
https://gitee.com/NewLifeX/X/commit/bcb90e8bffbfe998c0b6e9167c96c60e3ae6371f
|
||||
https://gitee.com/NewLifeX/X/commit/a07810c61ba0e9dc792ab1af13ebf7c8a3a5d2da
|
||||
|
||||
修改大部分CA规则,后期优化减少字符串操作
|
@@ -13,13 +13,21 @@ public static class SpanHelper
|
||||
/// <param name="span"></param>
|
||||
/// <param name="encoding"></param>
|
||||
/// <returns></returns>
|
||||
public static String ToStr(this ReadOnlySpan<Byte> span, Encoding? encoding = null) => (encoding ?? Encoding.UTF8).GetString(span);
|
||||
public static String ToStr(this ReadOnlySpan<Byte> span, Encoding? encoding = null)
|
||||
{
|
||||
if (span.Length == 0) return String.Empty;
|
||||
return (encoding ?? Encoding.UTF8).GetString(span);
|
||||
}
|
||||
|
||||
/// <summary>转字符串</summary>
|
||||
/// <param name="span"></param>
|
||||
/// <param name="encoding"></param>
|
||||
/// <returns></returns>
|
||||
public static String ToStr(this Span<Byte> span, Encoding? encoding = null) => (encoding ?? Encoding.UTF8).GetString(span);
|
||||
public static String ToStr(this Span<Byte> span, Encoding? encoding = null)
|
||||
{
|
||||
if (span.Length == 0) return String.Empty;
|
||||
return (encoding ?? Encoding.UTF8).GetString(span);
|
||||
}
|
||||
|
||||
/// <summary>获取字符串的字节数组</summary>
|
||||
public static unsafe Int32 GetBytes(this Encoding encoding, ReadOnlySpan<Char> chars, Span<Byte> bytes)
|
||||
|
@@ -201,15 +201,17 @@ public class DefaultConvert
|
||||
// 特殊处理字符串,也是最常见的
|
||||
if (value is String str)
|
||||
{
|
||||
if (Int32.TryParse(str, out var n)) return n;
|
||||
|
||||
// 拷贝而来的逗号分隔整数
|
||||
Span<Char> tmp = stackalloc Char[str.Length];
|
||||
var rs = TrimNumber(str.AsSpan(), tmp);
|
||||
if (rs == 0) return defaultValue;
|
||||
|
||||
#if NETCOREAPP || NETSTANDARD2_1_OR_GREATER
|
||||
return Int32.TryParse(tmp[..rs], out var n) ? n : defaultValue;
|
||||
return Int32.TryParse(tmp[..rs], out n) ? n : defaultValue;
|
||||
#else
|
||||
return Int32.TryParse(tmp[..rs].ToString(), out var n) ? n : defaultValue;
|
||||
return Int32.TryParse(tmp[..rs].ToString(), out n) ? n : defaultValue;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -290,15 +292,17 @@ public class DefaultConvert
|
||||
// 特殊处理字符串,也是最常见的
|
||||
if (value is String str)
|
||||
{
|
||||
if (Int64.TryParse(str, out var n)) return n;
|
||||
|
||||
// 拷贝而来的逗号分隔整数
|
||||
Span<Char> tmp = stackalloc Char[str.Length];
|
||||
var rs = TrimNumber(str.AsSpan(), tmp);
|
||||
if (rs == 0) return defaultValue;
|
||||
|
||||
#if NETCOREAPP || NETSTANDARD2_1_OR_GREATER
|
||||
return Int64.TryParse(tmp[..rs], out var n) ? n : defaultValue;
|
||||
return Int64.TryParse(tmp[..rs], out n) ? n : defaultValue;
|
||||
#else
|
||||
return Int64.TryParse(new String(tmp[..rs].ToArray()), out var n) ? n : defaultValue;
|
||||
return Int64.TryParse(new String(tmp[..rs].ToArray()), out n) ? n : defaultValue;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -374,14 +378,16 @@ public class DefaultConvert
|
||||
// 特殊处理字符串,也是最常见的
|
||||
if (value is String str)
|
||||
{
|
||||
if (Double.TryParse(str, out var n)) return n;
|
||||
|
||||
Span<Char> tmp = stackalloc Char[str.Length];
|
||||
var rs = TrimNumber(str.AsSpan(), tmp);
|
||||
if (rs == 0) return defaultValue;
|
||||
|
||||
#if NETCOREAPP || NETSTANDARD2_1_OR_GREATER
|
||||
return Double.TryParse(tmp[..rs], out var n) ? n : defaultValue;
|
||||
return Double.TryParse(tmp[..rs], out n) ? n : defaultValue;
|
||||
#else
|
||||
return Double.TryParse(new String(tmp[..rs].ToArray()), out var n) ? n : defaultValue;
|
||||
return Double.TryParse(new String(tmp[..rs].ToArray()), out n) ? n : defaultValue;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -422,14 +428,16 @@ public class DefaultConvert
|
||||
// 特殊处理字符串,也是最常见的
|
||||
if (value is String str)
|
||||
{
|
||||
if (Decimal.TryParse(str, NumberStyles.Number | NumberStyles.AllowExponent, null, out var n)) return n;
|
||||
|
||||
Span<Char> tmp = stackalloc Char[str.Length];
|
||||
var rs = TrimNumber(str.AsSpan(), tmp);
|
||||
if (rs == 0) return defaultValue;
|
||||
|
||||
#if NETCOREAPP || NETSTANDARD2_1_OR_GREATER
|
||||
return Decimal.TryParse(tmp[..rs], out var n) ? n : defaultValue;
|
||||
return Decimal.TryParse(tmp[..rs], out n) ? n : defaultValue;
|
||||
#else
|
||||
return Decimal.TryParse(new String(tmp[..rs].ToArray()), out var n) ? n : defaultValue;
|
||||
return Decimal.TryParse(new String(tmp[..rs].ToArray()), out n) ? n : defaultValue;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -577,11 +585,16 @@ public class DefaultConvert
|
||||
|
||||
// 处理UTC
|
||||
var utc = false;
|
||||
if (str.EndsWithIgnoreCase(" UTC") || str.EndsWith('Z') && str.Contains('T'))
|
||||
if (str.EndsWithIgnoreCase(" UTC"))
|
||||
{
|
||||
utc = true;
|
||||
str = str[0..^4];
|
||||
}
|
||||
else if (str.EndsWith('Z'))
|
||||
{
|
||||
utc = true;
|
||||
str = str[0..^1];
|
||||
}
|
||||
|
||||
if (!DateTime.TryParse(str, out var dt) &&
|
||||
!(str.Contains('-') && DateTime.TryParseExact(str, "yyyy-M-d", null, DateTimeStyles.None, out dt)) &&
|
||||
@@ -715,9 +728,19 @@ public class DefaultConvert
|
||||
ch = (Char)(input[i] - 0xFEE0);
|
||||
|
||||
// 数字和小数点 以外字符,认为非数字
|
||||
if (ch is not '.' and not '-' and (< '0' or > '9')) return 0;
|
||||
|
||||
output[idx++] = ch;
|
||||
if (ch is '.' or '-' or not < '0' and not > '9')
|
||||
output[idx++] = ch;
|
||||
else
|
||||
{
|
||||
// 支持科学计数法,e/E后面可以跟正负号,之后至少跟一个数字
|
||||
if ((ch is 'e' or 'E') && idx > 0 && i + 2 < input.Length && (input[i + 1] is '+' or '-'))
|
||||
{
|
||||
output[idx++] = ch;
|
||||
output[idx++] = input[++i];
|
||||
}
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return idx;
|
||||
|
@@ -1,4 +1,5 @@
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
@@ -102,10 +103,13 @@ public static class PacketHelper
|
||||
{
|
||||
// 总是有异常数据,这里屏蔽异常
|
||||
if (pk == null) return null!;
|
||||
if (pk.Total == 0) return String.Empty;
|
||||
|
||||
if (pk.Next == null)
|
||||
{
|
||||
if (count < 0) count = pk.Length - offset;
|
||||
if (count == 0) return String.Empty;
|
||||
|
||||
var span = pk.GetSpan();
|
||||
if (span.Length > count) span = span[..count];
|
||||
|
||||
@@ -301,6 +305,32 @@ public static class PacketHelper
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>尝试扩展头部,用于填充包头,减少内存分配</summary>
|
||||
/// <param name="pk">数据包</param>
|
||||
/// <param name="size">要扩大的头部大小,不包括负载数据</param>
|
||||
/// <param name="newPacket">扩展后的数据包</param>
|
||||
/// <returns></returns>
|
||||
[Obsolete]
|
||||
public static Boolean TryExpandHeader(this IPacket pk, Int32 size, [NotNullWhen(true)] out IPacket? newPacket)
|
||||
{
|
||||
newPacket = null;
|
||||
|
||||
if (pk is ArrayPacket ap && ap.Offset >= size)
|
||||
{
|
||||
newPacket = new ArrayPacket(ap.Buffer, ap.Offset - size, ap.Length + size) { Next = ap.Next };
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (pk is OwnerPacket owner && owner.Offset >= size)
|
||||
{
|
||||
newPacket = new OwnerPacket(owner, size);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>扩展头部,用于填充包头,减少内存分配</summary>
|
||||
/// <param name="pk">数据包</param>
|
||||
/// <param name="size">要扩大的头部大小,不包括负载数据</param>
|
||||
|
@@ -59,7 +59,7 @@ public abstract class Logger : ILog
|
||||
/// <param name="format"></param>
|
||||
/// <param name="args"></param>
|
||||
/// <returns></returns>
|
||||
protected virtual String Format(String format, Object?[]? args)
|
||||
internal protected virtual String Format(String format, Object?[]? args)
|
||||
{
|
||||
//处理时间的格式化
|
||||
if (args?.Length > 0)
|
||||
@@ -70,7 +70,7 @@ public abstract class Logger : ILog
|
||||
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
{
|
||||
if (args[i] != null && args[i] is DateTime dt)
|
||||
if (args[i] != null && args[i] is DateTime dt && format.Contains("{" + i + "}"))
|
||||
{
|
||||
// 根据时间值的精确度选择不同的格式化输出
|
||||
//var dt = (DateTime)args[i];
|
||||
|
@@ -37,6 +37,9 @@ public static class XTrace
|
||||
/// <param name="msg">信息</param>
|
||||
public static void WriteLine(String msg)
|
||||
{
|
||||
// 只过滤null,保留包含空格的字符串
|
||||
if (msg == null) return;
|
||||
|
||||
if (!InitLog()) return;
|
||||
|
||||
WriteVersion();
|
||||
@@ -49,6 +52,9 @@ public static class XTrace
|
||||
/// <param name="args"></param>
|
||||
public static void WriteLine(String format, params Object?[] args)
|
||||
{
|
||||
// 只过滤null,保留包含空格的字符串
|
||||
if (format == null) return;
|
||||
|
||||
if (!InitLog()) return;
|
||||
|
||||
WriteVersion();
|
||||
|
@@ -104,11 +104,14 @@ public abstract class Actor : DisposeBase, IActor
|
||||
#endregion
|
||||
|
||||
#region 方法
|
||||
|
||||
/// <summary>通知开始处理</summary>
|
||||
/// <remarks>
|
||||
/// 添加消息时自动触发
|
||||
/// </remarks>
|
||||
public virtual Task? Start()
|
||||
/// <param name="cancellationToken">取消令牌。可用于通知内部取消工作</param>
|
||||
/// <returns></returns>
|
||||
public virtual Task? Start(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (Active) return _task;
|
||||
lock (this)
|
||||
@@ -119,7 +122,7 @@ public abstract class Actor : DisposeBase, IActor
|
||||
|
||||
using var span = Tracer?.NewSpan("actor:Start", Name);
|
||||
|
||||
_source = new CancellationTokenSource();
|
||||
_source = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
MailBox ??= new BlockingCollection<ActorContext>(BoundedCapacity);
|
||||
|
||||
// 启动异步
|
||||
@@ -127,7 +130,7 @@ public abstract class Actor : DisposeBase, IActor
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
_task ??= OnStart();
|
||||
_task ??= OnStart(_source.Token);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +142,13 @@ public abstract class Actor : DisposeBase, IActor
|
||||
|
||||
/// <summary>开始时,返回执行线程包装任务</summary>
|
||||
/// <returns></returns>
|
||||
protected virtual Task OnStart() => Task.Factory.StartNew(DoActorWork, LongRunning ? TaskCreationOptions.LongRunning : TaskCreationOptions.None);
|
||||
protected virtual Task OnStart(CancellationToken cancellationToken)
|
||||
{
|
||||
var creationOptions = LongRunning ? TaskCreationOptions.LongRunning : TaskCreationOptions.None;
|
||||
var scheduler = TaskScheduler.Current ?? TaskScheduler.Default;
|
||||
return Task.Factory.StartNew(DoActorWork, cancellationToken, creationOptions, scheduler);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>通知停止添加消息,并等待处理完成</summary>
|
||||
/// <param name="msTimeout">等待的毫秒数。0表示不等待,-1表示无限等待</param>
|
||||
|
@@ -330,7 +330,7 @@ public class NetServer : DisposeBase, IServer, IExtend, ILogFeature
|
||||
{
|
||||
EnsureCreateServer();
|
||||
|
||||
if (Servers.Count == 0) throw new Exception("Failed to listen to all ports!");
|
||||
if (Servers.Count == 0) throw new Exception($"Failed to listen to all ports! Port=[{Port}]");
|
||||
|
||||
WriteLog("准备开始监听{0}个服务器", Servers.Count);
|
||||
|
||||
|
@@ -724,14 +724,22 @@ public class DefaultReflect : IReflect
|
||||
return value.ToDateTime();
|
||||
case TypeCode.Double:
|
||||
return value.ToDouble();
|
||||
case TypeCode.Single:
|
||||
return (Single)value.ToDouble();
|
||||
case TypeCode.Decimal:
|
||||
return value.ToDecimal();
|
||||
case TypeCode.Int16:
|
||||
return (Int16)value.ToInt();
|
||||
case TypeCode.Int32:
|
||||
return value.ToInt();
|
||||
case TypeCode.Int64:
|
||||
return value.ToLong();
|
||||
case TypeCode.UInt16:
|
||||
return (UInt16)value.ToInt();
|
||||
case TypeCode.UInt32:
|
||||
return (UInt32)value.ToInt();
|
||||
case TypeCode.UInt64:
|
||||
return (UInt64)value.ToLong();
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -758,8 +766,20 @@ public class DefaultReflect : IReflect
|
||||
// 支持IParsable<TSelf>接口
|
||||
if (conversionType.GetInterfaces().Any(e => e.IsGenericType && e.GetGenericTypeDefinition() == typeof(IParsable<>)))
|
||||
{
|
||||
var mi = conversionType.GetMethod("Parse", [typeof(String), typeof(IFormatProvider)]);
|
||||
if (mi != null) return mi.Invoke(null, [value, null]);
|
||||
// 获取 TryParse 静态方法
|
||||
var tryParse = conversionType.GetMethod("TryParse", [typeof(String), typeof(IFormatProvider), conversionType.MakeByRefType()]);
|
||||
if (tryParse != null)
|
||||
{
|
||||
var parameters = new Object?[] { str, null, null };
|
||||
var success = (Boolean)tryParse.Invoke(null, parameters)!;
|
||||
if (success) return parameters[2];
|
||||
//return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
var mi = conversionType.GetMethod("Parse", [typeof(String), typeof(IFormatProvider)]);
|
||||
if (mi != null) return mi.Invoke(null, [value, null]);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
@@ -10,7 +10,8 @@
|
||||
<AssemblyTitle>工具核心库</AssemblyTitle>
|
||||
<Description>ThingsGateway.NewLife.X</Description>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
|
||||
<SignAssembly>True</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>newlife.snk</AssemblyOriginatorKeyFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@@ -32,8 +32,12 @@ public static class PluginHelper
|
||||
var file = string.Empty;
|
||||
if (!dll.IsNullOrEmpty())
|
||||
{
|
||||
// 先检查当前目录,再检查插件目录
|
||||
file = dll.GetCurrentPath();
|
||||
// 先检查程序集所在目录,再检查当前目录、基准目录和插件目录。在应用发布时,插件很可能跟常规应用程序集放在同一目录下
|
||||
var exe = Assembly.GetEntryAssembly()?.Location;
|
||||
if (exe.IsNullOrEmpty()) exe = Assembly.GetCallingAssembly()?.Location;
|
||||
if (exe.IsNullOrEmpty()) exe = Assembly.GetExecutingAssembly()?.Location;
|
||||
if (!exe.IsNullOrEmpty()) file = Path.GetDirectoryName(exe).CombinePath(dll).GetFullPath();
|
||||
if (!File.Exists(file)) file = dll.GetCurrentPath();
|
||||
if (!File.Exists(file)) file = dll.GetFullPath();
|
||||
if (!File.Exists(file)) file = dll.GetBasePath();
|
||||
if (!File.Exists(file)) file = set.PluginPath.CombinePath(dll).GetFullPath();
|
||||
|
@@ -3,6 +3,7 @@
|
||||
/// <summary>资源定位。无限制解析Url地址</summary>
|
||||
public class UriInfo
|
||||
{
|
||||
#region 属性
|
||||
/// <summary>协议</summary>
|
||||
public String? Scheme { get; set; }
|
||||
|
||||
@@ -31,10 +32,6 @@ public class UriInfo
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>实例化</summary>
|
||||
/// <param name="value"></param>
|
||||
public UriInfo(String value) => Parse(value);
|
||||
|
||||
/// <summary>主机与端口。省略默认端口</summary>
|
||||
public String? Authority
|
||||
{
|
||||
@@ -51,12 +48,33 @@ public class UriInfo
|
||||
return $"{Host}:{Port}";
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 构造
|
||||
/// <summary>实例化</summary>
|
||||
public UriInfo() { }
|
||||
|
||||
/// <summary>实例化</summary>
|
||||
/// <param name="value"></param>
|
||||
public UriInfo(String value) => Parse(value);
|
||||
#endregion
|
||||
|
||||
#region 方法
|
||||
/// <summary>尝试解析Url字符串</summary>
|
||||
public static Boolean TryParse(String? value, out UriInfo? uriInfo)
|
||||
{
|
||||
uriInfo = null;
|
||||
if (value.IsNullOrWhiteSpace()) return false;
|
||||
|
||||
uriInfo = new UriInfo();
|
||||
return uriInfo.Parse(value);
|
||||
}
|
||||
|
||||
/// <summary>解析Url字符串</summary>
|
||||
/// <param name="value"></param>
|
||||
public void Parse(String value)
|
||||
public Boolean Parse(String value)
|
||||
{
|
||||
if (value.IsNullOrWhiteSpace()) return;
|
||||
if (value.IsNullOrWhiteSpace()) return false;
|
||||
|
||||
// 先处理头尾,再处理中间的主机和端口
|
||||
var p = value.IndexOf("://");
|
||||
@@ -89,7 +107,13 @@ public class UriInfo
|
||||
}
|
||||
}
|
||||
|
||||
// 如果主要部分都没有,标记为失败
|
||||
if (Scheme.IsNullOrEmpty() && Host.IsNullOrEmpty() && AbsolutePath.IsNullOrEmpty())
|
||||
return false;
|
||||
|
||||
if (AbsolutePath.IsNullOrEmpty()) AbsolutePath = "/";
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ParsePath(String value, Int32 p)
|
||||
@@ -122,6 +146,32 @@ public class UriInfo
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>拼接请求参数</summary>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
public UriInfo Append(String name, Object? value)
|
||||
{
|
||||
var q = Query;
|
||||
Query = q.IsNullOrEmpty() ? $"{name}={value}" : $"{q}&{name}={value}";
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>拼接请求参数(非空)</summary>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
public UriInfo AppendNotEmpty(String name, Object? value)
|
||||
{
|
||||
if (value?.ToString().IsNullOrEmpty() != false) return this;
|
||||
|
||||
var q = Query;
|
||||
Query = q.IsNullOrEmpty() ? $"{name}={value}" : $"{q}&{name}={value}";
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>已重载。</summary>
|
||||
/// <returns></returns>
|
||||
public override String? ToString()
|
||||
@@ -136,4 +186,5 @@ public class UriInfo
|
||||
|
||||
return $"{Scheme}://{authority}{PathAndQuery}";
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
BIN
src/Admin/ThingsGateway.NewLife.X/newlife.snk
Normal file
BIN
src/Admin/ThingsGateway.NewLife.X/newlife.snk
Normal file
Binary file not shown.
Reference in New Issue
Block a user