diff --git a/X1.sln b/X1.sln index 185d4e4..fd311c4 100644 --- a/X1.sln +++ b/X1.sln @@ -1,4 +1,5 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 + +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 @@ -16,6 +17,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "X1.Infrastructure", "src\X1 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "X1.WebSocket", "src\X1.WebSocket\X1.WebSocket.csproj", "{155D6C93-9A46-45CD-B21F-0F60719515D0}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "X1.DynamicClientCore", "src\X1.DynamicClientCore\X1.DynamicClientCore.csproj", "{6266232F-62BA-47E8-9046-7EA01E3DFD01}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -46,6 +49,10 @@ Global {155D6C93-9A46-45CD-B21F-0F60719515D0}.Debug|Any CPU.Build.0 = Debug|Any CPU {155D6C93-9A46-45CD-B21F-0F60719515D0}.Release|Any CPU.ActiveCfg = Release|Any CPU {155D6C93-9A46-45CD-B21F-0F60719515D0}.Release|Any CPU.Build.0 = Release|Any CPU + {6266232F-62BA-47E8-9046-7EA01E3DFD01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6266232F-62BA-47E8-9046-7EA01E3DFD01}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6266232F-62BA-47E8-9046-7EA01E3DFD01}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6266232F-62BA-47E8-9046-7EA01E3DFD01}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -57,5 +64,6 @@ Global {FE169440-2554-4810-97D6-CC44808AAA56} = {C0426B52-8F01-41DE-966C-ADC8DD078638} {6CF192C7-060B-4328-B9F4-B05BD7401232} = {C0426B52-8F01-41DE-966C-ADC8DD078638} {155D6C93-9A46-45CD-B21F-0F60719515D0} = {C0426B52-8F01-41DE-966C-ADC8DD078638} + {6266232F-62BA-47E8-9046-7EA01E3DFD01} = {C0426B52-8F01-41DE-966C-ADC8DD078638} EndGlobalSection EndGlobal diff --git a/src/X1.DynamicClientCore/Core/DynamicHttpClient.Core.cs b/src/X1.DynamicClientCore/Core/DynamicHttpClient.Core.cs new file mode 100644 index 0000000..35aaac4 --- /dev/null +++ b/src/X1.DynamicClientCore/Core/DynamicHttpClient.Core.cs @@ -0,0 +1,234 @@ +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using X1.DynamicClientCore.Models; + +namespace X1.DynamicClientCore.Core +{ + /// + /// DynamicHttpClient 核心执行部分 + /// 包含核心的HTTP请求执行逻辑 + /// + public partial class DynamicHttpClient + { + /// + /// 执行HTTP请求的统一方法 + /// + private async Task ExecuteRequestAsync(string serviceName, string endpoint, HttpMethod method, + object? data, RequestOptions? options, CancellationToken cancellationToken = default) + { + _logger.LogDebug("开始执行异步请求: {ServiceName}:{Endpoint}, 方法: {Method}", serviceName, endpoint, method); + + int requestTimeout = 0; // 声明超时变量,供异常处理使用 + + try + { + // 1. 获取服务端点 + _logger.LogDebug("正在获取服务端点: {ServiceName}", serviceName); + var serviceEndpoint = _endpointManager.GetEndpoint(serviceName); + if (serviceEndpoint == null) + { + _logger.LogWarning("服务端点未找到: {ServiceName}", serviceName); + throw new DynamicHttpClientException( + $"服务 '{serviceName}' 未找到", + DynamicHttpClientExceptionType.ServiceNotFound, + serviceName, + endpoint); + } + + if (!serviceEndpoint.Enabled) + { + _logger.LogWarning("服务端点已禁用: {ServiceName}", serviceName); + throw new DynamicHttpClientException( + $"服务 '{serviceName}' 已禁用", + DynamicHttpClientExceptionType.ServiceDisabled, + serviceName, + endpoint); + } + + _logger.LogDebug("服务端点获取成功: {ServiceName}, URL: {FullUrl}, 超时: {Timeout}s", + serviceName, serviceEndpoint.GetFullUrl(), serviceEndpoint.Timeout); + + // 2. 创建HTTP客户端 + _logger.LogDebug("正在创建HTTP客户端"); + var httpClient = _httpClientFactory.CreateClient(); + requestTimeout = options?.Timeout ?? serviceEndpoint.Timeout; + httpClient.Timeout = TimeSpan.FromSeconds(requestTimeout); + _logger.LogDebug("HTTP客户端创建完成,超时设置: {Timeout}s", requestTimeout); + + // 3. 构建请求URL + var url = $"{serviceEndpoint.GetFullUrl()}/{endpoint.TrimStart('/')}"; + _logger.LogDebug("构建请求URL: {Url}", url); + var request = new HttpRequestMessage(method, url); + + // 4. 添加请求头 + if (options?.Headers != null) + { + _logger.LogDebug("添加自定义请求头,数量: {HeaderCount}", options.Headers.Count); + foreach (var header in options.Headers) + { + request.Headers.Add(header.Key, header.Value); + _logger.LogDebug("添加请求头: {Key} = {Value}", header.Key, header.Value); + } + } + else + { + // 添加默认的JSON请求头 + request.Headers.Add("Accept", "application/json"); + _logger.LogDebug("添加默认JSON请求头: Accept = application/json"); + } + + // 5. 添加请求体 + if (data != null && (method == HttpMethod.Post || method == HttpMethod.Put)) + { + _logger.LogDebug("准备序列化请求数据,数据类型: {DataType}", data.GetType().Name); + try + { + var json = JsonConvert.SerializeObject(data); + _logger.LogDebug("请求数据序列化成功,长度: {JsonLength} 字符", json.Length); + request.Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json"); + _logger.LogDebug("请求体设置完成"); + } + catch (Exception ex) + { + _logger.LogError(ex, "请求数据序列化失败: {ServiceName}:{Endpoint}", serviceName, endpoint); + throw new DynamicHttpClientException( + "请求数据序列化失败", + DynamicHttpClientExceptionType.SerializationError, + serviceName, + endpoint, + innerException: ex); + } + } + else + { + _logger.LogDebug("无请求体数据"); + } + + // 6. 获取熔断器并执行请求 + HttpResponseMessage response; + + if (options?.EnableCircuitBreaker == true) + { + _logger.LogDebug("启用熔断器保护"); + var circuitBreaker = _circuitBreakerManager.GetOrCreateCircuitBreaker( + serviceEndpoint.Name, options?.CircuitBreaker); + _logger.LogDebug("熔断器获取成功: {ServiceName}", serviceEndpoint.Name); + + _logger.LogInformation("发送请求(带熔断器): {Method} {Url}", method, url); + response = await circuitBreaker.ExecuteAsync(async () => await httpClient.SendAsync(request, cancellationToken)); + } + else + { + _logger.LogDebug("跳过熔断器保护"); + _logger.LogInformation("发送请求(无熔断器): {Method} {Url}", method, url); + response = await httpClient.SendAsync(request, cancellationToken); + } + + _logger.LogDebug("收到响应: {StatusCode} {ReasonPhrase}", response.StatusCode, response.ReasonPhrase); + + if (response.IsSuccessStatusCode) + { + _logger.LogDebug("开始读取响应内容"); + var content = await response.Content.ReadAsStringAsync(cancellationToken); + _logger.LogDebug("响应内容读取完成,长度: {ContentLength} 字符", content.Length); + _logger.LogInformation("请求成功: {StatusCode}", response.StatusCode); + + if (typeof(T) == typeof(string)) + { + _logger.LogDebug("返回字符串类型响应"); + return (T)(object)content; + } + + _logger.LogDebug("开始反序列化响应数据,目标类型: {TargetType}", typeof(T).Name); + try + { + var result = JsonConvert.DeserializeObject(content); + _logger.LogDebug("响应数据反序列化成功"); + return result; + } + catch (Exception ex) + { + _logger.LogError(ex, "响应数据反序列化失败: {ServiceName}:{Endpoint}, 目标类型: {TargetType}", + serviceName, endpoint, typeof(T).Name); + throw new DynamicHttpClientException( + "响应数据反序列化失败", + DynamicHttpClientExceptionType.SerializationError, + serviceName, + endpoint, + (int)response.StatusCode, + ex); + } + } + else + { + _logger.LogError("请求失败: {StatusCode} {ReasonPhrase}, URL: {Url}", + response.StatusCode, response.ReasonPhrase, url); + throw new DynamicHttpClientException( + $"HTTP请求失败: {response.StatusCode} {response.ReasonPhrase}", + DynamicHttpClientExceptionType.HttpRequestFailed, + serviceName, + endpoint, + (int)response.StatusCode); + } + } + catch (DynamicHttpClientException ex) + { + // 记录自定义异常,然后重新抛出 + _logger.LogError(ex, "动态HTTP客户端异步异常: {ServiceName}:{Endpoint}, 类型: {ExceptionType}, 消息: {Message}", + serviceName, endpoint, ex.ExceptionType, ex.Message); + throw; + } + catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) + { + // 超时异常 + _logger.LogWarning("异步请求超时: {ServiceName}:{Endpoint}, 超时时间: {Timeout}s", + serviceName, endpoint, requestTimeout); + throw new DynamicHttpClientException( + "请求超时", + DynamicHttpClientExceptionType.Timeout, + serviceName, + endpoint, + innerException: ex); + } + catch (OperationCanceledException ex) + { + // 请求被取消 + _logger.LogInformation("异步请求被取消: {ServiceName}:{Endpoint}", serviceName, endpoint); + throw new DynamicHttpClientException( + "请求被取消", + DynamicHttpClientExceptionType.RequestCanceled, + serviceName, + endpoint, + innerException: ex); + } + catch (HttpRequestException ex) + { + // HTTP请求异常 + _logger.LogError(ex, "HTTP异步请求异常: {ServiceName}:{Endpoint}, 消息: {Message}", + serviceName, endpoint, ex.Message); + throw new DynamicHttpClientException( + "网络请求失败", + DynamicHttpClientExceptionType.NetworkError, + serviceName, + endpoint, + innerException: ex); + } + catch (Exception ex) + { + // 其他未知异常 + _logger.LogError(ex, "异步请求未知异常: {ServiceName}:{Endpoint}, 异常类型: {ExceptionType}, 消息: {Message}", + serviceName, endpoint, ex.GetType().Name, ex.Message); + throw new DynamicHttpClientException( + "请求执行失败", + DynamicHttpClientExceptionType.Unknown, + serviceName, + endpoint, + innerException: ex); + } + finally + { + _logger.LogDebug("异步请求执行完成: {ServiceName}:{Endpoint}", serviceName, endpoint); + } + } + } +} \ No newline at end of file diff --git a/src/X1.DynamicClientCore/Core/DynamicHttpClient.Sync.cs b/src/X1.DynamicClientCore/Core/DynamicHttpClient.Sync.cs new file mode 100644 index 0000000..60335af --- /dev/null +++ b/src/X1.DynamicClientCore/Core/DynamicHttpClient.Sync.cs @@ -0,0 +1,49 @@ +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using X1.DynamicClientCore.Models; + +namespace X1.DynamicClientCore.Core +{ + /// + /// DynamicHttpClient 同步方法部分 + /// 提供同步HTTP请求功能 + /// + public partial class DynamicHttpClient + { + #region 同步HTTP方法 + + /// + /// 发送同步GET请求 + /// + public T? Get(string serviceName, string endpoint, RequestOptions? options = null) + { + return ExecuteRequest(serviceName, endpoint, HttpMethod.Get, null, options); + } + + /// + /// 发送同步POST请求 + /// + public T? Post(string serviceName, string endpoint, object? data = null, RequestOptions? options = null) + { + return ExecuteRequest(serviceName, endpoint, HttpMethod.Post, data, options); + } + + /// + /// 发送同步PUT请求 + /// + public T? Put(string serviceName, string endpoint, object? data = null, RequestOptions? options = null) + { + return ExecuteRequest(serviceName, endpoint, HttpMethod.Put, data, options); + } + + /// + /// 发送同步DELETE请求 + /// + public T? Delete(string serviceName, string endpoint, RequestOptions? options = null) + { + return ExecuteRequest(serviceName, endpoint, HttpMethod.Delete, null, options); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/X1.DynamicClientCore/Core/DynamicHttpClient.SyncCore.cs b/src/X1.DynamicClientCore/Core/DynamicHttpClient.SyncCore.cs new file mode 100644 index 0000000..85097e4 --- /dev/null +++ b/src/X1.DynamicClientCore/Core/DynamicHttpClient.SyncCore.cs @@ -0,0 +1,225 @@ +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Polly; +using Polly.CircuitBreaker; +using X1.DynamicClientCore.Models; + +namespace X1.DynamicClientCore.Core +{ + /// + /// DynamicHttpClient 同步核心执行部分 + /// 包含同步HTTP请求的核心执行逻辑 + /// + public partial class DynamicHttpClient + { + /// + /// 执行同步HTTP请求的统一方法 + /// 避免使用 GetAwaiter().GetResult() 防止死锁 + /// + private T? ExecuteRequest(string serviceName, string endpoint, HttpMethod method, object? data, RequestOptions? options) + { + _logger.LogDebug("开始执行同步请求: {ServiceName}:{Endpoint}, 方法: {Method}", serviceName, endpoint, method); + + int requestTimeout = 0; // 声明超时变量,供异常处理使用 + + try + { + // 1. 获取服务端点 + _logger.LogDebug("正在获取服务端点: {ServiceName}", serviceName); + var serviceEndpoint = _endpointManager.GetEndpoint(serviceName); + if (serviceEndpoint == null) + { + _logger.LogWarning("服务端点未找到: {ServiceName}", serviceName); + throw new DynamicHttpClientException( + $"服务 '{serviceName}' 未找到", + DynamicHttpClientExceptionType.ServiceNotFound, + serviceName, + endpoint); + } + + if (!serviceEndpoint.Enabled) + { + _logger.LogWarning("服务端点已禁用: {ServiceName}", serviceName); + throw new DynamicHttpClientException( + $"服务 '{serviceName}' 已禁用", + DynamicHttpClientExceptionType.ServiceDisabled, + serviceName, + endpoint); + } + + _logger.LogDebug("服务端点获取成功: {ServiceName}, URL: {FullUrl}, 超时: {Timeout}s", + serviceName, serviceEndpoint.GetFullUrl(), serviceEndpoint.Timeout); + + // 2. 创建HTTP客户端 + _logger.LogDebug("正在创建HTTP客户端"); + var httpClient = _httpClientFactory.CreateClient(); + requestTimeout = options?.Timeout ?? serviceEndpoint.Timeout; + httpClient.Timeout = TimeSpan.FromSeconds(requestTimeout); + _logger.LogDebug("HTTP客户端创建完成,超时设置: {Timeout}s", requestTimeout); + + // 3. 构建请求URL + var url = $"{serviceEndpoint.GetFullUrl()}/{endpoint.TrimStart('/')}"; + _logger.LogDebug("构建请求URL: {Url}", url); + var request = new HttpRequestMessage(method, url); + + // 4. 添加请求头 + if (options?.Headers != null) + { + _logger.LogDebug("添加自定义请求头,数量: {HeaderCount}", options.Headers.Count); + foreach (var header in options.Headers) + { + request.Headers.Add(header.Key, header.Value); + _logger.LogDebug("添加请求头: {Key} = {Value}", header.Key, header.Value); + } + } + else + { + // 添加默认的JSON请求头 + request.Headers.Add("Accept", "application/json"); + _logger.LogDebug("添加默认JSON请求头: Accept = application/json"); + } + + // 5. 添加请求体 + if (data != null && (method == HttpMethod.Post || method == HttpMethod.Put)) + { + _logger.LogDebug("准备序列化请求数据,数据类型: {DataType}", data.GetType().Name); + try + { + var json = JsonConvert.SerializeObject(data); + _logger.LogDebug("请求数据序列化成功,长度: {JsonLength} 字符", json.Length); + request.Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json"); + _logger.LogDebug("请求体设置完成"); + } + catch (Exception ex) + { + _logger.LogError(ex, "请求数据序列化失败: {ServiceName}:{Endpoint}", serviceName, endpoint); + throw new DynamicHttpClientException( + "请求数据序列化失败", + DynamicHttpClientExceptionType.SerializationError, + serviceName, + endpoint, + innerException: ex); + } + } + else + { + _logger.LogDebug("无请求体数据"); + } + + // 6. 获取熔断器并执行同步请求 + HttpResponseMessage response; + + if (options?.EnableCircuitBreaker == true) + { + _logger.LogDebug("启用同步熔断器保护"); + var circuitBreaker = _circuitBreakerManager.GetOrCreateSyncCircuitBreaker( + serviceEndpoint.Name, options?.CircuitBreaker); + _logger.LogDebug("同步熔断器获取成功: {ServiceName}", serviceEndpoint.Name); + + _logger.LogInformation("发送同步请求(带熔断器): {Method} {Url}", method, url); + response = circuitBreaker.Execute(() => httpClient.Send(request)); + } + else + { + _logger.LogDebug("跳过同步熔断器保护"); + _logger.LogInformation("发送同步请求(无熔断器): {Method} {Url}", method, url); + response = httpClient.Send(request); + } + _logger.LogDebug("收到响应: {StatusCode} {ReasonPhrase}", response.StatusCode, response.ReasonPhrase); + + if (response.IsSuccessStatusCode) + { + _logger.LogDebug("开始读取响应内容"); + var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + _logger.LogDebug("响应内容读取完成,长度: {ContentLength} 字符", content.Length); + _logger.LogInformation("同步请求成功: {StatusCode}", response.StatusCode); + + if (typeof(T) == typeof(string)) + { + _logger.LogDebug("返回字符串类型响应"); + return (T)(object)content; + } + + _logger.LogDebug("开始反序列化响应数据,目标类型: {TargetType}", typeof(T).Name); + try + { + var result = JsonConvert.DeserializeObject(content); + _logger.LogDebug("响应数据反序列化成功"); + return result; + } + catch (Exception ex) + { + _logger.LogError(ex, "响应数据反序列化失败: {ServiceName}:{Endpoint}, 目标类型: {TargetType}", + serviceName, endpoint, typeof(T).Name); + throw new DynamicHttpClientException( + "响应数据反序列化失败", + DynamicHttpClientExceptionType.SerializationError, + serviceName, + endpoint, + (int)response.StatusCode, + ex); + } + } + else + { + _logger.LogError("同步请求失败: {StatusCode} {ReasonPhrase}, URL: {Url}", + response.StatusCode, response.ReasonPhrase, url); + throw new DynamicHttpClientException( + $"HTTP请求失败: {response.StatusCode} {response.ReasonPhrase}", + DynamicHttpClientExceptionType.HttpRequestFailed, + serviceName, + endpoint, + (int)response.StatusCode); + } + } + catch (DynamicHttpClientException ex) + { + // 记录自定义异常,然后重新抛出 + _logger.LogError(ex, "动态HTTP客户端同步异常: {ServiceName}:{Endpoint}, 类型: {ExceptionType}, 消息: {Message}", + serviceName, endpoint, ex.ExceptionType, ex.Message); + throw; + } + + catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) + { + // 超时异常 + _logger.LogWarning("同步请求超时: {ServiceName}:{Endpoint}, 超时时间: {Timeout}s", + serviceName, endpoint, requestTimeout); + throw new DynamicHttpClientException( + "请求超时", + DynamicHttpClientExceptionType.Timeout, + serviceName, + endpoint, + innerException: ex); + } + catch (HttpRequestException ex) + { + // HTTP请求异常 + _logger.LogError(ex, "HTTP同步请求异常: {ServiceName}:{Endpoint}, 消息: {Message}", + serviceName, endpoint, ex.Message); + throw new DynamicHttpClientException( + "网络请求失败", + DynamicHttpClientExceptionType.NetworkError, + serviceName, + endpoint, + innerException: ex); + } + catch (Exception ex) + { + // 其他未知异常 + _logger.LogError(ex, "同步请求未知异常: {ServiceName}:{Endpoint}, 异常类型: {ExceptionType}, 消息: {Message}", + serviceName, endpoint, ex.GetType().Name, ex.Message); + throw new DynamicHttpClientException( + "请求执行失败", + DynamicHttpClientExceptionType.Unknown, + serviceName, + endpoint, + innerException: ex); + } + finally + { + _logger.LogDebug("同步请求执行完成: {ServiceName}:{Endpoint}", serviceName, endpoint); + } + } + } +} \ No newline at end of file diff --git a/src/X1.DynamicClientCore/Core/DynamicHttpClient.cs b/src/X1.DynamicClientCore/Core/DynamicHttpClient.cs new file mode 100644 index 0000000..af8d02f --- /dev/null +++ b/src/X1.DynamicClientCore/Core/DynamicHttpClient.cs @@ -0,0 +1,59 @@ +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using X1.DynamicClientCore.Interfaces; +using X1.DynamicClientCore.Models; + +namespace X1.DynamicClientCore.Core +{ + /// + /// 动态HTTP客户端实现 + /// 支持同步/异步HTTP请求和文件操作 + /// 使用partial类拆分,便于维护和扩展 + /// + public partial class DynamicHttpClient : IDynamicHttpClient + { + private readonly IHttpClientFactory _httpClientFactory; + private readonly IServiceEndpointManager _endpointManager; + private readonly ICircuitBreakerManager _circuitBreakerManager; + private readonly ILogger _logger; + + /// + /// 初始化动态HTTP客户端 + /// + public DynamicHttpClient( + IHttpClientFactory httpClientFactory, + IServiceEndpointManager endpointManager, + ICircuitBreakerManager circuitBreakerManager, + ILogger logger) + { + _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); + _endpointManager = endpointManager ?? throw new ArgumentNullException(nameof(endpointManager)); + _circuitBreakerManager = circuitBreakerManager ?? throw new ArgumentNullException(nameof(circuitBreakerManager)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + #region 异步HTTP方法 + + public async Task GetAsync(string serviceName, string endpoint, RequestOptions? options = null, CancellationToken cancellationToken = default) + { + return await ExecuteRequestAsync(serviceName, endpoint, HttpMethod.Get, null, options, cancellationToken); + } + + public async Task PostAsync(string serviceName, string endpoint, object? data = null, RequestOptions? options = null, CancellationToken cancellationToken = default) + { + return await ExecuteRequestAsync(serviceName, endpoint, HttpMethod.Post, data, options, cancellationToken); + } + + public async Task PutAsync(string serviceName, string endpoint, object? data = null, RequestOptions? options = null, CancellationToken cancellationToken = default) + { + return await ExecuteRequestAsync(serviceName, endpoint, HttpMethod.Put, data, options, cancellationToken); + } + + public async Task DeleteAsync(string serviceName, string endpoint, RequestOptions? options = null, CancellationToken cancellationToken = default) + { + return await ExecuteRequestAsync(serviceName, endpoint, HttpMethod.Delete, null, options, cancellationToken); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/X1.DynamicClientCore/Extensions/ServiceCollectionExtensions.cs b/src/X1.DynamicClientCore/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..953182f --- /dev/null +++ b/src/X1.DynamicClientCore/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,200 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using X1.DynamicClientCore.Interfaces; +using X1.DynamicClientCore.Models; +using X1.DynamicClientCore.Core; +using X1.DynamicClientCore.Infrastructure; + +namespace X1.DynamicClientCore.Extensions +{ + /// + /// 服务集合扩展类 + /// + public static class ServiceCollectionExtensions + { + /// + /// 添加动态服务客户端 + /// + /// 服务集合 + /// 配置委托 + /// 服务集合 + public static IServiceCollection AddDynamicServiceClient( + this IServiceCollection services, + Action? configure = null) + { + if (services == null) + throw new ArgumentNullException(nameof(services)); + + // 注册HttpClient工厂 + services.AddHttpClient(); + + // 配置选项 + var options = new DynamicServiceClientOptions(); + configure?.Invoke(options); + + // 创建并配置服务端点管理器 + var endpointManager = CreateAndConfigureEndpointManager(options); + + // 注册核心服务(单例) + services.AddSingleton(endpointManager); + services.AddSingleton(); + + // 注册动态HTTP客户端(单例) + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + return services; + } + + /// + /// 创建并配置服务端点管理器 + /// + /// 配置选项 + /// 配置好的服务端点管理器 + private static ServiceEndpointManager CreateAndConfigureEndpointManager(DynamicServiceClientOptions options) + { + var endpointManager = new ServiceEndpointManager(); + + if (options.PreConfiguredEndpoints?.Any() == true) + { + var validEndpoints = options.PreConfiguredEndpoints + .Where(endpoint => endpoint != null) + .ToList(); + + // 验证端点配置 + if (options.ValidateEndpoints) + { + var invalidEndpoints = validEndpoints + .Where(endpoint => !endpoint.IsValid()) + .ToList(); + + if (invalidEndpoints.Any()) + { + var invalidNames = string.Join(", ", invalidEndpoints.Select(e => e.Name)); + System.Diagnostics.Debug.WriteLine($"Warning: Found {invalidEndpoints.Count} invalid endpoints: {invalidNames}"); + } + + validEndpoints = validEndpoints.Where(endpoint => endpoint.IsValid()).ToList(); + } + + // 添加有效端点 + foreach (var endpoint in validEndpoints) + { + try + { + endpointManager.AddOrUpdateEndpoint(endpoint); + System.Diagnostics.Debug.WriteLine($"Successfully added endpoint: {endpoint.Name} -> {endpoint.GetFullUrl()}"); + } + catch (Exception ex) + { + // 记录错误但继续处理其他端点 + System.Diagnostics.Debug.WriteLine($"Failed to add endpoint {endpoint?.Name}: {ex.Message}"); + } + } + + // 连接性验证(可选) + if (options.ValidateEndpointConnectivity && validEndpoints.Any()) + { + ValidateEndpointConnectivity(endpointManager, options.ConnectivityValidationTimeout); + } + } + + return endpointManager; + } + + /// + /// 验证服务端点连接性 + /// + /// 服务端点管理器 + /// 超时时间(秒) + private static void ValidateEndpointConnectivity(ServiceEndpointManager endpointManager, int timeoutSeconds) + { + var endpoints = endpointManager.GetAllEndpoints().ToList(); + + foreach (var endpoint in endpoints) + { + try + { + using var httpClient = new HttpClient(); + httpClient.Timeout = TimeSpan.FromSeconds(timeoutSeconds); + + var url = endpoint.GetFullUrl(); + var response = httpClient.GetAsync(url).Result; + + System.Diagnostics.Debug.WriteLine($"Connectivity check passed for {endpoint.Name}: {url}"); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Connectivity check failed for {endpoint.Name}: {ex.Message}"); + } + } + } + + /// + /// 添加动态服务客户端(带预配置端点) + /// + /// 服务集合 + /// 预配置的服务端点 + /// 服务集合 + public static IServiceCollection AddDynamicServiceClient( + this IServiceCollection services, + IEnumerable endpoints) + { + return services.AddDynamicServiceClient(options => + { + options.PreConfiguredEndpoints = endpoints.ToList(); + }); + } + + /// + /// 添加动态服务客户端(带单个预配置端点) + /// + /// 服务集合 + /// 预配置的服务端点 + /// 服务集合 + public static IServiceCollection AddDynamicServiceClient( + this IServiceCollection services, + ServiceEndpoint endpoint) + { + return services.AddDynamicServiceClient(new[] { endpoint }); + } + } + + /// + /// 动态服务客户端配置选项 + /// + public class DynamicServiceClientOptions + { + /// + /// 预配置的服务端点 + /// + public List? PreConfiguredEndpoints { get; set; } + + /// + /// 默认请求选项 + /// + public RequestOptions? DefaultRequestOptions { get; set; } + + /// + /// 是否启用默认日志记录 + /// + public bool EnableDefaultLogging { get; set; } = true; + + /// + /// 是否验证服务端点配置 + /// + public bool ValidateEndpoints { get; set; } = true; + + /// + /// 是否在启动时验证服务端点可达性 + /// + public bool ValidateEndpointConnectivity { get; set; } = false; + + /// + /// 连接性验证超时时间(秒) + /// + public int ConnectivityValidationTimeout { get; set; } = 5; + } +} \ No newline at end of file diff --git a/src/X1.DynamicClientCore/FileOperations/DynamicHttpClient.FileDownload.cs b/src/X1.DynamicClientCore/FileOperations/DynamicHttpClient.FileDownload.cs new file mode 100644 index 0000000..7837821 --- /dev/null +++ b/src/X1.DynamicClientCore/FileOperations/DynamicHttpClient.FileDownload.cs @@ -0,0 +1,101 @@ +using Microsoft.Extensions.Logging; +using X1.DynamicClientCore.Models; + +namespace X1.DynamicClientCore.Core +{ + /// + /// DynamicHttpClient 文件下载部分 + /// 提供文件下载功能 + /// + public partial class DynamicHttpClient + { + #region 文件下载方法 + + /// + /// 异步下载文件到指定路径 + /// + public async Task DownloadFileAsync(string serviceName, string endpoint, string localFilePath, RequestOptions? options = null) + { + try + { + var fileBytes = await DownloadFileAsBytesAsync(serviceName, endpoint, options); + if (fileBytes != null) + { + await File.WriteAllBytesAsync(localFilePath, fileBytes); + return true; + } + return false; + } + catch (Exception ex) + { + _logger.LogError(ex, "文件下载失败: {ServiceName}:{Endpoint} -> {LocalPath}", serviceName, endpoint, localFilePath); + return false; + } + } + + /// + /// 异步下载文件到流 + /// + public async Task DownloadFileToStreamAsync(string serviceName, string endpoint, Stream outputStream, RequestOptions? options = null) + { + try + { + var fileBytes = await DownloadFileAsBytesAsync(serviceName, endpoint, options); + if (fileBytes != null) + { + await outputStream.WriteAsync(fileBytes); + return true; + } + return false; + } + catch (Exception ex) + { + _logger.LogError(ex, "文件下载到流失败: {ServiceName}:{Endpoint}", serviceName, endpoint); + return false; + } + } + + /// + /// 异步下载文件为字节数组 + /// + public async Task DownloadFileAsBytesAsync(string serviceName, string endpoint, RequestOptions? options = null) + { + try + { + var response = await ExecuteRequestAsync(serviceName, endpoint, HttpMethod.Get, null, options); + return response; + } + catch (Exception ex) + { + _logger.LogError(ex, "文件下载为字节数组失败: {ServiceName}:{Endpoint}", serviceName, endpoint); + return null; + } + } + + /// + /// 同步下载文件到指定路径 + /// + public bool DownloadFile(string serviceName, string endpoint, string localFilePath, RequestOptions? options = null) + { + return DownloadFileAsync(serviceName, endpoint, localFilePath, options).GetAwaiter().GetResult(); + } + + /// + /// 同步下载文件到流 + /// + public bool DownloadFileToStream(string serviceName, string endpoint, Stream outputStream, RequestOptions? options = null) + { + return DownloadFileToStreamAsync(serviceName, endpoint, outputStream, options).GetAwaiter().GetResult(); + } + + /// + /// 同步下载文件为字节数组 + /// + public byte[]? DownloadFileAsBytes(string serviceName, string endpoint, RequestOptions? options = null) + { + return DownloadFileAsBytesAsync(serviceName, endpoint, options).GetAwaiter().GetResult(); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/X1.DynamicClientCore/FileOperations/DynamicHttpClient.FileInfo.cs b/src/X1.DynamicClientCore/FileOperations/DynamicHttpClient.FileInfo.cs new file mode 100644 index 0000000..f56f1b5 --- /dev/null +++ b/src/X1.DynamicClientCore/FileOperations/DynamicHttpClient.FileInfo.cs @@ -0,0 +1,31 @@ +using X1.DynamicClientCore.Models; + +namespace X1.DynamicClientCore.Core +{ + /// + /// DynamicHttpClient 文件信息部分 + /// 提供文件信息获取功能 + /// + public partial class DynamicHttpClient + { + #region 文件信息方法 + + /// + /// 异步获取文件信息 + /// + public async Task GetFileInfoAsync(string serviceName, string endpoint, RequestOptions? options = null) + { + return await GetAsync(serviceName, endpoint, options); + } + + /// + /// 同步获取文件信息 + /// + public T? GetFileInfo(string serviceName, string endpoint, RequestOptions? options = null) + { + return GetFileInfoAsync(serviceName, endpoint, options).GetAwaiter().GetResult(); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/X1.DynamicClientCore/FileOperations/DynamicHttpClient.FileUpload.cs b/src/X1.DynamicClientCore/FileOperations/DynamicHttpClient.FileUpload.cs new file mode 100644 index 0000000..070ce6a --- /dev/null +++ b/src/X1.DynamicClientCore/FileOperations/DynamicHttpClient.FileUpload.cs @@ -0,0 +1,314 @@ +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using X1.DynamicClientCore.Models; + +namespace X1.DynamicClientCore.Core +{ + /// + /// DynamicHttpClient 文件上传部分 + /// 提供文件上传功能 + /// + public partial class DynamicHttpClient + { + #region 文件上传方法 + + /// + /// 异步上传单个文件 + /// + public async Task UploadFileAsync(string serviceName, string endpoint, string filePath, string formFieldName = "file", RequestOptions? options = null) + { + if (!File.Exists(filePath)) + { + throw new DynamicHttpClientException( + $"文件不存在: {filePath}", + DynamicHttpClientExceptionType.ConfigurationError, + serviceName, + endpoint); + } + + using var fileStream = File.OpenRead(filePath); + var fileName = Path.GetFileName(filePath); + return await UploadFileStreamAsync(serviceName, endpoint, fileStream, fileName, formFieldName, options); + } + + /// + /// 异步上传多个文件 + /// + public async Task UploadFilesAsync(string serviceName, string endpoint, IEnumerable filePaths, string formFieldName = "files", RequestOptions? options = null) + { + var files = filePaths.ToList(); + if (!files.Any()) + { + throw new DynamicHttpClientException( + "文件列表为空", + DynamicHttpClientExceptionType.ConfigurationError, + serviceName, + endpoint); + } + + return await ExecuteFileUploadRequestAsync(serviceName, endpoint, files, formFieldName, options); + } + + /// + /// 异步上传文件流 + /// + public async Task UploadFileStreamAsync(string serviceName, string endpoint, Stream fileStream, string fileName, string formFieldName = "file", RequestOptions? options = null) + { + var files = new List<(Stream Stream, string FileName)> { (fileStream, fileName) }; + return await ExecuteFileUploadRequestAsync(serviceName, endpoint, files, formFieldName, options); + } + + /// + /// 同步上传单个文件 + /// + public T? UploadFile(string serviceName, string endpoint, string filePath, string formFieldName = "file", RequestOptions? options = null) + { + return UploadFileAsync(serviceName, endpoint, filePath, formFieldName, options).GetAwaiter().GetResult(); + } + + /// + /// 同步上传多个文件 + /// + public T? UploadFiles(string serviceName, string endpoint, IEnumerable filePaths, string formFieldName = "files", RequestOptions? options = null) + { + return UploadFilesAsync(serviceName, endpoint, filePaths, formFieldName, options).GetAwaiter().GetResult(); + } + + #endregion + + /// + /// 执行文件上传请求 + /// + private async Task ExecuteFileUploadRequestAsync(string serviceName, string endpoint, List filePaths, string formFieldName, RequestOptions? options) + { + try + { + // 1. 获取服务端点 + var serviceEndpoint = _endpointManager.GetEndpoint(serviceName); + if (serviceEndpoint == null) + { + throw new DynamicHttpClientException( + $"服务 '{serviceName}' 未找到", + DynamicHttpClientExceptionType.ServiceNotFound, + serviceName, + endpoint); + } + + if (!serviceEndpoint.Enabled) + { + throw new DynamicHttpClientException( + $"服务 '{serviceName}' 已禁用", + DynamicHttpClientExceptionType.ServiceDisabled, + serviceName, + endpoint); + } + + // 2. 创建HTTP客户端 + var httpClient = _httpClientFactory.CreateClient(); + var timeout = options?.Timeout ?? serviceEndpoint.Timeout; + httpClient.Timeout = TimeSpan.FromSeconds(timeout); + + // 3. 构建请求URL + var url = $"{serviceEndpoint.GetFullUrl()}/{endpoint.TrimStart('/')}"; + var request = new HttpRequestMessage(HttpMethod.Post, url); + + // 4. 创建MultipartFormDataContent + var content = new MultipartFormDataContent(); + foreach (var filePath in filePaths) + { + if (File.Exists(filePath)) + { + var fileStream = File.OpenRead(filePath); + var fileName = Path.GetFileName(filePath); + var streamContent = new StreamContent(fileStream); + content.Add(streamContent, formFieldName, fileName); + } + } + + request.Content = content; + + // 5. 添加请求头 + if (options?.Headers != null) + { + foreach (var header in options.Headers) + { + request.Headers.Add(header.Key, header.Value); + } + } + + // 6. 获取熔断器并执行请求 + var circuitBreaker = _circuitBreakerManager.GetOrCreateCircuitBreaker( + serviceEndpoint.Name, options?.CircuitBreaker); + + _logger.LogInformation("上传文件: {Method} {Url}, 文件数量: {FileCount}", HttpMethod.Post, url, filePaths.Count); + + var response = await circuitBreaker.ExecuteAsync(async () => await httpClient.SendAsync(request)); + + if (response.IsSuccessStatusCode) + { + var responseContent = await response.Content.ReadAsStringAsync(); + _logger.LogInformation("文件上传成功: {StatusCode}", response.StatusCode); + + if (typeof(T) == typeof(string)) + { + return (T)(object)responseContent; + } + + try + { + return JsonConvert.DeserializeObject(responseContent); + } + catch (Exception ex) + { + throw new DynamicHttpClientException( + "响应数据反序列化失败", + DynamicHttpClientExceptionType.SerializationError, + serviceName, + endpoint, + (int)response.StatusCode, + ex); + } + } + else + { + _logger.LogError("文件上传失败: {StatusCode} {ReasonPhrase}", response.StatusCode, response.ReasonPhrase); + throw new DynamicHttpClientException( + $"文件上传失败: {response.StatusCode} {response.ReasonPhrase}", + DynamicHttpClientExceptionType.HttpRequestFailed, + serviceName, + endpoint, + (int)response.StatusCode); + } + } + catch (DynamicHttpClientException) + { + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "文件上传异常: {ServiceName}:{Endpoint}", serviceName, endpoint); + throw new DynamicHttpClientException( + "文件上传失败", + DynamicHttpClientExceptionType.Unknown, + serviceName, + endpoint, + innerException: ex); + } + } + + /// + /// 执行文件流上传请求 + /// + private async Task ExecuteFileUploadRequestAsync(string serviceName, string endpoint, List<(Stream Stream, string FileName)> files, string formFieldName, RequestOptions? options) + { + try + { + // 1. 获取服务端点 + var serviceEndpoint = _endpointManager.GetEndpoint(serviceName); + if (serviceEndpoint == null) + { + throw new DynamicHttpClientException( + $"服务 '{serviceName}' 未找到", + DynamicHttpClientExceptionType.ServiceNotFound, + serviceName, + endpoint); + } + + if (!serviceEndpoint.Enabled) + { + throw new DynamicHttpClientException( + $"服务 '{serviceName}' 已禁用", + DynamicHttpClientExceptionType.ServiceDisabled, + serviceName, + endpoint); + } + + // 2. 创建HTTP客户端 + var httpClient = _httpClientFactory.CreateClient(); + var timeout = options?.Timeout ?? serviceEndpoint.Timeout; + httpClient.Timeout = TimeSpan.FromSeconds(timeout); + + // 3. 构建请求URL + var url = $"{serviceEndpoint.GetFullUrl()}/{endpoint.TrimStart('/')}"; + var request = new HttpRequestMessage(HttpMethod.Post, url); + + // 4. 创建MultipartFormDataContent + var content = new MultipartFormDataContent(); + foreach (var (stream, fileName) in files) + { + var streamContent = new StreamContent(stream); + content.Add(streamContent, formFieldName, fileName); + } + + request.Content = content; + + // 5. 添加请求头 + if (options?.Headers != null) + { + foreach (var header in options.Headers) + { + request.Headers.Add(header.Key, header.Value); + } + } + + // 6. 获取熔断器并执行请求 + var circuitBreaker = _circuitBreakerManager.GetOrCreateCircuitBreaker( + serviceEndpoint.Name, options?.CircuitBreaker); + + _logger.LogInformation("上传文件流: {Method} {Url}, 文件数量: {FileCount}", HttpMethod.Post, url, files.Count); + + var response = await circuitBreaker.ExecuteAsync(async () => await httpClient.SendAsync(request)); + + if (response.IsSuccessStatusCode) + { + var responseContent = await response.Content.ReadAsStringAsync(); + _logger.LogInformation("文件流上传成功: {StatusCode}", response.StatusCode); + + if (typeof(T) == typeof(string)) + { + return (T)(object)responseContent; + } + + try + { + return JsonConvert.DeserializeObject(responseContent); + } + catch (Exception ex) + { + throw new DynamicHttpClientException( + "响应数据反序列化失败", + DynamicHttpClientExceptionType.SerializationError, + serviceName, + endpoint, + (int)response.StatusCode, + ex); + } + } + else + { + _logger.LogError("文件流上传失败: {StatusCode} {ReasonPhrase}", response.StatusCode, response.ReasonPhrase); + throw new DynamicHttpClientException( + $"文件流上传失败: {response.StatusCode} {response.ReasonPhrase}", + DynamicHttpClientExceptionType.HttpRequestFailed, + serviceName, + endpoint, + (int)response.StatusCode); + } + } + catch (DynamicHttpClientException) + { + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "文件流上传异常: {ServiceName}:{Endpoint}", serviceName, endpoint); + throw new DynamicHttpClientException( + "文件流上传失败", + DynamicHttpClientExceptionType.Unknown, + serviceName, + endpoint, + innerException: ex); + } + } + } +} \ No newline at end of file diff --git a/src/X1.DynamicClientCore/Infrastructure/CircuitBreakerManager.cs b/src/X1.DynamicClientCore/Infrastructure/CircuitBreakerManager.cs new file mode 100644 index 0000000..5884a1e --- /dev/null +++ b/src/X1.DynamicClientCore/Infrastructure/CircuitBreakerManager.cs @@ -0,0 +1,308 @@ +using Microsoft.Extensions.Logging; +using Polly; +using Polly.CircuitBreaker; +using Polly.Extensions.Http; +using X1.DynamicClientCore.Interfaces; +using X1.DynamicClientCore.Models; + +namespace X1.DynamicClientCore.Infrastructure +{ + /// + /// 熔断器管理器 - 负责管理每个服务的熔断器实例 + /// + /// + /// 为每个服务端点维护独立的熔断器,支持动态配置和状态监控 + /// + public class CircuitBreakerManager : ICircuitBreakerManager + { + private readonly Dictionary _circuitBreakers = new(); + private readonly Dictionary _syncCircuitBreakers = new(); + private readonly Dictionary _options = new(); + private readonly ILogger _logger; + private readonly object _lock = new(); + + /// + /// 初始化熔断器管理器 + /// + /// 日志记录器 + public CircuitBreakerManager(ILogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// 获取或创建服务的异步熔断器策略 + /// + /// 服务名称 + /// 熔断器配置 + /// 异步熔断器策略 + public AsyncCircuitBreakerPolicy GetOrCreateCircuitBreaker(string serviceName, CircuitBreakerOptions? options = null) + { + lock (_lock) + { + if (_circuitBreakers.TryGetValue(serviceName, out var existingPolicy)) + { + // 如果配置发生变化,更新熔断器 + if (options != null && HasOptionsChanged(serviceName, options)) + { + _logger.LogInformation("更新服务 {ServiceName} 的异步熔断器配置", serviceName); + _circuitBreakers.Remove(serviceName); + _options.Remove(serviceName); + } + else + { + return existingPolicy; + } + } + + // 创建新的异步熔断器策略 + var policy = CreateCircuitBreakerPolicy(serviceName, options); + _circuitBreakers[serviceName] = policy; + _options[serviceName] = options ?? new CircuitBreakerOptions(); + + _logger.LogInformation("为服务 {ServiceName} 创建异步熔断器策略", serviceName); + return policy; + } + } + + /// + /// 获取或创建服务的同步熔断器策略 + /// + /// 服务名称 + /// 熔断器配置 + /// 同步熔断器策略 + public CircuitBreakerPolicy GetOrCreateSyncCircuitBreaker(string serviceName, CircuitBreakerOptions? options = null) + { + lock (_lock) + { + if (_syncCircuitBreakers.TryGetValue(serviceName, out var existingPolicy)) + { + // 如果配置发生变化,更新熔断器 + if (options != null && HasOptionsChanged(serviceName, options)) + { + _logger.LogInformation("更新服务 {ServiceName} 的同步熔断器配置", serviceName); + _syncCircuitBreakers.Remove(serviceName); + _options.Remove(serviceName); + } + else + { + return existingPolicy; + } + } + + // 创建新的同步熔断器策略 + var policy = CreateSyncCircuitBreakerPolicy(serviceName, options); + _syncCircuitBreakers[serviceName] = policy; + _options[serviceName] = options ?? new CircuitBreakerOptions(); + + _logger.LogInformation("为服务 {ServiceName} 创建同步熔断器策略", serviceName); + return policy; + } + } + + /// + /// 获取熔断器状态 + /// + /// 服务名称 + /// 熔断器状态 + public CircuitState? GetCircuitState(string serviceName) + { + lock (_lock) + { + // 优先返回异步熔断器状态,如果没有则返回同步熔断器状态 + if (_circuitBreakers.TryGetValue(serviceName, out var asyncPolicy)) + { + return asyncPolicy.CircuitState; + } + if (_syncCircuitBreakers.TryGetValue(serviceName, out var syncPolicy)) + { + return syncPolicy.CircuitState; + } + return null; + } + } + + /// + /// 手动重置熔断器 + /// + /// 服务名称 + public void ResetCircuitBreaker(string serviceName) + { + lock (_lock) + { + bool reset = false; + if (_circuitBreakers.TryGetValue(serviceName, out var asyncPolicy)) + { + asyncPolicy.Reset(); + reset = true; + } + if (_syncCircuitBreakers.TryGetValue(serviceName, out var syncPolicy)) + { + syncPolicy.Reset(); + reset = true; + } + if (reset) + { + _logger.LogInformation("手动重置服务 {ServiceName} 的熔断器", serviceName); + } + } + } + + /// + /// 移除服务的熔断器 + /// + /// 服务名称 + public void RemoveCircuitBreaker(string serviceName) + { + lock (_lock) + { + bool removed = false; + if (_circuitBreakers.Remove(serviceName)) + { + removed = true; + } + if (_syncCircuitBreakers.Remove(serviceName)) + { + removed = true; + } + if (removed) + { + _options.Remove(serviceName); + _logger.LogInformation("移除服务 {ServiceName} 的熔断器", serviceName); + } + } + } + + /// + /// 获取所有熔断器状态 + /// + /// 所有熔断器状态 + public Dictionary GetAllCircuitStates() + { + lock (_lock) + { + var states = new Dictionary(); + + // 添加异步熔断器状态 + foreach (var kvp in _circuitBreakers) + { + states[kvp.Key] = kvp.Value.CircuitState; + } + + // 添加同步熔断器状态(如果异步熔断器不存在) + foreach (var kvp in _syncCircuitBreakers) + { + if (!states.ContainsKey(kvp.Key)) + { + states[kvp.Key] = kvp.Value.CircuitState; + } + } + + return states; + } + } + + /// + /// 创建异步熔断器策略 + /// + /// 服务名称 + /// 熔断器配置 + /// 异步熔断器策略 + private AsyncCircuitBreakerPolicy CreateCircuitBreakerPolicy(string serviceName, CircuitBreakerOptions? options) + { + options ??= new CircuitBreakerOptions(); + + if (!options.Enabled) + { + // 如果禁用熔断器,返回一个永远不会熔断的策略 + return Policy.Handle().CircuitBreakerAsync(1000, TimeSpan.FromDays(365)); + } + + return Policy + .Handle() + .Or() + .Or() + .AdvancedCircuitBreakerAsync( + failureThreshold: options.FailureRateThreshold, + samplingDuration: TimeSpan.FromSeconds(options.SamplingDuration), + minimumThroughput: options.MinimumThroughput, + durationOfBreak: TimeSpan.FromSeconds(options.DurationOfBreak), + onBreak: (exception, duration) => + { + _logger.LogWarning("服务 {ServiceName} 异步熔断器打开,持续时间: {Duration}秒", + serviceName, duration.TotalSeconds); + }, + onReset: () => + { + _logger.LogInformation("服务 {ServiceName} 异步熔断器重置", serviceName); + }, + onHalfOpen: () => + { + _logger.LogInformation("服务 {ServiceName} 异步熔断器进入半开状态", serviceName); + }); + } + + /// + /// 创建同步熔断器策略 + /// + /// 服务名称 + /// 熔断器配置 + /// 同步熔断器策略 + private CircuitBreakerPolicy CreateSyncCircuitBreakerPolicy(string serviceName, CircuitBreakerOptions? options) + { + options ??= new CircuitBreakerOptions(); + + if (!options.Enabled) + { + // 如果禁用熔断器,返回一个永远不会熔断的策略 + return Policy.Handle().CircuitBreaker(1000, TimeSpan.FromDays(365)); + } + + return Policy + .Handle() + .Or() + .Or() + .AdvancedCircuitBreaker( + failureThreshold: options.FailureRateThreshold, + samplingDuration: TimeSpan.FromSeconds(options.SamplingDuration), + minimumThroughput: options.MinimumThroughput, + durationOfBreak: TimeSpan.FromSeconds(options.DurationOfBreak), + onBreak: (exception, duration) => + { + _logger.LogWarning("服务 {ServiceName} 同步熔断器打开,持续时间: {Duration}秒", + serviceName, duration.TotalSeconds); + }, + onReset: () => + { + _logger.LogInformation("服务 {ServiceName} 同步熔断器重置", serviceName); + }, + onHalfOpen: () => + { + _logger.LogInformation("服务 {ServiceName} 同步熔断器进入半开状态", serviceName); + }); + } + + /// + /// 检查配置是否发生变化 + /// + /// 服务名称 + /// 新配置 + /// 是否发生变化 + private bool HasOptionsChanged(string serviceName, CircuitBreakerOptions newOptions) + { + if (!_options.TryGetValue(serviceName, out var existingOptions)) + { + return true; + } + + return existingOptions.Enabled != newOptions.Enabled + || existingOptions.FailureThreshold != newOptions.FailureThreshold + || existingOptions.DurationOfBreak != newOptions.DurationOfBreak + || existingOptions.SamplingDuration != newOptions.SamplingDuration + || existingOptions.MinimumThroughput != newOptions.MinimumThroughput + || Math.Abs(existingOptions.FailureRateThreshold - newOptions.FailureRateThreshold) > 0.001 + || existingOptions.EnableHalfOpen != newOptions.EnableHalfOpen + || existingOptions.HandledEventsAllowedBeforeBreaking != newOptions.HandledEventsAllowedBeforeBreaking; + } + } +} \ No newline at end of file diff --git a/src/X1.DynamicClientCore/Infrastructure/ServiceEndpointManager.cs b/src/X1.DynamicClientCore/Infrastructure/ServiceEndpointManager.cs new file mode 100644 index 0000000..118be03 --- /dev/null +++ b/src/X1.DynamicClientCore/Infrastructure/ServiceEndpointManager.cs @@ -0,0 +1,121 @@ +using X1.DynamicClientCore.Interfaces; +using X1.DynamicClientCore.Models; + +namespace X1.DynamicClientCore.Infrastructure +{ + /// + /// 服务端点管理器 - 负责管理所有服务端点的配置信息 + /// + /// + /// 提供线程安全的服务端点存储和管理功能,支持动态添加、更新、删除和查询操作 + /// + public class ServiceEndpointManager : IServiceEndpointManager + { + /// + /// 服务端点存储字典 + /// + private readonly Dictionary _endpoints = new(); + + /// + /// 线程同步锁 + /// + private readonly object _lock = new(); + + /// + /// 获取所有服务端点 + /// + /// 所有服务端点 + public IEnumerable GetAllEndpoints() + { + lock (_lock) + { + return _endpoints.Values.ToList(); + } + } + + /// + /// 获取服务端点 + /// + /// 服务名称 + /// 服务端点 + public ServiceEndpoint? GetEndpoint(string serviceName) + { + lock (_lock) + { + return _endpoints.TryGetValue(serviceName, out var endpoint) ? endpoint : null; + } + } + + /// + /// 添加或更新服务端点 + /// + /// 服务端点 + public void AddOrUpdateEndpoint(ServiceEndpoint endpoint) + { + if (endpoint == null) + throw new ArgumentNullException(nameof(endpoint)); + + if (!endpoint.IsValid()) + throw new ArgumentException("服务端点配置无效", nameof(endpoint)); + + lock (_lock) + { + _endpoints[endpoint.Name] = endpoint; + } + } + + /// + /// 移除服务端点 + /// + /// 服务名称 + /// 是否成功移除 + public bool RemoveEndpoint(string serviceName) + { + if (string.IsNullOrEmpty(serviceName)) + throw new ArgumentNullException(nameof(serviceName)); + + lock (_lock) + { + return _endpoints.Remove(serviceName); + } + } + + /// + /// 检查服务端点是否存在 + /// + /// 服务名称 + /// 是否存在 + public bool Exists(string serviceName) + { + lock (_lock) + { + return _endpoints.ContainsKey(serviceName); + } + } + + /// + /// 清空所有服务端点 + /// + public void Clear() + { + lock (_lock) + { + _endpoints.Clear(); + } + } + + /// + /// 获取服务端点数量 + /// + public int Count + { + get + { + lock (_lock) + { + return _endpoints.Count; + } + } + } + } +} \ No newline at end of file diff --git a/src/X1.DynamicClientCore/Interfaces/IAsyncHttpClient.cs b/src/X1.DynamicClientCore/Interfaces/IAsyncHttpClient.cs new file mode 100644 index 0000000..8cd8777 --- /dev/null +++ b/src/X1.DynamicClientCore/Interfaces/IAsyncHttpClient.cs @@ -0,0 +1,57 @@ +using X1.DynamicClientCore.Models; + +namespace X1.DynamicClientCore.Interfaces +{ + /// + /// 异步HTTP客户端接口 + /// 提供异步的HTTP请求方法 + /// + public interface IAsyncHttpClient + { + /// + /// 发送异步GET请求 + /// + /// 响应数据类型 + /// 服务名称 + /// API端点 + /// 请求选项 + /// 取消令牌 + /// 响应数据 + Task GetAsync(string serviceName, string endpoint, RequestOptions? options = null, CancellationToken cancellationToken = default); + + /// + /// 发送异步POST请求 + /// + /// 响应数据类型 + /// 服务名称 + /// API端点 + /// 请求数据 + /// 请求选项 + /// 取消令牌 + /// 响应数据 + Task PostAsync(string serviceName, string endpoint, object? data = null, RequestOptions? options = null, CancellationToken cancellationToken = default); + + /// + /// 发送异步PUT请求 + /// + /// 响应数据类型 + /// 服务名称 + /// API端点 + /// 请求数据 + /// 请求选项 + /// 取消令牌 + /// 响应数据 + Task PutAsync(string serviceName, string endpoint, object? data = null, RequestOptions? options = null, CancellationToken cancellationToken = default); + + /// + /// 发送异步DELETE请求 + /// + /// 响应数据类型 + /// 服务名称 + /// API端点 + /// 请求选项 + /// 取消令牌 + /// 响应数据 + Task DeleteAsync(string serviceName, string endpoint, RequestOptions? options = null, CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/src/X1.DynamicClientCore/Interfaces/ICircuitBreakerManager.cs b/src/X1.DynamicClientCore/Interfaces/ICircuitBreakerManager.cs new file mode 100644 index 0000000..db83fa4 --- /dev/null +++ b/src/X1.DynamicClientCore/Interfaces/ICircuitBreakerManager.cs @@ -0,0 +1,54 @@ +using Polly; +using Polly.CircuitBreaker; +using X1.DynamicClientCore.Models; + +namespace X1.DynamicClientCore.Interfaces +{ + /// + /// 熔断器管理器接口 + /// 负责管理每个服务的熔断器实例 + /// + public interface ICircuitBreakerManager + { + /// + /// 获取或创建服务的异步熔断器策略 + /// + /// 服务名称 + /// 熔断器配置 + /// 异步熔断器策略 + AsyncCircuitBreakerPolicy GetOrCreateCircuitBreaker(string serviceName, CircuitBreakerOptions? options = null); + + /// + /// 获取或创建服务的同步熔断器策略 + /// + /// 服务名称 + /// 熔断器配置 + /// 同步熔断器策略 + CircuitBreakerPolicy GetOrCreateSyncCircuitBreaker(string serviceName, CircuitBreakerOptions? options = null); + + /// + /// 获取熔断器状态 + /// + /// 服务名称 + /// 熔断器状态 + CircuitState? GetCircuitState(string serviceName); + + /// + /// 手动重置熔断器 + /// + /// 服务名称 + void ResetCircuitBreaker(string serviceName); + + /// + /// 移除服务的熔断器 + /// + /// 服务名称 + void RemoveCircuitBreaker(string serviceName); + + /// + /// 获取所有熔断器状态 + /// + /// 所有熔断器状态 + Dictionary GetAllCircuitStates(); + } +} \ No newline at end of file diff --git a/src/X1.DynamicClientCore/Interfaces/IDynamicHttpClient.cs b/src/X1.DynamicClientCore/Interfaces/IDynamicHttpClient.cs new file mode 100644 index 0000000..8c4720f --- /dev/null +++ b/src/X1.DynamicClientCore/Interfaces/IDynamicHttpClient.cs @@ -0,0 +1,23 @@ +using X1.DynamicClientCore.Models; + +namespace X1.DynamicClientCore.Interfaces +{ + /// + /// 动态HTTP客户端接口 + /// 提供统一的HTTP请求服务,支持动态服务配置和增强功能 + /// 使用多继承方式组合基础HTTP功能和文件操作功能 + /// + public interface IDynamicHttpClient : IHttpClientBase, IFileHttpClient + { + // 继承自 IHttpClientBase 的方法: + // - 异步方法:GetAsync, PostAsync, PutAsync, DeleteAsync + // - 同步方法:Get, Post, Put, Delete + + // 继承自 IFileHttpClient 的方法: + // - 文件上传:UploadFileAsync, UploadFilesAsync, UploadFileStreamAsync + // - 文件下载:DownloadFileAsync, DownloadFileToStreamAsync, DownloadFileAsBytesAsync + // - 文件信息:GetFileInfoAsync + + // 可以在这里添加动态HTTP客户端特有的方法 + } +} \ No newline at end of file diff --git a/src/X1.DynamicClientCore/Interfaces/IFileHttpClient.cs b/src/X1.DynamicClientCore/Interfaces/IFileHttpClient.cs new file mode 100644 index 0000000..baa6a99 --- /dev/null +++ b/src/X1.DynamicClientCore/Interfaces/IFileHttpClient.cs @@ -0,0 +1,162 @@ +using X1.DynamicClientCore.Models; + +namespace X1.DynamicClientCore.Interfaces +{ + /// + /// 文件HTTP客户端接口 + /// 专门处理文件上传和下载操作 + /// + public interface IFileHttpClient + { + #region 文件上传 + + /// + /// 异步上传单个文件 + /// + /// 响应数据类型 + /// 服务名称 + /// API端点 + /// 文件路径 + /// 表单字段名称 + /// 请求选项 + /// 响应数据 + Task UploadFileAsync(string serviceName, string endpoint, string filePath, string formFieldName = "file", RequestOptions? options = null); + + /// + /// 异步上传多个文件 + /// + /// 响应数据类型 + /// 服务名称 + /// API端点 + /// 文件路径列表 + /// 表单字段名称 + /// 请求选项 + /// 响应数据 + Task UploadFilesAsync(string serviceName, string endpoint, IEnumerable filePaths, string formFieldName = "files", RequestOptions? options = null); + + /// + /// 异步上传文件流 + /// + /// 响应数据类型 + /// 服务名称 + /// API端点 + /// 文件流 + /// 文件名 + /// 表单字段名称 + /// 请求选项 + /// 响应数据 + Task UploadFileStreamAsync(string serviceName, string endpoint, Stream fileStream, string fileName, string formFieldName = "file", RequestOptions? options = null); + + /// + /// 同步上传单个文件 + /// + /// 响应数据类型 + /// 服务名称 + /// API端点 + /// 文件路径 + /// 表单字段名称 + /// 请求选项 + /// 响应数据 + T? UploadFile(string serviceName, string endpoint, string filePath, string formFieldName = "file", RequestOptions? options = null); + + /// + /// 同步上传多个文件 + /// + /// 响应数据类型 + /// 服务名称 + /// API端点 + /// 文件路径列表 + /// 表单字段名称 + /// 请求选项 + /// 响应数据 + T? UploadFiles(string serviceName, string endpoint, IEnumerable filePaths, string formFieldName = "files", RequestOptions? options = null); + + #endregion + + #region 文件下载 + + /// + /// 异步下载文件到指定路径 + /// + /// 服务名称 + /// API端点 + /// 本地文件保存路径 + /// 请求选项 + /// 下载结果 + Task DownloadFileAsync(string serviceName, string endpoint, string localFilePath, RequestOptions? options = null); + + /// + /// 异步下载文件到流 + /// + /// 服务名称 + /// API端点 + /// 输出流 + /// 请求选项 + /// 下载结果 + Task DownloadFileToStreamAsync(string serviceName, string endpoint, Stream outputStream, RequestOptions? options = null); + + /// + /// 异步下载文件为字节数组 + /// + /// 服务名称 + /// API端点 + /// 请求选项 + /// 文件字节数组 + Task DownloadFileAsBytesAsync(string serviceName, string endpoint, RequestOptions? options = null); + + /// + /// 同步下载文件到指定路径 + /// + /// 服务名称 + /// API端点 + /// 本地文件保存路径 + /// 请求选项 + /// 下载结果 + bool DownloadFile(string serviceName, string endpoint, string localFilePath, RequestOptions? options = null); + + /// + /// 同步下载文件到流 + /// + /// 服务名称 + /// API端点 + /// 输出流 + /// 请求选项 + /// 下载结果 + bool DownloadFileToStream(string serviceName, string endpoint, Stream outputStream, RequestOptions? options = null); + + /// + /// 同步下载文件为字节数组 + /// + /// 服务名称 + /// API端点 + /// 请求选项 + /// 文件字节数组 + byte[]? DownloadFileAsBytes(string serviceName, string endpoint, RequestOptions? options = null); + + #endregion + + #region 文件信息 + + /// + /// 异步获取文件信息 + /// + /// 响应数据类型 + /// 服务名称 + /// API端点 + /// 请求选项 + /// 文件信息 + Task GetFileInfoAsync(string serviceName, string endpoint, RequestOptions? options = null); + + /// + /// 同步获取文件信息 + /// + /// 响应数据类型 + /// 服务名称 + /// API端点 + /// 请求选项 + /// 文件信息 + T? GetFileInfo(string serviceName, string endpoint, RequestOptions? options = null); + + #endregion + } +} \ No newline at end of file diff --git a/src/X1.DynamicClientCore/Interfaces/IHttpClientBase.cs b/src/X1.DynamicClientCore/Interfaces/IHttpClientBase.cs new file mode 100644 index 0000000..225de2e --- /dev/null +++ b/src/X1.DynamicClientCore/Interfaces/IHttpClientBase.cs @@ -0,0 +1,18 @@ +using X1.DynamicClientCore.Models; + +namespace X1.DynamicClientCore.Interfaces +{ + /// + /// 基础HTTP客户端接口 + /// 组合同步和异步的HTTP请求方法 + /// 实现接口隔离原则,支持按需使用 + /// + public interface IHttpClientBase : IAsyncHttpClient, ISyncHttpClient + { + // 继承自 IAsyncHttpClient 的方法: + // - GetAsync, PostAsync, PutAsync, DeleteAsync + + // 继承自 ISyncHttpClient 的方法: + // - Get, Post, Put, Delete + } +} \ No newline at end of file diff --git a/src/X1.DynamicClientCore/Interfaces/IServiceEndpointManager.cs b/src/X1.DynamicClientCore/Interfaces/IServiceEndpointManager.cs new file mode 100644 index 0000000..895a312 --- /dev/null +++ b/src/X1.DynamicClientCore/Interfaces/IServiceEndpointManager.cs @@ -0,0 +1,54 @@ +using X1.DynamicClientCore.Models; + +namespace X1.DynamicClientCore.Interfaces +{ + /// + /// 服务端点管理器接口 + /// 负责管理所有服务端点的配置信息 + /// + public interface IServiceEndpointManager + { + /// + /// 获取所有服务端点 + /// + /// 所有服务端点 + IEnumerable GetAllEndpoints(); + + /// + /// 获取服务端点 + /// + /// 服务名称 + /// 服务端点 + ServiceEndpoint? GetEndpoint(string serviceName); + + /// + /// 添加或更新服务端点 + /// + /// 服务端点 + void AddOrUpdateEndpoint(ServiceEndpoint endpoint); + + /// + /// 移除服务端点 + /// + /// 服务名称 + /// 是否成功移除 + bool RemoveEndpoint(string serviceName); + + /// + /// 检查服务端点是否存在 + /// + /// 服务名称 + /// 是否存在 + bool Exists(string serviceName); + + /// + /// 清空所有服务端点 + /// + void Clear(); + + /// + /// 获取服务端点数量 + /// + int Count { get; } + } +} \ No newline at end of file diff --git a/src/X1.DynamicClientCore/Interfaces/ISyncHttpClient.cs b/src/X1.DynamicClientCore/Interfaces/ISyncHttpClient.cs new file mode 100644 index 0000000..cdd862a --- /dev/null +++ b/src/X1.DynamicClientCore/Interfaces/ISyncHttpClient.cs @@ -0,0 +1,53 @@ +using X1.DynamicClientCore.Models; + +namespace X1.DynamicClientCore.Interfaces +{ + /// + /// 同步HTTP客户端接口 + /// 提供同步的HTTP请求方法 + /// + public interface ISyncHttpClient + { + /// + /// 发送同步GET请求 + /// + /// 响应数据类型 + /// 服务名称 + /// API端点 + /// 请求选项 + /// 响应数据 + T? Get(string serviceName, string endpoint, RequestOptions? options = null); + + /// + /// 发送同步POST请求 + /// + /// 响应数据类型 + /// 服务名称 + /// API端点 + /// 请求数据 + /// 请求选项 + /// 响应数据 + T? Post(string serviceName, string endpoint, object? data = null, RequestOptions? options = null); + + /// + /// 发送同步PUT请求 + /// + /// 响应数据类型 + /// 服务名称 + /// API端点 + /// 请求数据 + /// 请求选项 + /// 响应数据 + T? Put(string serviceName, string endpoint, object? data = null, RequestOptions? options = null); + + /// + /// 发送同步DELETE请求 + /// + /// 响应数据类型 + /// 服务名称 + /// API端点 + /// 请求选项 + /// 响应数据 + T? Delete(string serviceName, string endpoint, RequestOptions? options = null); + } +} \ No newline at end of file diff --git a/src/X1.DynamicClientCore/Models/CircuitBreakerOptions.cs b/src/X1.DynamicClientCore/Models/CircuitBreakerOptions.cs new file mode 100644 index 0000000..d2d19f3 --- /dev/null +++ b/src/X1.DynamicClientCore/Models/CircuitBreakerOptions.cs @@ -0,0 +1,76 @@ +using System.Text.Json.Serialization; + +namespace X1.DynamicClientCore.Models +{ + /// + /// 熔断器配置选项 + /// + /// + /// 定义熔断器的行为参数,包括失败阈值、恢复时间等配置 + /// + public class CircuitBreakerOptions + { + /// + /// 是否启用熔断器 + /// + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } = true; + + /// + /// 失败阈值 - 连续失败多少次后触发熔断 + /// + [JsonPropertyName("failureThreshold")] + public int FailureThreshold { get; set; } = 5; + + /// + /// 熔断持续时间(秒)- 熔断后多长时间尝试恢复 + /// + [JsonPropertyName("durationOfBreak")] + public int DurationOfBreak { get; set; } = 30; + + /// + /// 采样持续时间(秒)- 统计失败次数的时间窗口 + /// + [JsonPropertyName("samplingDuration")] + public int SamplingDuration { get; set; } = 60; + + /// + /// 最小吞吐量 - 在采样期间内最少需要多少请求才进行熔断判断 + /// + [JsonPropertyName("minimumThroughput")] + public int MinimumThroughput { get; set; } = 10; + + /// + /// 失败率阈值 - 失败率超过多少百分比时触发熔断(0.0-1.0) + /// + [JsonPropertyName("failureRateThreshold")] + public double FailureRateThreshold { get; set; } = 0.5; + + /// + /// 是否启用半开状态 - 熔断器在恢复期间是否允许部分请求通过 + /// + [JsonPropertyName("enableHalfOpen")] + public bool EnableHalfOpen { get; set; } = true; + + /// + /// 半开状态下的请求数量 - 允许通过的请求数量 + /// + [JsonPropertyName("handledEventsAllowedBeforeBreaking")] + public int HandledEventsAllowedBeforeBreaking { get; set; } = 2; + + /// + /// 验证配置是否有效 + /// + /// 验证结果 + public bool IsValid() + { + return FailureThreshold > 0 + && DurationOfBreak > 0 + && SamplingDuration > 0 + && MinimumThroughput > 0 + && FailureRateThreshold > 0.0 + && FailureRateThreshold <= 1.0 + && HandledEventsAllowedBeforeBreaking > 0; + } + } +} \ No newline at end of file diff --git a/src/X1.DynamicClientCore/Models/DynamicHttpClientException.cs b/src/X1.DynamicClientCore/Models/DynamicHttpClientException.cs new file mode 100644 index 0000000..f20d999 --- /dev/null +++ b/src/X1.DynamicClientCore/Models/DynamicHttpClientException.cs @@ -0,0 +1,136 @@ +using System.Runtime.Serialization; + +namespace X1.DynamicClientCore.Models +{ + /// + /// 动态HTTP客户端异常 + /// 用于生产环境的安全异常处理 + /// + [Serializable] + public class DynamicHttpClientException : Exception + { + /// + /// 异常类型 + /// + public DynamicHttpClientExceptionType ExceptionType { get; } + + /// + /// 服务名称 + /// + public string? ServiceName { get; } + + /// + /// 端点 + /// + public string? Endpoint { get; } + + /// + /// HTTP状态码(如果有) + /// + public int? StatusCode { get; } + + /// + /// 初始化动态HTTP客户端异常 + /// + /// 异常消息 + /// 异常类型 + /// 服务名称 + /// 端点 + /// HTTP状态码 + /// 内部异常 + public DynamicHttpClientException( + string message, + DynamicHttpClientExceptionType exceptionType, + string? serviceName = null, + string? endpoint = null, + int? statusCode = null, + Exception? innerException = null) + : base(message, innerException) + { + ExceptionType = exceptionType; + ServiceName = serviceName; + Endpoint = endpoint; + StatusCode = statusCode; + } + + /// + /// 序列化构造函数 + /// + protected DynamicHttpClientException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + ExceptionType = (DynamicHttpClientExceptionType)info.GetValue(nameof(ExceptionType), typeof(DynamicHttpClientExceptionType)); + ServiceName = info.GetString(nameof(ServiceName)); + Endpoint = info.GetString(nameof(Endpoint)); + StatusCode = info.GetInt32(nameof(StatusCode)); + } + + /// + /// 获取序列化信息 + /// + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + info.AddValue(nameof(ExceptionType), ExceptionType); + info.AddValue(nameof(ServiceName), ServiceName); + info.AddValue(nameof(Endpoint), Endpoint); + info.AddValue(nameof(StatusCode), StatusCode ?? 0); + } + } + + /// + /// 动态HTTP客户端异常类型 + /// + public enum DynamicHttpClientExceptionType + { + /// + /// 未知异常 + /// + Unknown, + + /// + /// 服务端点未找到 + /// + ServiceNotFound, + + /// + /// 服务端点已禁用 + /// + ServiceDisabled, + + /// + /// HTTP请求失败 + /// + HttpRequestFailed, + + /// + /// 超时异常 + /// + Timeout, + + /// + /// 网络连接异常 + /// + NetworkError, + + /// + /// 序列化异常 + /// + SerializationError, + + /// + /// 熔断器异常 + /// + CircuitBreakerOpen, + + /// + /// 配置异常 + /// + ConfigurationError, + + /// + /// 请求被取消 + /// + RequestCanceled + } +} \ No newline at end of file diff --git a/src/X1.DynamicClientCore/Models/HttpRequestOptions.cs b/src/X1.DynamicClientCore/Models/HttpRequestOptions.cs new file mode 100644 index 0000000..8616207 --- /dev/null +++ b/src/X1.DynamicClientCore/Models/HttpRequestOptions.cs @@ -0,0 +1,43 @@ +using System.Text.Json.Serialization; + +namespace X1.DynamicClientCore.Models +{ + /// + /// HTTP请求选项 - 定义HTTP请求的配置参数 + /// + /// + /// 包含超时、请求头、日志、熔断器等配置选项 + /// + public class RequestOptions + { + /// + /// 超时时间(秒) + /// + [JsonPropertyName("timeout")] + public int Timeout { get; set; } = 10; + + /// + /// 请求头 + /// + [JsonPropertyName("headers")] + public Dictionary Headers { get; set; } = new(); + + /// + /// 是否记录日志 + /// + [JsonPropertyName("enableLogging")] + public bool EnableLogging { get; set; } = true; + + /// + /// 是否启用熔断器 + /// + [JsonPropertyName("enableCircuitBreaker")] + public bool EnableCircuitBreaker { get; set; } = true; + + /// + /// 熔断器配置 + /// + [JsonPropertyName("circuitBreaker")] + public CircuitBreakerOptions? CircuitBreaker { get; set; } + } +} \ No newline at end of file diff --git a/src/X1.DynamicClientCore/Models/ServiceEndpoint.cs b/src/X1.DynamicClientCore/Models/ServiceEndpoint.cs new file mode 100644 index 0000000..94e66ce --- /dev/null +++ b/src/X1.DynamicClientCore/Models/ServiceEndpoint.cs @@ -0,0 +1,77 @@ +using System.Text.Json.Serialization; + +namespace X1.DynamicClientCore.Models +{ + /// + /// 服务端点配置模型 - 定义单个服务的连接配置信息 + /// + /// + /// 包含服务的网络地址、协议、超时等配置,提供URL生成和配置验证功能 + /// + public class ServiceEndpoint + { + /// + /// 服务名称(如 "B1", "B2", "B3") + /// + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// + /// 服务IP地址 + /// + [JsonPropertyName("ip")] + public string Ip { get; set; } = string.Empty; + + /// + /// 服务端口 + /// + [JsonPropertyName("port")] + public int Port { get; set; } + + /// + /// 协议类型(http/https) + /// + [JsonPropertyName("protocol")] + public string Protocol { get; set; } = "http"; + + /// + /// 超时时间(秒) + /// + [JsonPropertyName("timeout")] + public int Timeout { get; set; } = 10; + + /// + /// 是否启用 + /// + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } = true; + + /// + /// 基础路径 + /// + [JsonPropertyName("basePath")] + public string BasePath { get; set; } = string.Empty; + + /// + /// 获取完整的服务URL + /// + /// 完整的服务URL + public string GetFullUrl() + { + var baseUrl = $"{Protocol}://{Ip}:{Port}"; + return string.IsNullOrEmpty(BasePath) ? baseUrl : $"{baseUrl}/{BasePath.TrimStart('/')}"; + } + + /// + /// 验证配置是否有效 + /// + /// 验证结果 + public bool IsValid() + { + return !string.IsNullOrEmpty(Name) + && !string.IsNullOrEmpty(Ip) + && Port > 0 + && Port <= 65535; + } + } +} \ No newline at end of file diff --git a/src/X1.DynamicClientCore/X1.DynamicClientCore.csproj b/src/X1.DynamicClientCore/X1.DynamicClientCore.csproj new file mode 100644 index 0000000..0b6d07c --- /dev/null +++ b/src/X1.DynamicClientCore/X1.DynamicClientCore.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + \ No newline at end of file