Skip to content

Commit

Permalink
bff - Add local client (#1782)
Browse files Browse the repository at this point in the history
  • Loading branch information
Erwinvandervalk authored Feb 11, 2025
1 parent bf544fc commit cfc54f0
Show file tree
Hide file tree
Showing 8 changed files with 195 additions and 48 deletions.
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

0 comments on commit cfc54f0

Please sign in to comment.