「新增厂商」是在已有域里加一家供应商;「创建全新的域」是从零搭起一个能力领域。这篇教程用 Captcha(验证码) 域做样本,演示如何定义领域契约、接入第一家厂商、再做一个聚合包收尾。
库里的 Captcha 域已经存在,本教程复用它的真实结构作示例素材,便于你对照源码学习。
域的全貌
一个新域由三个包组成,遵循三层包模式:
抽象包定义「这个域干什么」,厂商包定义「某家具体怎么做」,聚合包一键拉起所有厂商。
第 1 步:创建抽象包
包名 Bitzsoft.Integrations.{域}(如 Bitzsoft.Integrations.Captcha),引用 Core 和 Compatibility 两个基础包。抽象包不含任何厂商逻辑,也不做 DI 注册。
<!-- src/Bitzsoft.Integrations.Captcha/Bitzsoft.Integrations.Captcha.csproj --><Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFrameworks>net5.0;net8.0;net10.0</TargetFrameworks> <PackageId>Bitzsoft.Integrations.Captcha</PackageId> <Description>验证码抽象层 — 统一接口定义与基础模型</Description> <PackageTags>captcha;abstraction;integration;Bitzsoft</PackageTags> </PropertyGroup>
<ItemGroup> <ProjectReference Include="..\Bitzsoft.Integrations.Core\Bitzsoft.Integrations.Core.csproj" /> <ProjectReference Include="..\Bitzsoft.Integrations.Compatibility\Bitzsoft.Integrations.Compatibility.csproj" /> </ItemGroup>
<ItemGroup> <!-- ① 向未来的厂商包和聚合包开放 internal 成员 --> <InternalsVisibleTo Include="Bitzsoft.Integrations.Captcha.Aliyun" /> <InternalsVisibleTo Include="Bitzsoft.Integrations.Captcha.Tencent" /> <InternalsVisibleTo Include="Bitzsoft.Integrations.Captcha.GeeTest" /> <InternalsVisibleTo Include="Bitzsoft.Integrations.Captcha.All" /> </ItemGroup></Project>抽象包要定义五类成员,缺一不可。
1.1 领域接口
参照 Payment 域的 IPaymentProvider,定义域的能力契约。方法签名都要返回 CaptchaResult<T>,不要抛异常给调用方。
using Bitzsoft.Integrations.Captcha.Models;
namespace Bitzsoft.Integrations.Captcha;
/// <summary>验证码统一抽象接口</summary>/// <remarks>/// 封装无感验证码的「生成挑战」与「服务端二次校验」能力。/// 前端 SDK 自渲染挑战 → 用户无感验证 → 前端提交 token → 后端调校验 API。/// </remarks>public interface ICaptchaProvider{ /// <summary>供应商编码</summary> string Code { get; }
/// <summary>生成验证码挑战,返回前端 SDK 初始化所需凭证</summary> Task<CaptchaResult<CaptchaChallenge>> GenerateAsync( CaptchaGenerateRequest request, CancellationToken ct = default);
/// <summary>服务端二次校验前端提交的 token</summary> Task<CaptchaResult<CaptchaVerification>> VerifyAsync( CaptchaVerifyRequest request, CancellationToken ct = default);}1.2 配置提供器基接口
抽象包定义一个泛型基接口,厂商包各自继承。HttpClientName 让厂商实现能拿到命名客户端。
namespace Bitzsoft.Integrations.Captcha;
/// <summary>验证码配置提供器接口</summary>/// <typeparam name="TOptions">供应商配置选项类型</typeparam>public interface ICaptchaConfigProvider<TOptions> where TOptions : class{ /// <summary>当前供应商配置选项</summary> TOptions Current { get; }
/// <summary>HttpClient 注册名称,用于 IHttpClientFactory 创建命名客户端</summary> string HttpClientName { get; }}1.3 结果类型
仿照 PaymentResult<T>,定义带数据和不带数据两种。提供 Success / Fail 工厂方法。
namespace Bitzsoft.Integrations.Captcha;
/// <summary>验证码结果包装</summary>public sealed class CaptchaResult<T>{ public bool IsSuccess { get; init; } public T? Data { get; init; } public string? ErrorCode { get; init; } public string? ErrorMessage { get; init; }
public static CaptchaResult<T> Success(T data) => new() { IsSuccess = true, Data = data };
public static CaptchaResult<T> Fail(string errorCode, string errorMessage) => new() { IsSuccess = false, ErrorCode = errorCode, ErrorMessage = errorMessage };}
/// <summary>无数据结果,仅表示成功 / 失败</summary>public sealed class CaptchaResult{ public bool IsSuccess { get; init; } public string? ErrorCode { get; init; } public string? ErrorMessage { get; init; }
public static CaptchaResult Success() => new() { IsSuccess = true };
public static CaptchaResult Fail(string errorCode, string errorMessage) => new() { IsSuccess = false, ErrorCode = errorCode, ErrorMessage = errorMessage };}1.4 领域异常
继承 IntegrationException,构造时传 domain 标识和厂商原始错误码。HttpClient 封装遇到非 2xx 抛这个异常,Provider 捕获后转 CaptchaResult。
using Bitzsoft.Integrations.Core;
namespace Bitzsoft.Integrations.Captcha;
/// <summary>验证码统一异常</summary>public class CaptchaException : IntegrationException{ public string ProviderName => Provider ?? string.Empty; public new string? ErrorCode => base.ErrorCode; public new string? ProviderMessage => base.ProviderMessage;
public CaptchaException(string providerName, string? errorCode, string? providerMessage) : base( domain: "Captcha", // ① 域标识,与目录里的 domain 字段一致 message: $"[{providerName}] {errorCode}: {providerMessage}", provider: providerName, errorCode: errorCode, providerMessage: providerMessage) { }}1.5 厂商标识常量
这是 Provider ID 的事实源。每接入一家厂商在这里加一个常量,发布后不可改名。
namespace Bitzsoft.Integrations.Captcha.Models;
/// <summary>验证码供应商标识</summary>public static class CaptchaProviderCode{ public const string Aliyun = "Aliyun"; // 阿里云验证码 2.0 public const string Tencent = "Tencent"; // 腾讯天御 public const string GeeTest = "GeeTest"; // 极验}第 2 步:创建第一家厂商包
流程和新增厂商 Provider 指南完全一致——这里只列关键骨架,完整 9 步参考那篇指南。
包名 Bitzsoft.Integrations.{域}.{厂商}(如 Bitzsoft.Integrations.Captcha.Aliyun),引用抽象包 + Compatibility + RequestLogging:
<!-- src/Bitzsoft.Integrations.Captcha.Aliyun/Bitzsoft.Integrations.Captcha.Aliyun.csproj --><ItemGroup> <ProjectReference Include="..\Bitzsoft.Integrations.Compatibility\..." /> <ProjectReference Include="..\Bitzsoft.Integrations.Captcha\Bitzsoft.Integrations.Captcha.csproj" /> <ProjectReference Include="..\Bitzsoft.Integrations.RequestLogging\..." /></ItemGroup>厂商包关键文件:
// Options:字符串字段 = string.Empty,HttpClientName = nameof(Provider)public sealed class AliyunOptions{ public string AccessKeyId { get; set; } = string.Empty; public string AccessKeySecret { get; set; } = string.Empty; public string SceneId { get; set; } = string.Empty; public string Endpoint { get; set; } = "captcha.aliyuncs.com"; public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(30); public string HttpClientName { get; set; } = nameof(AliyunCaptchaProvider); // ① 对齐类名}
// Provider:public sealed,ctor internal,异常处理 PaymentException → HttpRequestExceptionpublic sealed class AliyunCaptchaProvider : ICaptchaProvider{ public string Code => CaptchaProviderCode.Aliyun;
public async Task<CaptchaResult<CaptchaVerification>> VerifyAsync( CaptchaVerifyRequest request, CancellationToken ct = default) { try { var json = await _httpClient.PostAsync("/verify", request, ct); // ... 解析 return CaptchaResult<CaptchaVerification>.Success(new CaptchaVerification { Passed = true }); } catch (CaptchaException ex) // ② 先领域异常 { return CaptchaResult<CaptchaVerification>.Fail(ex.ErrorCode ?? "VERIFY_FAILED", ex.ProviderMessage ?? ex.Message); } catch (HttpRequestException ex) // ③ 后网络异常 { return CaptchaResult<CaptchaVerification>.Fail("HTTP_ERROR", ex.Message); } }}
// DI 扩展:namespace Microsoft.Extensions.DependencyInjection,方法名 AddBitzsoftAliyunCaptchapublic static class AliyunCaptchaServiceCollectionExtensions{ public static IServiceCollection AddBitzsoftAliyunCaptcha( this IServiceCollection services, IConfiguration configuration, string sectionKey = "Captcha:Aliyun") { // ... AddHttpClient(name).AddRequestLogging(name) // ... AddIntegrationProviderCapability<ICaptchaProvider, AliyunCaptchaProvider>(descriptor, factory) }}
// Descriptor:providerId 与抽象层常量一致internal static class AliyunCaptchaProviderDescriptor{ public static readonly IntegrationProviderDescriptor Instance = new( providerId: CaptchaProviderCode.Aliyun, displayName: "阿里云验证码", domain: "Captcha", capabilityTypes: new[] { typeof(ICaptchaProvider) }, stability: IntegrationProviderStability.Preview, vendor: "Alibaba Cloud", apiVersion: "2.0", authenticationType: "AK/SK", regions: new[] { "CN" }, environments: new[] { "Production" });}第 3 步:创建聚合包
包名 Bitzsoft.Integrations.{域}.All(如 Bitzsoft.Integrations.Captcha.All)。聚合包有两个反直觉的设定:IncludeBuildOutput=false(自身几乎不产 IL),并用 _._ 占位文件填进各 TFM 的 lib 目录。
<!-- src/Bitzsoft.Integrations.Captcha.All/Bitzsoft.Integrations.Captcha.All.csproj --><Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFrameworks>net5.0;net8.0;net10.0</TargetFrameworks> <PackageId>Bitzsoft.Integrations.Captcha.All</PackageId> <Description>验证码聚合包 — 包含阿里云 / 腾讯 / 极验全部实现</Description> <PackageTags>captcha;all;integration;Bitzsoft</PackageTags> <IncludeBuildOutput>false</IncludeBuildOutput> <!-- ① 不产 IL --> <IncludeSymbols>false</IncludeSymbols> </PropertyGroup>
<ItemGroup> <!-- ② _._ 占位文件放进各 TFM 的 lib 目录,避免 NuGet "空 lib" 警告 --> <None Include="$(MSBuildThisFileDirectory)..\..\build\NuGet\_._" Pack="true" PackagePath="lib\net5.0\_._" Visible="false" /> <None Include="$(MSBuildThisFileDirectory)..\..\build\NuGet\_._" Pack="true" PackagePath="lib\net8.0\_._" Visible="false" /> <None Include="$(MSBuildThisFileDirectory)..\..\build\NuGet\_._" Pack="true" PackagePath="lib\net10.0\_._" Visible="false" /> </ItemGroup>
<ItemGroup> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" /> </ItemGroup>
<ItemGroup> <!-- ③ ProjectReference 把所有厂商拉进来 --> <ProjectReference Include="..\Bitzsoft.Integrations.Compatibility\..." /> <ProjectReference Include="..\Bitzsoft.Integrations.Captcha\..." /> <ProjectReference Include="..\Bitzsoft.Integrations.Captcha.Aliyun\..." /> <ProjectReference Include="..\Bitzsoft.Integrations.Captcha.Tencent\..." /> <ProjectReference Include="..\Bitzsoft.Integrations.Captcha.GeeTest\..." /> </ItemGroup></Project>聚合包唯一的编译产物是一个配置驱动的注册方法:消费者只装一个 .All 包、写一段配置、调一个方法,就能按配置节的存在性自动注册对应厂商。
using Microsoft.Extensions.Configuration;
namespace Microsoft.Extensions.DependencyInjection;
public static class CaptchaServiceCollectionExtensions{ /// <summary>注册全部验证码供应商(仅注册配置中存在的)</summary> public static IServiceCollection AddBitzsoftCaptchaAll( this IServiceCollection services, IConfiguration configuration, string sectionKey = "Captcha") { var section = configuration.GetSection(sectionKey);
// ① 按配置节存在性自动注册——不存在的厂商不会被注册 if (section.GetSection("Aliyun").Exists()) services.AddBitzsoftAliyunCaptcha(configuration, $"{sectionKey}:Aliyun"); if (section.GetSection("Tencent").Exists()) services.AddBitzsoftTencentCaptcha(configuration, $"{sectionKey}:Tencent"); if (section.GetSection("GeeTest").Exists()) services.AddBitzsoftGeeTestCaptcha(configuration, $"{sectionKey}:GeeTest");
return services; }}为什么用 IncludeBuildOutput=false + _._
聚合包的价值是「集合 + 一键注册」,不是它自己的代码量。_._ 是 NuGet 约定的占位文件,让包结构合规。
第 4 步:更新根 README 包索引
在仓库根 README.md 的包索引表里,把这个新域加进去,并标注稳定性。这是贡献者发现新域的入口。
## 验证码(Captcha)
| 包 | 说明 | 稳定性 || --- | --- | --- || Bitzsoft.Integrations.Captcha | 验证码抽象层 | Stable || Bitzsoft.Integrations.Captcha.Aliyun | 阿里云验证码 2.0 | Preview || Bitzsoft.Integrations.Captcha.Tencent | 腾讯天御 | Preview || Bitzsoft.Integrations.Captcha.GeeTest | 极验 GeeTest | Preview || Bitzsoft.Integrations.Captcha.All | 聚合包 | Preview |完整清单
| 步骤 | 产物 | 关键校验点 |
|---|---|---|
| 抽象包 | {域} 接口 / DTO / Result / Exception / ProviderCode / ConfigProvider 接口 / InternalsVisibleTo | 不含 Options、不做 DI 注册 |
| 厂商包 | Options / HttpClient / Provider / DI 扩展 / Descriptor | HttpClientName=nameof(Provider),异常处理顺序正确 |
| 聚合包 | .All csproj + AddBitzsoft{域}All() | IncludeBuildOutput=false、_._ 占位、配置驱动注册 |
| 文档 | 根 README 包索引 | 抽象层 Stable,厂商 Preview |
下一步
- 新增厂商 Provider:第 2 步的厂商包,9 步细节全在这篇。
- Provider 目录与解析:理解 Descriptor、Catalog、Resolver 如何跨域协作。
- 编码约定:命名、Options、错误处理的全库统一规则。