proxy benchmarks (#2039)

This commit is contained in:
Erwin van der Valk 2025-06-10 11:22:38 +02:00 committed by GitHub
parent a250d551aa
commit 7584c4e414
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 554 additions and 2 deletions

2
.gitignore vendored
View file

@ -225,3 +225,5 @@ artifacts
#exclude for verify.tests
*.received.txt
*.Artifacts/

View file

@ -3,6 +3,7 @@
<PackageVersion Include="AngleSharp" Version="1.1.2" />
<PackageVersion Include="Aspire.Hosting.AppHost" Version="9.2.1" />
<PackageVersion Include="Aspire.Hosting.Testing" Version="9.2.1" />
<PackageVersion Include="BenchmarkDotNet" Version="0.15.0" />
<PackageVersion Include="BullsEye" Version="5.0.0" />
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="Duende.AccessTokenManagement" Version="3.2.0" />

View file

@ -0,0 +1,12 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using BenchmarkDotNet.Attributes;
namespace Bff.Benchmarks;
[ShortRunJob]
public class BenchmarkBase
{
}

View file

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<WarningsAsErrors>false</WarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Duende.IdentityServer" />
<PackageReference Include="Duende.IdentityModel" />
<PackageReference Include="BenchmarkDotNet" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Bff.Yarp\Bff.Yarp.csproj" />
<ProjectReference Include="..\..\src\Bff\Bff.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,40 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
namespace Bff.Benchmarks.Hosts;
public class ApiHost : Host
{
public ApiHost(Uri identityServerUri) : base()
{
OnConfigureServices += services =>
{
services.AddAuthentication("token")
.AddJwtBearer("token", options =>
{
options.Authority = identityServerUri.ToString();
options.MapInboundClaims = false;
});
};
OnConfigure += app =>
{
app.Use(async (c, n) =>
{
await n();
});
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("{**catch-all}", () => "ok");
};
}
}

View file

@ -0,0 +1,64 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Duende.Bff;
using Duende.Bff.AccessTokenManagement;
using Duende.Bff.DynamicFrontends;
using Duende.Bff.Yarp;
using Microsoft.AspNetCore.Builder;
namespace Bff.Benchmarks.Hosts;
public class BffHost : Host
{
public BffHost(Uri identityServer, Uri apiUri)
{
OnConfigureServices += services =>
{
services.AddBff()
.WithDefaultOpenIdConnectOptions(oidc =>
{
oidc.ClientId = "bff";
oidc.ClientSecret = "secret";
oidc.Authority = identityServer.ToString();
oidc.SaveTokens = true;
oidc.GetClaimsFromUserInfoEndpoint = true;
oidc.ResponseType = "code";
oidc.ResponseMode = "query";
oidc.MapInboundClaims = false;
oidc.GetClaimsFromUserInfoEndpoint = true;
oidc.SaveTokens = true;
// request scopes + refresh tokens
oidc.Scope.Clear();
oidc.Scope.Add("openid");
oidc.Scope.Add("profile");
oidc.Scope.Add("api");
})
.AddRemoteApis();
};
OnConfigure += app =>
{
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseBff();
app.MapGet("/", () => "bff");
app.MapBffManagementEndpoints();
app.MapRemoteBffApiEndpoint("/allow_anon", apiUri.ToString());
app.MapRemoteBffApiEndpoint("/client_token", apiUri.ToString())
.WithAccessToken(RequiredTokenType.Client);
app.MapRemoteBffApiEndpoint("/user_token", apiUri.ToString())
.WithAccessToken(RequiredTokenType.User);
};
}
public void AddFrontend(Uri uri) => GetService<IFrontendCollection>().AddOrUpdate(new BffFrontend().MappedToOrigin(Origin.Parse(uri)));
}

View file

@ -0,0 +1,54 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Bff.Benchmarks.Hosts;
public abstract class Host : IAsyncDisposable
{
private WebApplication _app = null!;
private WebApplicationBuilder _builder = null!;
public event Action<IServiceCollection> OnConfigureServices = _ => { };
public event Action<WebApplication> OnConfigure = _ => { };
public Host()
{
_builder = WebApplication.CreateBuilder();
// Logs interfere with the benchmarks, so we clear them
_builder.Logging.ClearProviders();
// Ensure dev certificate is used for SSL
_builder.WebHost
.UseUrls("https://127.0.0.1:0");
_builder.Services.AddAuthentication();
_builder.Services.AddAuthorization();
_builder.Services.AddRouting();
}
public T GetService<T>() where T : notnull => _app.Services.GetRequiredService<T>();
public void Initialize()
{
OnConfigureServices(_builder.Services);
_app = _builder.Build();
OnConfigure(_app);
_app.Start();
}
public Uri Url => new Uri("https://localhost:" + new Uri(_app.Urls.First()).Port);
public async ValueTask DisposeAsync()
{
await _app.StopAsync();
await _app.DisposeAsync();
}
}

View file

@ -0,0 +1,62 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using System.Security.Claims;
using Duende.IdentityServer.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
namespace Bff.Benchmarks.Hosts;
public class IdentityServerHost : Host
{
public IdentityServerHost()
{
OnConfigureServices += services =>
{
var identityServer = services.AddIdentityServer(options =>
{
options.EmitStaticAudienceClaim = true;
options.UserInteraction.CreateAccountUrl = "/account/create";
})
.AddInMemoryClients(Clients)
.AddInMemoryIdentityResources(IdentityResources)
.AddInMemoryApiScopes(ApiScopes);
identityServer.AddBackChannelLogoutHttpClient();
};
OnConfigure += app =>
{
app.UseRouting();
app.UseIdentityServer();
app.UseAuthorization();
app.MapGet("/account/login", async ctx =>
{
await ctx.SignInAsync(UserToSignIn);
});
};
}
public List<Client> Clients { get; set; } = new();
public List<IdentityResource> IdentityResources { get; set; } = new()
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
};
public List<ApiScope> ApiScopes { get; set; } = [new ApiScope("api")];
public ClaimsPrincipal UserToSignIn { get; set; } = new ClaimsPrincipal(new ClaimsIdentity([
new Claim("name", "bob"),
new Claim("sub", "bob"),
], "test", "name", "role"));
}

