当前位置: 首页 > ai >正文

领域防腐层(ACL)在遗留系统改造中的落地

领域防腐层(ACL)在遗留系统改造中的落地


📚目录

  • 领域防腐层(ACL)在遗留系统改造中的落地
    • TL;DR 🎯
    • 二、目录结构与交付物 📦
    • 三、背景与问题定义 🧩
    • 四、架构与边界 🧭
      • 请求全链路(含租户与追踪) 🔗
    • 五、ABP 落地 🏷️
    • 六、Ports / Adapters / Translators / Policy(骨架) 🔧
      • 依赖包清单(放在“注册”前)📦
      • Resilience Pipeline 结构(读/写分离 + 限流 + 指标) 🛡️
    • 七、语义对齐与 `semantic-map.yaml`(配置即契约) 📜
    • 八、可观测性 🔎
    • 九、契约测试与回归矩阵(CI 门禁) 🧪
      • CI/CD 门禁流程 🧱
    • 十、灰度/双写/对账与回滚(SOP) 🚦
    • 十一、性能与容量(压测与基线) 📈
    • 十二、安全与输入校验 🔐
    • 十三、Demo & Compose 🚀
    • 参考资料 📚


TL;DR 🎯

  • 端口在 Domain/Domain.Shared;Adapter 在 Infrastructure;编排在 Application
  • Application 不碰 HTTP 细节,HttpApi 层做 ProblemDetails 映射。
  • ICurrentTenant/CorrelationId 全链路(W3C Trace Context)。
  • semantic-map.yaml + 启动强校验 + 覆盖率。
  • HTTP 用 标准 Resilience Handler;自定义用 Polly v8 Keyed Pipeline
  • 契约/回归进 CI 门禁;灰度双写 + 对账 + 回滚。

二、目录结构与交付物 📦

