| name | consume-remote-services |
| description | Call a remote ABP HTTP API from C# through generated client proxies instead of raw HttpClient. USE FOR: setting up dynamic (runtime) or static (build-time) client proxies, configuring AbpRemoteServiceOptions (BaseUrl / named endpoints), wiring tiered / microservice calls, authenticating proxy calls (forward the current user token with AbpHttpClientIdentityModelWebModule, or client credentials with AbpHttpClientIdentityModelModule + IdentityClients), adding Polly retry to proxy clients. DO NOT USE FOR: exposing your own services as HTTP APIs or auto controllers (use expose-http-apis); choosing sync HTTP vs async events between services or the outbox/inbox (use design-module-and-service-communication); routing proxies through Dapr service invocation (use integrate-dapr-services).
|
| license | MIT |
Consume ABP Remote Services with C# Client Proxies
ABP generates client proxies so you call a remote HTTP API through its C# interface (await _bookService.GetListAsync()) without touching HttpClient. The proxies handle route/verb mapping, JSON (de)serialization, correlation id, current tenant/culture, API versioning, and error → exception translation. Authentication is not automatic — by default no token is attached (see Authentication below).
Both flavors need a shared service interface that extends IRemoteService — IApplicationService already does, so any app-service interface works. Put it in a shared project (typically *.Application.Contracts).
When to Use
- Calling a remote ABP HTTP API from C# through its service interface.
- Choosing between dynamic (runtime) and static (build-time) client proxies.
- Pointing proxies at a
BaseUrl (or named endpoint) via AbpRemoteServiceOptions.
- Wiring tiered / microservice calls where each service has its own endpoint.
- Adding retry/Polly policies to proxy HTTP clients.
When Not to Use
- Exposing your own application services as REST endpoints — use the expose-http-apis skill.
- Deciding whether the call should be synchronous HTTP or an asynchronous distributed event, or setting up outbox/inbox — use the design-module-and-service-communication skill.
- Routing the proxy calls through Dapr service invocation — use the integrate-dapr-services skill.
How it works
Dynamic proxies (runtime)
Generated at runtime — easiest to develop, no re-generation step. Add Volo.Abp.Http.Client (abp add-package Volo.Abp.Http.Client), depend on AbpHttpClientModule, and register the proxies for an assembly:
[DependsOn(
typeof(AbpHttpClientModule),
typeof(BookStoreApplicationContractsModule) // holds the service interfaces
)]
public class MyClientAppModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddHttpClientProxies(
typeof(BookStoreApplicationContractsModule).Assembly);
}
}
AddHttpClientProxies scans the assembly, finds every IRemoteService interface, and registers a proxy for each. Full signature:
AddHttpClientProxies(
Assembly assembly,
string remoteServiceConfigurationName = "Default",
bool asDefaultServices = true,
ApplicationServiceTypes applicationServiceTypes = ApplicationServiceTypes.All)
Static proxies (build-time)
Code is generated into your project at development time — a bit faster at runtime (no need to fetch the API definition), but you must re-run the generator whenever the API changes. Setup uses AddStaticHttpClientProxies plus the virtual file system for the generated app-generate-proxy.json:
[DependsOn(
typeof(AbpHttpClientModule),
typeof(AbpVirtualFileSystemModule)
)]
public class MyClientAppModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddStaticHttpClientProxies(typeof(MyClientAppModule).Assembly);
Configure<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<MyClientAppModule>();
});
}
}
AddStaticHttpClientProxies scans the assembly you pass for public IRemoteService interfaces, so which assembly to pass depends on the contracts mode:
- With contracts (default): the generator writes the interfaces/DTOs into this client module, so pass the client module assembly (
typeof(MyClientAppModule).Assembly).
- Without contracts (
--without-contracts): the interfaces come from the target's *.Application.Contracts (which you must depend on), so pass the Application.Contracts assembly:
[DependsOn(
typeof(AbpHttpClientModule),
typeof(AbpVirtualFileSystemModule),
typeof(BookStoreApplicationContractsModule) // holds the reused service interfaces
)]
public class MyClientAppModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddStaticHttpClientProxies(
typeof(BookStoreApplicationContractsModule).Assembly);
Configure<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<MyClientAppModule>();
});
}
}
AddStaticHttpClientProxies(Assembly, string remoteServiceConfigurationName = "Default", ApplicationServiceTypes = All) — note there is no asDefaultServices parameter here (that one is dynamic-only).
Generate the code with the server running on the configured BaseUrl:
abp generate-proxy -t csharp -u http://localhost:53929/
abp generate-proxy -t csharp -u http://localhost:53929/ --without-contracts
Use --without-contracts when the client already references the target's *.Application.Contracts (so DTOs/interfaces are reused). Use with contracts for fully independent microservices that must not depend on the target's contracts package. Generated *ClientProxy.Generated.cs are partial — add custom code in a sibling partial and ABP won't overwrite it.
Usage (identical for both)
Inject the interface — the proxy is the registered implementation:
public class MyService : ITransientDependency
{
private readonly IBookAppService _bookService;
public MyService(IBookAppService bookService) => _bookService = bookService;
public async Task DoItAsync()
{
var books = await _bookService.GetListAsync();
}
}
If you registered dynamic proxies with asDefaultServices: false (e.g. a local implementation already exists and you don't want to replace it), inject IHttpClientProxy<IBookAppService> and call its .Service property instead.
AbpRemoteServiceOptions & endpoints
The proxy's target address comes from the RemoteServices section of appsettings.json, bound to AbpRemoteServiceOptions:
{
"RemoteServices": {
"Default": { "BaseUrl": "http://localhost:53929/" },
"BookStore": { "BaseUrl": "http://localhost:48392/" }
}
}
AbpRemoteServiceOptions.RemoteServices is a RemoteServiceConfigurationDictionary (string → RemoteServiceConfiguration). Each RemoteServiceConfiguration exposes BaseUrl and optional Version. Configure/override it in code:
context.Services.Configure<AbpRemoteServiceOptions>(options =>
{
options.RemoteServices.Default =
new RemoteServiceConfiguration("http://localhost:53929/");
});
Tiered / microservice calls (named endpoints)
Give a proxy registration a named endpoint via remoteServiceConfigurationName; it maps to a key in the config above. If the named endpoint isn't defined, it falls back to Default:
context.Services.AddHttpClientProxies(
typeof(BookStoreApplicationContractsModule).Assembly,
remoteServiceConfigurationName: "BookStore");
This is the standard pattern for microservices where each service (and the API gateway) has its own BaseUrl. To read a resolved config at runtime, inject IRemoteServiceConfigurationProvider and call GetConfigurationOrDefaultAsync("BookStore") (returns a RemoteServiceConfiguration with the BaseUrl).
Authentication (not automatic)
By default the proxy attaches no auth token — the built-in NullRemoteServiceHttpClientAuthenticator does nothing, so a call to a [Authorize] endpoint gets 401 unless you wire authentication. Two patterns:
Forward the current user's token (web host). A web app calling a remote API on behalf of the signed-in user adds Volo.Abp.Http.Client.IdentityModel.Web and depends on AbpHttpClientIdentityModelWebModule; it takes the current request's access token and adds it as a bearer header. This is what the tiered Blazor/MVC startup templates use.
Client credentials (server-to-server). A back-end service calling another with its own identity (no user context) adds Volo.Abp.Http.Client.IdentityModel and depends on AbpHttpClientIdentityModelModule, then configures an IdentityClients entry (per named remote service) in appsettings.json:
{
"IdentityClients": {
"Default": {
"GrantType": "client_credentials",
"ClientId": "MyApp_OrderService",
"ClientSecret": "...",
"Authority": "https://localhost:44322",
"Scope": "ProductService"
}
}
}
Keep the ClientSecret out of source control — read it from user-secrets, an environment variable, or a secret store rather than committing it to appsettings.json.
The IdentityClients key matches the named remote service (Default, BookStore, …) used for the proxy. To force forwarding the current user's token for a specific named service, call configuration.SetUseCurrentAccessToken(true) on its RemoteServiceConfiguration — it's an extension method, not a property (the class itself exposes only BaseUrl/Version) — or set "UseCurrentAccessToken": "true" in the RemoteServices entry.
Retry / Polly
Only retry safe/idempotent requests. A blind retry on a timed-out POST/create can duplicate the side effect, so scope the policy per named client (or per operation), require an idempotency key for commands, and add jitter. Configure AbpHttpClientBuilderOptions in PreConfigureServices (needs Microsoft.Extensions.Http.Polly and using Polly;):
PreConfigure<AbpHttpClientBuilderOptions>(options =>
{
options.ProxyClientBuildActions.Add((remoteServiceName, clientBuilder) =>
{
if (remoteServiceName != "ReportingService")
{
return;
}
clientBuilder.AddTransientHttpErrorPolicy(builder =>
builder.WaitAndRetryAsync(3, i =>
TimeSpan.FromSeconds(Math.Pow(2, i))
+ TimeSpan.FromMilliseconds(Random.Shared.Next(0, 250))));
});
});
Validation
- Inject the service interface and make a call (e.g.
await _bookService.GetListAsync()) against a running target host; a successful response confirms the proxy resolves the right BaseUrl and maps route/verb correctly.
- Confirm the
RemoteServices section (or code config) contains the endpoint name the proxy was registered under; a missing named endpoint falls back to Default.
- For static proxies, re-run
abp generate-proxy and confirm the *ClientProxy.Generated.cs files regenerate after an API change.
Common Pitfalls
- Static proxies must be re-generated whenever the target API changes. Dynamic proxies need no client code regeneration, but a running client won't pick up a changed API at runtime either: the API definition is fetched once per
baseUrl and held in the singleton ApiDescriptionCache with no refresh path, so restart the client to pick up API changes.
AddStaticHttpClientProxies has no asDefaultServices parameter — that argument is dynamic-only.
- When
asDefaultServices: false, the interface is not the default registration — inject IHttpClientProxy<IBookAppService> and use .Service.
- A named endpoint that isn't defined in config silently falls back to
Default.
- Don't hand-edit generated
*ClientProxy.Generated.cs; they're partial, so put custom code in a sibling partial that ABP won't overwrite.
- Polly retry needs the
Microsoft.Extensions.Http.Polly package and using Polly;.
- Assuming the proxy forwards auth automatically — it doesn't. With only
AbpHttpClientModule, no token is attached and a protected endpoint returns 401. Add an IdentityModel module (AbpHttpClientIdentityModelWebModule to forward the current user's token, or AbpHttpClientIdentityModelModule + IdentityClients for client credentials).