View file

@ -0,0 +1,47 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Yarp.ReverseProxy.Configuration;
namespace Bff.Benchmarks.Hosts;
public class PlainYarpProxy : Host
{
public PlainYarpProxy(Uri api)
{
OnConfigureServices += services =>
{
services.AddReverseProxy()
.LoadFromMemory(
[
new RouteConfig()
{
RouteId = "1",
ClusterId = "cluster_id",
Match = new RouteMatch()
{
Path = "/yarp/{**catch-all}",
}
}
],
[
new ClusterConfig()
{
ClusterId = "cluster_id",
Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)
{
{ "destination1", new DestinationConfig { Address = api.ToString() } }
}
}
]);
};
OnConfigure += app =>
{
app.UseRouting();
app.MapReverseProxy();
};
}
}

View file

@ -0,0 +1,10 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using BenchmarkDotNet.Running;
public class Program
{
static void Main(string[] args) => BenchmarkRunner.Run(typeof(Program).Assembly);
}

View file

@ -0,0 +1,110 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using System.Net;
using BenchmarkDotNet.Attributes;
namespace Bff.Benchmarks;
public class ProxyBenchmarks : BenchmarkBase
{
private ProxyFixture _fixture = null!;
private HttpClient _authenticatedBffClient = null!;
private HttpClient _manyFrontendsBffClient = null!;
private HttpClient _directHttpClient = null!;
private HttpClient _yarpHttpClient = null!;
[GlobalSetup]
public async Task Start()
{
_fixture = new ProxyFixture();
_authenticatedBffClient = new HttpClient()
{
BaseAddress = _fixture.Bff.Url
};
await _authenticatedBffClient.GetAsync("/bff/login")
.EnsureStatusCode();
_manyFrontendsBffClient = new HttpClient()
{
BaseAddress = _fixture.BffWithManyFrontends.Url
};
await _manyFrontendsBffClient.GetAsync("/bff/login")
.EnsureStatusCode();
_directHttpClient = new HttpClient
{
BaseAddress = _fixture.Api.Url
};
_yarpHttpClient = new HttpClient
{
BaseAddress = _fixture.YarpProxy.Url
};
}
[Benchmark]
public async Task DirectToApi() => await _directHttpClient.GetAsync("/")
.EnsureStatusCode();
[Benchmark]
public async Task YarpProxy() => await _yarpHttpClient.GetAsync("/yarp/test")
.EnsureStatusCode();
[Benchmark]
public async Task BffUserToken() => await _authenticatedBffClient
.GetWithCSRF("/user_token")
.EnsureStatusCode();
[Benchmark]
public async Task BffWithManyFrontends() => await _manyFrontendsBffClient
.GetWithCSRF("/user_token")
.EnsureStatusCode();
[Benchmark]
public async Task BffClientCredentialsToken() => await _authenticatedBffClient
.GetWithCSRF("/client_token")
.EnsureStatusCode();
[GlobalCleanup]
public async Task Stop() => await _fixture.DisposeAsync();
public async Task DisposeAsync()
{
await _fixture.DisposeAsync();
_authenticatedBffClient.Dispose();
_directHttpClient.Dispose();
_yarpHttpClient.Dispose();
}
}
public static class HttpClientExtensions
{
public static Task<HttpResponseMessage> GetWithCSRF(this HttpClient client, string uri)
{
var request = new HttpRequestMessage(HttpMethod.Get, uri)
{
Headers =
{
{"x-csrf", "1"}
}
};
return client.SendAsync(request);
}
public static async Task<HttpResponseMessage> EnsureStatusCode(this Task<HttpResponseMessage> task, HttpStatusCode? statusCode = HttpStatusCode.OK)
{
var response = await task;
if (response.StatusCode != statusCode)
{
throw new HttpRequestException($"Expected status code {statusCode}, but got {response.StatusCode}");
}
return response;
}
}

