引入微服务通用通讯客户端及标准化响应

新增 Microservice.Common 通用库,基于 Nacos 和 Refit 实现 IServiceClient,统一微服务间调用方式和 API 响应结构。Demo 与 Demo2 服务集成 ServiceClient,接口返回结构标准化。新增单元测试项目,完善服务间通讯的可测试性。调整依赖与日志配置,提升项目可维护性和扩展性。
This commit is contained in:
YangQiang 2026-02-10 14:22:36 +08:00
parent 590d5ab936
commit cf3ad910c2
21 changed files with 942 additions and 20 deletions

View File

@ -1,4 +1,6 @@
<Solution>
<Project Path="MicoService.Demo/MicoService.Demo.csproj" />
<Project Path="MicoService.Demo2/MicoService.Demo2.csproj" Id="efd7de63-eb5d-4687-8e58-967bc9d939e1" />
<Project Path="Microservice.Common.Tests/Microservice.Common.Tests.csproj" />
<Project Path="Microservice.Common/Microservice.Common.csproj" />
</Solution>

View File

@ -1,6 +1,10 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Nacos.V2;
using System.Threading.Tasks;
using Microservice.Common;
using Microservice.Common.Models;
namespace MicoService.Demo.Controllers
{
@ -11,44 +15,74 @@ namespace MicoService.Demo.Controllers
private readonly IConfiguration _configuration;
private readonly INacosNamingService _nacosNamingService;
private readonly IServiceClient _serviceClient;
private readonly ILogger<TestController> _logger;
public TestController(IConfiguration configuration,
INacosNamingService nacosNamingService)
INacosNamingService nacosNamingService,
IServiceClient serviceClient,
ILogger<TestController> logger)
{
this._configuration = configuration;
this._nacosNamingService = nacosNamingService;
this._serviceClient = serviceClient;
this._logger = logger;
}
/// <summary>
/// 演示:读取 Nacos 配置中心配置
/// 演示:读取 Nacos 配置中心配置
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
[HttpGet("config/{key}")]
public string GetConfig(string key)
public IActionResult GetConfig(string key)
{
return _configuration[key];
var value = _configuration[key];
return Ok(ApiResponseHelper.Success(value, "获取配置成功"));
}
/// <summary>
/// 演示:调用其他微服务
/// 演示:提供给其他服务调用的接口
/// </summary>
[HttpGet("config/info")]
public IActionResult GetConfigInfo()
{
var data = new ConfigInfoModel("Hello from MicoService.Demo", "This is a test config info");
return Ok(ApiResponseHelper.Success(data, "获取配置信息成功"));
}
/// <summary>
/// 演示:使用微服务通讯客户端调用其他微服务
/// </summary>
[HttpGet("call")]
public async Task CallOtherService()
public async Task<IActionResult> CallOtherService()
{
// 核心API获取单个实例内置负载均衡
var instance = await _nacosNamingService.SelectOneHealthyInstance("Mico_Demo2222");
if (instance == null)
try
{
Console.WriteLine("无可用实例");
// 使用微服务通讯客户端调用 Mico_Demo2222 服务
var result = await this._serviceClient.GetAsync<ConfigInfoModel>("Mico_Demo2222", "/User/config/info");
var data = new ServiceCallResultModel<ConfigInfoModel>("调用成功", result);
return Ok(ApiResponseHelper.Success(data, "服务调用成功"));
}
catch (ApiException ex)
{
return StatusCode(ex.StatusCode, ApiResponseHelper.Error(System.Net.HttpStatusCode.InternalServerError, ex.Message));
}
catch (Exception ex)
{
return StatusCode(500, ApiResponseHelper.Error(System.Net.HttpStatusCode.InternalServerError, "调用失败: " + ex.Message));
}
}
// 拼接调用地址http://192.168.2.10:8080/api/xxx
var callUrl = $"http://{instance.Ip}:{instance.Port}/user/config/info";
/// <summary>
/// 演示:测试控制器是否正常工作
/// </summary>
[HttpGet("test")]
public IActionResult Test()
{
return Ok(ApiResponseHelper.Success("测试成功", "控制器正常工作"));
}

View File

