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

优化排名推广教程网站建筑设计公司资质

优化排名推广教程网站,建筑设计公司资质,深圳市城乡和住房建设局,多少企业需要网站建设API 资产治理:ETag/Cache-Control/分页/排序/投影的“契约基线” 🚀 📚 目录API 资产治理:ETag/Cache-Control/分页/排序/投影的“契约基线” 🚀0. TL;DR 🧭合约基线鸟瞰1. 背景与目标 🎯2. 适用…

API 资产治理:ETag/Cache-Control/分页/排序/投影的“契约基线” 🚀


📚 目录

  • API 资产治理:ETag/Cache-Control/分页/排序/投影的“契约基线” 🚀
    • 0. TL;DR 🧭
      • 合约基线鸟瞰
    • 1. 背景与目标 🎯
    • 2. 适用范围 📐
    • 3. 资源命名与版本治理(ABP 落地) 🧩
    • 4. 查询契约:分页 / 过滤 / 排序 / 投影 🔎
      • 4.1 分页(游标优先) 🧭
      • 4.2 排序(ThenBy 链式、字段白名单) 📑
      • 4.3 投影(fields) ✂️
      • 4.4 OData(可选,限幅一致) 🧱
    • 5. 缓存契约:ETag / 条件请求 / Cache-Control ⚡
      • 5.1 强/弱 ETag 与比较语义 🧠
      • 5.2 统一 ETag 生成与 `Vary` 合并 🧩
      • 5.3 条件 GET/HEAD(含 `*`)与并发写 🔒
      • 5.4 列表端点:弱 ETag(**本页摘要**)与 304 元数据、绝对 `Link` 🔁
    • 6. 错误与一致性:ProblemDetails + Trace Context 🆘
    • 7. 安全与最小暴露 🛡️
    • 8. 可观测与 SLO 🔭
    • 9. “契约校验器”与门禁(Spectral + 代码级) 🧪
      • 9.1 OpenAPI Lint(规则节选)
      • 9.2 CI/门禁流水线(Mermaid)
    • 10. 开发者门户(Swagger/Redoc) 📘
    • 11. 兼容性测试集(Newman/Playwright/BDN) 🧪🧪
    • 12. ABP vNext 工程化落地 🧱
    • 13. 路线图 🗺️
    • 常见误区 ✅
    • 可复现指南(最小步骤) 🧰


0. TL;DR 🧭

  • 契约基线:命名/版本/鉴权 → 分页/过滤/排序/投影缓存(ETag/条件请求/Cache-Control)错误(ProblemDetails)可观测/配额/变更管理

  • 关键语义

    • If-Match 强比较(强 ETag),不匹配 → 412
    • If-None-Match 弱比较(含 *),命中 → 304
    • 304 必带可变元数据(ETag/Cache-Control/Vary/...)。
  • 三件套:OpenAPI Lint(Spectral)🧪+ 门户 OperationFilter 📘 + 兼容性测试集(Newman/BDN)🧰。

  • 亮点:列表弱 ETag 以“本页 RowVersion 摘要”生成 📈;Vary 合并追加(含 Accept & Accept-Encoding);绝对 LinkLinkGenerator 生成(适配反向代理/CDN)。

合约基线鸟瞰

数据与性能
响应变瘦 💡
分页/排序/投影
304 命中率 ↑
ETag/条件请求
设计规范 📚
OpenAPI 契约 🧾
Spectral Lint 🧪
CI 门禁 ✅
运行时拦截 🔒
开发者门户 📘
调用方集成 🤝
可观测/告警 🔭

1. 背景与目标 🎯

  • 痛点:接口风格分裂、无上限分页、过取/欠取、弱缓存/无条件请求、错误契约不统一。
  • 目标:在 ABP vNext 体系沉淀可执行的 HTTP 合约基线(可被 Lint/拦截/文档化/测试守门);以游标分页+投影降带宽,以ETag+条件请求降尾延迟与负载。

2. 适用范围 📐

  • 适用:RESTful/HTTP API(Controller、Minimal API、OData 网关)。
  • 非目标:领域建模与 GraphQL;本文专注HTTP 契约层

