Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bff - Add local client #1782

Merged
merged 1 commit into from
Feb 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@page "/weather"
@inject HttpClient Http
@inject WeatherHttpClient Http

@attribute [Authorize]

Expand Down Expand Up @@ -43,14 +43,7 @@ else

protected override async Task OnInitializedAsync()
{
_forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
_forecasts = await Http.GetWeatherForecasts();
}

private class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Duende.Bff.Blazor.Client;
using Hosts.Bff.Blazor.WebAssembly.Client;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;

var builder = WebAssemblyHostBuilder.CreateDefault(args);
Expand All @@ -8,11 +9,6 @@
.AddBffBlazorClient() // Provides auth state provider that polls the /bff/user endpoint
.AddCascadingAuthenticationState();

builder.Services.AddScoped(sp =>
new HttpClient
{
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress),
DefaultRequestHeaders = { {"x-csrf", "1" }}
});
builder.Services.AddLocalApiHttpClient<WeatherHttpClient>();

await builder.Build().RunAsync();
await builder.Build().RunAsync();
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
internal class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int) (TemperatureC / 0.5556);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Net.Http.Json;
using System.Text.Json;

namespace Hosts.Bff.Blazor.WebAssembly.Client;

internal class WeatherHttpClient(HttpClient client)
{
public async Task<WeatherForecast[]> GetWeatherForecasts() => await client.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast")
?? throw new JsonException("Failed to deserialize");
}
117 changes: 104 additions & 13 deletions bff/src/Bff.Blazor.Client/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ private static string GetStateProviderBaseAddress(IServiceProvider sp)
}
}

private static string GetBaseAddress(IServiceProvider sp)
private static string GetRemoteBaseAddress(IServiceProvider sp)
{
var opt = sp.GetRequiredService<IOptions<BffBlazorOptions>>();
if (opt.Value.RemoteApiBaseAddress != null)
Expand All @@ -65,40 +65,76 @@ private static string GetBaseAddress(IServiceProvider sp)
}
else
{
var hostEnv = sp.GetRequiredService<IWebAssemblyHostEnvironment>();
return hostEnv.BaseAddress;
return GetLocalBaseAddress(sp);
}
}

private static string GetLocalBaseAddress(IServiceProvider sp)
{
var hostEnv = sp.GetRequiredService<IWebAssemblyHostEnvironment>();
return hostEnv.BaseAddress;
}

private static string GetRemoteApiPath(IServiceProvider sp)
{
var opt = sp.GetRequiredService<IOptions<BffBlazorOptions>>();
return opt.Value.RemoteApiPath;
}

