跳到主要内容
版本:3.0.0

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/1001
  • CreateAsync(...) -> 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 状态码时,会按失败处理。
如果对方接口会用 400404422 这类状态码返回业务信息,就要显式接管。

[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] 都能按预期生成请求
  • 远程接口返回失败状态时,你选的返回类型和处理方式能覆盖业务场景
  • 需要固定版本号、租户号或鉴权信息时,优先沉淀到契约层,而不是散落在业务代码里

下一步看哪里