diff --git a/src/Duende.Bff.Blazor.Client/BffBlazorOptions.cs b/src/Duende.Bff.Blazor.Client/BffBlazorOptions.cs
index 5e73bf3d..549ae63f 100644
--- a/src/Duende.Bff.Blazor.Client/BffBlazorOptions.cs
+++ b/src/Duende.Bff.Blazor.Client/BffBlazorOptions.cs
@@ -19,6 +19,13 @@ public class BffBlazorOptions
///
public string? RemoteApiBaseAddress { get; set; } = null;
+ ///
+ /// The base address to use for the state provider's calls to the
+ /// /bff/user endpoint. If unset (the default), the blazor hosting
+ /// environment's base address is used.
+ ///
+ public string? StateProviderBaseAddress { get; set; } = null;
+
///
/// The delay, in milliseconds, before the AuthenticationStateProvider
/// will start polling the /bff/user endpoint. Defaults to 1000 ms.
diff --git a/src/Duende.Bff.Blazor.Client/BffClientAuthenticationStateProvider.cs b/src/Duende.Bff.Blazor.Client/BffClientAuthenticationStateProvider.cs
index 60fa9bc8..cf22b28c 100644
--- a/src/Duende.Bff.Blazor.Client/BffClientAuthenticationStateProvider.cs
+++ b/src/Duende.Bff.Blazor.Client/BffClientAuthenticationStateProvider.cs
@@ -32,7 +32,7 @@ public BffClientAuthenticationStateProvider(
IOptions options,
ILogger logger)
{
- _client = factory.CreateClient("BffAuthenticationStateProvider");
+ _client = factory.CreateClient(nameof(BffClientAuthenticationStateProvider));
_logger = logger;
_cachedUser = GetPersistedUser(state);
if (_cachedUser.Identity?.IsAuthenticated == true)
diff --git a/src/Duende.Bff.Blazor.Client/ServiceCollectionExtensions.cs b/src/Duende.Bff.Blazor.Client/ServiceCollectionExtensions.cs
index 17ab858f..ae1970ea 100644
--- a/src/Duende.Bff.Blazor.Client/ServiceCollectionExtensions.cs
+++ b/src/Duende.Bff.Blazor.Client/ServiceCollectionExtensions.cs
@@ -21,16 +21,31 @@ public static IServiceCollection AddBffBlazorClient(this IServiceCollection serv
services
.AddAuthorizationCore()
.AddScoped()
+ // TODO - Should this have a different lifetime?
.AddTransient()
- .AddHttpClient("BffAuthenticationStateProvider", (sp, client) =>
+ .AddHttpClient(nameof(BffClientAuthenticationStateProvider), (sp, client) =>
{
- var baseAddress = GetBaseAddress(sp);
+ var baseAddress = GetStateProviderBaseAddress(sp);
client.BaseAddress = new Uri(baseAddress);
}).AddHttpMessageHandler();
return services;
}
+ private static string GetStateProviderBaseAddress(IServiceProvider sp)
+ {
+ var opt = sp.GetRequiredService>();
+ if (opt.Value.StateProviderBaseAddress != null)
+ {
+ return opt.Value.StateProviderBaseAddress;
+ }
+ else
+ {
+ var hostEnv = sp.GetRequiredService();
+ return hostEnv.BaseAddress;
+ }
+ }
+
private static string GetBaseAddress(IServiceProvider sp)
{
var opt = sp.GetRequiredService>();
@@ -96,6 +111,16 @@ private static void SetBaseAddress(IServiceProvider sp, HttpClient client)
client.BaseAddress = new Uri(new Uri(baseAddress), remoteApiPath);
}
+ ///
+ /// Adds a named for use when invoking remote APIs
+ /// proxied through Duende.Bff and configures the client with a callback.
+ ///
+ /// The name of that to
+ /// configure. A common use case is to use the same named client in multiple
+ /// render contexts that are automatically switched between via interactive
+ /// render modes.
+ /// A configuration callback used to set up
+ /// the .
public static IHttpClientBuilder AddRemoteApiHttpClient(this IServiceCollection services, string clientName,
Action configureClient)
{
@@ -103,6 +128,17 @@ public static IHttpClientBuilder AddRemoteApiHttpClient(this IServiceCollection
.AddHttpMessageHandler();
}
+ ///
+ /// Adds a named for use when invoking remote APIs
+ /// proxied through Duende.Bff and configures the client with a callback
+ /// that has access to the underlying service provider.
+ ///
+ /// The name of that to
+ /// configure. A common use case is to use the same named client in multiple
+ /// render contexts that are automatically switched between via interactive
+ /// render modes.
+ /// A configuration callback used to set up
+ /// the .
public static IHttpClientBuilder AddRemoteApiHttpClient(this IServiceCollection services, string clientName,
Action? configureClient = null)
{
@@ -110,6 +146,7 @@ public static IHttpClientBuilder AddRemoteApiHttpClient(this IServiceCollection
.AddHttpMessageHandler();
}
+ // TODO - Manually test this API.
public static IHttpClientBuilder AddRemoteApiHttpClient(this IServiceCollection services,
Action configureClient)
where T : class
diff --git a/test/Duende.Bff.Blazor.Client.UnitTests/AntiforgeryHandlerTests.cs b/test/Duende.Bff.Blazor.Client.UnitTests/AntiforgeryHandlerTests.cs
new file mode 100644
index 00000000..5e4ed834
--- /dev/null
+++ b/test/Duende.Bff.Blazor.Client.UnitTests/AntiforgeryHandlerTests.cs
@@ -0,0 +1,30 @@
+using NSubstitute;
+using Shouldly;
+
+namespace Duende.Bff.Blazor.Client.UnitTests;
+
+public class AntiforgeryHandlerTests
+{
+ [Fact]
+ public async Task Adds_expected_header()
+ {
+ var sut = new TestAntiforgeryHandler()
+ {
+ InnerHandler = Substitute.For()
+ };
+
+ var request = new HttpRequestMessage();
+
+ await sut.SendAsync(request, CancellationToken.None);
+
+ request.Headers.ShouldContain(h => h.Key == "X-CSRF" && h.Value.Contains("1"));
+ }
+}
+
+public class TestAntiforgeryHandler : AntiforgeryHandler
+{
+ public new Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ return base.SendAsync(request, cancellationToken);
+ }
+}
\ No newline at end of file
diff --git a/test/Duende.Bff.Blazor.Client.UnitTests/Duende.Bff.Blazor.Client.UnitTests.csproj b/test/Duende.Bff.Blazor.Client.UnitTests/Duende.Bff.Blazor.Client.UnitTests.csproj
new file mode 100644
index 00000000..3c9c9ae9
--- /dev/null
+++ b/test/Duende.Bff.Blazor.Client.UnitTests/Duende.Bff.Blazor.Client.UnitTests.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/Duende.Bff.Blazor.Client.UnitTests/ServiceCollectionExtensionsTests.cs b/test/Duende.Bff.Blazor.Client.UnitTests/ServiceCollectionExtensionsTests.cs
new file mode 100644
index 00000000..81945850
--- /dev/null
+++ b/test/Duende.Bff.Blazor.Client.UnitTests/ServiceCollectionExtensionsTests.cs
@@ -0,0 +1,106 @@
+using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
+using Microsoft.Extensions.DependencyInjection;
+using NSubstitute;
+using Shouldly;
+
+namespace Duende.Bff.Blazor.Client.UnitTests;
+
+public class ServiceCollectionExtensionsTests
+{
+ [Theory]
+ [InlineData("https://example.com/", "https://example.com/")]
+ [InlineData("https://example.com", "https://example.com/")]
+ public void When_base_address_option_is_set_AddBffBlazorClient_configures_HttpClient_base_address(string configuredRemoteAddress, string expectedBaseAddress)
+ {
+ var sut = new ServiceCollection();
+ sut.AddBffBlazorClient();
+ sut.Configure(opt =>
+ {
+ opt.StateProviderBaseAddress = configuredRemoteAddress;
+ });
+
+
+ var sp = sut.BuildServiceProvider();
+ var httpClientFactory = sp.GetService();
+ var httpClient = httpClientFactory?.CreateClient(nameof(BffClientAuthenticationStateProvider));
+ httpClient.ShouldNotBeNull();
+ httpClient.BaseAddress.ShouldNotBeNull();
+ httpClient.BaseAddress.AbsoluteUri.ShouldBe(expectedBaseAddress);
+ }
+
+ [Fact]
+ public void When_base_address_option_is_default_AddBffBlazorClient_configures_HttpClient_base_address_from_host_env()
+ {
+ var expectedBaseAddress = "https://example.com/";
+
+ var sut = new ServiceCollection();
+ sut.AddBffBlazorClient();
+ var env = Substitute.For();
+ env.BaseAddress.Returns(expectedBaseAddress);
+ sut.AddSingleton(env);
+
+ var sp = sut.BuildServiceProvider();
+ var httpClientFactory = sp.GetService();
+ var httpClient = httpClientFactory?.CreateClient(nameof(BffClientAuthenticationStateProvider));
+ httpClient.ShouldNotBeNull();
+ httpClient.BaseAddress.ShouldNotBeNull();
+ httpClient.BaseAddress.AbsoluteUri.ShouldBe(expectedBaseAddress);
+ }
+
+ [Theory]
+ [InlineData("https://example.com/", "remote-apis", "https://example.com/remote-apis/")]
+ [InlineData("https://example.com/", null, "https://example.com/remote-apis/")]
+ [InlineData("https://example.com", null, "https://example.com/remote-apis/")]
+ [InlineData("https://example.com", "custom/route/to/apis", "https://example.com/custom/route/to/apis/")]
+ [InlineData("https://example.com/with/base/path", "custom/route/to/apis", "https://example.com/with/base/path/custom/route/to/apis/")]
+ [InlineData("https://example.com/with/base/path/", "custom/route/to/apis", "https://example.com/with/base/path/custom/route/to/apis/")]
+ [InlineData("https://example.com/with/base/path", "/custom/route/to/apis", "https://example.com/with/base/path/custom/route/to/apis/")]
+ [InlineData("https://example.com/with/base/path/", "/custom/route/to/apis", "https://example.com/with/base/path/custom/route/to/apis/")]
+ [InlineData("https://example.com/with/base/path", null, "https://example.com/with/base/path/remote-apis/")]
+ public void AddRemoteApiHttpClient_configures_HttpClient_base_address(string configuredRemoteAddress, string? configuredRemotePath, string expectedBaseAddress)
+ {
+ var sut = new ServiceCollection();
+ sut.AddBffBlazorClient();
+ sut.AddRemoteApiHttpClient("clientName");
+ sut.Configure(opt =>
+ {
+ if (configuredRemoteAddress != null)
+ {
+ opt.RemoteApiBaseAddress = configuredRemoteAddress;
+ }
+ if (configuredRemotePath != null)
+ {
+ opt.RemoteApiPath = configuredRemotePath;
+ }
+ });
+
+
+ var sp = sut.BuildServiceProvider();
+ var httpClientFactory = sp.GetService();
+ var httpClient = httpClientFactory?.CreateClient("clientName");
+ httpClient.ShouldNotBeNull();
+ httpClient.BaseAddress.ShouldNotBeNull();
+ httpClient.BaseAddress.AbsoluteUri.ShouldBe(expectedBaseAddress);
+ }
+
+ [Fact]
+ public void When_base_address_option_is_default_AddRemoteApiHttpClient_configures_HttpClient_base_address_from_host_env()
+ {
+ var hostBaseAddress = "https://example.com/";
+ var expectedBaseAddress = "https://example.com/remote-apis/";
+
+ var sut = new ServiceCollection();
+ sut.AddBffBlazorClient();
+ sut.AddRemoteApiHttpClient("clientName");
+ var env = Substitute.For();
+ env.BaseAddress.Returns(hostBaseAddress);
+ sut.AddSingleton(env);
+
+ var sp = sut.BuildServiceProvider();
+ var httpClientFactory = sp.GetService();
+ var httpClient = httpClientFactory?.CreateClient("clientName");
+ httpClient.ShouldNotBeNull();
+ httpClient.BaseAddress.ShouldNotBeNull();
+ httpClient.BaseAddress.AbsoluteUri.ShouldBe(expectedBaseAddress);
+ }
+}
\ No newline at end of file