Intent.Integration.HttpClients
Introduction
The Intent.Integration.HttpClients
module in Intent Architect generates HTTP clients. These clients are used to make HTTP calls to API endpoints defined in other Intent Architect applications. This module leverages the Service Proxies designer to model what the client will look like from a data-contract perspective.
Http Client Settings
The Intent.Integration.HttpClients
module provides several settings that affect the generated HTTP client code:
- Authorization Setup:
- Authorization Header Provider: Use dependency injection to supply the
Authorization Header
on proxy requests . - Client Access Token Management: Uses client credentials to obtain and refresh access tokens.
- Transmittable Access Token: Passes the existing access token from the HTTP context.
- None: No authorization setup is applied.
- Authorization Header Provider: Use dependency injection to supply the
These settings determine how the clients authenticate requests to secured endpoints.
How to Model it in Intent Architect
Setting Up the Integration
To get started with the Intent.Integration.HttpClients
module, follow these steps:
Install the Module: Ensure the module is installed in your Intent Architect application.
Create or Use Two Applications:
- API Application: This application will host the API endpoints.
- Client Application: This application will contain the client-side code to make HTTP calls to the API endpoints.
Configure the API Application:
- In the API application, define Application Services or Command/Query elements.
- Expose these elements as HTTP endpoints using the "Expose as HTTP endpoint" context menu option.
Configure the Client Application:
- Open the Service Proxy Designer within the client application.
- Create a new Service Proxy Package if one does not exist.
- Add a package reference to the API application's Service package, which houses the API endpoint metadata.
- Create a new Service Proxy element, which will open a dialog to select the package with the API elements and choose the desired endpoints.
Configuring your service proxies in your appsettings.json
Intent Architect will create a configuration section per package involved - the URL is determined using the following settings, in order of priority:
Service URL
Defined in theEndpoint Settings
stereotype on the Api Service package in the Services Designer.- Automatically populated when importing an OpenAPI document from an external service.
- Can also be set manually for a modeled service.
Base URL
Defined in theLaunch Settings
stereotype on the Api startup project in the Visual Studio Designer.- Automatically populated if the required version of
Intent.VisualStudio.Projects
is installed.
- Automatically populated if the required version of
Default value
If no value is found in the above settings, a default ofhttps://localhost:44350/
will be used (as shown below)- This should be updated to the correct URL for your Api project.
- In development, you can find the URL in the Api project’s
Properties/launchSettings.json
file.
{
"HttpClients": {
"APIApplication.Services": {
"Uri": "https://localhost:44350/",
"Timeout": "00:01:00"
}
}
}
If you need to tailor a specific service you can simply add a configuration for that service.
{
"HttpClients": {
//General config for Proxies from APIApplication
"APIApplication.Services": {
"Uri": "https://localhost:44350/",
"Timeout": "00:01:00"
},
//Specialized config for AccountsService
"AccountsService": {
"Uri": "https://localhost:44351/",
"Timeout": "00:02:00"
}
}
}
These string need to match the string in the HttpClientConfiguration
file.
ApplyAppSettings(http, configuration, "APIApplication.Services", "AccountsService");
Generated Code
When you run the Software Factory, the following code is generated by default:
Interface Definitions
public interface IAccountsService : IDisposable
{
Task<Guid> CreateAccountAsync(CreateAccountCommand command, CancellationToken cancellationToken = default);
Task UpdateAccountAsync(Guid id, UpdateAccountCommand command, CancellationToken cancellationToken = default);
Task<AccountDto> GetAccountByIdAsync(Guid id, CancellationToken cancellationToken = default);
Task<List<AccountDto>> GetAccountsAsync(CancellationToken cancellationToken = default);
}
public interface IClientsService : IDisposable
{
Task<Guid> CreateClientAsync(ClientCreateDto dto, CancellationToken cancellationToken = default);
Task<ClientDto> FindClientByIdAsync(Guid id, CancellationToken cancellationToken = default);
Task<List<ClientDto>> FindClientsAsync(CancellationToken cancellationToken = default);
Task UpdateClientAsync(Guid id, ClientUpdateDto dto, CancellationToken cancellationToken = default);
}
Using the Services in Handler Code
public class CreateClientCommandHandler : IRequestHandler<CreateClientCommand, Guid>
{
private readonly IClientsService _clientsService;
public CreateClientCommandHandler(IClientsService clientsService)
{
_clientsService = clientsService;
}
public async Task<Guid> Handle(CreateClientCommand request, CancellationToken cancellationToken)
{
var result = await _clientsService.CreateClientAsync(new ClientCreateDto
{
Name = request.Name
}, cancellationToken);
return result;
}
}
HTTP Client Implementations
Only a portion of the implementation is shown here for illustrative purposes:
public class AccountsServiceHttpClient : IAccountsService
{
private readonly JsonSerializerOptions _serializerOptions;
private readonly HttpClient _httpClient;
public AccountsServiceHttpClient(HttpClient httpClient)
{
_httpClient = httpClient;
_serializerOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
}
public async Task<Guid> CreateAccountAsync(CreateAccountCommand command, CancellationToken cancellationToken = default)
{
var relativeUri = $"api/account";
var httpRequest = new HttpRequestMessage(HttpMethod.Post, relativeUri);
httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var content = JsonSerializer.Serialize(command, _serializerOptions);
httpRequest.Content = new StringContent(content, Encoding.UTF8, "application/json");
using (var response = await _httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false))
{
if (!response.IsSuccessStatusCode)
{
throw await HttpClientRequestException.Create(_httpClient.BaseAddress!, httpRequest, response, cancellationToken).ConfigureAwait(false);
}
using (var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false))
{
var wrappedObj = (await JsonSerializer.DeserializeAsync<JsonResponse<Guid>>(contentStream, _serializerOptions, cancellationToken).ConfigureAwait(false))!;
return wrappedObj!.Value;
}
}
}
// Other methods...
public void Dispose() { }
}
Dependency Injection Configuration
public static class HttpClientConfiguration
{
public static void AddHttpClients(this IServiceCollection services, IConfiguration configuration)
{
services
.AddHttpClient<IAccountsService, AccountsServiceHttpClient>(http =>
{
ApplyAppSettings(http, configuration, "APIApplication.Services", "AccountsService");
});
services
.AddHttpClient<IClientsService, ClientsServiceHttpClient>(http =>
{
ApplyAppSettings(http, configuration, "APIApplication.Services", "ClientsService");
});
}
private static void ApplyAppSettings(
HttpClient client,
IConfiguration configuration,
string groupName,
string serviceName)
{
client.BaseAddress = configuration.GetValue<Uri>($"HttpClients:{serviceName}:Uri") ?? configuration.GetValue<Uri>($"HttpClients:{groupName}:Uri");
client.Timeout = configuration.GetValue<TimeSpan?>($"HttpClients:{serviceName}:Timeout") ?? configuration.GetValue<TimeSpan?>($"HttpClients:{groupName}:Timeout") ?? TimeSpan.FromSeconds(100);
}
}
Default Appsettings.json Configuration
{
"HttpClients": {
"APIApplication.Services": {
"Uri": "https://localhost:44350/",
"Timeout": "00:01:00"
},
}
}
Detailed Authorization Setup
Authorization Header Provider
The HttpClient registrations will change as follows, note the AddAuthorizationHeader
invocation:
HttpClientConfiguration update:
public static void AddHttpClients(this IServiceCollection services, IConfiguration configuration)
{
services.AddHttpContextAccessor();
services
.AddHttpClient<IAccountsService, AccountsServiceHttpClient>(http =>
{
ApplyAppSettings(http, configuration, "APIApplication.Services", "AccountsService");
})
.AddAuthorizationHeader();
services
.AddHttpClient<IClientsService, ClientsServiceHttpClient>(http =>
{
ApplyAppSettings(http, configuration, "APIApplication.Services", "ClientsService");
})
.AddAuthorizationHeader();
}
The Following interface is generated.
public interface IAuthorizationHeaderProvider
{
string? GetAuthorizationHeader();
}
You can now simply implement this interface and register it up with your DI container.
Illustrative implementations of IAuthorizationHeaderProvider
internal class MyHeaderProvider : IAuthorizationHeaderProvider
{
private readonly IHttpContextAccessor _httpContextAccessor;
public MyHeaderProvider(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public string? GetAuthorizationHeader()
{
//Propagating the current authorization header
if (_httpContextAccessor.HttpContext!.Request.Headers.TryGetValue("Authorization", out var value))
{
return value.ToString();
}
return null;
}
}
internal class MyHeaderProvider2 : IAuthorizationHeaderProvider
{
public MyHeaderProvider()
{
}
public string? GetAuthorizationHeader()
{
//Bearer Token Sample
return new AuthenticationHeaderValue("Bearer", "your_access_token").ToString();
//Basic Authentication Sample
var byteArray = Encoding.ASCII.GetBytes("username:password");
return AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray)).ToString();
//Custom Token Sample
return new AuthenticationHeaderValue("CustomScheme", "your_custom_token").ToString();
}
}
DI Registration
services.AddScoped<IAuthorizationHeaderProvider, MyHeaderProvider>();
Client Access Token Management
This configuration uses the Client ID and Secret flow, which will use a manager to obtain an Access Token with the Client Id and Secret provided in the configuration file and regularly refresh Access Tokens to ensure the Http Client is always ready to make the next HTTP invocation.
Appsettings.json:
{
"IdentityClients": {
"default": {
"Address": "https://localhost:{sts_port}/connect/token",
"ClientId": "clientId",
"ClientSecret": "secret",
"Scope": "api"
}
},
"HttpClients": {
"APIApplication.Services": {
"Uri": "https://localhost:44350/",
"IdentityClientKey": "default",
"Timeout": "00:01:00"
}
}
}
HttpClientConfiguration update:
public static void AddHttpClients(this IServiceCollection services, IConfiguration configuration)
{
services.AddAccessTokenManagement(options =>
{
configuration.GetSection("IdentityClients").Bind(options.Client.Clients);
}).ConfigureBackchannelHttpClient();
services
.AddHttpClient<IAccountsService, AccountsServiceHttpClient>(http =>
{
ApplyAppSettings(http, configuration, "APIApplication.Services", "AccountsService");
})
.AddClientAccessTokenHandler(configuration.GetValue<string>("HttpClients:AccountsService:IdentityClientKey") ?? "default");
services
.AddHttpClient<IClientsService, ClientsServiceHttpClient>(http =>
{
ApplyAppSettings(http, configuration, "APIApplication.Services", "ClientsService");
})
.AddClientAccessTokenHandler(configuration.GetValue<string>("HttpClients:ClientsService:IdentityClientKey") ?? "default");
}
Transmittable Access Token
This configuration ensures that the Authorization
header is passed through from the HTTP context to the API call.
HttpClientConfiguration update:
public static void AddHttpClients(this IServiceCollection services, IConfiguration configuration)
{
services.AddHttpContextAccessor();
services
.AddHttpClient<IAccountsService, AccountsServiceHttpClient>(http =>
{
ApplyAppSettings(http, configuration, "APIApplication.Services", "AccountsService");
})
.AddHeaders(config =>
{
config.AddFromHeader("Authorization");
});
services
.AddHttpClient<IClientsService, ClientsServiceHttpClient>(http =>
{
ApplyAppSettings(http, configuration, "APIApplication.Services", "ClientsService");
})
.AddHeaders(config =>
{
config.AddFromHeader("Authorization");
});
}