@ -10,6 +10,11 @@
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.2" />
<PackageReference Include="nacos-sdk-csharp.AspNetCore" Version="1.3.10" />
<PackageReference Include="nacos-sdk-csharp.Extensions.Configuration" Version="1.3.10" />
<PackageReference Include="Refit" Version="10.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Microservice.Common\Microservice.Common.csproj" />
</ItemGroup>
</Project>

View File

@ -1,6 +1,7 @@

using Nacos.AspNetCore.V2;
using Nacos.V2.DependencyInjection;
using Microservice.Common;
namespace MicoService.Demo
{
@ -27,6 +28,9 @@ namespace MicoService.Demo
builder.Services.AddControllers();
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
// 注册微服务通讯客户端
builder.Services.AddServiceClient();
var app = builder.Build();

View File

@ -1,8 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
"Default": "Debug",
"Microsoft.AspNetCore": "Debug"
}
},
"AllowedHosts": "*"

View File

@ -1,4 +1,6 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
using Microservice.Common;
using Microservice.Common.Models;
namespace MicoService.Demo2.Controllers
{
@ -7,20 +9,56 @@ namespace MicoService.Demo2.Controllers
public class UserController : ControllerBase
{
public UserController( IConfiguration configuration)
public UserController( IConfiguration configuration, IServiceClient serviceClient)
{
_configuration = configuration;
_serviceClient = serviceClient;
}
private readonly IConfiguration _configuration;
private readonly IServiceClient _serviceClient;
/// <summary>
/// 演示:读取 Nacos 配置中心配置
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
[HttpGet("config/{key}")]
public string GetConfig(string key)
public IActionResult GetConfig(string key)
{
return _configuration[key];
var value = _configuration[key];
return Ok(ApiResponseHelper.Success(value, "获取配置成功"));
}
/// <summary>
/// 演示:提供给其他服务调用的接口
/// </summary>
[HttpGet("config/info")]
public IActionResult GetConfigInfo()
{
var data = new ConfigInfoModel("Hello from MicoService.Demo2", "This is a test config info");
return Ok(ApiResponseHelper.Success(data, "获取配置信息成功"));
}
/// <summary>
/// 演示:调用其他微服务
/// </summary>
[HttpGet("call-other")]
public async Task<IActionResult> CallOtherService()
{
try
{
// 使用微服务通讯客户端调用 Mico_Demo1111 服务
var result = await _serviceClient.GetAsync<ConfigInfoModel>("Mico_Demo1111", "/test/config/info");
var data = new ServiceCallResultModel<ConfigInfoModel>("调用成功", result);
return Ok(ApiResponseHelper.Success(data, "服务调用成功"));
}
catch (ApiException ex)
{
return StatusCode(ex.StatusCode, ApiResponseHelper.Error(System.Net.HttpStatusCode.InternalServerError, ex.Message));
}
catch (Exception ex)
{
return StatusCode(500, ApiResponseHelper.Error(System.Net.HttpStatusCode.InternalServerError, "调用失败: " + ex.Message));
}
}

View File

@ -10,6 +10,11 @@
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.2" />
<PackageReference Include="nacos-sdk-csharp.AspNetCore" Version="1.3.10" />
<PackageReference Include="nacos-sdk-csharp.Extensions.Configuration" Version="1.3.10" />
<PackageReference Include="Refit" Version="10.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Microservice.Common\Microservice.Common.csproj" />
</ItemGroup>
</Project>

View File

@ -1,6 +1,7 @@

using Nacos.AspNetCore.V2;
using Nacos.V2.DependencyInjection;
using Microservice.Common;
namespace MicoService.Demo2
{
@ -25,6 +26,9 @@ namespace MicoService.Demo2
builder.Services.AddControllers();
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
// 注册微服务通讯客户端
builder.Services.AddServiceClient();
var app = builder.Build();

View File

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Microservice.Common\Microservice.Common.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,256 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Logging;
using Moq;
using Nacos.V2;
using Nacos.V2.Common;
using Nacos.V2.Naming.Dtos;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
using Xunit;
namespace Microservice.Common.Tests
{
public class ServiceClientTests
{
private readonly Mock<INacosNamingService> _nacosNamingServiceMock;
private readonly HttpClient _httpClient;
private readonly Mock<ILogger<ServiceClient>> _loggerMock;
private readonly ServiceClient _serviceClient;
public ServiceClientTests()
{
// 初始化模拟对象
_nacosNamingServiceMock = new Mock<INacosNamingService>();
_loggerMock = new Mock<ILogger<ServiceClient>>();
// 创建一个测试用的 HttpClient
var httpClientHandler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true
};
_httpClient = new HttpClient(httpClientHandler);
// 创建 ServiceClient 实例
_serviceClient = new ServiceClient(_nacosNamingServiceMock.Object, _loggerMock.Object);
}
[Fact]
public async Task GetAsync_ShouldReturnSuccess_WhenServiceIsAvailable()
{
// Arrange
var serviceName = "TestService";
var endpoint = "/api/test";
var expectedResponse = new { Message = "Test Response" };
// 模拟 Nacos 服务发现返回一个健康的实例
var instance = new Instance
{
Ip = "127.0.0.1",
Port = 8080,
Healthy = true,
Enabled = true
};
_nacosNamingServiceMock.Setup(x => x.SelectOneHealthyInstance(serviceName)).ReturnsAsync(instance);
// 创建一个测试服务器
var testServer = new TestServer(new WebApplicationFactory<object>().WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
// 配置测试服务
});
builder.Configure(app =>
{
app.MapGet("/api/test", () => Results.Ok(expectedResponse));
});
}));
// 使用测试服务器的 HttpClient
var testHttpClient = testServer.CreateClient();
var testServiceClient = new ServiceClient(_nacosNamingServiceMock.Object, testHttpClient, _loggerMock.Object);
// Act
var result = await testServiceClient.GetAsync<object>(serviceName, endpoint);
// Assert
Assert.NotNull(result);
Assert.Equal(expectedResponse.Message, ((dynamic)result).Message);
}
[Fact]
public async Task GetAsync_ShouldThrowException_WhenServiceIsNotAvailable()
{
// Arrange
var serviceName = "NonExistentService";
var endpoint = "/api/test";
// 模拟 Nacos 服务发现返回 null
_nacosNamingServiceMock.Setup(x => x.SelectOneHealthyInstance(serviceName)).ReturnsAsync((Instance)null);
// Act & Assert
await Assert.ThrowsAsync<Exception>(() => _serviceClient.GetAsync<object>(serviceName, endpoint));
}
[Fact]
public async Task GetAsync_ShouldHandleDifferentEndpointFormats()
{
// Arrange
var serviceName = "TestService";
var expectedResponse = new { Message = "Test Response" };
// 模拟 Nacos 服务发现返回一个健康的实例
var instance = new Instance
{
Ip = "127.0.0.1",
Port = 8080,
Healthy = true
};
_nacosNamingServiceMock.Setup(x => x.SelectOneHealthyInstance(serviceName)).ReturnsAsync(instance);
// 创建测试服务器
var testServer = new TestServer(new WebApplicationFactory<object>().WithWebHostBuilder(builder =>
{
builder.Configure(app =>
{
app.Run(async context =>
{
// 验证请求路径是否以 '/' 开头
Assert.StartsWith("/", context.Request.Path);
await context.Response.WriteAsJsonAsync(new { StatusCode = 200, Message = "Success", Data = expectedResponse });
});
});
}));
// 测试不同格式的端点
var endpoints = new[] { "/api/test", "api/test", "test", "/test" };
foreach (var endpoint in endpoints)
{
// Act & Assert
var result = await _serviceClient.GetAsync<object>(serviceName, endpoint);
Assert.NotNull(result);
}
}
[Fact]
public async Task PostAsync_ShouldReturnSuccess_WhenServiceIsAvailable()
{
// Arrange
var serviceName = "TestService";
var endpoint = "/api/test";
var requestData = new { Name = "Test" };
var expectedResponse = new { Message = "Test Response" };
// 模拟 Nacos 服务发现返回一个健康的实例
var instance = new Instance
{
Ip = "127.0.0.1",
Port = 8080,
Healthy = true,
Enabled = true
};
_nacosNamingServiceMock.Setup(x => x.SelectOneHealthyInstance(serviceName)).ReturnsAsync(instance);
// 创建一个测试服务器
var testServer = new TestServer(new WebApplicationFactory<object>().WithWebHostBuilder(builder =>
{
builder.Configure(app =>
{
app.MapPost("/api/test", (object data) => Results.Ok(expectedResponse));
});
}));
// 使用测试服务器的 HttpClient
var testHttpClient = testServer.CreateClient();
var testServiceClient = new ServiceClient(_nacosNamingServiceMock.Object, testHttpClient, _loggerMock.Object);
// Act
var result = await testServiceClient.PostAsync<object>(serviceName, endpoint, requestData);
// Assert
Assert.NotNull(result);
Assert.Equal(expectedResponse.Message, ((dynamic)result).Message);
}
[Fact]
public async Task PutAsync_ShouldReturnSuccess_WhenServiceIsAvailable()
{
// Arrange
var serviceName = "TestService";
var endpoint = "/api/test";
var requestData = new { Name = "Test" };
var expectedResponse = new { Message = "Test Response" };
// 模拟 Nacos 服务发现返回一个健康的实例
var instance = new Instance
{
Ip = "127.0.0.1",
Port = 8080,
Healthy = true,
Enabled = true
};
_nacosNamingServiceMock.Setup(x => x.SelectOneHealthyInstance(serviceName)).ReturnsAsync(instance);
// 创建一个测试服务器
var testServer = new TestServer(new WebApplicationFactory<object>().WithWebHostBuilder(builder =>
{
builder.Configure(app =>
{
app.MapPut("/api/test", (object data) => Results.Ok(expectedResponse));
});
}));
// 使用测试服务器的 HttpClient
var testHttpClient = testServer.CreateClient();
var testServiceClient = new ServiceClient(_nacosNamingServiceMock.Object, testHttpClient, _loggerMock.Object);
// Act
var result = await testServiceClient.PutAsync<object>(serviceName, endpoint, requestData);
// Assert
Assert.NotNull(result);
Assert.Equal(expectedResponse.Message, ((dynamic)result).Message);
}
[Fact]
public async Task DeleteAsync_ShouldReturnSuccess_WhenServiceIsAvailable()
{
// Arrange
var serviceName = "TestService";
var endpoint = "/api/test";
var expectedResponse = new { Message = "Test Response" };
// 模拟 Nacos 服务发现返回一个健康的实例
var instance = new Instance
{
Ip = "127.0.0.1",
Port = 8080,
Healthy = true,
Enabled = true
};
_nacosNamingServiceMock.Setup(x => x.SelectOneHealthyInstance(serviceName)).ReturnsAsync(instance);
// 创建一个测试服务器
var testServer = new TestServer(new WebApplicationFactory<object>().WithWebHostBuilder(builder =>
{
builder.Configure(app =>
{
app.MapDelete("/api/test", () => Results.Ok(expectedResponse));
});
}));
// 使用测试服务器的 HttpClient
var testHttpClient = testServer.CreateClient();
var testServiceClient = new ServiceClient(_nacosNamingServiceMock.Object, testHttpClient, _loggerMock.Object);
// Act
var result = await testServiceClient.DeleteAsync<object>(serviceName, endpoint);
// Assert
Assert.NotNull(result);
Assert.Equal(expectedResponse.Message, ((dynamic)result).Message);
}
}
}

