<p>微服务本质是协作约定,C# 实践需规范项目结构、端口隔离、IHttpClientFactory 调用、Consul 主动注册及 Shared.Core 仅限接口与 DTO。</p>

微服务不是靠“加个 nuget 包”或者“改个配置”就能跑起来的架构,它本质是一套协作约定。C# 做微服务,核心不在语言本身,而在你如何组织项目、隔离依赖、管理通信和部署边界。
怎么用 dotnet CLI 初始化微服务项目结构
别从一个空解决方案开始拖项目,容易漏引用、端口冲突、命名混乱。先用命令行固化结构习惯:
- 统一根目录:
dotnet new sln -n MyEcommerce - 每个服务独立 Web API 项目,强制命名带
Service后缀(避免后期跟类库混淆):dotnet new webapi -n UserService、dotnet new webapi -n OrderService - 共享层必须是
Class Library,不是Web API或Console App:dotnet new classlib -n Shared.Contracts - 添加引用时用完整路径,防止 IDE 自动选错版本:
dotnet add UserService reference Shared.Contracts
关键点:所有服务的 launchSettings.json 必须手动设不同 applicationUrl,比如 https://localhost:5001、https://localhost:5002。不改就全挤在 5000 端口,启动时直接报 Address already in use。
为什么不能直接 new HttpClient() 调用其他服务
写 new HttpClient() 看似简单,但会引发 DNS 缓存泄漏、连接池耗尽、超时不可控三大问题。.NET 的 IHttpClientFactory 是专为服务间调用设计的工厂,它自动复用连接、管理生命周期、集成 Polly 熔断。
- 注册客户端时必须起逻辑名,不是 URL:
builder.Services.AddHttpClient("UserServiceClient", c => c.BaseAddress = new Uri("https://localhost:5001")) - 控制器里通过构造函数注入,别用
IServiceProvider.GetService手动取:private readonly HttpClient _userServiceClient; - 调用时用
await _userServiceClient.GetAsync("/users/123"),别拼接完整 URL —— BaseAddress 已经兜底了路径前缀
常见错误:把 AddHttpClient 写在 Program.cs 末尾,或写在某个 if 分支里,导致服务未注册,运行时报 InvalidOperationException: No service for type 'IHttpClientFactory'。
Consul 注册失败的典型原因和验证方式
Consul 不是“配完就自动发现”的黑盒。C# 服务要主动向 Consul 的 HTTP API 发送注册请求,失败通常卡在三处:
- Consul Agent 没跑起来:
consul agent -dev -client=0.0.0.0 -bind=127.0.0.1必须先执行,且终端不能关 - 服务启动早于 Consul 可用:
HealthCheck未通过就注册,Consul 直接拒收。应在Program.cs中加重试逻辑,或用HostApplicationBuilder.Services.AddHostedService<ConsulRegistrationService>()延后注册时机 - 注册体 JSON 字段名写错:必须是
Address(不是address)、Port(不是port)、Service对象下嵌套Name和ID,大小写敏感
验证是否注册成功,别只看 Consul UI。直接 curl:curl http://localhost:8500/v1/health/service/user-service,返回空数组说明没注册,返回带节点信息的 JSON 才算落地。
Shared.Core 库里绝对不能放什么
共享库看似“方便”,实则是微服务解耦的第一道防线。以下内容一旦放进 Shared.Core,等于把服务绑死在同一技术栈、同一部署节奏上:
- EF Core 的
DbContext类或DbSet<>属性 —— 这会让所有服务强依赖同一数据库连接字符串和迁移策略 - ASP.NET Core 的中间件类(如
JwtBearerMiddleware)、ControllerBase子类 —— 违反“表现层隔离”原则 - 任何具体实现类,比如
RedisCacheService、SmtpEmailSender—— 共享的只能是接口(IEmailSender)和 DTO(UserCreatedEvent) -
appsettings.json的绑定类(如DatabaseOptions)—— 配置应由各服务自己解析,否则环境变量无法差异化
最常被忽略的一点:DTO 中不要用 DateTime,改用 DateTimeOffset。服务分布在不同时区机器上,DateTime.Kind 丢失会导致时间比较逻辑全线崩溃。


