3. 资源命名与版本治理(ABP 落地) 🧩

  • 命名:复数资源、层级:/orders/{id}/items;动作用副资源/作业资源/exports/jobs)。
  • 版本:ABP 集成 ASP.NET API Versioning 与 ApiExplorer/Swagger 分组。
// ApiContractBaselineModule.ConfigureServices(...)
context.Services.AddAbpApiVersioning(o =>
{o.ReportApiVersions = true;o.AssumeDefaultVersionWhenUnspecified = true;
});
  • 弃用与下线:文档与响应宣布 Deprecation/Sunset;门户提供时间线。 🗓️

4. 查询契约:分页 / 过滤 / 排序 / 投影 🔎

4.1 分页(游标优先) 🧭

ClientAPI ServiceGET /resources?limit=201200 data[20], nextCursor=abc2Link: https://api.example.com/...abc\nrel="next"GET /resources?cursor=abc&limit=203200 data[20], nextCursor=def4Link: https://api.example.com/...def\nrel="next"ClientAPI Service

输出 Link绝对 URI,由 LinkGenerator 生成,适配代理/CDN。

4.2 排序(ThenBy 链式、字段白名单) 📑

public static class QueryableSortExtensions
{private static readonly HashSet<string> Allowed = new(StringComparer.OrdinalIgnoreCase){ "id", "name", "price", "createdAt" };public static IQueryable<Product> ApplySort(this IQueryable<Product> q, string? sort){if (string.IsNullOrWhiteSpace(sort)) return q.OrderByDescending(x => x.Id);IOrderedQueryable<Product>? ordered = null;foreach (var token in sort.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)){var desc = token.StartsWith("-");var key  = desc ? token[1..] : token;if (!Allowed.Contains(key)) continue;ordered = (ordered, key, desc) switch{(null, "price", false)     => q.OrderBy(x => x.Price),(null, "price", true)      => q.OrderByDescending(x => x.Price),(null, "createdAt", false) => q.OrderBy(x => x.CreatedAt),(null, "createdAt", true)  => q.OrderByDescending(x => x.CreatedAt),(null, "name", false)      => q.OrderBy(x => x.Name),(null, "name", true)       => q.OrderByDescending(x => x.Name),(null, _, false)           => q.OrderBy(x => x.Id),(null, _, true)            => q.OrderByDescending(x => x.Id),(not null, "price", false)     => ordered!.ThenBy(x => x.Price),(not null, "price", true)      => ordered!.ThenByDescending(x => x.Price),(not null, "createdAt", false) => ordered!.ThenBy(x => x.CreatedAt),(not null, "createdAt", true)  => ordered!.ThenByDescending(x => x.CreatedAt),(not null, "name", false)      => ordered!.ThenBy(x => x.Name),(not null, "name", true)       => ordered!.ThenByDescending(x => x.Name),_ => ordered!};}return ordered ?? q.OrderByDescending(x => x.Id);}
}

4.3 投影(fields) ✂️

public sealed record QuerySpec(string? Cursor = null,int     Limit  = 50,string? Sort   = null,string? Fields = null // "id,name,price"
)
{public int Take => Math.Clamp(Limit, 1, 200);
}

生产建议:在 IQueryable 层 Select瘦 DTO(或 AutoMapper ProjectTo),避免过取与二次分配。

4.4 OData(可选,限幅一致) 🧱

services.AddControllers().AddOData(opt => opt.Select().OrderBy().Count().SetMaxTop(200));[EnableQuery(MaxTop = 200,MaxExpansionDepth = 2,AllowedQueryOptions =AllowedQueryOptions.Select |AllowedQueryOptions.Filter |AllowedQueryOptions.OrderBy |AllowedQueryOptions.Top |AllowedQueryOptions.Skip |AllowedQueryOptions.Count)]
public IQueryable<ProductDto> Get() => _svc.Query();

5. 缓存契约:ETag / 条件请求 / Cache-Control ⚡

5.1 强/弱 ETag 与比较语义 🧠