View File

@ -0,0 +1,10 @@
namespace Microservice.Common.Tests;
public class UnitTest1
{
[Fact]
public void Test1()
{
}
}

View File

@ -0,0 +1,25 @@
using System;
namespace Microservice.Common
{
/// <summary>
/// API 异常类
/// </summary>
public class ApiException : Exception
{
/// <summary>
/// 状态码
/// </summary>
public int StatusCode { get; }
/// <summary>
/// 构造函数
/// </summary>
/// <param name="statusCode">状态码</param>
/// <param name="message">异常消息</param>
public ApiException(int statusCode, string message) : base(message)
{
StatusCode = statusCode;
}
}
}

View File

@ -0,0 +1,74 @@
using System;
namespace Microservice.Common
{
/// <summary>
/// 标准化 API 响应格式
/// </summary>
/// <typeparam name="T">响应数据类型</typeparam>
public class ApiResponse<T>
{
/// <summary>
/// 状态码
/// </summary>
public int StatusCode { get; set; }
/// <summary>
/// 响应消息
/// </summary>
public string Message { get; set; }
/// <summary>
/// 时间戳
/// </summary>
public DateTime Timestamp { get; set; }
/// <summary>
/// 数据负载
/// </summary>
public T Data { get; set; }
/// <summary>
/// 构造函数
/// </summary>
public ApiResponse()
{
Timestamp = DateTime.UtcNow;
}
/// <summary>
/// 构造函数
/// </summary>
/// <param name="statusCode">状态码</param>
/// <param name="message">响应消息</param>
/// <param name="data">数据负载</param>
public ApiResponse(int statusCode, string message, T data)
{
StatusCode = statusCode;
Message = message;
Data = data;
Timestamp = DateTime.UtcNow;
}
}
/// <summary>
/// 非泛型 API 响应格式
/// </summary>
public class ApiResponse : ApiResponse<object>
{
/// <summary>
/// 构造函数
/// </summary>
public ApiResponse() : base()
{ }
/// <summary>
/// 构造函数
/// </summary>
/// <param name="statusCode">状态码</param>
/// <param name="message">响应消息</param>
/// <param name="data">数据负载</param>
public ApiResponse(int statusCode, string message, object data) : base(statusCode, message, data)
{ }
}
}