private static Action<IServiceProvider, HttpClient> SetBaseAddress(
private static Action<IServiceProvider, HttpClient> SetRemoteApiBaseAddress(
Action<IServiceProvider, HttpClient>? configureClient)
{
return (sp, client) =>
{
SetBaseAddress(sp, client);
SetRemoteApiBaseAddress(sp, client);
configureClient?.Invoke(sp, client);
};
}

private static Action<IServiceProvider, HttpClient> SetBaseAddress(
private static Action<IServiceProvider, HttpClient> SetRemoteApiBaseAddress(
Action<HttpClient>? configureClient)
{
return (sp, client) =>
{
SetBaseAddress(sp, client);
SetRemoteApiBaseAddress(sp, client);
configureClient?.Invoke(client);
};
}

private static void SetBaseAddress(IServiceProvider sp, HttpClient client)
private static void SetLocalApiBaseAddress(IServiceProvider sp, HttpClient client)
{
var baseAddress = GetBaseAddress(sp);
var baseAddress = GetLocalBaseAddress(sp);
if (!baseAddress.EndsWith("/"))
{
baseAddress += "/";
}

client.BaseAddress = new Uri(baseAddress);

}

private static Action<IServiceProvider, HttpClient> SetLocalApiBaseAddress(
Action<HttpClient>? configureClient)
{
return (sp, client) =>
{
SetLocalApiBaseAddress(sp, client);
configureClient?.Invoke(client);
};
}

private static Action<IServiceProvider, HttpClient> SetLocalApiBaseAddress(
Action<IServiceProvider, HttpClient>? configureClient)
{
return (sp, client) =>
{
SetLocalApiBaseAddress(sp, client);
configureClient?.Invoke(sp, client);
};
}
private static void SetRemoteApiBaseAddress(IServiceProvider sp, HttpClient client)
{
var baseAddress = GetRemoteBaseAddress(sp);
if (!baseAddress.EndsWith("/"))
{
baseAddress += "/";
Expand All @@ -121,6 +157,61 @@ private static void SetBaseAddress(IServiceProvider sp, HttpClient client)
client.BaseAddress = new Uri(new Uri(baseAddress), remoteApiPath);
}


/// <summary>
/// Adds a named <see cref="HttpClient"/> for use when invoking local APIs
/// and configures the client with a callback.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="clientName">The name of that <see cref="HttpClient"/> 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. In that case, ensure both the client and server project
/// define the HttpClient appropriately.</param>
/// <param name="configureClient">A configuration callback used to set up
/// the <see cref="HttpClient"/>.</param>
public static IHttpClientBuilder AddLocalApiHttpClient(this IServiceCollection services, string clientName,
Action<HttpClient> configureClient)
{
return services.AddHttpClient(clientName, SetLocalApiBaseAddress(configureClient))
.AddHttpMessageHandler<AntiforgeryHandler>();
}

/// <summary>
/// Adds a named <see cref="HttpClient"/> for use when invoking local APIs
/// and configures the client with a callback.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="clientName">The name of that <see cref="HttpClient"/> 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. In that case, ensure both the client and server project
/// define the HttpClient appropriately.</param>
/// <param name="configureClient">A configuration callback used to set up
/// the <see cref="HttpClient"/>.</param>
public static IHttpClientBuilder AddLocalApiHttpClient(this IServiceCollection services, string clientName,
Action<IServiceProvider, HttpClient>? configureClient = null)
{
return services.AddHttpClient(clientName, SetLocalApiBaseAddress(configureClient))
.AddHttpMessageHandler<AntiforgeryHandler>();
}

/// <summary>
/// Adds a typed <see cref="HttpClient"/> 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.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="configureClient">A configuration callback used to set up
/// the <see cref="HttpClient"/>.</param>
public static IHttpClientBuilder AddLocalApiHttpClient<T>(this IServiceCollection services,
Action<IServiceProvider, HttpClient>? configureClient = null)
where T : class
{
return services.AddHttpClient<T>(SetLocalApiBaseAddress(configureClient))
.AddHttpMessageHandler<AntiforgeryHandler>();
}

/// <summary>
/// Adds a named <see cref="HttpClient"/> for use when invoking remote APIs
/// proxied through Duende.Bff and configures the client with a callback.
Expand All @@ -136,7 +227,7 @@ private static void SetBaseAddress(IServiceProvider sp, HttpClient client)
public static IHttpClientBuilder AddRemoteApiHttpClient(this IServiceCollection services, string clientName,
Action<HttpClient> configureClient)
{
return services.AddHttpClient(clientName, SetBaseAddress(configureClient))
return services.AddHttpClient(clientName, SetRemoteApiBaseAddress(configureClient))
.AddHttpMessageHandler<AntiforgeryHandler>();
}

Expand All @@ -156,7 +247,7 @@ public static IHttpClientBuilder AddRemoteApiHttpClient(this IServiceCollection
public static IHttpClientBuilder AddRemoteApiHttpClient(this IServiceCollection services, string clientName,
Action<IServiceProvider, HttpClient>? configureClient = null)
{
return services.AddHttpClient(clientName, SetBaseAddress(configureClient))
return services.AddHttpClient(clientName, SetRemoteApiBaseAddress(configureClient))
.AddHttpMessageHandler<AntiforgeryHandler>();
}

Expand All @@ -171,7 +262,7 @@ public static IHttpClientBuilder AddRemoteApiHttpClient<T>(this IServiceCollecti
Action<HttpClient> configureClient)
where T : class
{
return services.AddHttpClient<T>(SetBaseAddress(configureClient))
return services.AddHttpClient<T>(SetRemoteApiBaseAddress(configureClient))
.AddHttpMessageHandler<AntiforgeryHandler>();
}

Expand All @@ -187,7 +278,7 @@ public static IHttpClientBuilder AddRemoteApiHttpClient<T>(this IServiceCollecti
Action<IServiceProvider, HttpClient>? configureClient = null)
where T : class
{
return services.AddHttpClient<T>(SetBaseAddress(configureClient))
return services.AddHttpClient<T>(SetRemoteApiBaseAddress(configureClient))
.AddHttpMessageHandler<AntiforgeryHandler>();
}
}
12 changes: 10 additions & 2 deletions bff/test/Bff.Blazor.Client.UnitTests/AntiforgeryHandlerTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using NSubstitute;
using System.Net;
using Shouldly;

namespace Duende.Bff.Blazor.Client.UnitTests;
Expand All @@ -10,7 +10,7 @@ public async Task Adds_expected_header()
{
var sut = new TestAntiforgeryHandler()
{
InnerHandler = Substitute.For<HttpMessageHandler>()
InnerHandler = new NoOpHttpMessageHandler()
};

var request = new HttpRequestMessage();
Expand All @@ -27,4 +27,12 @@ public class TestAntiforgeryHandler : AntiforgeryHandler
{
return base.SendAsync(request, cancellationToken);
}
}

public class NoOpHttpMessageHandler : HttpMessageHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));
}
}
Loading