含 *
标签列表
命中
未命中
存在且强比较不匹配
不存在/匹配
读取请求头
If-None-Match 存在?
资源存在?
304 + ETag/Cache-Control/Vary
继续处理
弱比较命中?
If-Match 存在?
412 ProblemDetails
执行 GET/PUT/PATCH
  • 强 ETagBase64(RowVersion),用于 If-Match强比较)。
  • 弱 ETagW/"...",建议用于列表/聚合,If-None-Match弱比较)可命中。
  • 304:必须携带可变元数据(如 ETag/Cache-Control/Vary/...)。

5.2 统一 ETag 生成与 Vary 合并 🧩

public interface IETagProvider
{string CreateStrongEtag(byte[] rowVersion);string CreateWeakEtag(string composite); // e.g., page RowVersions
}public sealed class DefaultETagProvider : IETagProvider
{public string CreateStrongEtag(byte[] rowVersion)=> $"\"{Convert.ToBase64String(rowVersion)}\""; // 强 ETag(无 W/)public string CreateWeakEtag(string composite){using var sha1 = System.Security.Cryptography.SHA1.Create();var hash = sha1.ComputeHash(System.Text.Encoding.UTF8.GetBytes(composite));return $"W/\"{Convert.ToBase64String(hash)}\"";}
}static void AddVary(HttpResponse res, params string[] tokens)
{var existing = res.Headers[HeaderNames.Vary].ToString().Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToHashSet(StringComparer.OrdinalIgnoreCase);foreach (var t in tokens) existing.Add(t);res.Headers[HeaderNames.Vary] = string.Join(", ", existing);
}

5.3 条件 GET/HEAD(含 *)与并发写 🔒

using Microsoft.Net.Http.Headers;[ApiController, Route("api/products")]
public class ProductsController : ControllerBase
{private readonly AppDbContext _db;private readonly IETagProvider _etag;private readonly LinkGenerator _links;public ProductsController(AppDbContext db, IETagProvider etag, LinkGenerator links){ _db = db; _etag = etag; _links = links; }[HttpGet("{id:long}")][HttpHead("{id:long}")] // HEAD 支持:与 GET 共用条件判定与头public async Task<IActionResult> GetById(long id, CancellationToken ct){var e = await _db.Products.AsNoTracking().Select(x => new { x.Id, x.Name, x.Price, x.UpdatedAt, x.RowVersion }).FirstOrDefaultAsync(x => x.Id == id, ct);if (e is null) return NotFound();var strong = new EntityTagHeaderValue(_etag.CreateStrongEtag(e.RowVersion));// If-None-Match: * 或 标签列表(弱比较)if (Request.Headers.TryGetValue(HeaderNames.IfNoneMatch, out var inm)){if (inm.Count == 1 && inm[0].Trim() == "*")return NotModifiedWithMetadata(strong); // 资源存在即命中if (EntityTagHeaderValue.TryParseList(inm, out var etags) &&etags.Any(tag => EntityTagHeaderValue.Compare(tag, strong, useStrongComparison: false)))return NotModifiedWithMetadata(strong);}WithCacheMetadata(Response, strong, maxAgeSeconds: 60);return Ok(e);}[HttpPut("{id:long}")]public async Task<IActionResult> Update(long id, [FromBody] UpdateProduct dto, CancellationToken ct){var entity = await _db.Products.FirstOrDefaultAsync(x => x.Id == id, ct);if (entity is null) return NotFound();var current = new EntityTagHeaderValue(_etag.CreateStrongEtag(entity.RowVersion));// If-Match(强比较)保护并发if (!Request.Headers.TryGetValue(HeaderNames.IfMatch, out var ifm) ||!EntityTagHeaderValue.TryParseList(ifm, out var candidates) ||!candidates.Any(tag => EntityTagHeaderValue.Compare(tag, current, useStrongComparison: true))){return StatusCode(StatusCodes.Status412PreconditionFailed,new ProblemDetails { Title = "Precondition Failed", Status = 412, Detail = "ETag mismatch." });}// ... mutate entity & SaveChangesawait _db.SaveChangesAsync(ct);return NoContent();}// ---- helpers ----IActionResult NotModifiedWithMetadata(EntityTagHeaderValue etag){WithCacheMetadata(Response, etag, maxAgeSeconds: 60);return StatusCode(StatusCodes.Status304NotModified);}static void WithCacheMetadata(HttpResponse res, EntityTagHeaderValue etag, int maxAgeSeconds){var headers = res.GetTypedHeaders();headers.ETag = etag;headers.CacheControl = new CacheControlHeaderValue { Private = true, MaxAge = TimeSpan.FromSeconds(maxAgeSeconds) };AddVary(res, "Accept", "Accept-Encoding"); // 合并追加// Date 由服务器自动写入}
}

