技术栈.NET 8 nacos-sdk-csharp Grpc.AspNetCore NacosNetCore.Extensions本文从一个真实项目出发手把手带你完成 .NET Core 接入 Nacos 的服务注册/发现并基于 gRPC 实现 .NET 与 JavaDubbo之间的跨语言互调。所有代码均来自实际项目可直接复用。一、背景与痛点在微服务架构中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 SDK8.0dotnet --version验证Nacos Server2.2.0Docker 部署或独立部署均可IDEVS 2022 / Rider支持 .NET 8 项目四、项目搭建4.1 创建项目dotnet new webapi -n NacosDemo-order --no-https cd NacosDemo-order4.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.NamespaceNacos 命名空间 ID不是名称在 Nacos 控制台创建命名空间后获取nacos.PreferredNetworks多网卡环境下指定注册 IP 的前缀避免注册了 127.0.0.1 或内网不可达的 IPnacos.Port注册到 Nacos 的端口号Java 侧通过这个端口发现你的 HTTP 服务Kestrel.Endpoints双端口配置5073 承载 REST Swagger8084 专供 gRPCHTTP/2踩坑提示gRPC 要求 HTTP/2而浏览器和部分客户端走 HTTP/1.1。Kestrel 的双端口方案让两种协议各走各的互不干扰。5073 端口设置Http1AndHttp2是为了支持 Swagger UI 和 gRPC-Web。5.2 csproj 配置 Proto 文件Project SdkMicrosoft.NET.Sdk.Web PropertyGroup TargetFrameworknet8.0/TargetFramework Nullableenable/Nullable ImplicitUsingsenable/ImplicitUsings RootNamespaceNacosDemo_order/RootNamespace /PropertyGroup ​ ItemGroup !-- 消费端 proto生成 gRPC Client -- Protobuf IncludeProtos\Consumer\dictservice.proto GrpcServicesClient / !-- 生产端 proto生成 gRPC Server -- Protobuf IncludeProtos\Producer\classification_service_generated.proto GrpcServicesServer / /ItemGroup ​ !-- ... NuGet 引用省略 ... -- /ProjectGrpcServicesClient和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?serviceNameorder-service-newgroupNamedubbo_core_groupnamespaceIddubbo七、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 { TaskClassificationListResponse GetClassificationList(ClassificationRequest request); TaskHealthCheckResponse HealthCheck(HealthCheckRequest request); }Business 层纯业务逻辑// Services/Business/ClassificationService.cs public class ClassificationService : IClassificationService { public async TaskClassificationListResponse 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.DeserializeListClassificationInfo(json); if (!string.IsNullOrEmpty(request.key)) { classificationList classificationList? .Where(x x.name.Contains(request.key)).ToList(); } ​ return new ClassificationListResponse { classifications classificationList ?? new ListClassificationInfo() }; } ​ public async TaskHealthCheckResponse 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 ILoggerClassificationGrpcService _logger; ​ public ClassificationGrpcService( IClassificationService classificationService, ILoggerClassificationGrpcService logger) { _classificationService classificationService; _logger logger; } ​ public override async TaskProtos.ClassificationListResponse GetClassificationList( Protos.ClassificationRequest request, ServerCallContext context) { try { _logger.LogInformation($收到 gRPC 调用 GetClassificationListkey{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 ILoggerValuesController _logger; ​ public ValuesController( Nacos.V2.INacosNamingService svc, ILoggerValuesController logger) { _svc svc; _logger logger; } ​ [HttpGet(TestCallBygRPC)] public async Taskstring 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需要硬编码或通过元数据获取SelectOneHealthyInstanceNacos 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→IClassificationService9.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 生成的ServiceBase9.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 ListClassificationInfo classifications { get; set; } }10.4 核心生成器生成器通过反射扫描程序集自动完成找到所有[ProtoService]标记的接口收集接口方法中用到的请求/响应类型额外收集[ProtoMessage]标记的类型生成message和service定义C# 类型自动映射为 proto 类型string→string、int→int32、ListT→repeated T属性名自动转 snake_caseClassificationList→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();十二、踩坑记录坑 1Nacos 注册 IP 不对现象Nacos 控制台显示的 IP 是127.0.0.1或某个内网不可达的 IPJava 侧调不通。解决配置PreferredNetworks指定注册 IP 的前缀匹配PreferredNetworks: XX.XXX.SDK 会优先选择 IP 前缀匹配的网卡地址注册。坑 2gRPC 端口与 Dubbo 端口混淆现象从 Nacos 拿到实例后直接用instance.Port连 gRPC报连接失败。解决Dubbo 注册到 Nacos 的端口是 Dubbo 协议端口如 20880gRPC 通常在另一个端口如 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};坑 3Proto 包名不匹配现象Java 侧通过 Nacos 找到服务但 gRPC 调用报UNIMPLEMENTED。解决.proto文件中的package必须与 Java Dubbo 接口的包名完全一致。Java 侧是com.cn.order.api.dubboproto 也必须是同样的包名否则 gRPC 的 service 路径对不上。坑 4HTTP/2 和浏览器不兼容现象Swagger 页面正常但 gRPC 调用失败。解决Kestrel 双端口方案HTTP 端口5073走Http1AndHttp2gRPC 专用端口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命名空间dubbogRPC 端口8084十四、扩展指南新增业务服务只需 3 步# 1. 写接口 Interface/IOrderService.cs ​ # 2. 写业务实现 Services/Business/OrderService.cs ​ # 3. 写 gRPC 适配 Services/Grpc/OrderGrpcService.cs编译启动自动注册零配置。新增 Proto 消费把 Java 团队的.proto文件放到Protos/Consumer/在.csproj中添加Protobuf IncludeProtos/Consumer/xxx.proto GrpcServicesClient /注入INacosNamingService发现服务用生成的 Client 类发起调用十五、总结本文从实战角度完整覆盖了 .NET Core 接入 Nacos gRPC 的关键路径模块核心要点Nacos 注册AddNacosAspNet一行搞定注意PreferredNetworksgRPC Provider分层架构Business Grpc协议与逻辑解耦gRPC ConsumerNacos 发现 → 拼地址 → 创建 Client → 调用自动注册反射扫描约定优于配置新增服务零配置Proto 生成C# 代码 → .proto 文件跨团队协作利器双端口方案Kestrel 多 EndpointHTTP/1.1 和 HTTP/2 各得其所