When working with ABP Framework to build layered applications, developers often encounter the HttpApi.Client
project within their solution structure. While this project plays a crucial role in the overall architecture, its purpose and implementation can initially seem complex or unclear. This comprehensive guide will demystify the HttpApi.Client
project, explaining its fundamental purpose and demonstrating practical usage scenarios that will enhance your understanding of ABP's remote service capabilities.
Solution Structure of an ABP-Based Layered Application
Throughout this article, I'll demonstrate concepts using a sample MVC application. However, it's applicable for other UIs.
When you scaffold a new layered application using ABP Framework, the generated solution contains several projects that follow clean architecture principles. For instance, when creating an application with MVC/Razor Pages UI, your solution will include the following project structure:
The solution architecture is built around well-defined layers, each serving specific responsibilities. You'll find the Domain
layer (consisting of Domain
and Domain.Shared
projects), the Application
layer (encompassing Application
and Application.Contracts
projects), the Infrastructure
layer (in this example, configured with MongoDB), and the presentation layer represented by the Web
project.
Beyond these four foundational layers, the solution includes several specialized projects that enhance functionality and development workflow:
DbMigrator
: A dedicated console application responsible for database schema migrations and initial data seeding operations.HttpApi
: This project is used to define your API Controllers. (ABP automatically creates controllers based on your application services. See Auto API Controllers documentation for more details.)HttpApi.Client
: We will focus on this project in this post but basically, this project is used to generate client-side proxies that enable seamless consumption of your application's APIs.
While most of these projects align with familiar layered architecture patterns, the HttpApi.Client
project often raises questions among developers new to ABP (or layered architecture in general). Let's dive into the project and understand its purpose.
HttpApi.Client
Project
The ABP Framework documentation provides a foundational definition of the HttpApi.Client
project:
"This is a project that defines C# client proxies to use the HTTP APIs of the solution. You can share this library to 3rd-party clients, so they can easily consume your HTTP APIs in their Dotnet applications"
While this definition captures the essence of the project, it only scratches the surface of its capabilities and practical applications. To truly understand its value, let's examine the project's internal structure and configuration.
When we look at the HttpApi.Client
project and examine the BookStoreHttpApiClientModule
class, we can see its dependencies:
[DependsOn( | |
typeof(BookStoreApplicationContractsModule), | |
//Module dependencies | |
typeof(AbpPermissionManagementHttpApiClientModule), | |
typeof(AbpFeatureManagementHttpApiClientModule), | |
typeof(AbpIdentityHttpApiClientModule), | |
typeof(AbpAccountAdminHttpApiClientModule), | |
typeof(AbpAccountPublicHttpApiClientModule), | |
typeof(SaasHostHttpApiClientModule), | |
typeof(AbpSettingManagementHttpApiClientModule) | |
)] | |
public class BookStoreHttpApiClientModule : AbpModule | |
{ | |
public const string RemoteServiceName = "Default"; | |
public override void ConfigureServices(ServiceConfigurationContext context) | |
{ | |
context.Services.AddHttpClientProxies( | |
typeof(BookStoreApplicationContractsModule).Assembly, | |
RemoteServiceName | |
); | |
} | |
} |
Looking at the module configuration, we can see that the HttpApi.Client
project depends on several ABP modules. However, the key dependency is on BookStoreApplicationContractsModule
. This means the project only has access to the contracts (interfaces and DTOs) defined in the Application.Contracts
project, and nothing else.
The HttpApi.Client
project only knows about interfaces like IBookAppService
, but not their actual implementations like BookAppService
. This keeps client code cleanly separated from the backend implementation details.
The primary purpose becomes clear: the HttpApi.Client
project enables client applications to consume application services through HTTP requests without creating tight coupling to the core business logic implementations. This approach facilitates distributed architectures where client applications (such as Blazor WebAssembly apps, mobile applications, or third-party integrations) can interact with your backend services through well-defined contracts.
Consider a practical scenario: when developing a Blazor WebAssembly application, you can reference the
HttpApi.Client
project and utilize application services from the client-side without depending on the core implementation. The only requirement is configuring the backend application's URL, and ABP handles the rest of the communication infrastructure.
The module's ConfigureServices
method contains another crucial configuration:
context.Services.AddHttpClientProxies(
typeof(BookStoreApplicationContractsModule).Assembly,
RemoteServiceName
);
The AddHttpClientProxies
method scans the assembly, finds all service interfaces, and creates proxy classes for them automatically. This feature is called Dynamic C# API Client Proxies. When you define interfaces in the Application.Contracts
project, ABP creates the proxy implementations for you at runtime - you don't need to write any proxy code yourself.
Seeing HttpApi.Client
in Action
Now that we understand the theoretical foundation, let's observe the practical implementation of HTTP API client proxies. The core concept revolves around creating proxy implementations that communicate with remote HTTP APIs. When you define an IBookAppService
interface, ABP automatically generates a corresponding proxy class (conceptually BookAppServiceProxy
) that handles the HTTP communication seamlessly, allowing you to work with remote services as if they were local implementations.
To demonstrate this functionality in a real-world scenario, we'll examine the Acme.BookStore.HttpApi.Client
test project included in our solution:
This test project serves as a practical demonstration of client proxy usage. It's implemented as a console application with a deliberately minimal dependency structure, it only references the Acme.BookStore.HttpApi.Client
project:
[DependsOn( | |
//👇 Only depends on the HttpApi.Client project | |
typeof(BookStoreHttpApiClientModule), | |
//framework dependencies | |
typeof(AbpAutofacModule), | |
typeof(AbpHttpClientIdentityModelModule) | |
)] | |
public class BookStoreConsoleApiClientModule : AbpModule | |
{ | |
} |
Also, this test project contains a simple ClientDemoService
class that is used to show the static proxy usage:
using System; | |
using System.Threading.Tasks; | |
using Volo.Abp.DependencyInjection; | |
using Volo.Abp.Identity; | |
using Volo.Abp.Account; | |
namespace Acme.BookStore.HttpApi.Client.ConsoleTestApp; | |
public class ClientDemoService : ITransientDependency | |
{ | |
private readonly IProfileAppService _profileAppService; | |
private readonly IIdentityUserAppService _identityUserAppService; | |
public ClientDemoService( | |
IProfileAppService profileAppService, | |
IIdentityUserAppService identityUserAppService) | |
{ | |
_profileAppService = profileAppService; | |
_identityUserAppService = identityUserAppService; | |
} | |
public async Task RunAsync() | |
{ | |
var profileDto = await _profileAppService.GetAsync(); | |
Console.WriteLine($"UserName : {profileDto.UserName}"); | |
Console.WriteLine($"Email : {profileDto.Email}"); | |
Console.WriteLine($"Name : {profileDto.Name}"); | |
Console.WriteLine($"Surname : {profileDto.Surname}"); | |
Console.WriteLine(); | |
var resultDto = await _identityUserAppService.GetListAsync(new GetIdentityUsersInput()); | |
Console.WriteLine($"Total users: {resultDto.TotalCount}"); | |
foreach (var identityUserDto in resultDto.Items) | |
{ | |
Console.WriteLine($"- [{identityUserDto.Id}] {identityUserDto.Name}"); | |
} | |
} | |
} |
This service implementation demonstrates several key concepts. We're injecting IProfileAppService
(from the Account module) and IIdentityUserAppService
(from the Identity module) interfaces directly into our service. The remarkable aspect is that despite having no direct dependencies on the actual Account and Identity module implementations, these services function seamlessly through the proxy mechanism.
So how does this magic work behind the scenes?
The answer lies in the configuration. Examining the appsettings.json
file reveals the critical connection:
{ | |
"RemoteServices": { | |
"Default": { | |
"BaseUrl": "https://localhost:44352/" | |
} | |
}, | |
"IdentityClients": { | |
"Default": { | |
"GrantType": "password", | |
"ClientId": "BookStore_App", | |
"UserName": "admin", | |
"UserPassword": "1q2w3E*", | |
"Authority": "https://localhost:44352", | |
"Scope": "BookStore" | |
} | |
} | |
} |
This configuration establishes two fundamental aspects of the remote service communication:
Remote Service Configuration: The
BaseUrl
property under theDefault
remote service name specifies https://localhost:44352/ as our backend application endpoint. This is where our web application exposes the HTTP API endpoints that the proxies will consume. TheDefault
name here matches with theRemoteServiceName
constant we defined in the module.Identity Client Configuration: The authentication settings enable the client to authenticate with the remote service using the configured credentials.
Once you provide the remote service URL, ABP handles the HTTP communication, serialization, and proxy generation automatically. When you inject an interface like IProfileAppService
, ABP creates a proxy that communicates with the remote endpoint.
To observe the dependency on remote services, try running the test project without starting the web application. You'll encounter the following error:
This error clearly demonstrates the HTTP-based nature of the communication. The proxy attempts to send requests to https://localhost:44352/ using the Default
remote service configuration but fails because the backend service isn't running.
However, when both the web application and test project are running simultaneously, you'll see successful communication:
This output confirms that the IProfileAppService
and IIdentityUserAppService
interfaces are successfully communicating with the remote backend through HTTP requests, retrieving real data from the running application.
Recap of the Dynamic C# API Client Proxies
Let's consolidate our understanding of how the dynamic proxy system operates by tracing the configuration flow from definition to execution.
You might recall that we configured the remote service URL in the appsettings.json
file, but where exactly does ABP utilize this configuration? The connection becomes apparent when we revisit the BookStoreHttpApiClientModule
class and examine this constant definition:
public const string RemoteServiceName = "Default";
This RemoteServiceName
constant plays a crucial role in linking the module configuration to the actual remote service settings. It's referenced in the ConfigureServices
method:
context.Services.AddHttpClientProxies(
typeof(BookStoreApplicationContractsModule).Assembly,
RemoteServiceName
);
This configuration establishes the binding between the assembly containing your application service interfaces and the named remote service that will handle the HTTP communication.
Following this module configuration, we define the corresponding remote service settings in our client application's appsettings.json
file:
"RemoteServices": {
"Default": {
"BaseUrl": "https://localhost:44352/"
}
}
This creates a complete configuration chain: the BookStoreHttpApiClientModule
declares a dependency on the "Default" remote service, and the appsettings.json
file provides the concrete endpoint information for that service name.
When configured, ABP automatically creates proxy classes for your application service interfaces. These proxies handle all HTTP communication behind the scenes, allowing you to work with remote services as if they were local classes.
Understanding the RemoteServices
As we explored the basic remote service configuration, let's look at an important aspect - the RemoteServices
section in the configuration. While we've been using the Default
remote service name in our examples through the BookStoreHttpApiClientModule
class, ABP's architecture is more flexible than that.
Remember our ClientDemoService
class where we injected the IProfileAppService
and IIdentityUserAppService
interfaces from the Account
and Identity
modules? This is where ABP's modular architecture shines - each module can define its own remote service identifier, allowing for more sophisticated scenarios where different modules can be configured independently or even hosted on separate servers.
The modules we've been working with, Account
and Identity
; each define their own unique remote service names. To discover these module-specific identifiers, ABP provides a built-in API definition endpoint. You can query https://localhost:44352/api/abp/api-definition
to retrieve comprehensive information about all available APIs and their associated module configurations.
When you examine the API definition response, you'll find detailed information about each module's remote service configuration. For the Account
module, specifically the services we've been using like IProfileAppService
, the remote service name is defined as AbpAccountPublic
:
Similarly, the Identity
module, which provides services like IIdentityUserAppService
, uses the remote service name AbpIdentity
:
This module-specific naming convention enables more granular control over service communication. Instead of routing all requests through a single Default
remote service, you can configure each module to communicate with its dedicated endpoint.
Let's implement this more sophisticated configuration by updating our appsettings.json
file to use module-specific remote service names:
"RemoteServices": { | |
"AbpAccountPublic": { | |
"BaseUrl": "https://localhost:44352/" | |
}, | |
"AbpIdentity": { | |
"BaseUrl": "https://localhost:44352/" | |
} | |
} |
This configuration demonstrates ABP's flexibility in handling distributed architectures. While both modules currently point to the same BaseUrl
, in a real-world microservices architecture, you might have:
The Account services running on https://account-service.company.com/
The Identity services running on https://identity-service.company.com/
Your core business services running on https://main-api.company.com/
When you run the test project with this updated configuration, the behavior remains identical to the previous setup:
So, if you seperate your modules into different services, you can use their remote service names to consume the application services from the client-side and with a single configuration, you can use different remote service URLs for different modules.
Summary
In this post, we explored the HttpApi.Client
project and how to use it to consume application services from the client-side. We also covered the RemoteServices
configuration in appsettings.json
and how to use different remote service URLs for different modules.
I hope you found this post helpful. Feel free to subscribe for more updates and share your thoughts in the comments below.
Thank you for reading :)
Thanks for the article. I found it helpful, but I’m still a bit unclear on a few points. When comparing your article with the official ABP documentation on static C# API clients, I didn’t notice many differences.
What I’m particularly interested in is how to configure third party APIs using ABP’s features. For instance, let’s say I want to consume a Token Metrics API that requires sending an api-key in the request header. While this is straightforward with HttpClient, I’d like to know how to achieve the same using ABP's built-in capabilities.
Would you consider updating the blog post to include an example like that? It would really help developers looking to fully leverage ABP when working with external APIs.
Okay, can you check that if the remote service url is correct or not on the test project? Because there was a bug relates to that, it was pointing localhost:3000 for all the time. It's fixed now, but your template might be old, i assume.