View File

@ -0,0 +1,132 @@
using System.Net;
namespace Microservice.Common
{
/// <summary>
/// API 响应工具类
/// </summary>
public static class ApiResponseHelper
{
/// <summary>
/// 生成成功响应
/// </summary>
/// <typeparam name="T">响应数据类型</typeparam>
/// <param name="data">数据负载</param>
/// <param name="message">响应消息</param>
/// <returns>标准化 API 响应</returns>
public static ApiResponse<T> Success<T>(T data, string message = "Success")
{
return new ApiResponse<T>(
statusCode: (int)HttpStatusCode.OK,
message: message,
data: data
);
}
/// <summary>
/// 生成成功响应(无数据)
/// </summary>
/// <param name="message">响应消息</param>
/// <returns>标准化 API 响应</returns>
public static ApiResponse Success(string message = "Success")
{
return new ApiResponse(
statusCode: (int)HttpStatusCode.OK,
message: message,
data: null
);
}
/// <summary>
/// 生成失败响应
/// </summary>
/// <typeparam name="T">响应数据类型</typeparam>
/// <param name="statusCode">状态码</param>
/// <param name="message">响应消息</param>
/// <param name="data">数据负载</param>
/// <returns>标准化 API 响应</returns>
public static ApiResponse<T> Error<T>(HttpStatusCode statusCode, string message, T data = default)
{
return new ApiResponse<T>(
statusCode: (int)statusCode,
message: message,
data: data
);
}
/// <summary>
/// 生成失败响应(无数据)
/// </summary>
/// <param name="statusCode">状态码</param>
/// <param name="message">响应消息</param>
/// <returns>标准化 API 响应</returns>
public static ApiResponse Error(HttpStatusCode statusCode, string message)
{
return new ApiResponse(
statusCode: (int)statusCode,
message: message,
data: null
);
}
/// <summary>
/// 生成 400 Bad Request 响应
/// </summary>
/// <typeparam name="T">响应数据类型</typeparam>
/// <param name="message">响应消息</param>
/// <param name="data">数据负载</param>
/// <returns>标准化 API 响应</returns>
public static ApiResponse<T> BadRequest<T>(string message = "Bad Request", T data = default)
{
return Error(HttpStatusCode.BadRequest, message, data);
}
/// <summary>
/// 生成 401 Unauthorized 响应
/// </summary>
/// <typeparam name="T">响应数据类型</typeparam>
/// <param name="message">响应消息</param>
/// <param name="data">数据负载</param>
/// <returns>标准化 API 响应</returns>
public static ApiResponse<T> Unauthorized<T>(string message = "Unauthorized", T data = default)
{
return Error(HttpStatusCode.Unauthorized, message, data);
}
/// <summary>
/// 生成 403 Forbidden 响应
/// </summary>
/// <typeparam name="T">响应数据类型</typeparam>
/// <param name="message">响应消息</param>
/// <param name="data">数据负载</param>
/// <returns>标准化 API 响应</returns>
public static ApiResponse<T> Forbidden<T>(string message = "Forbidden", T data = default)
{
return Error(HttpStatusCode.Forbidden, message, data);
}
/// <summary>
/// 生成 404 Not Found 响应
/// </summary>
/// <typeparam name="T">响应数据类型</typeparam>
/// <param name="message">响应消息</param>
/// <param name="data">数据负载</param>
/// <returns>标准化 API 响应</returns>
public static ApiResponse<T> NotFound<T>(string message = "Not Found", T data = default)
{
return Error(HttpStatusCode.NotFound, message, data);
}
/// <summary>
/// 生成 500 Internal Server Error 响应
/// </summary>
/// <typeparam name="T">响应数据类型</typeparam>
/// <param name="message">响应消息</param>
/// <param name="data">数据负载</param>
/// <returns>标准化 API 响应</returns>
public static ApiResponse<T> InternalServerError<T>(string message = "Internal Server Error", T data = default)
{
return Error(HttpStatusCode.InternalServerError, message, data);
}
}
}

