.NET Core 接入 Nacos + gRPC 实战:从零搭建跨语言微服务
技术栈:.NET 8 + nacos-sdk-csharp + Grpc.AspNetCore + NacosNetCore.Extensions
本文从一个真实项目出发,手把手带你完成 .NET Core 接入 Nacos 的服务注册/发现,并基于 gRPC 实现 .NET 与 Java(Dubbo)之间的跨语言互调。所有代码均来自实际项目,可直接复用。
一、背景与痛点
在微服务架构中,Java 生态有 Nacos + Dubbo 的成熟方案,但 .NET 侧接入 Nacos 的资料相对较少。当你的团队需要:
.NET 服务注册到 Nacos,让 Java 侧能发现并调用
.NET 作为消费者,从 Nacos 发现并调用 Java Dubbo-gRPC 服务
REST 和 gRPC 双协议共存,对外提供 HTTP API 的同时支持 gRPC 高性能调用
网上能搜到的文章多是"跑通 Hello World",缺少真实项目的分层设计、自动注册、Proto 生成等工程化实践。本文补上这块空白。
二、最终效果
项目跑起来后,你将获得:
| 能力 | 说明 |
|---|---|
| 服务自动注册到 Nacos | 启动即注册,无需手动操作 |
| 提供 gRPC 服务给 Java 调用 | Java 通过 Dubbo-gRPC 协议直接调用 |
| 提供 REST API 给前端调用 | 标准 HTTP 接口,Swagger 文档自动生成 |
| 调用远程 Java gRPC 服务 | 从 Nacos 发现实例,发起 gRPC 调用 |
| 业务层与协议层分离 | 加新接口只需写业务逻辑,协议层自动适配 |
| Proto 文件自动生成 | C# 代码定义接口,一键生成 .proto 交给 Java 团队 |
三、环境准备
| 依赖 | 版本 | 说明 |
|---|---|---|
| .NET SDK | 8.0+ | dotnet --version验证 |
| Nacos Server | 2.2.0+ | Docker 部署或独立部署均可 |
| IDE | VS 2022 / Rider | 支持 .NET 8 项目 |
四、项目搭建
4.1 创建项目
dotnet new webapi -n NacosDemo-order --no-https cd NacosDemo-order
4.2 安装 NuGet 包
这是本文用到的核心包,版本亲测可用:
# Nacos SDK dotnet add package nacos-sdk-csharp --version 1.3.10 dotnet add package nacos-sdk-csharp.AspNetCore --version 1.3.10 dotnet add package nacos-sdk-csharp.Extensions.Configuration --version 1.3.10 dotnet add package nacos-sdk-csharp.IniParser --version 1.3.10 dotnet add package nacos-sdk-csharp.YamlParser --version 1.3.10 # Nacos 扩展(简化注册) dotnet add package NacosNetCore.Extensions --version 1.0.4.4 # gRPC dotnet add package Grpc.AspNetCore --version 2.80.0 dotnet add package Grpc.Tools --version 2.80.0 dotnet add package Google.Protobuf --version 3.34.1 dotnet add package Google.Api.CommonProtos --version 2.17.0 # 其他 dotnet add package Swashbuckle.AspNetCore --version 10.1.7 dotnet add package Newtonsoft.Json --version 13.0.4
踩坑提示:
nacos-sdk-csharp的 ASP.NET Core 集成需要配合NacosNetCore.Extensions使用,后者封装了AddNacosAspNet方法,一行代码搞定注册。如果只用nacos-sdk-csharp.AspNetCore,需要手动配置较多内容。
五、核心配置
5.1 appsettings.json
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning", "Microsoft.AspNetCore.Hosting.Diagnostics": "Error", "Grpc": "Information" } }, "AllowedHosts": "*", "nacos": { "ServerAddresses": [ "http://xx.xxx.xxx.xxx:8848" ], "Namespace": "dubbo", "ServiceName": "order-service-new", "GroupName": "dubbo_core_group", "UserName": "nacos", "Password": "nacos", "PreferredNetworks": "xx.xxx.", "Port": 5073 }, "Kestrel": { "Endpoints": { "Http": { "Url": "http://0.0.0.0:5073", "Protocols": "Http1AndHttp2" }, "Grpc": { "Url": "http://0.0.0.0:8084", "Protocols": "Http2" } } } }几个关键配置说明:
| 配置项 | 说明 |
|---|---|
nacos.Namespace | Nacos 命名空间 ID,不是名称!在 Nacos 控制台创建命名空间后获取 |
nacos.PreferredNetworks | 多网卡环境下指定注册 IP 的前缀,避免注册了 127.0.0.1 或内网不可达的 IP |
nacos.Port | 注册到 Nacos 的端口号,Java 侧通过这个端口发现你的 HTTP 服务 |
Kestrel.Endpoints | 双端口配置:5073 承载 REST + Swagger,8084 专供 gRPC(HTTP/2) |
踩坑提示:gRPC 要求 HTTP/2,而浏览器和部分客户端走 HTTP/1.1。Kestrel 的双端口方案让两种协议各走各的,互不干扰。5073 端口设置
Http1AndHttp2是为了支持 Swagger UI 和 gRPC-Web。
5.2 csproj 配置 Proto 文件
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> <RootNamespace>NacosDemo_order</RootNamespace> </PropertyGroup> <ItemGroup> <!-- 消费端 proto:生成 gRPC Client --> <Protobuf Include="Protos\Consumer\dictservice.proto" GrpcServices="Client" /> <!-- 生产端 proto:生成 gRPC Server --> <Protobuf Include="Protos\Producer\classification_service_generated.proto" GrpcServices="Server" /> </ItemGroup> <!-- ... NuGet 引用省略 ... --> </Project>
GrpcServices="Client"和"Server"决定了生成的是客户端桩代码还是服务端基类,别搞反了。
六、Nacos 服务注册
6.1 一行代码注册 Nacos
在Program.cs中:
var builder = WebApplication.CreateBuilder(args); // 注册 Nacos —— 核心就这一行 builder.Services.AddNacosAspNet(builder.Configuration, "nacos"); var app = builder.Build(); app.Run();
AddNacosAspNet是NacosNetCore.Extensions提供的扩展方法,它会:
读取
appsettings.json中"nacos"节点的配置向 Nacos Server 注册当前服务实例
启动心跳保活(Nacos 2.x 使用 gRPC 长连接,不再依赖 HTTP 心跳)
应用关闭时自动注销
6.2 验证注册成功
启动项目后,打开 Nacos 控制台 → 服务列表,应该能看到:
服务名:
order-service-new分组:
dubbo_core_group命名空间:
dubbo实例 IP 和端口正确
也可以通过 HTTP 接口验证:
curl http://localhost:8848/nacos/v1/ns/instance/list?serviceName=order-service-new&groupName=dubbo_core_group&namespaceId=dubbo
七、gRPC 服务提供(Provider 端)
这部分是 .NET 作为 gRPC 服务提供者,让 Java 侧能通过 Dubbo-gRPC 调用我们。
7.1 定义 Proto 文件
syntax = "proto3"; package com.cn.order.api.dubbo; option csharp_namespace = "NacosDemo_order.Protos"; message ClassificationRequest { string key = 1; } message ClassificationListResponse { repeated ClassificationInfo classifications = 1; } message ClassificationInfo { string id = 1; string name = 2; int32 sort = 3; } message HealthCheckRequest { } message HealthCheckResponse { string status = 1; string message = 2; string version = 3; } service ClassificationDubboService { rpc GetClassificationList(ClassificationRequest) returns (ClassificationListResponse); rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse); }注意:
package名称必须和 Java Dubbo 侧的包名完全一致(如com.cn.order.api.dubbo),否则 Java 侧通过 Nacos 找到服务后无法正确路由到 gRPC 方法。
7.2 分层架构实现 gRPC 服务
这里我采用了Business 层 + Grpc 层的分层设计:
Services/ ├── Business/ # 业务逻辑层(核心) │ └── ClassificationService # 纯 C# 业务逻辑,无协议依赖 └── Grpc/ # gRPC 协议层(适配器) └── ClassificationGrpcService # 协议转换,调用 Business 层
为什么要分层?因为业务逻辑不应被 gRPC 协议绑架。将来如果要加 REST、GraphQL 或消息队列,只需新增对应的协议适配层,Business 层不动。
接口定义
// Interface/IClassificationService.cs [ProtoService( Package = "com.cn.order.api.dubbo", ServiceName = "ClassificationDubboService", CSharpNamespace = "NacosDemo_order.Protos" )] public interface IClassificationService { Task<ClassificationListResponse> GetClassificationList(ClassificationRequest request); Task<HealthCheckResponse> HealthCheck(HealthCheckRequest request); }Business 层(纯业务逻辑)
// Services/Business/ClassificationService.cs public class ClassificationService : IClassificationService { public async Task<ClassificationListResponse> GetClassificationList(ClassificationRequest request) { // 模拟数据源 string json = @" [ {""id"":""1"",""name"":""AA"",""sort"":1}, {""id"":""2"",""name"":""BB"",""sort"":2}, {""id"":""3"",""name"":""CC"",""sort"":3}, {""id"":""4"",""name"":""DD"",""sort"":4} ]"; var classificationList = JsonSerializer.Deserialize<List<ClassificationInfo>>(json); if (!string.IsNullOrEmpty(request.key)) { classificationList = classificationList? .Where(x => x.name.Contains(request.key)).ToList(); } return new ClassificationListResponse { classifications = classificationList ?? new List<ClassificationInfo>() }; } public async Task<HealthCheckResponse> HealthCheck(HealthCheckRequest request) { return new HealthCheckResponse { status = "UP", message = "服务运行正常", version = "1.0.0" }; } }Grpc 层(协议适配)
// Services/Grpc/ClassificationGrpcService.cs public class ClassificationGrpcService : Protos.ClassificationDubboService.ClassificationDubboServiceBase { private readonly IClassificationService _classificationService; private readonly ILogger<ClassificationGrpcService> _logger; public ClassificationGrpcService( IClassificationService classificationService, ILogger<ClassificationGrpcService> logger) { _classificationService = classificationService; _logger = logger; } public override async Task<Protos.ClassificationListResponse> GetClassificationList( Protos.ClassificationRequest request, ServerCallContext context) { try { _logger.LogInformation($"收到 gRPC 调用 GetClassificationList,key={request.Key}"); // DTO 转换 var dtoRequest = new DTO.ClassificationRequest { key = request.Key }; var dtoResponse = await _classificationService.GetClassificationList(dtoRequest); // Proto 转换 var response = new Protos.ClassificationListResponse(); foreach (var item in dtoResponse.classifications) { response.Classifications.Add(new Protos.ClassificationInfo { Id = item.id, Name = item.name, Sort = item.sort }); } return response; } catch (Exception ex) { _logger.LogError(ex, "gRPC GetClassificationList 调用失败"); throw; } } }Grpc 层只做三件事:接收请求 → 转换 DTO → 调用 Business 层 → 转换响应。不写任何业务逻辑。
八、gRPC 服务消费(Consumer 端)
.NET 同时也是消费者,需要调用 Java 侧的 Dubbo-gRPC 服务(如字典服务)。
8.1 获取 Java 侧的 Proto 文件
从 Java 团队拿到.proto文件后放到Protos/Consumer/目录:
// Protos/Consumer/dictservice.proto syntax = "proto3"; package com.cn.common.api.dubbo; option csharp_namespace = "GrpcServiceDemo"; message DictRequest { int32 tenant_id = 1; string key = 2; } message DictListResponse { repeated DictResponse dicts = 1; } // ... 其他消息定义省略 ... service DictDubboService { rpc getDictsByKey(DictRequest) returns (DictListResponse); // ... 其他方法 ... }8.2 从 Nacos 发现服务并发起 gRPC 调用
// Controllers/ValuesController.cs [ApiController] [Route("api/[controller]")] public class ValuesController : ControllerBase { private readonly Nacos.V2.INacosNamingService _svc; private readonly ILogger<ValuesController> _logger; public ValuesController( Nacos.V2.INacosNamingService svc, ILogger<ValuesController> logger) { _svc = svc; _logger = logger; } [HttpGet("TestCallBygRPC")] public async Task<string> TestCallBygRPC() { try { // 1. 从 Nacos 获取健康实例 var instance = await _svc.SelectOneHealthyInstance( "providers:com.cn.common.api.dubbo.DictDubboService::", "dubbo_core_group" ); if (instance == null) return "服务实例不可用"; // 2. 拼接 gRPC 地址(注意:用 gRPC 端口,不是 Dubbo 注册的端口) var address = $"http://{instance.Ip}:8084"; var channel = GrpcChannel.ForAddress(address); // 3. 构造请求 var request = new DictRequest { TenantId = 0, Key = "busReqTypeDict" }; // 4. 调用远程服务 var client = new DictDubboService.DictDubboServiceClient(channel); var response = await client.getDictsByKeyAsync(request); return response.ToString(); } catch (RpcException ex) { _logger.LogError(ex, $"gRPC 调用失败:{ex.Status}"); return $"错误:{ex.Status.Detail}"; } } }几个关键点:
服务名格式:Dubbo 注册到 Nacos 的服务名是
providers:{接口全限定名}::这种格式,不是简单的服务名端口问题:Nacos 返回的
instance.Port是 Dubbo 协议端口,gRPC 通常在不同端口(如 8084),需要硬编码或通过元数据获取SelectOneHealthyInstance:Nacos SDK 自带负载均衡,自动选择一个健康实例
九、自动注册机制(告别手动配置)
每新增一个 Service 或 gRPC 服务都要手动注册?太累了。通过反射扫描实现自动注册。
9.1 自动注册 Business 层
// Common/ServiceCollectionExtensions.cs public static IServiceCollection AddServicesByAssembly(this IServiceCollection services) { var assembly = Assembly.GetExecutingAssembly(); var serviceTypes = assembly.GetTypes() .Where(t => t.IsClass && !t.IsAbstract && t.Name.EndsWith("Service") && t.GetInterfaces().Any() ).ToList(); foreach (var type in serviceTypes) { var interfaceType = type.GetInterfaces() .FirstOrDefault(i => i.Name == $"I{type.Name}"); if (interfaceType != null) { services.AddScoped(interfaceType, type); } } return services; }约定规则:
类名以
Service结尾实现了对应的
I{Name}Service接口例如
ClassificationService→IClassificationService
9.2 自动映射 gRPC 服务
public static IEndpointRouteBuilder MapGrpcServicesByAssembly(this IEndpointRouteBuilder endpoints) { var assembly = Assembly.GetExecutingAssembly(); var grpcServiceTypes = assembly.GetTypes() .Where(t => t.IsClass && !t.IsAbstract && t.Name.EndsWith("GrpcService") && t.BaseType != null && t.BaseType.Name.EndsWith("ServiceBase") ).ToList(); var mapGrpcServiceMethod = typeof(GrpcEndpointRouteBuilderExtensions) .GetMethods(BindingFlags.Public | BindingFlags.Static) .FirstOrDefault(m => m.Name == "MapGrpcService" && m.IsGenericMethod && m.GetParameters().Length == 1 ); foreach (var serviceType in grpcServiceTypes) { if (mapGrpcServiceMethod != null) { var genericMethod = mapGrpcServiceMethod.MakeGenericMethod(serviceType); genericMethod.Invoke(null, new object[] { endpoints }); } } return endpoints; }约定规则:
类名以
GrpcService结尾继承自 proto 生成的
ServiceBase
9.3 Program.cs 一行搞定
// 自动注册所有 Service builder.Services.AddServicesByAssembly(); // ... 其他配置 ... // 自动映射所有 gRPC 服务 app.MapGrpcServicesByAssembly();
之后新增业务,只需要:
写
I{Name}Service接口写
{Name}Service实现类写
{Name}GrpcService适配类
零配置,自动生效。
十、Proto 自动生成工具(亮点功能)
这是本项目最值得分享的功能:从 C# 代码反向生成 .proto 文件。
传统流程是先写.proto→ 生成 C# 代码 → 写业务逻辑。但在 .NET 先行的项目中,我们往往先设计 C# 接口,再把契约给 Java 团队。手动写.proto容易出错且重复劳动。
10.1 设计思路
C# 接口定义 + DTO → [ProtoService] / [ProtoMessage] 标记 → 运行生成工具 → 输出 .proto 文件
10.2 自定义属性
// Common/ProtoGen/ProtoAttributes.cs /// <summary> /// 标记需要生成 .proto 的服务接口 /// </summary> [AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class)] public class ProtoServiceAttribute : Attribute { public string Package { get; set; } // proto 包名 public string ServiceName { get; set; } // 服务名 public string CSharpNamespace { get; set; } // C# 命名空间 } /// <summary> /// 标记需要生成 proto 消息的类 /// </summary> [AttributeUsage(AttributeTargets.Class | AttributeTargets.Enum)] public class ProtoMessageAttribute : Attribute { } /// <summary> /// 标记字段编号 /// </summary> [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] public class ProtoFieldAttribute : Attribute { public int Number { get; set; } public ProtoFieldAttribute(int number) => Number = number; }10.3 标记 DTO
// DTO/ProtoMessageClasses.cs [ProtoMessage] public class ClassificationRequest { [ProtoField(1)] public string key { get; set; } } [ProtoMessage] public class ClassificationInfo { [ProtoField(1)] public string id { get; set; } [ProtoField(2)] public string name { get; set; } [ProtoField(3)] public int sort { get; set; } } [ProtoMessage] public class ClassificationListResponse { [ProtoField(1)] public List<ClassificationInfo> classifications { get; set; } }10.4 核心生成器
生成器通过反射扫描程序集,自动完成:
找到所有
[ProtoService]标记的接口收集接口方法中用到的请求/响应类型
额外收集
[ProtoMessage]标记的类型生成
message和service定义C# 类型自动映射为 proto 类型(
string→string、int→int32、List<T>→repeated T)属性名自动转 snake_case(
ClassificationList→classification_list)
// 核心类型映射 private string GetProtoType(Type type) { if (type == typeof(string)) return "string"; if (type == typeof(int) || type == typeof(int?)) return "int32"; if (type == typeof(long) || type == typeof(long?)) return "int64"; if (type == typeof(bool) || type == typeof(bool?)) return "bool"; if (type == typeof(double) || type == typeof(double?)) return "double"; if (type == typeof(float) || type == typeof(float?)) return "float"; if (type == typeof(byte[])) return "bytes"; if (type.IsEnum) return type.Name; return type.Name; // 自定义消息类型 }10.5 一键生成
dotnet run -- --generate-proto
输出到Protos/Producer/classification_service_generated.proto,直接交给 Java 团队即可。
10.6 生成结果
syntax = "proto3"; package com.cn.order.api.dubbo; option csharp_namespace = "NacosDemo_order.Protos"; message ClassificationRequest { string key = 1; } message ClassificationListResponse { repeated ClassificationInfo classifications = 1; } message HealthCheckRequest { } message HealthCheckResponse { string status = 1; string message = 2; string version = 3; } message ClassificationInfo { string id = 1; string name = 2; int32 sort = 3; } service ClassificationDubboService { rpc GetClassificationList(ClassificationRequest) returns (ClassificationListResponse); rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse); }十一、Program.cs 完整配置
using Nacos.AspNetCore.V2; using NacosDemo_order.Common; using NacosDemo_order.Tools; // Proto 生成工具入口(独立运行) if (args.Length > 0 && args[0] == "--generate-proto") { ProtoGenerationTool.Run(args); return; } var builder = WebApplication.CreateBuilder(args); // 1. 自动注册 Business 层服务 builder.Services.AddServicesByAssembly(); // 2. 注册控制器和 gRPC builder.Services.AddControllers(); builder.Services.AddGrpc(); builder.Services.AddHealthChecks(); // 3. 注册 Nacos(一行搞定) builder.Services.AddNacosAspNet(builder.Configuration, "nacos"); // 4. 注册 HttpClientFactory builder.Services.AddHttpClient("nacosService"); // 5. Swagger builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(options => { options.SwaggerDoc("v1", new OpenApiInfo { Title = "Order Service API", Version = "v1", Description = "基于 .NET Core + Nacos + gRPC 的订单服务接口文档" }); }); var app = builder.Build(); // 中间件 if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(options => { options.SwaggerEndpoint("/swagger/v1/swagger.json", "Order Service v1"); options.RoutePrefix = string.Empty; }); } app.UseRouting(); app.UseAuthorization(); // 映射控制器 app.MapControllers(); // 自动映射所有 gRPC 服务(无需手动添加) app.MapGrpcServicesByAssembly(); // 健康检查端点 app.MapHealthChecks("/health"); app.Run();十二、踩坑记录
坑 1:Nacos 注册 IP 不对
现象:Nacos 控制台显示的 IP 是127.0.0.1或某个内网不可达的 IP,Java 侧调不通。
解决:配置PreferredNetworks,指定注册 IP 的前缀匹配:
"PreferredNetworks": "XX.XXX."
SDK 会优先选择 IP 前缀匹配的网卡地址注册。
坑 2:gRPC 端口与 Dubbo 端口混淆
现象:从 Nacos 拿到实例后直接用instance.Port连 gRPC,报连接失败。
解决:Dubbo 注册到 Nacos 的端口是 Dubbo 协议端口(如 20880),gRPC 通常在另一个端口(如 8084)。需要单独约定或通过 Nacos 元数据传递:
// 方式 1:硬编码 gRPC 端口(简单场景) var address = $"http://{instance.Ip}:8084"; // 方式 2:通过元数据获取(推荐) var grpcPort = instance.Metadata.TryGetValue("grpcPort", out var port) ? port : "8084"; var address = $"http://{instance.Ip}:{grpcPort}";坑 3:Proto 包名不匹配
现象:Java 侧通过 Nacos 找到服务,但 gRPC 调用报UNIMPLEMENTED。
解决:.proto文件中的package必须与 Java Dubbo 接口的包名完全一致。Java 侧是com.cn.order.api.dubbo,proto 也必须是同样的包名,否则 gRPC 的 service 路径对不上。
坑 4:HTTP/2 和浏览器不兼容
现象:Swagger 页面正常,但 gRPC 调用失败。
解决:Kestrel 双端口方案,HTTP 端口(5073)走Http1AndHttp2,gRPC 专用端口(8084)走纯Http2。这样 Swagger 和 gRPC 各走各的。
十三、Java 侧调用指南
把生成的.proto文件给 Java 团队后,他们需要:
将 proto 文件放入项目中,使用
protoc生成 Java Stub通过 Nacos 发现 .NET 服务实例
使用 Dubbo-gRPC 协议调用
服务发现信息:
| 项 | 值 |
|---|---|
| Nacos 服务名 | providers:com.cn.order.api.dubbo.ClassificationDubboService:: |
| 分组 | dubbo_core_group |
| 命名空间 | dubbo |
| gRPC 端口 | 8084 |
十四、扩展指南
新增业务服务
只需 3 步:
# 1. 写接口 Interface/IOrderService.cs # 2. 写业务实现 Services/Business/OrderService.cs # 3. 写 gRPC 适配 Services/Grpc/OrderGrpcService.cs
编译启动,自动注册,零配置。
新增 Proto 消费
把 Java 团队的
.proto文件放到Protos/Consumer/在
.csproj中添加<Protobuf Include="Protos/Consumer/xxx.proto" GrpcServices="Client" />注入
INacosNamingService发现服务,用生成的 Client 类发起调用
十五、总结
本文从实战角度,完整覆盖了 .NET Core 接入 Nacos + gRPC 的关键路径:
| 模块 | 核心要点 |
|---|---|
| Nacos 注册 | AddNacosAspNet一行搞定,注意PreferredNetworks |
| gRPC Provider | 分层架构(Business + Grpc),协议与逻辑解耦 |
| gRPC Consumer | Nacos 发现 → 拼地址 → 创建 Client → 调用 |
| 自动注册 | 反射扫描,约定优于配置,新增服务零配置 |
| Proto 生成 | C# 代码 → .proto 文件,跨团队协作利器 |
| 双端口方案 | Kestrel 多 Endpoint,HTTP/1.1 和 HTTP/2 各得其所 |