反向代理提示:在 Program.cs 配置 app.UseForwardedHeaders() 以尊重 X-Forwarded-*,并在 Kestrel/反代层正确设置可信代理。

5.4 列表端点:弱 ETag(本页摘要)与 304 元数据、绝对 Link 🔁

[HttpGet]
public async Task<IActionResult> List([FromQuery] QuerySpec q, CancellationToken ct)
{var baseQuery = _db.Products.AsNoTracking();var items = await baseQuery.ApplySort(q.Sort).Take(q.Take + 1).ToListAsync(ct);var hasMore    = items.Count > q.Take;var page       = hasMore ? items.Take(q.Take).ToList() : items;var nextCursor = hasMore ? items.Last().Id.ToString() : null;// 弱 ETag:仅反映**本页** RowVersion 摘要(提升命中率)var rvConcat = page.Count == 0? "empty" // 空页稳定 ETag: string.Join('|', page.Select(p => Convert.ToBase64String(p.RowVersion)));var weak = new EntityTagHeaderValue(_etag.CreateWeakEtag(rvConcat));// If-None-Match: * 或 标签列表(弱比较)if (Request.Headers.TryGetValue(HeaderNames.IfNoneMatch, out var inm)){if (inm.Count == 1 && inm[0].Trim() == "*"){WithCacheMetadata(Response, weak, 30);return StatusCode(StatusCodes.Status304NotModified);}if (EntityTagHeaderValue.TryParseList(inm, out var etags) &&etags.Any(tag => EntityTagHeaderValue.Compare(tag, weak, useStrongComparison: false))){WithCacheMetadata(Response, weak, 30);return StatusCode(StatusCodes.Status304NotModified);}}WithCacheMetadata(Response, weak, 30);if (nextCursor != null){// 使用 LinkGenerator 生成绝对 URL(适配反代)var nextUrl = _links.GetUriByAction(httpContext: HttpContext,action: nameof(List),controller: "Products",values: new { cursor = nextCursor, limit = q.Limit });Response.Headers.Append(HeaderNames.Link, $"<{nextUrl}>; rel=\"next\"");}return Ok(new { data = page, nextCursor });
}

6. 错误与一致性:ProblemDetails + Trace Context 🆘

ClientAPI GatewayServiceRequest1forward with traceparent2Exception thrown500 ProblemDetails (traceId)3500 application/problem+json + traceparent4ClientAPI GatewayService
builder.Services.AddProblemDetails();app.UseForwardedHeaders(); // 反向代理/负载均衡场景建议启用
app.Use(async (ctx, next) =>
{var id = System.Diagnostics.Activity.Current?.Id;if (!string.IsNullOrEmpty(id))ctx.Response.Headers["traceparent"] = id; // 进入时写await next();
});
app.UseExceptionHandler(); // 未处理异常 → problem+json

7. 安全与最小暴露 🛡️

  • 仅绑定 DTO 字段;输出投影最小化、敏感字段脱敏;
  • 速率限制头 RateLimit/RateLimit-Policy草案)可选,不做强制门禁;
  • OData:限制 AllowedQueryOptions/MaxTop/MaxExpansionDepth,必要时限制表达式复杂度。

8. 可观测与 SLO 🔭

  • 指标:304_ratioavg_body_bytesp95_latencyetag_missing_totalprojection_hit_ratio
  • 日志:记录分页/过滤/排序/投影与条件请求命中/落空原因;
  • 告警:分页无上限、投影缺失导致响应过大、ETag 漏配、条件请求无效。

9. “契约校验器”与门禁(Spectral + 代码级) 🧪

9.1 OpenAPI Lint(规则节选)