View File

@ -0,0 +1,49 @@
using Refit;
using System.Threading.Tasks;
namespace Microservice.Common
{
/// <summary>
/// 通用服务 API 接口定义
/// </summary>
public interface IServiceApi
{
/// <summary>
/// 发送 GET 请求
/// </summary>
/// <typeparam name="T">响应数据类型</typeparam>
/// <param name="endpoint">API 端点</param>
/// <returns>API 响应</returns>
[Get("/{**endpoint}")]
Task<ApiResponse<T>> GetAsync<T>([AliasAs("endpoint")] string endpoint);
/// <summary>
/// 发送 POST 请求
/// </summary>
/// <typeparam name="T">响应数据类型</typeparam>
/// <param name="endpoint">API 端点</param>
/// <param name="data">请求数据</param>
/// <returns>API 响应</returns>
[Post("/{**endpoint}")]
Task<ApiResponse<T>> PostAsync<T>([AliasAs("endpoint")] string endpoint, [Body] object data);
/// <summary>
/// 发送 PUT 请求
/// </summary>
/// <typeparam name="T">响应数据类型</typeparam>
/// <param name="endpoint">API 端点</param>
/// <param name="data">请求数据</param>
/// <returns>API 响应</returns>
[Put("/{**endpoint}")]
Task<ApiResponse<T>> PutAsync<T>([AliasAs("endpoint")] string endpoint, [Body] object data);
/// <summary>
/// 发送 DELETE 请求
/// </summary>
/// <typeparam name="T">响应数据类型</typeparam>
/// <param name="endpoint">API 端点</param>
/// <returns>API 响应</returns>
[Delete("/{**endpoint}")]
Task<ApiResponse<T>> DeleteAsync<T>([AliasAs("endpoint")] string endpoint);
}
}

