Authlink.Portal.Client.Http
2.2.2
See the version list below for details.
dotnet add package Authlink.Portal.Client.Http --version 2.2.2
NuGet\Install-Package Authlink.Portal.Client.Http -Version 2.2.2
<PackageReference Include="Authlink.Portal.Client.Http" Version="2.2.2" />
<PackageVersion Include="Authlink.Portal.Client.Http" Version="2.2.2" />
<PackageReference Include="Authlink.Portal.Client.Http" />
paket add Authlink.Portal.Client.Http --version 2.2.2
#r "nuget: Authlink.Portal.Client.Http, 2.2.2"
#:package Authlink.Portal.Client.Http@2.2.2
#addin nuget:?package=Authlink.Portal.Client.Http&version=2.2.2
#tool nuget:?package=Authlink.Portal.Client.Http&version=2.2.2
๐ฆ Package Overview
Authlink.Portal.Client.Http provides a ready-to-use, production-grade implementation of the IPortalClient abstraction defined in Authlink.Portal.Client.Core.
๐ Install
dotnet add package Authlink.Portal.Client.Http
Or via Package Manager:
PM> Install-Package Authlink.Portal.Client.Http
๐ What's New
v2.2.2
Quality Improvements:
- โ Integration Tests: Added 39 comprehensive integration tests covering all client operations
- โ CI/CD Enhancement: GitHub Actions workflow now runs integration tests before publishing to NuGet
- โ Documentation: Added detailed integration test documentation and troubleshooting guide
- โ Reliability: Publishing blocked if any integration test fails, ensuring only tested code reaches production
Test Coverage:
- Success path testing for all GET and mutation operations
- HTTP error handling (400, 401, 403, 404, 408, 409, 500, 502, 503)
- Exception scenarios (network errors, timeouts, deserialization failures)
Impact:
- Increased confidence in package reliability through automated testing
- Faster issue detection before release
- Better developer experience with comprehensive test documentation
v2.2.1
Bug Fixes:
- ๐ DI Registration Fix: Resolved ambiguous constructor issue in
MutableAccessTokenProviderthat prevented proper dependency injection resolution - โ
Improved Reliability: Factory method now explicitly selects the correct constructor for
IAccessTokenProviderregistration
Impact:
- Fixes
InvalidOperationException: Unable to activate type 'MutableAccessTokenProvider'. The following constructors are ambiguouswhen resolvingIPortalClient - Ensures stable service resolution in all DI scenarios
v2.2.0
New Features:
- โ
Status filtering: Filter users by lifecycle status (Active, Inactive, Deleted) in
GetUsersAsync - โ
Tenant filtering: Filter users by
TenantIdfor multi-tenant scenarios - โ Enhanced queries: Updated HTTP client to properly serialize Status and TenantId query parameters
Example - Filter by Status:
var result = await _client.GetUsersAsync(new GetUsersRequest
{
Status = UserStatus.Active,
PageNumber = 1,
PageSize = 20
});
Example - Filter by Tenant:
var result = await _client.GetUsersAsync(new GetUsersRequest
{
TenantId = tenantId,
SearchText = "john",
PageNumber = 1,
PageSize = 20
});
v2.1.0
New Features:
- โ
New lookup methods:
GetUserByEmailAsyncandGetUserByGovernmentIdAsyncfor flexible user retrieval - โ
Optional authentication: Set
AccessTokendirectly in options or provide customIAccessTokenProvider - โ
Runtime token updates: New
MutableAccessTokenProviderallows updating tokens without restarting - โ Automatic auth detection: Extensions method intelligently handles authentication based on configuration
Example - Simple token setup:
builder.Services.AddPortalHttpClient(options =>
{
options.BaseUrl = "https://portal.authlink.co.za";
options.AccessToken = "your-token"; // New in v2.1
});
Example - Runtime token refresh:
if (_tokenProvider is MutableAccessTokenProvider mutableProvider)
{
mutableProvider.SetAccessToken(newToken); // Update token on the fly
}
v2.0
Breaking Changes:
All methods now return ErrorOr<TResponse> instead of throwing exceptions. This provides:
- โ Type-safe error handling
- โ Comprehensive error information (status codes, response bodies, exception details)
- โ Better control flow without try-catch blocks
- โ Rich metadata for debugging and logging
๐ ๏ธ Usage Examples
Setup
The client supports three authentication approaches. Choose the one that fits your needs:
Option 1: Simple - Static Token (Recommended for Most Cases)
Set the token directly in options. Perfect for service-to-service communication with a static API key:
builder.Services.AddPortalHttpClient(options =>
{
options.BaseUrl = "https://portal.authlink.co.za";
options.AccessToken = builder.Configuration["Authlink:Portal:AccessToken"];
});
appsettings.json:
{
"Authlink": {
"Portal": {
"BaseUrl": "https://portal.authlink.co.za",
"AccessToken": "your-access-token-here"
}
}
}
Updating the token at runtime:
When using the static token approach, you can update the token at runtime by injecting MutableAccessTokenProvider:
public class TokenRefreshService
{
private readonly IAccessTokenProvider _tokenProvider;
public TokenRefreshService(IAccessTokenProvider tokenProvider)
{
_tokenProvider = tokenProvider;
}
public async Task RefreshTokenAsync()
{
// Get new token from your auth service
var newToken = await GetNewTokenFromAuthService();
// Update the token - cast to MutableAccessTokenProvider
if (_tokenProvider is MutableAccessTokenProvider mutableProvider)
{
mutableProvider.SetAccessToken(newToken);
}
}
}
Or configure a background service to refresh tokens automatically:
public class TokenRefreshBackgroundService : BackgroundService
{
private readonly IAccessTokenProvider _tokenProvider;
private readonly ILogger<TokenRefreshBackgroundService> _logger;
public TokenRefreshBackgroundService(
IAccessTokenProvider tokenProvider,
ILogger<TokenRefreshBackgroundService> logger)
{
_tokenProvider = tokenProvider;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
// Refresh token every 50 minutes (assuming 60 min expiry)
await Task.Delay(TimeSpan.FromMinutes(50), stoppingToken);
var newToken = await RefreshTokenAsync();
if (_tokenProvider is MutableAccessTokenProvider mutableProvider)
{
mutableProvider.SetAccessToken(newToken);
_logger.LogInformation("Portal access token refreshed successfully");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to refresh portal access token");
}
}
}
private async Task<string> RefreshTokenAsync()
{
// Your token refresh logic here
throw new NotImplementedException();
}
}
// Register the background service
builder.Services.AddHostedService<TokenRefreshBackgroundService>();
Option 2: Advanced - Custom Token Provider
Implement IAccessTokenProvider for dynamic tokens (e.g., from user context, token refresh, etc.):
// 1. Implement your token provider
public class HttpContextTokenProvider : IAccessTokenProvider
{
private readonly IHttpContextAccessor _httpContextAccessor;
public HttpContextTokenProvider(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public Task<string> GetAccessTokenAsync(CancellationToken cancellationToken = default)
{
var token = _httpContextAccessor.HttpContext?.Request.Headers["Authorization"]
.FirstOrDefault()?.Replace("Bearer ", "");
return Task.FromResult(token ?? string.Empty);
}
}
// 2. Register your provider BEFORE calling AddPortalHttpClient
builder.Services.AddScoped<IAccessTokenProvider, HttpContextTokenProvider>();
// 3. Register the client (it will use your custom provider)
builder.Services.AddPortalHttpClient(options =>
{
options.BaseUrl = "https://portal.authlink.co.za";
});
Other custom provider examples:
// Token from a service/cache with live updates
public class TokenServiceProvider : IAccessTokenProvider
{
private readonly ITokenService _tokenService;
public TokenServiceProvider(ITokenService tokenService)
{
_tokenService = tokenService;
}
public async Task<string> GetAccessTokenAsync(CancellationToken cancellationToken = default)
{
// Fetch the latest token on every request
return await _tokenService.GetCurrentTokenAsync(cancellationToken);
}
}
๐ก Tip: If you need token refresh with a custom provider, use Option 1 (static token) with MutableAccessTokenProvider and update it via a background service. This gives you the best of both worlds: simple setup with runtime updates.
Option 3: No Authentication
For public endpoints or when you're handling authentication manually:
builder.Services.AddPortalHttpClient(options =>
{
options.BaseUrl = "https://portal.authlink.co.za";
// No AccessToken set, no custom provider registered
});
Basic Usage with Error Handling
public class MyService
{
private readonly IPortalClient _client;
public MyService(IPortalClient client)
{
_client = client;
}
public async Task GetUsersAsync()
{
var request = new GetUsersRequest
{
PageNumber = 1,
PageSize = 10
};
var result = await _client.GetUsersAsync(request);
if (result.IsError)
{
// Handle error
var error = result.FirstError;
Console.WriteLine($"Error: {error.Code} - {error.Description}");
// Access metadata
if (error.Metadata.TryGetValue("StatusCode", out var statusCode))
{
Console.WriteLine($"HTTP Status: {statusCode}");
}
if (error.Metadata.TryGetValue("ResponseBody", out var body))
{
Console.WriteLine($"Response: {body}");
}
return;
}
// Success - use the value
var response = result.Value;
Console.WriteLine($"Found {response.TotalCount} users");
foreach (var user in response.Users)
{
Console.WriteLine($"- {user.FirstName} {user.LastName}");
}
}
}
Get User by Email
public async Task<ErrorOr<UserDto>> GetUserByEmailAsync(string email)
{
var result = await _client.GetUserByEmailAsync(new GetUserByEmailRequest
{
Email = email
});
if (result.IsError)
{
return result.Errors;
}
return result.Value.User;
}
Get User by Government Identifier
public async Task<ErrorOr<UserDto>> GetUserByGovernmentIdAsync(
IdentifierKind identifierKind,
string countryIso2,
string identifier)
{
var result = await _client.GetUserByGovernmentIdAsync(new GetUserByGovernmentIdRequest
{
IdentifierKind = identifierKind,
CountryIso2 = countryIso2,
Identifier = identifier
});
if (result.IsError)
{
return result.Errors;
}
return result.Value.User;
}
Pattern Matching
var result = await _client.GetUserByIdAsync(new GetUserByIdRequest { UserId = userId });
return result.Match(
user => Ok(user),
errors => errors[0].Type switch
{
ErrorType.NotFound => NotFound(),
ErrorType.Unauthorized => Unauthorized(),
ErrorType.Forbidden => Forbid(),
_ => Problem(errors[0].Description)
}
);
Switching on Error Type
var result = await _client.CreateUserAsync(request);
if (result.IsError)
{
var error = result.FirstError;
return error.Type switch
{
ErrorType.Validation => BadRequest(error.Description),
ErrorType.Conflict => Conflict(error.Description),
ErrorType.Unauthorized => Unauthorized(),
ErrorType.Forbidden => Forbid(),
_ => StatusCode(500, error.Description)
};
}
return Ok(result.Value);
๐ง Error Types
The client returns specific error types for common scenarios:
| Error Code | Error Type | HTTP Status | Description |
|---|---|---|---|
Portal.ValidationFailed |
Validation | 400 | Request validation failed |
Portal.Unauthorized |
Unauthorized | 401 | Authentication required |
Portal.Forbidden |
Forbidden | 403 | Insufficient permissions |
Portal.NotFound |
NotFound | 404 | Resource not found |
Portal.Conflict |
Conflict | 409 | Resource conflict |
Portal.Timeout |
Failure | 408 | Request timed out |
Portal.NetworkError |
Failure | - | Network/connection error |
Portal.DeserializationFailed |
Failure | - | JSON deserialization failed |
Portal.HttpRequestFailed |
Failure | Any | General HTTP failure |
๐ Error Metadata
All errors include metadata for detailed diagnostics:
if (result.IsError)
{
var error = result.FirstError;
// Available metadata keys:
// - StatusCode: HTTP status code (int)
// - StatusCodeName: Status code name (string)
// - ResponseBody: Raw response content (string)
// - RequestPath: The path that failed (string)
// - ExceptionType: Exception type name (string)
// - ExceptionMessage: Exception message (string)
// - TypeName: Type that failed to deserialize (string)
}
๐งช Testing
Integration Tests
The Authlink.Portal.Client.Http package includes comprehensive integration tests to ensure reliability and correct behavior across all client operations.
Test Coverage:
- โ 39 integration tests covering all client methods
- โ Success path testing for GET and mutation operations
- โ HTTP error handling (400, 401, 403, 404, 408, 409, 500, 502, 503)
- โ Exception handling (network errors, timeouts, deserialization failures)
Running Tests Locally:
# Run all integration tests
dotnet test Authlink.Portal.Client.Http.Test/Authlink.Portal.Client.Http.Test.csproj
# Run with detailed output
dotnet test Authlink.Portal.Client.Http.Test/Authlink.Portal.Client.Http.Test.csproj --verbosity normal
# Run specific test class
dotnet test --filter "FullyQualifiedName~PortalHttpClientGetOperationsTests"
CI/CD Integration:
Integration tests run automatically in the CI/CD pipeline before every NuGet package release. The workflow:
- Restores dependencies
- Builds all projects in Release mode
- Runs integration tests - Publishing is blocked if any test fails
- Packs the NuGet package
- Publishes to NuGet.org
This ensures that only thoroughly tested code is released to production.
Test Documentation:
For detailed information about the integration tests, including:
- Test structure and organization
- Mock HTTP server configuration
- Writing new tests
- Troubleshooting guide
See the Integration Tests Documentation.
๐ Migration from v1.x
Before (v1.x)
// Registration
builder.Services.AddPortalHttpClient(options =>
{
options.BaseUrl = "https://portal.authlink.co.za";
});
// Usage
try
{
var response = await _client.UsersAsync(request);
return Ok(response);
}
catch (HttpRequestException ex)
{
return StatusCode(500, "Request failed");
}
After (v2.0)
// Registration - Simple approach with static token
builder.Services.AddPortalHttpClient(options =>
{
options.BaseUrl = "https://portal.authlink.co.za";
options.AccessToken = "your-token"; // New in v2.0
});
// Usage - ErrorOr-based error handling
var result = await _client.GetUsersAsync(request);
if (result.IsError)
{
var error = result.FirstError;
return Problem(error.Description);
}
return Ok(result.Value);
Breaking Changes Summary
- Authentication: Now supports optional authentication via
AccessTokenproperty or customIAccessTokenProvider - Method Naming: Methods follow Get* pattern (e.g.,
GetUsersAsync,GetUserByIdAsync) - Return Type: All methods return
ErrorOr<TResponse>instead ofTResponse - No Exceptions: HTTP errors are returned as
Errorobjects instead of throwing exceptions - New Methods: Added
GetUserByEmailAsyncandGetUserByGovernmentIdAsync
๐ Documentation
๐ License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net9.0
- Authlink.Portal.Client.Core (>= 2.2.2)
- ErrorOr (>= 2.0.1)
- Microsoft.Extensions.DependencyInjection (>= 9.0.9)
- Microsoft.Extensions.Http (>= 9.0.9)
- Microsoft.Extensions.Options (>= 9.0.9)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.10.0 | 32 | 3/6/2026 |
| 2.9.0 | 86 | 2/18/2026 |
| 2.8.0 | 89 | 2/18/2026 |
| 2.7.0 | 107 | 1/30/2026 |
| 2.6.0 | 93 | 1/29/2026 |
| 2.5.0 | 99 | 1/29/2026 |
| 2.4.0 | 91 | 1/28/2026 |
| 2.3.0 | 105 | 1/7/2026 |
| 2.2.4 | 91 | 1/7/2026 |
| 2.2.3 | 99 | 1/7/2026 |
| 2.2.2 | 296 | 11/12/2025 |
| 2.2.1 | 243 | 11/10/2025 |
| 2.2.0 | 244 | 11/10/2025 |
| 2.1.0 | 206 | 11/4/2025 |
| 2.0.0 | 217 | 11/3/2025 |
| 1.0.1 | 200 | 10/29/2025 |
| 1.0.0 | 175 | 10/10/2025 |