extends: "spectral:oas"
rules:get-must-document-etag:description: "GET 响应需声明 ETag 或 304 分支"given: "$.paths[*][get].responses"then:function: schemafunctionOptions:schema:type: objectanyOf:- required: ["304"]- properties:"200":properties:headers:type: objectpatternProperties:"(?i)^etag$": {}write-must-require-if-match:description: "PUT/PATCH/DELETE 需文档化 If-Match 请求头"given: "$.paths[*][put,patch,delete]"then:field: parametersfunction: truthyproblem-details-on-4xx-5xx:description: "4xx/5xx 必须提供 application/problem+json"given: "$.paths[*][*].responses[?(@key.match(/^[45]\\d\\d$/))].content"then:field: application/problem+jsonfunction: truthy

9.2 CI/门禁流水线(Mermaid)

pass
fail
pass
fail
Push/PR
Run Spectral Lint 🧪
dotnet build/test 🧱
阻断合并 ⛔
Newman 回放 🔁
允许合并 ✅

10. 开发者门户(Swagger/Redoc) 📘

  • OperationFilter:为 GET 增加 If-None-Match 参数、304 头;为 PUT/PATCH/DELETE 增加 If-Match 参数与 412 说明;统一 ProblemDetails Schema。
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;public class ETagOperationFilter : IOperationFilter
{public void Apply(OpenApiOperation op, OperationFilterContext ctx){var method = ctx.ApiDescription.HttpMethod?.ToUpperInvariant();op.Parameters ??= new List<OpenApiParameter>();if (method == "GET"){op.Parameters.Add(new OpenApiParameter{Name = "If-None-Match", In = ParameterLocation.Header,Schema = new OpenApiSchema{ Type = "string" },Description = "ETag for conditional GET (supports weak compare and *)"});op.Responses.TryAdd("304", new OpenApiResponse{Description = "Not Modified",Headers = new Dictionary<string, OpenApiHeader>{["ETag"]          = new(){ Schema = new() { Type = "string" } },["Cache-Control"] = new(){ Schema = new() { Type = "string" } },["Vary"]          = new(){ Schema = new() { Type = "string" } }}});}if (method is "PUT" or "PATCH" or "DELETE"){op.Parameters.Add(new OpenApiParameter{Name = "If-Match", In = ParameterLocation.Header, Required = true,Schema = new OpenApiSchema{ Type = "string" },Description = "Strong ETag required (strong comparison)"});op.Responses.TryAdd("412", new OpenApiResponse{Description = "Precondition Failed (ProblemDetails)",Content = new Dictionary<string, OpenApiMediaType>{["application/problem+json"] = new() {Schema = new OpenApiSchema {Reference = new OpenApiReference{ Id="ProblemDetails", Type=ReferenceType.Schema } } }}});}}
}

若需要在 Swagger 中显式 ProblemDetails Schema,可在 AddSwaggerGenMapType<ProblemDetails>(_ => new OpenApiSchema{ ... }) 或引入共享 Schema。


11. 兼容性测试集(Newman/Playwright/BDN) 🧪🧪

  • 条件 GET:首 GET 取 ETag → If-None-Match(含 *)→ 304;断言 ETag/Cache-Control/Vary
  • 并发写:用旧强 ETag412;用新强 ETag204/200
  • 游标分页:极值/去重/不回退;Link 绝对 URI 校验。
  • 投影fields 不含字段不返回且不报错。
  • ProblemDetails:4xx/5xx 均为 application/problem+json
# 条件 GET + 304
etag=$(curl -sI http://localhost:5000/api/products/1 | awk -F': ' '/^ETag:/{print $2}' | tr -d '\r')
curl -i -H "If-None-Match: $etag" http://localhost:5000/api/products/1
curl -i -H "If-None-Match: *"    http://localhost:5000/api/products/1   # 通配命中# 并发写 412
curl -i -X PUT -H "If-Match: \"stale-etag\"" -H "Content-Type: application/json" \-d '{"name":"N","price":1}' http://localhost:5000/api/products/1

12. ABP vNext 工程化落地 🧱

Module: Abp.ApiContractBaseline
Add Filters/Middlewares
Register IETagProvider
Swagger: ETagOperationFilter
OpenAPI 文档 📘
运行时拦截 🔒
Spectral 门禁 🧪
可观测/告警 🔭
  • 配置项appsettings.json):分页上限/字段白名单/缓存策略/OData QueryOptions;
  • CI 门禁:Spectral + 单测 + 回归集;
  • 门户:定制 Swagger UI 页签展示契约基线、弃用时间线与复制片段。
  • 反向代理UseForwardedHeaders + 可信代理列表 + LinkGenerator 生成绝对 URI。