View File

@ -0,0 +1,12 @@
using System.Threading.Tasks;
namespace Microservice.Common
{
public interface IServiceClient
{
Task<TResponse> GetAsync<TResponse>(string serviceName, string endpoint, object queryParams = null);
Task<TResponse> PostAsync<TResponse>(string serviceName, string endpoint, object data);
Task<TResponse> PutAsync<TResponse>(string serviceName, string endpoint, object data);
Task<TResponse> DeleteAsync<TResponse>(string serviceName, string endpoint);
}
}

View File

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.2" />
<PackageReference Include="nacos-sdk-csharp" Version="1.3.10" />
<PackageReference Include="Polly" Version="7.2.3" />
<PackageReference Include="Refit" Version="10.0.1" />
</ItemGroup>
<ItemGroup>
<Folder Include="Dto\" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,36 @@
namespace Microservice.Common.Models
{
/// <summary>
/// 配置信息模型
/// </summary>
public class ConfigInfoModel
{
/// <summary>
/// 消息
/// </summary>
public string Message { get; set; }
/// <summary>
/// 配置信息
/// </summary>
public string ConfigInfo { get; set; }
/// <summary>
/// 构造函数
/// </summary>
public ConfigInfoModel()
{
}
/// <summary>
/// 构造函数
/// </summary>
/// <param name="message">消息</param>
/// <param name="configInfo">配置信息</param>
public ConfigInfoModel(string message, string configInfo)
{
Message = message;
ConfigInfo = configInfo;
}
}
}

View File

@ -0,0 +1,37 @@
namespace Microservice.Common.Models
{
/// <summary>
/// 服务调用结果模型
/// </summary>
/// <typeparam name="TResult">结果类型</typeparam>
public class ServiceCallResultModel<TResult>
{
/// <summary>
/// 消息
/// </summary>
public string Message { get; set; }
/// <summary>
/// 结果
/// </summary>
public TResult Result { get; set; }
/// <summary>
/// 构造函数
/// </summary>
public ServiceCallResultModel()
{
}
/// <summary>
/// 构造函数
/// </summary>
/// <param name="message">消息</param>
/// <param name="result">结果</param>
public ServiceCallResultModel(string message, TResult result)
{
Message = message;
Result = result;
}
}
}