View file

@ -0,0 +1,75 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Bff.Benchmarks.Hosts;
using Duende.IdentityModel;
using Duende.IdentityServer.Models;
namespace Bff.Benchmarks;
public class ProxyFixture : IAsyncDisposable
{
public ApiHost Api;
public IdentityServerHost IdentityServer;
public BffHost Bff;
public BffHost BffWithManyFrontends;
public PlainYarpProxy YarpProxy;
public ProxyFixture()
{
IdentityServer = new IdentityServerHost();
IdentityServer.Initialize();
Api = new ApiHost(IdentityServer.Url);
Api.Initialize();
Bff = new BffHost(IdentityServer.Url, Api.Url);
Bff.Initialize();
BffWithManyFrontends = new BffHost(IdentityServer.Url, Api.Url);
BffWithManyFrontends.Initialize();
for (var i = 0; i < 1000; i++)
{
BffWithManyFrontends.AddFrontend(new Uri($"https://frontend{i}.example.com/"));
}
BffWithManyFrontends.AddFrontend(Bff.Url);
var bffUrls = new[] { Bff.Url, BffWithManyFrontends.Url };
IdentityServer.Clients.Add(new Client()
{
ClientId = "bff",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes =
{
GrantType.AuthorizationCode,
GrantType.ClientCredentials,
OidcConstants.GrantTypes.TokenExchange
},
RedirectUris = bffUrls.Select(x => $"{x}signin-oidc").ToArray(),
PostLogoutRedirectUris = bffUrls.Select(x => $"{x}signout-callback-oidc").ToArray(),
AllowOfflineAccess = true,
AllowedScopes = { "openid", "profile", "api" },
RefreshTokenExpiration = TokenExpiration.Absolute,
AbsoluteRefreshTokenLifetime = 300,
AccessTokenLifetime = 3000
});
YarpProxy = new PlainYarpProxy(Api.Url);
YarpProxy.Initialize();
}
public async ValueTask DisposeAsync()
{
await IdentityServer.DisposeAsync();
await Api.DisposeAsync();
await Bff.DisposeAsync();
await YarpProxy.DisposeAsync();
}
}

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<PropertyGroup>
<IsBffProject>true</IsBffProject>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<!-- <Import Project="../../src.props" /> -->
</Project>

View file

@ -20,7 +20,7 @@ internal class FrontendCollection : IDisposable, IFrontendCollection
private readonly IDisposable? _stopSubscription;
public event Action<BffFrontend> OnFrontendChanged = (_) => { };
internal event Action<BffFrontend> OnFrontendChanged = (_) => { };
public FrontendCollection(
IOptionsMonitor<BffConfiguration> bffConfiguration,
@ -193,5 +193,5 @@ internal class FrontendCollection : IDisposable, IFrontendCollection
// The _frontends array is completely replaced on add/update, so we don't need to lock here.
public IReadOnlyList<BffFrontend> GetAll() => _frontends.AsReadOnly();
public void Dispose() => _stopSubscription?.Dispose();
void IDisposable.Dispose() => _stopSubscription?.Dispose();
}

View file

@ -0,0 +1,34 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Bff.Benchmarks;
namespace Duende.Bff.Tests.Benchmarks;
public class BenchmarksTests(ProxyBenchmarksFixture benchmarks) : IClassFixture<ProxyBenchmarksFixture>
{
[Fact]
public async Task BffUserToken() =>
await benchmarks.BffUserToken();
[Fact]
public async Task YarpProxy() =>
await benchmarks.YarpProxy();
[Fact]
public async Task DirectToApi() =>
await benchmarks.DirectToApi();
[Fact]
public async Task BffClientCredentialsToken() =>
await benchmarks.BffClientCredentialsToken();
[Fact]
public async Task BffWithManyFrontends() =>
await benchmarks.BffWithManyFrontends();
}
public class ProxyBenchmarksFixture : ProxyBenchmarks, IAsyncLifetime
{
public async Task InitializeAsync() => await Start();
}

View file

@ -23,6 +23,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\performance\Bff.Benchmarks\Bff.Benchmarks.csproj" />
<ProjectReference Include="..\..\src\Bff.Blazor\Bff.Blazor.csproj" />
<ProjectReference Include="..\..\src\Bff.EntityFramework\Bff.EntityFramework.csproj" />
<ProjectReference Include="..\..\src\Bff\Bff.csproj" />

View file

@ -41,6 +41,9 @@
<Folder Name="/bff/migrations/">
<Project Path="bff/migrations/UserSessionDb/UserSessionDb.csproj" />
</Folder>
<Folder Name="/bff/performance/">
<Project Path="bff/performance/Bff.Benchmarks/Bff.Benchmarks.csproj" />
</Folder>
<Folder Name="/bff/src/">
<Project Path="bff/src/Bff.Blazor.Client/Bff.Blazor.Client.csproj" />
<Project Path="bff/src/Bff.Blazor/Bff.Blazor.csproj" />