HTTP请求组件
这页只讨论 Broker 的 HTTP / REST 调用方式,不再混入 WebService 说明。如果你当前要对接的是普通 HTTP 接口、内部 Web API 或第三方 REST 服务,这页就是直接可用的接入手册。
什么时候该用这一页
适合场景:
- 对接内部 Web API
- 对接第三方 REST 接口
- 希望用契约接口替代手写
HttpClient - 想把 URL、Header、参数和返回类型约定固定下来
如果你要对接的是 .svc 或 .asmx,请直接看 WebService调用。
最小可运行路径
第一步:准备远程服务地址
{
"RemoteServices": {
"PatientApi": "https://api.example.com"
}
}
第二步:定义契约接口
using Aegis.Net.Broker;
[BasePath("api/patient")]
public interface IPatientContract
{
[Get("list")]
Task<List<PatientDto>> GetListAsync([Query] PatientListRequest request);
[Post("create")]
Task<ApiResponse<PatientDto>> CreateAsync([Body] CreatePatientRequest request);
}
第三步:获取代理并调用
public class PatientGateway
{
private readonly IPatientContract _contract;
public PatientGateway()
{
_contract = BrokerClient.Get<IPatientContract>(
ConfigManager.Get("RemoteServices:PatientApi"));
}
public Task<List<PatientDto>> GetListAsync(PatientListRequest request)
{
return _contract.GetListAsync(request);
}
}
URL 是怎么拼出来的
Broker 的 HTTP 地址通常由三部分组成:
BaseAddress:基础地址,一般由BrokerClient.Get<T>(baseUrl)传入BasePath:接口级固定路径- 方法路径:
[Get("list")]、[Post("create")]这种方法特性上的路径
建议固定写法:
- 域名和环境地址放配置里
- 领域路径放接口上
- 具体动作路径放方法上
例如:
[BasePath("api/users")]
public interface IUserContract
{
[Get("list")]
Task<List<UserDto>> GetListAsync([Query] UserListRequest request);
[Get("{id}")]
Task<UserDto> GetByIdAsync([Path] string id);
[Post("create")]
Task<ApiResponse<UserDto>> CreateAsync([Body] CreateUserRequest request);
[Put("{id}")]
Task<ApiResponse> UpdateAsync([Path] string id, [Body] UpdateUserRequest request);
[Delete("{id}")]
Task<ApiResponse> DeleteAsync([Path] string id);
}
var contract = BrokerClient.Get<IUserContract>(
ConfigManager.Get("RemoteServices:UserApi"));
最终效果通常是:
GetListAsync(...)->GET https://host/api/users/list?...GetByIdAsync("1001")->GET https://host/api/users/1001CreateAsync(...)->POST https://host/api/users/create
常见 HTTP 调用方式
GET + 简单查询参数
[BasePath("api/report")]
public interface IReportContract
{
[Get("daily")]
Task<ReportDto> GetDailyAsync([Query] string date, [Query] string deptCode);
}
var contract = BrokerClient.Get<IReportContract>(
ConfigManager.Get("RemoteServices:ReportApi"));
var report = await contract.GetDailyAsync("2026-03-18", "CARD");
GET + 路径参数
[BasePath("api/order")]
public interface IOrderContract
{
[Get("{orderId}")]
Task<OrderDto> GetAsync([Path] string orderId);
}
var contract = BrokerClient.Get<IOrderContract>(
ConfigManager.Get("RemoteServices:OrderApi"));
var order = await contract.GetAsync("A1001");
POST + JSON Body
[BasePath("api/order")]
public interface IOrderContract
{
[Post("create")]
Task<ApiResponse<OrderDto>> CreateAsync([Body] CreateOrderRequest request);
}
var result = await contract.CreateAsync(new CreateOrderRequest
{
PatientId = "P001",
DoctorCode = "D001"
});
PUT / DELETE
[BasePath("api/order")]
public interface IOrderContract
{
[Put("{orderId}")]
Task<ApiResponse> UpdateAsync([Path] string orderId, [Body] UpdateOrderRequest request);
[Delete("{orderId}")]
Task<ApiResponse> DeleteAsync([Path] string orderId);
}
参数绑定该怎么写
[Query] 传复杂对象
这是 Broker 里最常用也最容易忽略的一点。复杂对象作为 [Query] 传入时,会按一层公共属性展开成多个 Query 参数。
using Aegis.Transfer.Requests;
using Aegis.Transfer.Responses;
using Aegis.Net.Broker;
public interface IUserContract
{
[Get("api/User/GetList")]
Task<ApiResponse> GetListAsync([Query] PageListRequest request);
}
var contract = BrokerClient.Get<IUserContract>(
ConfigManager.Get("RemoteServices:SelfApi"));
var result = await contract.GetListAsync(new PageListRequest
{
PageSize = 5,
PageIndex = 2
});
这类调用会展开成接近下面这种形式:
GET /api/User/GetList?pageSize=5&pageIndex=2
[Query] 做格式化
[Get("api/schedule/daily")]
Task<List<ScheduleDto>> GetScheduleAsync(
[Query(Format = "yyyy-MM-dd")] DateTime date,
[Query] string deptCode);
GET /api/schedule/daily?date=2026-03-18&deptCode=CARD
[QueryMap] 传动态查询条件
当筛选条件数量不固定,或者由页面动态拼装时,用字典比硬塞一长串方法参数更合适。
[BasePath("api/patient")]
public interface IPatientContract
{
[Get("search")]
Task<List<PatientDto>> SearchAsync(
[Query] string keyword,
[QueryMap] Dictionary<string, string> filters);
}
var result = await contract.SearchAsync(
"张",
new Dictionary<string, string>
{
["deptCode"] = "CARD",
["status"] = "InHospital"
});
GET /api/patient/search?keyword=张&deptCode=CARD&status=InHospital
[RawQueryString] 透传原始查询串
如果查询串已经在别处生成好了,或者对方接口的筛选语法不适合拆成 key=value,可以直接透传。
[BasePath("api/report")]
public interface IReportContract
{
[Get("query")]
Task<string> QueryAsync([RawQueryString] string rawQuery);
}
var content = await contract.QueryAsync(
"sort=visitTime&order=desc&fields=id,name,visitTime");
使用时注意:
- 传入内容会原样拼到 Query String 里
- 你自己负责编码和转义
- 不要手动再补开头的
?或结尾的&
[Body] 传 JSON
[Post("search")]
Task<List<OrderDto>> SearchAsync([Body] OrderSearchRequest request);
这是默认最常见的写法,适合大多数业务请求。
[Body] 提交表单
[Post("login")]
Task<TokenDto> LoginAsync(
[Body(BodySerializationMethod.UrlEncoded)] Dictionary<string, object> formData);
var token = await contract.LoginAsync(new Dictionary<string, object>
{
["userName"] = "alice",
["password"] = "secret"
});
[Path] 替换占位符
[Get("users/{userId}")]
Task<UserDto> GetUserAsync([Path] string userId);
如果占位符名和参数名不同,也可以显式指定:
[Get("users/{user_id}")]
Task<UserDto> GetUserAsync([Path("user_id")] string userId);
契约层可以共享哪些内容
除了方法参数,Broker 还支持把一部分“整个契约都共用”的信息固定在接口层。这一层写好之后,业务代码会更干净。
接口级固定 Header
所有请求都固定带同一组 Header 时,直接写在接口上。
[Header("X-System", "HIS")]
[Header("Accept", "application/json")]
[BasePath("api/report")]
public interface IReportContract
{
[Get("daily")]
Task<ReportDto> GetDailyAsync([Query] string date);
}
契约属性动态 Header
如果 Header 需要在运行时赋值,但又希望该接口下所有请求都复用,就定义成接口属性。
[BasePath("api/report")]
public interface IReportContract
{
[Header("Authorization")]
string Authorization { get; set; }
[Header("X-Tenant-Id")]
string TenantId { get; set; }
[Get("daily")]
Task<ReportDto> GetDailyAsync([Query] string date);
}
var contract = BrokerClient.Get<IReportContract>(
ConfigManager.Get("RemoteServices:ReportApi"));
contract.Authorization = "Bearer token-value";
contract.TenantId = "tenant-001";
var report = await contract.GetDailyAsync("2026-03-18");
方法参数动态 Header
只在单次请求里使用的 Header,继续放方法参数即可。
[BasePath("api/report")]
public interface IReportContract
{
[Get("daily")]
Task<ReportDto> GetDailyAsync(
[Header("Authorization")] string authorization,
[Header("X-Request-Id")] string requestId,
[Query] string date);
}
Header 覆盖顺序
如果同名 Header 同时出现在多个位置,按下面顺序覆盖:
方法参数 [Header] > 接口属性 [Header] > 方法级固定 Header > 接口级固定 Header
这意味着:
- 全局默认值放接口级最合适
- 会话态值放接口属性最合适
- 单次请求临时覆盖放方法参数最合适
契约属性共享 Query
如果某个查询条件每次都要带,而且更像“客户端上下文”而不是单次入参,也可以定义在接口属性上。
[BasePath("api/open")]
public interface IOpenApiContract
{
[Query("appId")]
string AppId { get; set; }
[Query("appSecret")]
string AppSecret { get; set; }
[Get("patient/list")]
Task<List<PatientDto>> GetPatientsAsync([Query] string keyword);
}
var contract = BrokerClient.Get<IOpenApiContract>(
ConfigManager.Get("RemoteServices:OpenApi"));
contract.AppId = "demo-app";
contract.AppSecret = "secret-value";
var result = await contract.GetPatientsAsync("张");
契约属性共享路径片段
当接口地址里有固定版本号、租户号、机构号,也可以用契约属性填充。
[BasePath("{version}/api/patient")]
public interface IVersionedPatientContract
{
[Path("version")]
string Version { get; set; }
[Get("list")]
Task<List<PatientDto>> GetListAsync();
}
var contract = BrokerClient.Get<IVersionedPatientContract>(
ConfigManager.Get("RemoteServices:PatientApi"));
contract.Version = "v2";
var list = await contract.GetListAsync();
地址、状态码和序列化还能怎么控制
[BaseAddress] 提供默认基础地址
一般还是推荐把地址放配置里,但如果某个契约天然只对应一个远端地址,也可以在接口上给默认值。
[BaseAddress("https://openapi.example.com")]
[BasePath("api/drug")]
public interface IDrugContract
{
[Get("list")]
Task<List<DrugDto>> GetListAsync();
}
如果你在 BrokerClient.Get<T>(...) 里又传了地址,运行时传入值会优先使用。
[AllowAnyStatusCode] 自己处理非 2xx 响应
默认情况下,远程接口返回非 2xx 状态码时,会按失败处理。
如果对方接口会用 400、404、422 这类状态码返回业务信息,就要显式接管。
[BasePath("api/patient")]
public interface IPatientContract
{
[Get("{patientId}")]
[AllowAnyStatusCode]
Task<Response<PatientDto>> TryGetAsync([Path] string patientId);
}
using var response = await contract.TryGetAsync("P001");
if (response.ResponseMessage.StatusCode == HttpStatusCode.NotFound)
{
return null;
}
var patient = response.GetContent();
[SerializationMethods] 设置接口默认序列化方式
这个特性适合对接有特殊约定的远端系统,例如 Query 参数要求整体序列化,而不是逐个展开。
[SerializationMethods(
Body = BodySerializationMethod.Serialized,
Query = QuerySerializationMethod.Serialized)]
[BasePath("api/search")]
public interface IAdvancedSearchContract
{
[Get("query")]
Task<string> QueryAsync([Query] SearchCondition condition);
}
参数级特性仍然可以覆盖接口默认值,所以它适合做“默认规则”,不是强制锁死。
文件上传和下载
上传流
[BasePath("api/file")]
public interface IFileContract
{
[Post("upload")]
Task<ApiResponse> UploadAsync(
[Header("Content-Disposition", "form-data; filename=\"demo.txt\"")]
[Header("Content-Type", "text/plain")]
[Body] Stream fileStream);
}
下载流
[BasePath("api/file")]
public interface IFileContract
{
[Get("download/{fileId}")]
Task<Stream> DownloadAsync([Path] string fileId);
}
using var stream = await contract.DownloadAsync("FILE-1001");
返回类型怎么选
Task<T>
最常用,适合远程接口稳定返回 JSON 对象时使用。
[Get("{id}")]
Task<UserDto> GetByIdAsync([Path] string id);
Task<Response<T>>
适合你既想要正文,也想看响应状态和响应头。
[Get("detail/{id}")]
Task<Response<OrderDetailDto>> GetDetailAsync([Path] string id);
using var response = await contract.GetDetailAsync("A1001");
if (!response.ResponseMessage.IsSuccessStatusCode)
{
throw new ApplicationException("远程接口返回失败");
}
var content = response.GetContent();
Task<HttpResponseMessage>
适合你完全自己处理原始响应。
[Get("export")]
Task<HttpResponseMessage> ExportAsync([Query] string date);
Task<string>
适合对方返回原始字符串、XML、HTML,或者你暂时不想做类型建模的场景。
[Get("raw")]
Task<string> GetRawAsync();
什么时候该用 EasyRequestAsync(...)
如果你只是临时验证接口、做一次性联调,或者还没来得及建契约,可以用:
var response = await BrokerClient.EasyRequestAsync(
ConfigManager.Get("RemoteServices:UserApi") + "/api/users/query",
"Post",
"{\"keyword\":\"alice\",\"pageIndex\":1,\"pageSize\":10}",
new Dictionary<string, string>
{
["Authorization"] = "Bearer token-value",
["X-System"] = "HIS"
});
var content = await response.Content.ReadAsStringAsync();
但只要请求开始稳定,就应该尽快沉淀成契约接口。长期维护里,契约式调用明显更清楚。
需要更细控制时怎么做
使用实例模式
var broker = new BrokerClient(ConfigManager.Get("RemoteServices:UserApi"))
{
JsonSerializerSettings = new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore
}
};
var contract = broker.Get<IUserContract>();
适合场景:
- 你要自定义 JSON 序列化规则
- 你不想所有远程服务共用完全相同的客户端设置
加过滤器
using Aegis.Net.Broker.Filters;
public class LoggingBrokerFilter : IBrokerFilter
{
public Task OnActionExecutingAsync(RequestContext context)
{
Console.WriteLine(
$"[Broker] {context.MethodType} {context.BaseAddress}{context.Path}");
return Task.CompletedTask;
}
public Task OnActionExecutedAsync(ResponseContext context)
{
Console.WriteLine($"[Broker] Status: {context.StatusCode}");
return Task.CompletedTask;
}
}
var broker = new BrokerClient(ConfigManager.Get("RemoteServices:UserApi"))
{
Filters = new[] { new LoggingBrokerFilter() }
};
var contract = broker.Get<IUserContract>();
接入完成后怎么确认成功
你至少应该确认这些点:
- 远程接口地址来自配置而不是硬编码
- 接口级 Header、契约属性 Header、方法参数 Header 的职责分工清楚
- 复杂
[Query]对象、[QueryMap]、[RawQueryString]都能按预期生成请求 - 远程接口返回失败状态时,你选的返回类型和处理方式能覆盖业务场景
- 需要固定版本号、租户号或鉴权信息时,优先沉淀到契约层,而不是散落在业务代码里
下一步看哪里
- 组件级入口:远程调用客户端(Aegis.Net.Broker)
- WebService 场景:WebService调用