View File

@ -0,0 +1,136 @@
using Microsoft.Extensions.Logging;
using Nacos.V2;
using Nacos.V2.Common;
using Polly;
using Polly.Retry;
using Refit;
using System;
using System.Text.Json;
using System.Threading.Tasks;
namespace Microservice.Common
{
public class ServiceClient : IServiceClient
{
private readonly INacosNamingService _nacosNamingService;
private readonly ILogger<ServiceClient> _logger;
private readonly AsyncRetryPolicy _retryPolicy;
public ServiceClient(INacosNamingService nacosNamingService, ILogger<ServiceClient> logger)
{
_nacosNamingService = nacosNamingService;
_logger = logger;
// 配置重试策略
_retryPolicy = Policy
.Handle<Exception>()
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt))
);
}
public async Task<TResponse> GetAsync<TResponse>(string serviceName, string endpoint, object queryParams = null)
{
try
{
var result = await SendRequestAsync<TResponse>(serviceName, endpoint, "GET", null, queryParams);
return result;
}
catch (Exception)
{
throw;
}
}
public async Task<TResponse> PostAsync<TResponse>(string serviceName, string endpoint, object data)
{
return await SendRequestAsync<TResponse>(serviceName, endpoint, "POST", data);
}
public async Task<TResponse> PutAsync<TResponse>(string serviceName, string endpoint, object data)
{
return await SendRequestAsync<TResponse>(serviceName, endpoint, "PUT", data);
}
public async Task<TResponse> DeleteAsync<TResponse>(string serviceName, string endpoint)
{
return await SendRequestAsync<TResponse>(serviceName, endpoint, "DELETE");
}
private async Task<TResponse> SendRequestAsync<TResponse>(string serviceName, string endpoint, string method, object data = null, object queryParams = null)
{
try
{
// 从Nacos获取服务实例
var instance = await _nacosNamingService.SelectOneHealthyInstance(serviceName);
if (instance == null)
{
throw new Exception($"未找到服务 {serviceName} 的可用实例");
}
// 构建服务基础URL
var baseUrl = $"http://{instance.Ip}:{instance.Port}";
// 创建Refit服务实例
var serviceApi = RestService.For<IServiceApi>(baseUrl);
// 确保endpoint不包含前导的斜杠因为IServiceApi中的路径模板已经包含了一个斜杠
if (!string.IsNullOrEmpty(endpoint))
{
// 移除所有前导的'/'
endpoint = endpoint.TrimStart('/');
}
// 使用重试策略
TResponse result = default;
await _retryPolicy.ExecuteAsync(async () =>
{
ApiResponse<TResponse> apiResponse;
// 根据HTTP方法调用不同的API
switch (method.ToUpper())
{
case "GET":
apiResponse = await serviceApi.GetAsync<TResponse>(endpoint);
break;
case "POST":
apiResponse = await serviceApi.PostAsync<TResponse>(endpoint, data);
break;
case "PUT":
apiResponse = await serviceApi.PutAsync<TResponse>(endpoint, data);
break;
case "DELETE":
apiResponse = await serviceApi.DeleteAsync<TResponse>(endpoint);
break;
default:
throw new NotSupportedException($"不支持的HTTP方法: {method}");
}
// 检查响应状态码
if (apiResponse.StatusCode >= 200 && apiResponse.StatusCode < 300)
{
// 成功响应,返回数据
result = apiResponse.Data;
}
else
{
// 失败响应,抛出异常
throw new ApiException(apiResponse.StatusCode, apiResponse.Message);
}
});
return result;
}
catch (ApiException)
{
// 重新抛出 ApiException
throw;
}
catch (Exception)
{
throw;
}
}
}
}

View File

@ -0,0 +1,14 @@
using Microsoft.Extensions.DependencyInjection;
namespace Microservice.Common
{
public static class ServiceClientExtensions
{
public static IServiceCollection AddServiceClient(this IServiceCollection services)
{
services.AddTransient<IServiceClient, ServiceClient>();
return services;
}
}
}