交付物

  1. Acme.LegacyAcl 模块样板(Port/Adapter/Translator/Policy)
  2. semantic-map.yaml + 启动强校验 + “语义覆盖率”报告
  3. Pact 契约测试 + 回归矩阵(含阈值)
  4. Docker Compose(wiremock-legacy / acl-gateway / promtail+loki+grafana

参考目录

Acme.LegacyAcl/Domain.Shared/     // Ports、Domain DTOApplication/       // 用例编排、Result&错误语义映射Infrastructure/    // Adapters、Translators、Policies、PipelinesHttpApi/           // Controller & ProblemDetailsTests/Contract/        // PactRegression/      // 回归矩阵 + 覆盖率etc/semantic-map.yamlwiremock/        // __files & mappingsloki/local-config.yamlpromtail/config.ymldocker-compose.ymltests/perf/k6-smoke.js

三、背景与问题定义 🧩

痛点:字段同名异义、单位/时区不一致、状态机差异、错误码风格不一。
目标

  • 隔离腐化:以 DDD 的 Anti-Corruption Layer(ACL)屏蔽遗留语义入侵新域(Azure 架构中心 · ACL)。
  • 可回滚:灰度放量 + 一键回切(常与 Strangler 组合,见现代化指南)。
  • 可测试:契约测试 + 回归矩阵 → CI 门禁(Pact can-i-deploy)。

评估指标:成功率、p95、重试率、降级率、语义映射覆盖率、回归通过率。


四、架构与边界 🧭

  • Application 负责编排与领域语义;
  • Domain 只“看见” Port 接口;
  • Infrastructure 实现 Port,与遗留交互;
  • HttpApi 负责 HTTP/ProblemDetails/Headers;
  • Cross-cutting:ICurrentTenant、CorrelationId(traceparent)、Telemetry、Resilience。
CrossCutting
ICurrentTenant
CorrelationId & traceparent
ActivitySource/OTel
Polly v8 Pipelines
HttpApi Controller
Application
Domain.Shared Ports
Infrastructure Adapters
Legacy System

请求全链路(含租户与追踪) 🔗

ClientHttpApi(Controller)Application(AppService)Domain PortAdapter(Infra)Legacy APIGET /api/inventory/{id}\nX-Tenant-Id, X-Correlation-ID1附带 W3C traceparent/ tracestate调用用例(不含HTTP细节)2调用端口(领域语义)3端口实现(Infra)4HTTP 调用(Resilience Handler)\n传递 X-Tenant-Id / traceparent5响应(外部语义)6Result<.., AdapterError>7领域返回8领域返回9ProblemDetails/DTO(含 correlationId)10ClientHttpApi(Controller)Application(AppService)Domain PortAdapter(Infra)Legacy API

五、ABP 落地 🏷️

  • 租户作用域ICurrentTenant.Change(tenantId)(ABP 多租户)。
  • 灰度分流:ABP Feature Management(文档)。
  • 统一头X-Correlation-IDX-Tenant-Id 进出都透传。

六、Ports / Adapters / Translators / Policy(骨架) 🔧

依赖包清单(放在“注册”前)📦

dotnet add package Microsoft.Extensions.Http.Resilience
dotnet add package Polly --version 8.*
dotnet add package Polly.Extensions
dotnet add package Polly.RateLimiting
dotnet add package NetEscapades.Configuration.Yaml
dotnet add package PactNet   # 如使用 Pact 契约测试

Port(Domain.Shared)

public interface IInventoryPort {Task<Result<StockInfo, AdapterError>> GetStockAsync(ProductId id, TenantId tenant, CancellationToken ct);Task<Result<bool,   AdapterError>> ReserveAsync(ProductId id, int qty, ReservationId rid, TenantId tenant, CancellationToken ct);
}

Typed HttpClient(LegacyClient)
(HTTP 弹性:.NET 官方 Resilience Handler)

public sealed class LegacyClient
{private readonly HttpClient _http;public LegacyClient(HttpClient http) => _http = http;public async Task<LegacyItem?> GetItemAsync(ProductId id, TenantId tenant, CancellationToken ct){using var req = new HttpRequestMessage(HttpMethod.Get, $"/items/{id.Value}");req.Headers.TryAddWithoutValidation("X-Tenant-Id", tenant.Value.ToString());var res = await _http.SendAsync(req, ct);if (!res.IsSuccessStatusCode) return null;return await res.Content.ReadFromJsonAsync<LegacyItem>(cancellationToken: ct);}
}

Adapter 注册(Program.cs)

  • HTTP 走 标准 Resilience Handler
  • 非 HTTP 或自定义逻辑用 Polly v8 Pipeline(Keyed Services:.NET 8 的 Keyed DI;Polly 文档见 pollydocs.org)。
services.AddHttpClient<LegacyClient>(c => c.BaseAddress = new("http://wiremock-legacy:8081")).AddStandardResilienceHandler(); // 推荐默认策略services.AddResiliencePipeline("legacy.read", b => b.AddTimeout(TimeSpan.FromSeconds(2)).AddRetry(new() { MaxRetryAttempts = 2, BackoffType = DelayBackoffType.Exponential, UseJitter = true }).AddCircuitBreaker(new() { FailureRatio = 0.2, SamplingDuration = TimeSpan.FromSeconds(30),MinimumThroughput = 10, BreakDuration = TimeSpan.FromSeconds(15) }));services.AddResiliencePipeline("legacy.write", b => b.AddTimeout(TimeSpan.FromSeconds(3)).AddRetry(new() { MaxRetryAttempts = 1 }).AddRateLimiter(new RateLimiterStrategyOptions {RateLimiter = PartitionedRateLimiter.Create<string, string>(_ => RateLimitPartition.GetConcurrencyLimiter("legacy.write",_ => new ConcurrencyLimiterOptions {PermitLimit = 50, QueueLimit = 100,QueueProcessingOrder = QueueProcessingOrder.OldestFirst}))}));
// Adapter:不抛领域异常,只返回 Result/AdapterError
using Microsoft.Extensions.DependencyInjection; // FromKeyedServicespublic sealed class InventoryAdapter : IInventoryPort
{private readonly LegacyClient _cli;private readonly ITranslator<LegacyItem, DomainItem> _map;private readonly ResiliencePipeline _read;private readonly IErrorMapper _err;public InventoryAdapter(LegacyClient cli,ITranslator<LegacyItem, DomainItem> map,[FromKeyedServices("legacy.read")] ResiliencePipeline read,IErrorMapper err){ _cli = cli; _map = map; _read = read; _err = err; }public async Task<Result<StockInfo, AdapterError>> GetStockAsync(ProductId id, TenantId tenant, CancellationToken ct)=> await _read.ExecuteAsync(async token => {var res = await _cli.GetItemAsync(id, tenant, token);if (res is null) return AdapterError.NotFound("item");var d = _map.ToDomain(res);return Result.Success(new StockInfo(id, d.CurrentQty));}, ct);// ReserveAsync(...) 类似
}

Translator(受 semantic-map.yaml 驱动)

public sealed class ItemTranslator : ITranslator<LegacyItem, DomainItem> {private readonly SemanticMap _map;private static readonly HashSet<String> _coverage = [];public DomainItem ToDomain(LegacyItem s) {_coverage.Add($"status:{s.Status}");_coverage.Add($"unit:{s.Weight?.Unit}");return new(new ProductId(s.Id),s.DisplayName?.Trim(),UnitConvert.ToGram(s.Weight, _map.Units.WeightBase),StatusMap.ToDomain(s.Status, _map.StatusMap));}public static IReadOnlyCollection<string> GetCoverage() => _coverage;
}

Application 与 HttpApi 分层(避免在 Application 里处理 HTTP

// Application
public interface IInventoryAppService {Task<Result<StockInfo, AdapterError>> GetStockAsync(Guid productId, Guid tenantId, CancellationToken ct);
}public class InventoryAppService : ApplicationService, IInventoryAppService
{private readonly ICurrentTenant _ten; private readonly IInventoryPort _port;public InventoryAppService(ICurrentTenant ten, IInventoryPort port){ _ten = ten; _port = port; }public async Task<Result<StockInfo, AdapterError>> GetStockAsync(Guid productId, Guid tenantId, CancellationToken ct){using var scope = _ten.Change(tenantId);return await _port.GetStockAsync(new(productId), new(tenantId), ct);}
}// HttpApi
[Route("api/inventory")]
public class InventoryController : AbpController
{private readonly IInventoryAppService _svc; private readonly ProblemDetailsFactory _pdf;public InventoryController(IInventoryAppService svc, ProblemDetailsFactory pdf) { _svc = svc; _pdf = pdf; }[HttpGet("{productId}")]public async Task<IActionResult> GetStock(Guid productId, [FromHeader(Name="X-Tenant-Id")] Guid tenantId, CancellationToken ct){var res = await _svc.GetStockAsync(productId, tenantId, ct);return res.Match<IActionResult>(ok => Ok(ok),err => {var pd = _pdf.CreateProblemDetails(HttpContext, statusCode: err.ToHttpStatus(),title: err.Code, detail: err.Message);pd.Extensions["correlationId"] = HttpContext.TraceIdentifier;return new ObjectResult(pd){ StatusCode = pd.Status };});}
}

Resilience Pipeline 结构(读/写分离 + 限流 + 指标) 🛡️

Write Pipeline
ConcurrencyLimiter 50/Queue100
Metric: queue_len,pending
Timeout 3s
Retry x1
Read Pipeline
CircuitBreaker 20%/30s
Metric: breaks
Timeout 2s
Retry x2 + Jitter
Tag: retry_count

七、语义对齐与 semantic-map.yaml(配置即契约) 📜

  • YAML + 启动强校验:引入 YAML 配置提供器(NetEscapades.Configuration.Yaml),绑定根节点;
  • Options 验证:启动即失败(官方文档)。
# etc/semantic-map.yaml
units:weight_base: "g"legacy_units: ["g","kg"]
status_map:Cancelled: ["Voided","Cancel_OK","CNL"]
errors:L-INV-404: InventoryNotFoundL-INV-409: Conflict
// Program.cs —— 绑定与校验
builder.Configuration.AddYamlFile("etc/semantic-map.yaml", optional: false, reloadOnChange: true);services.AddOptions<SemanticMap>().Bind(builder.Configuration) // 绑定根.ValidateDataAnnotations().Validate(m => new[] {"g","kg"}.Contains(m.Units.WeightBase), "invalid weight_base").ValidateOnStart();

覆盖率:回归测试收集 ItemTranslator.GetCoverage(),生成“语义映射覆盖率”,CI 阈值建议 ≥95%。


八、可观测性 🔎

  • ActivitySource(.NET 官方推荐):分布式追踪
public static class Telemetry { public static readonly ActivitySource Source = new("Acme.LegacyAcl"); }app.Use(async (ctx, next) => {const string Key = "X-Correlation-ID";var corr = ctx.Request.Headers[Key].FirstOrDefault() ?? Guid.NewGuid().ToString("n");ctx.Response.Headers[Key] = corr;using var act = Telemetry.Source.StartActivity($"{ctx.Request.Method} {ctx.Request.Path}");act?.SetTag("tenant", ctx.Request.Headers["X-Tenant-Id"].ToString());act?.SetTag("correlation_id", corr);await next();
});
  • 指标:成功率、p50/p95、重试率、熔断次数、降级率、缓存命中;
  • 日志:按租户采样与脱敏(PII/订单号)。

九、契约测试与回归矩阵(CI 门禁) 🧪

  • Pact(.NET:PactNet):GitHub
  • can-i-deploy:作为合并/发布门槛(Docs)
[Fact]
public async Task GetStock_contract()
{using var pact = Pact.V3("acl-consumer", "legacy-provider", new PactConfig()).WithHttpInteractions();pact.UponReceiving("get stock").Given("item 1001 exists").WithRequest(HttpMethod.Get, "/items/1001").WillRespond().WithStatus(HttpStatusCode.OK).WithJsonBody(new { id="1001", qty=12, status="OK" });await pact.VerifyAsync(async ctx => {var cli = new HttpClient { BaseAddress = new Uri(ctx.MockServerUri) };var res = await cli.GetAsync("/items/1001");res.EnsureSuccessStatusCode();});
}

回归矩阵

用例输入映射点期望输出备注
库存查询-四舍五入sku=1001, qty=11.6精度规则qty=12半入策略
取消订单-状态映射status=Cancelled状态机legacy=Voided双向对齐
税价换算-含税price=100CNY(含),13%税税率/精度88.5精度=2

CI 门禁:Pact 通过 + 回归通过率 ≥95% + 语义覆盖率 ≥95%。

CI/CD 门禁流程 🧱

Yes
No
Git Push/PR
Build & Unit
Pact Consumer/Provider
Regression Matrix + 语义覆盖率
k6 性能阈值检查
can-i-deploy?
Deploy/灰度
Fail & Block Merge

十、灰度/双写/对账与回滚(SOP) 🚦

  • 灰度:Feature Flag 按租户/组织/用户组放量;
  • 双写:新域与遗留并写,ACL 记录差异快照哈希;
  • 对账:分可自动修复/需人工/忽略;不符即回滚(Feature 一键关闭)。
按租户/组织开关
指标达标/对账通过
指标异常/对账失败
DarkLaunch
Canary
GeneralAvailability
Rollback

十一、性能与容量(压测与基线) 📈

  • k6 文档:k6.io/docs
  • 阈值即 SLO(不达标→失败)
// tests/perf/k6-smoke.js
import http from 'k6/http';
import { check } from 'k6';
import { uuidv4 } from 'https://jslib.k6.io/k6-utils/1.5.0/index.js'; // ✅ 兼容的 UUIDexport const options = {vus: __ENV.VUS ? Number(__ENV.VUS) : 20,duration: __ENV.DUR ? __ENV.DUR : '2m',thresholds: {http_req_failed:   ['rate<0.001'],http_req_duration: ['p(95)<200']}
};export default function () {const h = { 'X-Tenant-Id': '00000000-0000-0000-0000-000000000001','X-Correlation-ID': uuidv4()};const res = http.get(`${__ENV.ACL_URL}/api/inventory/1001`, { headers: h });check(res, { 'status is 200': r => r.status === 200 });
}

十二、安全与输入校验 🔐

  • 入站 FluentValidation/DataAnnotations 做 Schema 校验;
  • 日志默认脱敏;
  • semantic-map.yaml 的变更必须过 启动期校验 + 回归

十三、Demo & Compose 🚀

docker-compose.yml(已修复 Promtail 容器日志采集)

version: "3.9"
services:wiremock-legacy:image: wiremock/wiremock:3.7.0ports: ["8081:8080"]volumes: [ "./etc/wiremock:/home/wiremock" ]acl-gateway:build: ./Acme.LegacyAclenvironment:- ASPNETCORE_URLS=http://+:8080ports: ["8080:8080"]depends_on: [ wiremock-legacy ]loki:image: grafana/loki:2.9.8command: -config.file=/etc/loki/local-config.yamlvolumes: [ "./etc/loki/local-config.yaml:/etc/loki/local-config.yaml" ]ports: ["3100:3100"]promtail:image: grafana/promtail:2.9.8command: -config.file=/etc/promtail/config.ymlvolumes:- "/var/run/docker.sock:/var/run/docker.sock"- "/var/lib/docker/containers:/var/lib/docker/containers:ro"   # ✅ 关键挂载- "./etc/promtail/config.yml:/etc/promtail/config.yml"depends_on: [ loki ]grafana:image: grafana/grafana:11.0.0ports: ["3000:3000"]depends_on: [ loki ]

Promtail 最小配置(etc/promtail/config.yml,已映射 json 日志)

server:http_listen_port: 9080grpc_listen_port: 0clients:- url: http://loki:3100/loki/api/v1/pushscrape_configs:- job_name: dockerdocker_sd_configs:- host: unix:///var/run/docker.sockrelabel_configs:- source_labels: ['__meta_docker_container_name']target_label: container- source_labels: ['__meta_docker_container_log_stream']target_label: stream- source_labels: ['__meta_docker_container_id']target_label: container_id- source_labels: ['__meta_docker_container_id']target_label: __path__replacement: /var/lib/docker/containers/$1/$1-json.log

Loki 最小配置(etc/loki/local-config.yaml)
(开发用,生产按官方文档加固:Loki Docs)

auth_enabled: false
server: { http_listen_port: 3100 }
ingester:lifecycler:ring: { kvstore: { store: inmemory }, replication_factor: 1 }
schema_config:configs:- from: 2023-01-01store: boltdb-shipperobject_store: filesystemschema: v13index: { prefix: index_, period: 24h }
storage_config:boltdb_shipper:active_index_directory: /tmp/loki/indexcache_location: /tmp/loki/cachefilesystem: { directory: /tmp/loki/chunks }
limits_config:ingestion_rate_mb: 8ingestion_burst_size_mb: 16

WireMock 映射(etc/wiremock/mappings/get-item-1001.json)

{"request": { "method": "GET", "url": "/items/1001" },"response": {"status": 200,"headers": { "Content-Type": "application/json" },"jsonBody": { "id": "1001", "qty": 12, "status": "OK", "weight": { "value": 0.5, "unit": "kg" } }}
}

运行

# 依赖包(再次提醒)
dotnet add package Microsoft.Extensions.Http.Resilience
dotnet add package Polly --version 8.*
dotnet add package Polly.Extensions
dotnet add package Polly.RateLimiting
dotnet add package NetEscapades.Configuration.Yaml
dotnet add package PactNet# 起服务
docker compose up -d --build# 压测(可调 VUS/DUR)
export ACL_URL=http://localhost:8080
k6 run tests/perf/k6-smoke.js

参考资料 📚

  • Anti-Corruption Layer(Azure)
  • 应用现代化生命周期总览
  • ABP 多租户与 ICurrentTenant
  • .NET Resilience(HTTP)
  • Polly v8 文档
  • .NET 8 Keyed Services
  • Options 验证/启动校验
  • 分布式追踪/ActivitySource
  • PactNet(.NET) / can-i-deploy
  • k6 文档与阈值
  • Loki/Promtail/Grafana

http://www.xdnf.cn/news/17845.html

相关文章:

  • 疯狂星期四文案网第40天运营日记
  • 分布式锁那些事
  • AI浪潮之巅:解码技术革命、重塑产业生态与构建责任未来
  • 超高车辆碰撞预警系统如何帮助提升城市立交隧道安全?
  • uniApp App 端日志本地存储方案:实现可靠的日志记录功能
  • 【python实用小脚本-187】Python一键批量改PDF文字:拖进来秒出新文件——再也不用Acrobat来回导
  • RH134 管理存储堆栈知识点
  • Day60--图论--94. 城市间货物运输 I(卡码网),95. 城市间货物运输 II(卡码网),96. 城市间货物运输 III(卡码网)
  • StarRocks集群部署
  • 顺丰面试题
  • 最长递增子序列-dp问题+二分优化
  • 金融业务安全增强方案:国密SM4/SM3加密+硬件加密机HSM+动态密钥管理+ShardingSphere加密
  • 【职场】-啥叫诚实
  • es7.x的客户端连接api以及Respository与template的区别
  • 基本电子元件:碳膜电阻器
  • pytorch 数据预处理,加载,训练,可视化流程
  • Ubuntu DNS 综合配置与排查指南
  • 研究学习3DGS的顺序
  • Golang信号处理实战
  • Linux操作系统从入门到实战(二十三)详细讲解进程虚拟地址空间
  • Canal 技术解析与实践指南
  • 【Spring框架】SpringAOP
  • Vue3从入门到精通: 4.4 复杂状态管理模式与架构设计
  • Python爬虫大师课:HTTP协议深度解析与工业级请求封装
  • dockerfile自定义镜像,乌班图版
  • MC0439符号统计
  • 智能家居【home assistant】(一)-在Windows电脑上运行home assistant
  • Webapi发布后IIS超时(.net8.0)
  • 什么是可信空间的全域节点、区域节点、业务节点?
  • Claude Opus 4.1深度解析:抢先GPT5发布,AI编程之王主动出击?