13. 路线图 🗺️

2025-09-072025-09-142025-09-212025-09-282025-10-052025-10-12Phase 1:2–3 个接口试点 Phase 2:新接口强制、旧接口告警 Phase 3:齐纳线门禁与例外登记 试点推广门禁契约基线落地路线图

常见误区 ✅

  • If-None-Match: * 已覆盖到详情与列表(GET/HEAD 命中 304)。
  • 列表弱 ETag 使用“本页摘要”,命中率更高;空页稳定弱 ETag。
  • Vary 合并追加(默认含 Accept, Accept-Encoding)。
  • 绝对 LinkLinkGenerator 生成(适配反代)。
  • OData MaxTop 全局=局部一致
  • 坚持:If-Match 强比较、If-None-Match 弱比较、304 必带元数据。

可复现指南(最小步骤) 🧰

dotnet new webapi -n ApiBaselineDemo
cd ApiBaselineDemodotnet add package Swashbuckle.AspNetCore
dotnet add package Asp.Versioning.Mvc
dotnet add package Asp.Versioning.Mvc.ApiExplorer
dotnet add package Microsoft.AspNetCore.OData# 将本文中的:
# - IETagProvider/DefaultETagProvider/AddVary/WithCacheMetadata
# - QuerySpec/QueryableSortExtensions
# - ProductsController(含 If-None-Match:*、本页弱 ETag、LinkGenerator 绝对 Link、HEAD 支持)
# - OperationFilter/ProblemDetails 中间件
# 复制进项目,net8.0 编译运行
dotnet run
http://www.dtcms.com/a/565368.html

相关文章:

  • 基于Springboot的旧物公益捐赠管理系统3726v22v(程序、源码、数据库、调试部署方案及开发环境)系统界面展示及获取方式置于文档末尾,可供参考。
  • Spring Boot + EasyExcel 枚举转换器:通用方案 vs 专用方案对比
  • 基于AWS服务的客户服务电话情感分析解决方案
  • 盲盒抽赏小程序一番赏玩法拓展:从模仿到创新的商业化落地
  • wordpress建淘宝客网站监理工程师查询系统入口
  • vps 建网站ip地址反查域名
  • 下载和导入原理图符号和封装
  • VinePPO:基于蒙特卡洛采样的无偏 credit assignment 进行价值估计,提升大模型推理能力
  • 静态化GTFOBins 本地部置教程
  • 自建网站公司ip子域名二级域名解析
  • 搭建出属于你自己的精彩网站!
  • 3DXML 转 3DXML 实操手册:从本地软件处理到在线工具推荐(含迪威模型网教程)
  • git小乌龟如何单个文件回退及整个版本回退
  • 班级同学录网站建设iis网站301重定向
  • 高性能负载均衡器HAProxy全解析
  • 《投资-151》PEG指标,衡量股票估值是否合理、特别是评估成长股的一个关键工具。
  • 广东省省考备考(第一百四十天11.3)——言语、判断推理(强化训练)
  • leetcode前缀和(C++)
  • 冬创网站建设培训中心高端网站建设公司有哪些
  • java面试:有了解过RocketMq架构么?详细讲解一下
  • JAVA国际版同城打车源码同城服务线下结账系统源码适配PAD支持Android+IOS+H5
  • Milvus:数据字段-主字段和自动识别(五)
  • 【深入浅出PyTorch】--8.1.PyTorch生态--torchvision
  • Blender新手入门,超详细!!!
  • Milvus:数据库层操作详解(二)
  • Blender入门学习09 - 制作动画
  • 网站建设终身不用维护网络推广主要内容
  • 金融知识详解:隔日差错处理机制与银行实战场景
  • 网站运营编辑浙江久天建设有限公司网站
  • 做网站销售说辞有赞商城官网登录