mirror of
https://github.com/DuendeSoftware/products
synced 2026-05-24 09:28:24 +00:00
First stab at perf tests. (#2070)
* First stab at perf tests. * Update bff/hosts/Hosts.IdentityServer/Config.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * reading values from env vars * dotnet format * cleanup --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
parent
aa6ebd68e5
commit
7702c8d758
35 changed files with 1172 additions and 193 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -226,4 +226,6 @@ artifacts
|
|||
#exclude for verify.tests
|
||||
*.received.txt
|
||||
|
||||
*.Artifacts/
|
||||
*.Artifacts/
|
||||
|
||||
reports
|
||||
|
|
@ -86,11 +86,15 @@
|
|||
<PackageVersion Include="Microsoft.IdentityModel.Logging" Condition="'$(TargetFramework)' == 'net9.0'" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Condition="'$(TargetFramework)' == 'net8.0'" Version="7.1.2" />
|
||||
<PackageVersion Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Condition="'$(TargetFramework)' == 'net9.0'" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Net.Http.Headers" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageVersion Include="Microsoft.NETCore.Jit" Version="2.0.8" />
|
||||
<PackageVersion Include="Microsoft.Playwright.Xunit" Version="1.50.0" />
|
||||
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
|
||||
<PackageVersion Include="MinVer" Version="6.0.0" />
|
||||
<PackageVersion Include="NBomber" Version="6.0.2" />
|
||||
<PackageVersion Include="NBomber.Http" Version="6.0.2" />
|
||||
<PackageVersion Include="NBomber.Sinks.Timescale" Version="0.8.0" />
|
||||
<PackageVersion Include="NSubstitute" Version="5.1.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.11.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.11.0" />
|
||||
|
|
@ -135,4 +139,4 @@
|
|||
<PackageVersion Include="System.Drawing.Common" Version="6.0.0" />
|
||||
<PackageVersion Include="Microsoft.Data.SqlClient" Version="5.2.2" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
"path": "..\\products.slnx",
|
||||
"projects": [
|
||||
"bff\\performance\\Bff.Benchmarks\\Bff.Benchmarks.csproj",
|
||||
"bff\\performance\\Bff.Performance\\Bff.Performance.csproj",
|
||||
".github\\workflow-gen\\workflow-gen.csproj",
|
||||
"bff\\hosts\\Blazor\\PerComponent\\Hosts.Bff.Blazor.PerComponent.Client\\Hosts.Bff.Blazor.PerComponent.Client.csproj",
|
||||
"bff\\hosts\\Blazor\\PerComponent\\Hosts.Bff.Blazor.PerComponent\\Hosts.Bff.Blazor.PerComponent.csproj",
|
||||
|
|
@ -12,6 +13,7 @@
|
|||
"bff\\hosts\\Hosts.Bff.DPoP\\Hosts.Bff.DPoP.csproj",
|
||||
"bff\\hosts\\Hosts.Bff.EF\\Hosts.Bff.EF.csproj",
|
||||
"bff\\hosts\\Hosts.Bff.InMemory\\Hosts.Bff.InMemory.csproj",
|
||||
"bff\\hosts\\Hosts.Bff.Performance\\Hosts.Bff.Performance.csproj",
|
||||
"bff\\hosts\\Hosts.Bff.MultiFrontend\\Hosts.Bff.MultiFrontend.csproj",
|
||||
"bff\\hosts\\Hosts.IdentityServer\\Hosts.IdentityServer.csproj",
|
||||
"bff\\hosts\\Hosts.ServiceDefaults\\Hosts.ServiceDefaults.csproj",
|
||||
|
|
|
|||
|
|
@ -17,9 +17,6 @@
|
|||
<ItemGroup>
|
||||
<ProjectReference Include="..\Hosts.Bff.MultiFrontend\Hosts.Bff.MultiFrontend.csproj" />
|
||||
<ProjectReference Include="..\Hosts.ServiceDefaults\Hosts.ServiceDefaults.csproj" IsAspireProjectResource="false" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\migrations\UserSessionDb\UserSessionDb.csproj" />
|
||||
<ProjectReference Include="..\RemoteApis\Hosts.RemoteApi.DPoP\Hosts.RemoteApi.DPoP.csproj" />
|
||||
<ProjectReference Include="..\RemoteApis\Hosts.RemoteApi.Isolated\Hosts.RemoteApi.Isolated.csproj" />
|
||||
|
|
@ -27,6 +24,7 @@
|
|||
<ProjectReference Include="..\Hosts.Bff.DPoP\Hosts.Bff.DPoP.csproj" />
|
||||
<ProjectReference Include="..\Hosts.Bff.EF\Hosts.Bff.EF.csproj" />
|
||||
<ProjectReference Include="..\Hosts.Bff.InMemory\Hosts.Bff.InMemory.csproj" />
|
||||
<ProjectReference Include="..\Hosts.Bff.Performance\Hosts.Bff.Performance.csproj" />
|
||||
<ProjectReference Include="..\Blazor\PerComponent\Hosts.Bff.Blazor.PerComponent.Client\Hosts.Bff.Blazor.PerComponent.Client.csproj" />
|
||||
<ProjectReference Include="..\Blazor\PerComponent\Hosts.Bff.Blazor.PerComponent\Hosts.Bff.Blazor.PerComponent.csproj" />
|
||||
<ProjectReference Include="..\Blazor\WebAssembly\Hosts.Bff.Blazor.WebAssembly.Client\Hosts.Bff.Blazor.WebAssembly.Client.csproj" />
|
||||
|
|
|
|||
|
|
@ -18,6 +18,14 @@ var bff = builder.AddProject<Projects.Hosts_Bff_InMemory>(AppHostServices.Bff)
|
|||
.WithAwaitedReference(api)
|
||||
;
|
||||
|
||||
var perf = builder.AddProject<Projects.Hosts_Bff_Performance>(AppHostServices.BffPerf)
|
||||
.WithExternalHttpEndpoints()
|
||||
.WithEndpoint(6100, isProxied: false, scheme: "https", name: "idsrv")
|
||||
.WithEndpoint(6001, isProxied: false, scheme: "https", name: "api")
|
||||
.WithEndpoint(6002, isProxied: false, scheme: "https", name: "single")
|
||||
.WithEndpoint(6003, isProxied: false, scheme: "https", name: "multi")
|
||||
;
|
||||
|
||||
var bffMulti = builder.AddProject<Projects.Hosts_Bff_MultiFrontend>(AppHostServices.BffMultiFrontend)
|
||||
.WithExternalHttpEndpoints()
|
||||
.WithUrl("https://app1.localhost:5005", "https://app1.localhost:5005")
|
||||
|
|
@ -58,6 +66,7 @@ builder.AddProject<Projects.UserSessionDb>(AppHostServices.Migrations);
|
|||
|
||||
idServer
|
||||
.WithReference(bff)
|
||||
.WithReference(perf)
|
||||
.WithReference(bffMulti)
|
||||
.WithReference(bffEf)
|
||||
.WithReference(bffBlazorPerComponent)
|
||||
|
|
@ -74,8 +83,6 @@ builder.AddProject<BffRemoteApi>(AppHostServices.TemplateBffRemote, launchProfil
|
|||
|
||||
builder.AddProject<BffBlazorAutoRenderMode>(AppHostServices.TemplateBffBlazor);
|
||||
|
||||
|
||||
|
||||
builder.Build().Run();
|
||||
|
||||
public static class Extensions
|
||||
|
|
|
|||
121
bff/hosts/Hosts.Bff.Performance/Config.cs
Normal file
121
bff/hosts/Hosts.Bff.Performance/Config.cs
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
|
||||
using Duende.IdentityModel;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Hosts.ServiceDefaults;
|
||||
|
||||
namespace IdentityServer;
|
||||
|
||||
public static class Config
|
||||
{
|
||||
public static IEnumerable<IdentityResource> IdentityResources =>
|
||||
[
|
||||
new IdentityResources.OpenId(),
|
||||
new IdentityResources.Profile(),
|
||||
];
|
||||
|
||||
public static IEnumerable<ApiScope> ApiScopes =>
|
||||
[
|
||||
new("api", ["name"]),
|
||||
new("scope-for-isolated-api", ["name"]),
|
||||
];
|
||||
|
||||
public static IEnumerable<ApiResource> ApiResources =>
|
||||
[
|
||||
new("urn:isolated-api", "isolated api")
|
||||
{
|
||||
RequireResourceIndicator = true,
|
||||
Scopes = { "scope-for-isolated-api" }
|
||||
}
|
||||
];
|
||||
// Get the BFF URL from the service discovery system. Then use this for building the redirect urls etc..
|
||||
private static Uri bffUrl = ServiceDiscovery.ResolveService(AppHostServices.Bff);
|
||||
private static Uri bffMultiFrontendUrl = ServiceDiscovery.ResolveService(AppHostServices.BffMultiFrontend);
|
||||
private static Uri bffDPopUrl = ServiceDiscovery.ResolveService(AppHostServices.BffDpop);
|
||||
private static Uri bffEfUrl = ServiceDiscovery.ResolveService(AppHostServices.BffEf);
|
||||
private static Uri bffBlazorPerComponentUrl = ServiceDiscovery.ResolveService(AppHostServices.BffBlazorPerComponent);
|
||||
private static Uri bffBlazorWebAssemblyUrl = ServiceDiscovery.ResolveService(AppHostServices.BffBlazorWebassembly); public static IEnumerable<Client> Clients =>
|
||||
[
|
||||
BuildClient("bff.perf",
|
||||
ServiceDiscovery.ResolveService(AppHostServices.BffPerf, "single"),
|
||||
ServiceDiscovery.ResolveService(AppHostServices.BffPerf, "multi"),
|
||||
new Uri("https://app1.localhost:6002")
|
||||
),
|
||||
|
||||
BuildClient("bff", client =>
|
||||
{
|
||||
client.AllowedScopes.Add("scope-for-isolated-api");
|
||||
}, bffUrl),
|
||||
|
||||
BuildClient("bff.multi-frontend.default", client =>
|
||||
{
|
||||
client.AllowedScopes.Add("scope-for-isolated-api");
|
||||
}, bffMultiFrontendUrl),
|
||||
|
||||
BuildClient("bff.multi-frontend.config", client =>
|
||||
{
|
||||
client.AllowedScopes.Add("scope-for-isolated-api");
|
||||
}, new Uri(bffMultiFrontendUrl, "from-config")),
|
||||
|
||||
BuildClient("bff.multi-frontend.with-path", client =>
|
||||
{
|
||||
client.AllowedScopes.Add("scope-for-isolated-api");
|
||||
}, new Uri(bffMultiFrontendUrl, "with-path")),
|
||||
|
||||
BuildClient("bff.multi-frontend.with-domain", client =>
|
||||
{
|
||||
client.AllowedScopes.Add("scope-for-isolated-api");
|
||||
}, new Uri("https://app1.localhost:5005")),
|
||||
|
||||
BuildClient("bff.dpop", client =>
|
||||
{
|
||||
client.RequireDPoP = true;
|
||||
client.AllowedScopes.Add("scope-for-isolated-api");
|
||||
}, bffDPopUrl),
|
||||
|
||||
BuildClient("bff.ef", client =>
|
||||
{
|
||||
client.BackChannelLogoutUri = $"{bffEfUrl}bff/backchannel";
|
||||
client.AllowedScopes.Add("scope-for-isolated-api");
|
||||
}, bffEfUrl),
|
||||
|
||||
BuildClient("blazor", client =>
|
||||
{
|
||||
client.AllowedScopes.Add("scope-for-isolated-api");
|
||||
}, bffBlazorWebAssemblyUrl, bffBlazorPerComponentUrl, new Uri("https://localhost:7035"))
|
||||
];
|
||||
|
||||
|
||||
private static Client BuildClient(string clientId, Action<Client> postConfigure, params Uri[] uris)
|
||||
{
|
||||
var client = BuildClient(clientId, uris);
|
||||
postConfigure(client);
|
||||
return client;
|
||||
}
|
||||
|
||||
private static Client BuildClient(string clientId, params Uri[] uris) => new Client
|
||||
{
|
||||
ClientId = clientId,
|
||||
ClientSecrets = { new Secret("secret".Sha256()) },
|
||||
|
||||
AllowedGrantTypes =
|
||||
{
|
||||
GrantType.AuthorizationCode,
|
||||
GrantType.ClientCredentials,
|
||||
OidcConstants.GrantTypes.TokenExchange
|
||||
},
|
||||
RedirectUris = uris.Select(u => new Uri(u, "signin-oidc").ToString()).ToList(),
|
||||
FrontChannelLogoutUri = new Uri(uris.First(), "signout-oidc").ToString(),
|
||||
PostLogoutRedirectUris = uris.Select(u => new Uri(u, "signout-callback-oidc").ToString()).ToList(),
|
||||
|
||||
AllowOfflineAccess = true,
|
||||
AllowedScopes = { "openid", "profile", "api" },
|
||||
|
||||
RefreshTokenExpiration = TokenExpiration.Absolute,
|
||||
AbsoluteRefreshTokenLifetime = 60,
|
||||
AccessTokenLifetime = 15 // Force refresh
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Hosts.Bff.Performance.Services;
|
||||
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
|
||||
|
||||
public static class DefaultOpenIdConfiguration
|
||||
{
|
||||
public static void Apply(OpenIdConnectOptions options, BffSettings settings)
|
||||
{
|
||||
options.Authority = settings.IdentityServerUrl.ToString();
|
||||
|
||||
// confidential client using code flow + PKCE
|
||||
options.ClientId = "bff.perf";
|
||||
options.ClientSecret = "secret";
|
||||
options.ResponseType = "code";
|
||||
options.ResponseMode = "query";
|
||||
|
||||
options.MapInboundClaims = false;
|
||||
options.GetClaimsFromUserInfoEndpoint = true;
|
||||
options.SaveTokens = true;
|
||||
|
||||
// request scopes + refresh tokens
|
||||
options.Scope.Clear();
|
||||
options.Scope.Add("openid");
|
||||
options.Scope.Add("profile");
|
||||
options.Scope.Add("api");
|
||||
options.Scope.Add("offline_access");
|
||||
}
|
||||
}
|
||||
19
bff/hosts/Hosts.Bff.Performance/Hosts.Bff.Performance.csproj
Normal file
19
bff/hosts/Hosts.Bff.Performance/Hosts.Bff.Performance.csproj
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Duende.IdentityServer" />
|
||||
<PackageReference Include="Duende.IdentityModel" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\bff\hosts\Hosts.ServiceDefaults\Hosts.ServiceDefaults.csproj" />
|
||||
<ProjectReference Include="..\..\src\Bff.Yarp\Bff.Yarp.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
33
bff/hosts/Hosts.Bff.Performance/Program.cs
Normal file
33
bff/hosts/Hosts.Bff.Performance/Program.cs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Hosts.Bff.Performance.Services;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder();
|
||||
|
||||
builder.Services.Configure<ApiSettings>(builder.Configuration);
|
||||
builder.Services.Configure<BffSettings>(builder.Configuration);
|
||||
builder.Services.Configure<IdentityServerSettings>(builder.Configuration);
|
||||
|
||||
builder.Services.AddHostedService<ApiHostedService>();
|
||||
builder.Services.AddHostedService<IdentityServerService>();
|
||||
builder.Services.AddHostedService<SingleFrontendBffService>();
|
||||
builder.Services.AddHostedService<MultiFrontendBffService>();
|
||||
// Add services to the container.
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
|
||||
// spin up multiple applications:
|
||||
// Plain yarp
|
||||
|
||||
|
||||
// single frontend
|
||||
// multi-frontend
|
||||
// bff with server side EF sessions
|
||||
|
||||
|
||||
|
||||
|
||||
app.Run();
|
||||
17
bff/hosts/Hosts.Bff.Performance/Properties/launchSettings.json
vendored
Normal file
17
bff/hosts/Hosts.Bff.Performance/Properties/launchSettings.json
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"Hosts.Bff.Performance": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"environmentVariables": {
|
||||
"ApiUrl": "https://localhost:6100",
|
||||
"IdentityServerUrl": "https://localhost:6001",
|
||||
"BffUrl1": "https://localhost:6002",
|
||||
"BffUrl2": "https://localhost:6003",
|
||||
"BffUrl3": "https://app1.localhost:6003"
|
||||
},
|
||||
"applicationUrl": "https://localhost:6100;https://localhost:6001;https://localhost:6002;https://localhost:6003;https://app1.localhost:6003"
|
||||
}
|
||||
}
|
||||
}
|
||||
28
bff/hosts/Hosts.Bff.Performance/Services/ApiHostedService.cs
Normal file
28
bff/hosts/Hosts.Bff.Performance/Services/ApiHostedService.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Hosts.Bff.Performance.Services;
|
||||
|
||||
public class ApiHostedService(IOptions<ApiSettings> apiSettings) : BackgroundService
|
||||
{
|
||||
public ApiSettings Settings { get; } = apiSettings.Value;
|
||||
|
||||
protected override Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
var builder = WebApplication.CreateBuilder();
|
||||
builder.AddServiceDefaults();
|
||||
|
||||
// Configure Kestrel to listen on the specified Uri
|
||||
builder.WebHost.UseUrls(Settings.ApiUrl.ToString());
|
||||
var app = builder.Build();
|
||||
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
app.MapGet("/", () => "ok");
|
||||
return app.RunAsync(stoppingToken);
|
||||
|
||||
}
|
||||
}
|
||||
10
bff/hosts/Hosts.Bff.Performance/Services/ApiSettings.cs
Normal file
10
bff/hosts/Hosts.Bff.Performance/Services/ApiSettings.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Hosts.Bff.Performance.Services;
|
||||
|
||||
public class ApiSettings
|
||||
{
|
||||
public required Uri ApiUrl { get; set; }
|
||||
public required Uri IdentityServerUrl { get; set; }
|
||||
}
|
||||
85
bff/hosts/Hosts.Bff.Performance/Services/BffService.cs
Normal file
85
bff/hosts/Hosts.Bff.Performance/Services/BffService.cs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
// 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.Yarp;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Hosts.Bff.Performance.Services;
|
||||
|
||||
public abstract class BffService(string[] urlConfigKeys, IConfiguration config, IOptions<BffSettings> bffSettings) : BackgroundService
|
||||
{
|
||||
public IConfiguration Config { get; } = config;
|
||||
public BffSettings Settings { get; } = bffSettings.Value;
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
var urls = urlConfigKeys
|
||||
.Select(x => Config[x])
|
||||
.OfType<string>()
|
||||
.ToArray();
|
||||
|
||||
var builder = WebApplication.CreateBuilder();
|
||||
builder.AddServiceDefaults();
|
||||
// Configure Kestrel to listen on the specified Uri
|
||||
builder.WebHost.UseUrls(urls);
|
||||
|
||||
builder.Services.AddAuthorization();
|
||||
ConfigureServices(builder.Services);
|
||||
|
||||
var bffBuilder = builder.Services.AddBff()
|
||||
.AddRemoteApis();
|
||||
|
||||
ConfigureBff(bffBuilder);
|
||||
|
||||
// Build and run the web app
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.UseBff();
|
||||
|
||||
ConfigureApp(app);
|
||||
|
||||
app.MapGet("/local_anon", () => DateTime.Now.ToString("s"))
|
||||
.AsBffApiEndpoint()
|
||||
.AllowAnonymous();
|
||||
|
||||
app.MapGet("/local", () => DateTime.Now.ToString("s"))
|
||||
.RequireAuthorization()
|
||||
.AsBffApiEndpoint();
|
||||
|
||||
app.MapRemoteBffApiEndpoint("/remote_anon", Settings.ApiUrl)
|
||||
.WithAccessToken(RequiredTokenType.None);
|
||||
|
||||
|
||||
app.MapRemoteBffApiEndpoint("/remote_user", Settings.ApiUrl)
|
||||
.WithAccessToken();
|
||||
|
||||
app.MapRemoteBffApiEndpoint("/remote_client", Settings.ApiUrl)
|
||||
.WithAccessToken(RequiredTokenType.Client);
|
||||
|
||||
// Todo: Make sure this is mapped implicitly
|
||||
app.MapBffManagementEndpoints();
|
||||
|
||||
await app.RunAsync(stoppingToken);
|
||||
}
|
||||
|
||||
public virtual void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void ConfigureBff(BffBuilder bff)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void ConfigureApp(WebApplication app)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Hosts.Bff.Performance.Services;
|
||||
|
||||
public class BffServiceSettings
|
||||
{
|
||||
public required string Uri { get; set; }
|
||||
|
||||
public Uri ApiUrl { get; } = new Uri("https://localhost:5999");
|
||||
}
|
||||
10
bff/hosts/Hosts.Bff.Performance/Services/BffSettings.cs
Normal file
10
bff/hosts/Hosts.Bff.Performance/Services/BffSettings.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Hosts.Bff.Performance.Services;
|
||||
|
||||
public class BffSettings
|
||||
{
|
||||
public required Uri IdentityServerUrl { get; set; }
|
||||
public required Uri ApiUrl { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using System.Security.Claims;
|
||||
using Duende.IdentityModel;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Duende.IdentityServer.Test;
|
||||
using IdentityServer;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Hosts.Bff.Performance.Services;
|
||||
|
||||
public class IdentityServerService(IOptions<IdentityServerSettings> settings, IConfiguration config) : BackgroundService
|
||||
{
|
||||
public IdentityServerSettings Settings { get; } = settings.Value;
|
||||
|
||||
protected override Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
var builder = WebApplication.CreateBuilder();
|
||||
builder.AddServiceDefaults();
|
||||
// Configure Kestrel to listen on the specified Uri
|
||||
builder.WebHost.UseUrls(Settings.IdentityServerUrl);
|
||||
|
||||
builder.Services.AddAuthorization();
|
||||
builder.Services.AddHttpLogging();
|
||||
|
||||
var isBuilder = builder.Services.AddIdentityServer(options =>
|
||||
{
|
||||
options.Events.RaiseErrorEvents = true;
|
||||
options.Events.RaiseInformationEvents = true;
|
||||
options.Events.RaiseFailureEvents = true;
|
||||
options.Events.RaiseSuccessEvents = true;
|
||||
|
||||
options.EmitStaticAudienceClaim = true;
|
||||
})
|
||||
.AddTestUsers([new TestUser()
|
||||
{
|
||||
SubjectId = "bob",
|
||||
Username = "bob",
|
||||
Password = "bob",
|
||||
Claims = [
|
||||
new Claim(JwtClaimTypes.Name, "Bob Smith"),
|
||||
new Claim(JwtClaimTypes.GivenName, "Bob"),
|
||||
new Claim(JwtClaimTypes.FamilyName, "Smith"),
|
||||
new Claim(JwtClaimTypes.Email, "bob@duende.com")
|
||||
],
|
||||
IsActive = true
|
||||
|
||||
}])
|
||||
;
|
||||
|
||||
// in-memory, code config
|
||||
isBuilder.AddInMemoryIdentityResources(Config.IdentityResources);
|
||||
isBuilder.AddInMemoryApiScopes(Config.ApiScopes);
|
||||
|
||||
var bffUrls = config.AsEnumerable()
|
||||
.Where(x => x.Key.StartsWith("BffUrl"))
|
||||
.Select(x => x.Value)
|
||||
.OfType<string>();
|
||||
|
||||
isBuilder.AddInMemoryClients([new Client
|
||||
{
|
||||
ClientId = "bff.perf",
|
||||
ClientSecrets = [new Secret("secret".Sha256())],
|
||||
RedirectUris = bffUrls.Select(x => x.TrimEnd('/') + "/signin-oidc").ToArray(),
|
||||
AllowOfflineAccess = true,
|
||||
AllowedScopes = { "openid", "profile", "api" },
|
||||
AllowedGrantTypes =
|
||||
{
|
||||
GrantType.AuthorizationCode,
|
||||
GrantType.ClientCredentials,
|
||||
OidcConstants.GrantTypes.TokenExchange
|
||||
},
|
||||
|
||||
RefreshTokenExpiration = TokenExpiration.Absolute,
|
||||
AbsoluteRefreshTokenLifetime = 60,
|
||||
AccessTokenLifetime = 15 // Force refresh
|
||||
|
||||
}]);
|
||||
isBuilder.AddInMemoryApiResources(Config.ApiResources);
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseHttpLogging();
|
||||
app.UseDeveloperExceptionPage();
|
||||
app.UseStaticFiles();
|
||||
|
||||
app.UseRouting();
|
||||
app.UseIdentityServer();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapGet("/", () => "identity server");
|
||||
|
||||
app.MapGet("/account/login", async ctx =>
|
||||
{
|
||||
var props = new AuthenticationProperties();
|
||||
await ctx.SignInAsync(new ClaimsPrincipal(new ClaimsIdentity(
|
||||
[
|
||||
new Claim(JwtClaimTypes.Subject, "bob"),
|
||||
new Claim(JwtClaimTypes.Name, "bob")
|
||||
],
|
||||
"login", "name", "role")), props);
|
||||
});
|
||||
|
||||
app.MapGet("/account/logout", async ctx =>
|
||||
{
|
||||
// signout as if the user were prompted
|
||||
await ctx.SignOutAsync();
|
||||
|
||||
var logoutId = ctx.Request.Query["logoutId"];
|
||||
var interaction = ctx.RequestServices.GetRequiredService<IIdentityServerInteractionService>();
|
||||
|
||||
var signOutContext = await interaction.GetLogoutContextAsync(logoutId);
|
||||
|
||||
ctx.Response.Redirect(signOutContext.PostLogoutRedirectUri ?? "/");
|
||||
});
|
||||
|
||||
return app.RunAsync(stoppingToken);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Hosts.Bff.Performance.Services;
|
||||
|
||||
public class IdentityServerSettings
|
||||
{
|
||||
public required string IdentityServerUrl { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.Bff;
|
||||
using Duende.Bff.DynamicFrontends;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Hosts.Bff.Performance.Services;
|
||||
|
||||
public class MultiFrontendBffService(IConfiguration config, IOptions<BffSettings> settings) : BffService(["BffUrl2", "BffUrl3"], config, settings)
|
||||
{
|
||||
public override void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
}
|
||||
|
||||
public override void ConfigureBff(BffBuilder bff) => bff.WithDefaultOpenIdConnectOptions(o => DefaultOpenIdConfiguration.Apply(o, Settings))
|
||||
.AddFrontends(new BffFrontend(BffFrontendName.Parse("default")))
|
||||
|
||||
// Note, in order for this to work, we'll need to inject this as config
|
||||
.AddFrontends(new BffFrontend(BffFrontendName.Parse("app1")).MappedToOrigin(Config.GetValue<Origin>("BffUrl3") ?? throw new InvalidOperationException("BFFUrl3 is null")));
|
||||
|
||||
public override void ConfigureApp(WebApplication app) => app.MapGet("/", (SelectedFrontend selectedFrontend) => "multi - " + selectedFrontend.Get().Name);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.Bff;
|
||||
using Duende.Bff.DynamicFrontends;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Hosts.Bff.Performance.Services;
|
||||
|
||||
public class SingleFrontendBffService(IConfiguration config, IOptions<BffSettings> settings) : BffService(["BffUrl1"], config, settings)
|
||||
{
|
||||
public override void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
}
|
||||
|
||||
public override void ConfigureBff(BffBuilder bff) => bff.WithDefaultOpenIdConnectOptions(o => DefaultOpenIdConfiguration.Apply(o, Settings))
|
||||
.AddFrontends(new BffFrontend(BffFrontendName.Parse("default")));
|
||||
|
||||
public override void ConfigureApp(WebApplication app) => app.MapGet("/", () => "single");
|
||||
}
|
||||
8
bff/hosts/Hosts.Bff.Performance/appsettings.Development.json
vendored
Normal file
8
bff/hosts/Hosts.Bff.Performance/appsettings.Development.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
bff/hosts/Hosts.Bff.Performance/appsettings.json
vendored
Normal file
9
bff/hosts/Hosts.Bff.Performance/appsettings.json
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
|
@ -36,203 +36,86 @@ public static class Config
|
|||
private static Uri bffDPopUrl = ServiceDiscovery.ResolveService(AppHostServices.BffDpop);
|
||||
private static Uri bffEfUrl = ServiceDiscovery.ResolveService(AppHostServices.BffEf);
|
||||
private static Uri bffBlazorPerComponentUrl = ServiceDiscovery.ResolveService(AppHostServices.BffBlazorPerComponent);
|
||||
private static Uri bffBlazorWebAssemblyUrl = ServiceDiscovery.ResolveService(AppHostServices.BffBlazorWebassembly);
|
||||
|
||||
public static IEnumerable<Client> Clients =>
|
||||
private static Uri bffBlazorWebAssemblyUrl = ServiceDiscovery.ResolveService(AppHostServices.BffBlazorWebassembly); public static IEnumerable<Client> Clients =>
|
||||
[
|
||||
new Client
|
||||
{
|
||||
ClientId = "bff",
|
||||
ClientSecrets = { new Secret("secret".Sha256()) },
|
||||
BuildClient("bff.perf",
|
||||
ServiceDiscovery.ResolveService(AppHostServices.BffPerf, "single"),
|
||||
ServiceDiscovery.ResolveService(AppHostServices.BffPerf, "multi"),
|
||||
new Uri("https://app1.localhost:6002")
|
||||
),
|
||||
|
||||
AllowedGrantTypes =
|
||||
{
|
||||
GrantType.AuthorizationCode,
|
||||
GrantType.ClientCredentials,
|
||||
OidcConstants.GrantTypes.TokenExchange
|
||||
},
|
||||
BuildClient("bff", client =>
|
||||
{
|
||||
client.AllowedScopes.Add("scope-for-isolated-api");
|
||||
}, bffUrl),
|
||||
|
||||
RedirectUris = { $"{bffUrl}signin-oidc" },
|
||||
FrontChannelLogoutUri = $"{bffUrl}signout-oidc",
|
||||
PostLogoutRedirectUris = { $"{bffUrl}signout-callback-oidc" },
|
||||
BuildClient("bff.multi-frontend.default", client =>
|
||||
{
|
||||
client.AllowedScopes.Add("scope-for-isolated-api");
|
||||
}, bffMultiFrontendUrl),
|
||||
|
||||
AllowOfflineAccess = true,
|
||||
AllowedScopes = { "openid", "profile", "api", "scope-for-isolated-api" },
|
||||
BuildClient("bff.multi-frontend.config", client =>
|
||||
{
|
||||
client.AllowedScopes.Add("scope-for-isolated-api");
|
||||
}, new Uri(bffMultiFrontendUrl, "from-config")),
|
||||
|
||||
RefreshTokenExpiration = TokenExpiration.Absolute,
|
||||
AbsoluteRefreshTokenLifetime = 60,
|
||||
AccessTokenLifetime = 15 // Force refresh
|
||||
},
|
||||
new Client
|
||||
{
|
||||
ClientId = "bff.multi-frontend.default",
|
||||
ClientSecrets = { new Secret("secret".Sha256()) },
|
||||
BuildClient("bff.multi-frontend.with-path", client =>
|
||||
{
|
||||
client.AllowedScopes.Add("scope-for-isolated-api");
|
||||
}, new Uri(bffMultiFrontendUrl, "with-path")),
|
||||
|
||||
AllowedGrantTypes =
|
||||
{
|
||||
GrantType.AuthorizationCode,
|
||||
GrantType.ClientCredentials,
|
||||
OidcConstants.GrantTypes.TokenExchange
|
||||
},
|
||||
RedirectUris = { $"{bffMultiFrontendUrl}signin-oidc" },
|
||||
FrontChannelLogoutUri = $"{bffMultiFrontendUrl}signout-oidc",
|
||||
PostLogoutRedirectUris = { $"{bffMultiFrontendUrl}signout-callback-oidc" },
|
||||
BuildClient("bff.multi-frontend.with-domain", client =>
|
||||
{
|
||||
client.AllowedScopes.Add("scope-for-isolated-api");
|
||||
}, new Uri("https://app1.localhost:5005")),
|
||||
|
||||
AllowOfflineAccess = true,
|
||||
AllowedScopes = { "openid", "profile", "api", "scope-for-isolated-api" },
|
||||
BuildClient("bff.dpop", client =>
|
||||
{
|
||||
client.RequireDPoP = true;
|
||||
client.AllowedScopes.Add("scope-for-isolated-api");
|
||||
}, bffDPopUrl),
|
||||
|
||||
RefreshTokenExpiration = TokenExpiration.Absolute,
|
||||
AbsoluteRefreshTokenLifetime = 60,
|
||||
AccessTokenLifetime = 15 // Force refresh
|
||||
},
|
||||
new Client
|
||||
{
|
||||
ClientId = "bff.multi-frontend.config",
|
||||
ClientSecrets = { new Secret("secret".Sha256()) },
|
||||
BuildClient("bff.ef", client =>
|
||||
{
|
||||
client.BackChannelLogoutUri = $"{bffEfUrl}bff/backchannel";
|
||||
client.AllowedScopes.Add("scope-for-isolated-api");
|
||||
}, bffEfUrl),
|
||||
|
||||
AllowedGrantTypes =
|
||||
{
|
||||
GrantType.AuthorizationCode,
|
||||
GrantType.ClientCredentials,
|
||||
OidcConstants.GrantTypes.TokenExchange
|
||||
},
|
||||
RedirectUris = { $"{bffMultiFrontendUrl}from-config/signin-oidc" },
|
||||
FrontChannelLogoutUri = $"{bffMultiFrontendUrl}from-config/signout-oidc",
|
||||
PostLogoutRedirectUris = { $"{bffMultiFrontendUrl}from-config/signout-callback-oidc" },
|
||||
BuildClient("blazor", client =>
|
||||
{
|
||||
client.AllowedScopes.Add("scope-for-isolated-api");
|
||||
}, bffBlazorWebAssemblyUrl, bffBlazorPerComponentUrl, new Uri("https://localhost:7035"))
|
||||
];
|
||||
|
||||
AllowOfflineAccess = true,
|
||||
AllowedScopes = { "openid", "profile", "api", "scope-for-isolated-api" },
|
||||
|
||||
RefreshTokenExpiration = TokenExpiration.Absolute,
|
||||
AbsoluteRefreshTokenLifetime = 60,
|
||||
AccessTokenLifetime = 15 // Force refresh
|
||||
},
|
||||
new Client
|
||||
{
|
||||
ClientId = "bff.multi-frontend.with-path",
|
||||
ClientSecrets = { new Secret("secret".Sha256()) },
|
||||
private static Client BuildClient(string clientId, Action<Client> postConfigure, params Uri[] uris)
|
||||
{
|
||||
var client = BuildClient(clientId, uris);
|
||||
postConfigure(client);
|
||||
return client;
|
||||
}
|
||||
|
||||
AllowedGrantTypes =
|
||||
{
|
||||
GrantType.AuthorizationCode,
|
||||
GrantType.ClientCredentials,
|
||||
OidcConstants.GrantTypes.TokenExchange
|
||||
},
|
||||
RedirectUris = { $"{bffMultiFrontendUrl}with-path/signin-oidc" },
|
||||
FrontChannelLogoutUri = $"{bffMultiFrontendUrl}signout-oidc",
|
||||
PostLogoutRedirectUris = { $"{bffMultiFrontendUrl}signout-callback-oidc" },
|
||||
private static Client BuildClient(string clientId, params Uri[] uris) => new Client
|
||||
{
|
||||
ClientId = clientId,
|
||||
ClientSecrets = { new Secret("secret".Sha256()) },
|
||||
|
||||
AllowOfflineAccess = true,
|
||||
AllowedScopes = { "openid", "profile", "api", "scope-for-isolated-api" },
|
||||
AllowedGrantTypes =
|
||||
{
|
||||
GrantType.AuthorizationCode,
|
||||
GrantType.ClientCredentials,
|
||||
OidcConstants.GrantTypes.TokenExchange
|
||||
},
|
||||
RedirectUris = uris.Select(u => new Uri(u, "signin-oidc").ToString()).ToList(),
|
||||
FrontChannelLogoutUri = new Uri(uris.First(), "signout-oidc").ToString(),
|
||||
PostLogoutRedirectUris = uris.Select(u => new Uri(u, "signout-callback-oidc").ToString()).ToList(),
|
||||
|
||||
RefreshTokenExpiration = TokenExpiration.Absolute,
|
||||
AbsoluteRefreshTokenLifetime = 60,
|
||||
AccessTokenLifetime = 15 // Force refresh
|
||||
},
|
||||
new Client
|
||||
{
|
||||
ClientId = "bff.multi-frontend.with-domain",
|
||||
ClientSecrets = { new Secret("secret".Sha256()) },
|
||||
|
||||
AllowedGrantTypes =
|
||||
{
|
||||
GrantType.AuthorizationCode,
|
||||
GrantType.ClientCredentials,
|
||||
OidcConstants.GrantTypes.TokenExchange
|
||||
},
|
||||
RedirectUris = { $"https://app1.localhost:5005/signin-oidc" },
|
||||
FrontChannelLogoutUri = $"https://app1.localhost:5005/signout-oidc",
|
||||
PostLogoutRedirectUris = { $"https://app1.localhost:5005/signout-callback-oidc" },
|
||||
|
||||
AllowOfflineAccess = true,
|
||||
AllowedScopes = { "openid", "profile", "api", "scope-for-isolated-api" },
|
||||
|
||||
RefreshTokenExpiration = TokenExpiration.Absolute,
|
||||
AbsoluteRefreshTokenLifetime = 60,
|
||||
AccessTokenLifetime = 15 // Force refresh
|
||||
},
|
||||
new Client
|
||||
{
|
||||
ClientId = "bff.dpop",
|
||||
ClientSecrets = { new Secret("secret".Sha256()) },
|
||||
RequireDPoP = true,
|
||||
|
||||
AllowedGrantTypes =
|
||||
{
|
||||
GrantType.AuthorizationCode,
|
||||
GrantType.ClientCredentials,
|
||||
OidcConstants.GrantTypes.TokenExchange
|
||||
},
|
||||
|
||||
RedirectUris = { $"{bffDPopUrl}signin-oidc" },
|
||||
FrontChannelLogoutUri = $"{bffDPopUrl}signout-oidc",
|
||||
PostLogoutRedirectUris = { $"{bffDPopUrl}signout-callback-oidc" },
|
||||
|
||||
AllowOfflineAccess = true,
|
||||
AllowedScopes = { "openid", "profile", "api", "scope-for-isolated-api" },
|
||||
|
||||
// Intentionally set lifetime short to see what happens when access and refresh tokens expire
|
||||
AccessTokenLifetime = 15,
|
||||
RefreshTokenExpiration = TokenExpiration.Absolute,
|
||||
AbsoluteRefreshTokenLifetime = 60
|
||||
},
|
||||
new Client
|
||||
{
|
||||
ClientId = "bff.ef",
|
||||
ClientSecrets = { new Secret("secret".Sha256()) },
|
||||
|
||||
AllowedGrantTypes =
|
||||
{
|
||||
GrantType.AuthorizationCode,
|
||||
GrantType.ClientCredentials,
|
||||
OidcConstants.GrantTypes.TokenExchange
|
||||
},
|
||||
RedirectUris = { $"{bffEfUrl}signin-oidc" },
|
||||
FrontChannelLogoutUri = $"{bffEfUrl}signout-oidc",
|
||||
BackChannelLogoutUri = $"{bffEfUrl}bff/backchannel",
|
||||
PostLogoutRedirectUris = { $"{bffEfUrl}signout-callback-oidc" },
|
||||
|
||||
AllowOfflineAccess = true,
|
||||
AllowedScopes = { "openid", "profile", "api", "scope-for-isolated-api" },
|
||||
|
||||
// Intentionally set lifetime short to see what happens when access and refresh tokens expire
|
||||
AccessTokenLifetime = 15,
|
||||
RefreshTokenExpiration = TokenExpiration.Absolute,
|
||||
AbsoluteRefreshTokenLifetime = 60
|
||||
},
|
||||
|
||||
new Client
|
||||
{
|
||||
ClientId = "blazor",
|
||||
ClientSecrets = { new Secret("secret".Sha256()) },
|
||||
|
||||
AllowedGrantTypes =
|
||||
{
|
||||
GrantType.AuthorizationCode,
|
||||
GrantType.ClientCredentials,
|
||||
OidcConstants.GrantTypes.TokenExchange
|
||||
},
|
||||
|
||||
RedirectUris =
|
||||
{
|
||||
$"{bffBlazorWebAssemblyUrl}signin-oidc",
|
||||
$"{bffBlazorPerComponentUrl}signin-oidc",
|
||||
"https://localhost:7035/signin-oidc"
|
||||
},
|
||||
PostLogoutRedirectUris =
|
||||
{
|
||||
$"{bffBlazorWebAssemblyUrl}signout-callback-oidc", $"{bffBlazorPerComponentUrl}signout-callback-oidc"
|
||||
},
|
||||
|
||||
AllowOfflineAccess = true,
|
||||
AllowedScopes = { "openid", "profile", "api", "scope-for-isolated-api" },
|
||||
|
||||
// Intentionally set lifetime short to see what happens when access and refresh tokens expire
|
||||
AccessTokenLifetime = 15,
|
||||
RefreshTokenExpiration = TokenExpiration.Absolute,
|
||||
AbsoluteRefreshTokenLifetime = 60
|
||||
}
|
||||
];
|
||||
AllowOfflineAccess = true,
|
||||
AllowedScopes = { "openid", "profile", "api" },
|
||||
|
||||
RefreshTokenExpiration = TokenExpiration.Absolute,
|
||||
AbsoluteRefreshTokenLifetime = 60,
|
||||
AccessTokenLifetime = 15 // Force refresh
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
namespace Hosts.ServiceDefaults;
|
||||
public static class AppHostServices
|
||||
{
|
||||
public const string BffPerf = "bff-perf";
|
||||
public const string IdentityServer = "identity-server";
|
||||
public const string Api = "api";
|
||||
public const string IsolatedApi = "api-isolated";
|
||||
|
|
@ -24,6 +25,7 @@ public static class AppHostServices
|
|||
Api,
|
||||
IsolatedApi,
|
||||
Bff,
|
||||
BffPerf,
|
||||
BffEf,
|
||||
BffBlazorWebassembly,
|
||||
BffBlazorPerComponent,
|
||||
|
|
@ -39,13 +41,12 @@ public static class AppHostServices
|
|||
|
||||
public static class ServiceDiscovery
|
||||
{
|
||||
public static Uri ResolveService(string serviceName)
|
||||
public static Uri ResolveService(string serviceName, string appName = "https")
|
||||
{
|
||||
var scheme = "https";
|
||||
var host = serviceName;
|
||||
|
||||
// Compose the environment variable key
|
||||
var envVarKey = $"services__{host}__{scheme}__0";
|
||||
var envVarKey = $"services__{host}__{appName}__0";
|
||||
|
||||
// Try to get the value from environment variables
|
||||
var value = Environment.GetEnvironmentVariable(envVarKey);
|
||||
|
|
|
|||
18
bff/performance/Bff.Performance/Bff.Performance.csproj
Normal file
18
bff/performance/Bff.Performance/Bff.Performance.csproj
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Net.Http.Headers" />
|
||||
<PackageReference Include="NBomber" />
|
||||
<PackageReference Include="NBomber.Http" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
53
bff/performance/Bff.Performance/Program.cs
Normal file
53
bff/performance/Bff.Performance/Program.cs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
// full login / logout flow
|
||||
|
||||
// Api calls:
|
||||
|
||||
// plain yarp call
|
||||
// Local api
|
||||
// multi-frontend index.html response
|
||||
// remote api user token
|
||||
// - yarp
|
||||
// - plain
|
||||
// - single multi-frontend
|
||||
// - 100 multi-frontends
|
||||
// - server side sessions
|
||||
// - large response payload
|
||||
|
||||
// remote api client credentials token
|
||||
// - yarp
|
||||
// - plain
|
||||
// - single multi-frontend
|
||||
// - 100 multi-frontends
|
||||
// - large response payload
|
||||
|
||||
// with distributed cache + short lifecycle
|
||||
|
||||
using Bff.Performance.Scenarios.Bff;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using NBomber.Contracts.Stats;
|
||||
using NBomber.CSharp;
|
||||
using NBomber.Http;
|
||||
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddEnvironmentVariables()
|
||||
.AddCommandLine(args)
|
||||
.AddJsonFile("appsettings.json", optional: true)
|
||||
.Build();
|
||||
|
||||
var urls = config.GetSection("BffUrls").Get<Uri[]>();
|
||||
|
||||
if (urls == null || urls.Length == 0)
|
||||
{
|
||||
throw new InvalidOperationException("BffUrls configuration is missing or empty.");
|
||||
}
|
||||
NBomberRunner
|
||||
.RegisterScenarios(new BffScenarios(urls).Scenarios)
|
||||
.WithWorkerPlugins(new HttpMetricsPlugin())
|
||||
.WithReportingInterval(TimeSpan.FromSeconds(5))
|
||||
.WithReportFormats(
|
||||
ReportFormat.Csv, ReportFormat.Html
|
||||
)
|
||||
.Run(args);
|
||||
11
bff/performance/Bff.Performance/Properties/launchSettings.json
vendored
Normal file
11
bff/performance/Bff.Performance/Properties/launchSettings.json
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"profiles": {
|
||||
"Bff.Performance": {
|
||||
"commandName": "Project",
|
||||
"environmentVariables": {
|
||||
"BffUrls__0": "https://localhost:6002",
|
||||
"BffUrls__1": "https://localhost:6003"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
45
bff/performance/Bff.Performance/Scenarios/BaseScenario.cs
Normal file
45
bff/performance/Bff.Performance/Scenarios/BaseScenario.cs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using System.Net;
|
||||
using NBomber.Contracts;
|
||||
using NBomber.CSharp;
|
||||
|
||||
|
||||
|
||||
namespace Bff.Performance.Scenarios;
|
||||
|
||||
public abstract class BaseScenario(string name)
|
||||
{
|
||||
public virtual Task Init(IScenarioInitContext c) => Task.CompletedTask;
|
||||
public string Name => GetType().Name + "_" + name;
|
||||
|
||||
public HttpStatusCode SuccessStatusCode { get; set; } = HttpStatusCode.OK;
|
||||
|
||||
public async Task<IResponse> Run(IScenarioContext context)
|
||||
{
|
||||
var result = await RunScenario(context);
|
||||
|
||||
if (result.StatusCode == SuccessStatusCode)
|
||||
{
|
||||
return Response.Ok(result.StatusCode, result.StatusCode.ToString(), result.Content.Headers.ContentLength ?? 0);
|
||||
}
|
||||
return Response.Fail(result.StatusCode, result.StatusCode.ToString(), "Returned an unexpected httpresult", result.Content.Headers.ContentLength ?? 0);
|
||||
}
|
||||
|
||||
public abstract Task<HttpResponseMessage> RunScenario(IScenarioContext context);
|
||||
|
||||
public TestClient Client { get; set; } = null!;
|
||||
|
||||
public static implicit operator ScenarioProps(BaseScenario scenario) => Scenario.Create(scenario.Name, scenario.Run)
|
||||
.WithoutWarmUp()
|
||||
.WithLoadSimulations(
|
||||
Simulation.Inject(rate: 10,
|
||||
interval: TimeSpan.FromSeconds(1),
|
||||
during: TimeSpan.FromSeconds(30))
|
||||
)
|
||||
.WithInit(scenario.Init);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using NBomber.Contracts;
|
||||
|
||||
namespace Bff.Performance.Scenarios.Bff;
|
||||
|
||||
public class BffScenarios
|
||||
{
|
||||
public ScenarioProps[] Scenarios;
|
||||
|
||||
public BffScenarios(Uri[] baseUris) => Scenarios = baseUris.SelectMany(x => new ScenarioProps[]
|
||||
{
|
||||
new CallAnonymousLocalApi(x),
|
||||
new CallAuthorizedLocalApi(x)
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using NBomber.Contracts;
|
||||
|
||||
namespace Bff.Performance.Scenarios.Bff;
|
||||
|
||||
public class CallAnonymousLocalApi(Uri baseUri) : BaseScenario(baseUri.ToString())
|
||||
{
|
||||
public override Task Init(IScenarioInitContext c)
|
||||
{
|
||||
Client = TestClient.Create(baseUri);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override async Task<HttpResponseMessage> RunScenario(IScenarioContext context) => await Client.GetAsync("/local_anon");
|
||||
}
|
||||
|
||||
public class CallAuthorizedLocalApi(Uri baseUri) : BaseScenario(baseUri.ToString())
|
||||
{
|
||||
public override async Task Init(IScenarioInitContext c)
|
||||
{
|
||||
Client = TestClient.Create(baseUri);
|
||||
await Client.TriggerLogin();
|
||||
}
|
||||
|
||||
public override async Task<HttpResponseMessage> RunScenario(IScenarioContext context)
|
||||
{
|
||||
var result = await Client.GetAsync("/local");
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
124
bff/performance/Bff.Performance/TestClient.cs
Normal file
124
bff/performance/Bff.Performance/TestClient.cs
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Bff.Performance.TestInfra;
|
||||
|
||||
namespace Bff.Performance;
|
||||
|
||||
public class TestClient(Uri baseAddress, CookieHandler cookies, HttpMessageHandler handler)
|
||||
{
|
||||
|
||||
public HttpClient Client = new(handler)
|
||||
{
|
||||
BaseAddress = baseAddress
|
||||
};
|
||||
public void ClearCookies() => cookies.ClearCookies();
|
||||
|
||||
public static TestClient Create(Uri baseAddress, CookieContainer? cookies = null)
|
||||
{
|
||||
var inner = new SocketsHttpHandler
|
||||
{
|
||||
// We need to disable cookies and follow redirects
|
||||
// because we do this manually (see below).
|
||||
UseCookies = false,
|
||||
AllowAutoRedirect = false
|
||||
};
|
||||
|
||||
var cookieHandler = new CookieHandler(inner, cookies);
|
||||
var handler = new AutoFollowRedirectHandler((_) => { })
|
||||
{
|
||||
InnerHandler = cookieHandler
|
||||
};
|
||||
|
||||
return new TestClient(baseAddress, cookieHandler, handler);
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> GetAsync(string path, Dictionary<string, string>? headers = null, CancellationToken ct = default)
|
||||
{
|
||||
var request = BuildRequest(HttpMethod.Get, path, headers);
|
||||
|
||||
return Client.SendAsync(request);
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> PostAsync(string path, object? body, Dictionary<string, string>? headers = null, CancellationToken ct = default)
|
||||
{
|
||||
var request = BuildRequest(HttpMethod.Post, path, headers);
|
||||
request.Content = JsonContent.Create(body);
|
||||
|
||||
return Client.SendAsync(request);
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> PostAsync<T>(Uri path, T body, Dictionary<string, string>? headers = null, CancellationToken ct = default)
|
||||
where T : HttpContent
|
||||
{
|
||||
var request = BuildRequest(HttpMethod.Post, path.ToString(), headers);
|
||||
request.Content = body;
|
||||
|
||||
return Client.SendAsync(request);
|
||||
}
|
||||
|
||||
private static HttpRequestMessage BuildRequest(HttpMethod httpMethod, string path,
|
||||
Dictionary<string, string>? headers = null)
|
||||
{
|
||||
var request = new HttpRequestMessage(httpMethod, path);
|
||||
|
||||
if (headers == null)
|
||||
{
|
||||
request.Headers.Add("x-csrf", "1");
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var header in headers)
|
||||
{
|
||||
request.Headers.Add(header.Key, header.Value);
|
||||
}
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
|
||||
public async Task<HttpResponseMessage> TriggerLogin(string userName = "alice", string password = "alice", CancellationToken ct = default) => await GetAsync("/bff/login");
|
||||
|
||||
public async Task<HttpResponseMessage> TriggerLogout()
|
||||
{
|
||||
// To trigger a logout, we need the logout claim
|
||||
var userClaims = await GetUserClaims();
|
||||
|
||||
var logoutLink = userClaims.FirstOrDefault(x => x.Type == "bff:logout_url")
|
||||
?? throw new InvalidOperationException("Failed to find logout link claim");
|
||||
|
||||
return await GetAsync(logoutLink.Value.ToString()!);
|
||||
}
|
||||
|
||||
public async Task<UserClaim[]> GetUserClaims()
|
||||
{
|
||||
var userClaimsString = await GetStringAsync("/bff/user");
|
||||
var userClaims = JsonSerializer.Deserialize<UserClaim[]>(userClaimsString, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
})!;
|
||||
return userClaims;
|
||||
}
|
||||
|
||||
private async Task<string> GetStringAsync(string path)
|
||||
{
|
||||
var response = await GetAsync(path);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to get string from {path}. Status code: {response.StatusCode}");
|
||||
}
|
||||
return await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
|
||||
public async Task<HttpResponseMessage> InvokeApi(string url) => await GetAsync(url);
|
||||
|
||||
public record UserClaim
|
||||
{
|
||||
public required string Type { get; init; }
|
||||
public required object Value { get; init; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using System.Net;
|
||||
|
||||
namespace Bff.Performance.TestInfra;
|
||||
|
||||
public class AutoFollowRedirectHandler(Action<string> writeOutput) : DelegatingHandler
|
||||
{
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var previousUri = request.RequestUri;
|
||||
for (var i = 0; i < 20; i++)
|
||||
{
|
||||
var result = await base.SendAsync(request, cancellationToken);
|
||||
if ((result.StatusCode == HttpStatusCode.Found || result.StatusCode == HttpStatusCode.RedirectKeepVerb) && result.Headers.Location != null)
|
||||
{
|
||||
writeOutput($"Redirecting from {previousUri} to {result.Headers.Location}");
|
||||
|
||||
var newUri = result.Headers.Location;
|
||||
if (!newUri.IsAbsoluteUri)
|
||||
{
|
||||
newUri = new Uri(previousUri!, newUri);
|
||||
}
|
||||
|
||||
var headers = request.Headers;
|
||||
request = new HttpRequestMessage(HttpMethod.Get, newUri);
|
||||
foreach (var header in headers)
|
||||
{
|
||||
request.Headers.Add(header.Key, header.Value);
|
||||
}
|
||||
|
||||
previousUri = request.RequestUri;
|
||||
continue;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Keeps redirecting forever");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Bff.Performance.TestInfra;
|
||||
|
||||
public class CloningHttpMessageHandler(HttpClient innerHttpClient) : HttpMessageHandler
|
||||
{
|
||||
private readonly HttpClient _innerHttpClient =
|
||||
innerHttpClient ?? throw new ArgumentNullException(nameof(innerHttpClient));
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Clone the incoming request
|
||||
var clonedRequest = await CloneHttpRequestMessageAsync(request);
|
||||
|
||||
// Send the cloned request using the inner HttpClient
|
||||
var response = await _innerHttpClient.SendAsync(clonedRequest, cancellationToken);
|
||||
|
||||
// Clone the response and return it
|
||||
return await CloneHttpResponseMessageAsync(response);
|
||||
}
|
||||
|
||||
private async Task<HttpRequestMessage> CloneHttpRequestMessageAsync(HttpRequestMessage original)
|
||||
{
|
||||
var cloned = new HttpRequestMessage(original.Method, original.RequestUri)
|
||||
{
|
||||
Version = original.Version
|
||||
};
|
||||
|
||||
// Copy the content if present
|
||||
if (original.Content != null)
|
||||
{
|
||||
//var memoryStream = new MemoryStream();
|
||||
//await original.Content.CopyToAsync(memoryStream);
|
||||
//memoryStream.Position = 0;
|
||||
//cloned.Content = new StreamContent(memoryStream);
|
||||
cloned.Content = new StreamContent(await original.Content.ReadAsStreamAsync());
|
||||
|
||||
// Copy headers from the original content to the cloned content
|
||||
foreach (var header in original.Content.Headers)
|
||||
{
|
||||
cloned.Content.Headers.Add(header.Key, header.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy headers
|
||||
foreach (var header in original.Headers)
|
||||
{
|
||||
cloned.Headers.Add(header.Key, header.Value);
|
||||
}
|
||||
|
||||
return cloned;
|
||||
}
|
||||
|
||||
private async Task<HttpResponseMessage> CloneHttpResponseMessageAsync(HttpResponseMessage original)
|
||||
{
|
||||
var cloned = new HttpResponseMessage(original.StatusCode)
|
||||
{
|
||||
Version = original.Version,
|
||||
ReasonPhrase = original.ReasonPhrase,
|
||||
RequestMessage = original.RequestMessage
|
||||
};
|
||||
|
||||
cloned.Content = new StreamContent(await original.Content.ReadAsStreamAsync());
|
||||
|
||||
// Copy headers from the original content to the cloned content
|
||||
foreach (var header in original.Content.Headers)
|
||||
{
|
||||
cloned.Content.Headers.Add(header.Key, header.Value);
|
||||
}
|
||||
|
||||
// Copy headers
|
||||
foreach (var header in original.Headers)
|
||||
{
|
||||
cloned.Headers.Add(header.Key, header.Value);
|
||||
}
|
||||
|
||||
return cloned;
|
||||
}
|
||||
}
|
||||
43
bff/performance/Bff.Performance/TestInfra/CookieHandler.cs
Normal file
43
bff/performance/Bff.Performance/TestInfra/CookieHandler.cs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using System.Net;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Bff.Performance.TestInfra;
|
||||
|
||||
public class CookieHandler(HttpMessageHandler innerHandler, CookieContainer? cookies = null) : DelegatingHandler(innerHandler)
|
||||
{
|
||||
public void ClearCookies() => CookieContainer = new CookieContainer();
|
||||
public CookieContainer CookieContainer { get; private set; } = cookies ?? new CookieContainer();
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken ct)
|
||||
{
|
||||
var requestUri = request.RequestUri;
|
||||
var header = CookieContainer.GetCookieHeader(requestUri!);
|
||||
if (!string.IsNullOrEmpty(header))
|
||||
{
|
||||
request.Headers.Add(HeaderNames.Cookie, header);
|
||||
}
|
||||
|
||||
var response = await base.SendAsync(request, ct);
|
||||
|
||||
if (response.Headers.TryGetValues(HeaderNames.SetCookie, out var setCookieHeaders))
|
||||
{
|
||||
foreach (var cookieHeader in SetCookieHeaderValue.ParseList(setCookieHeaders.ToList()))
|
||||
{
|
||||
var cookie = new Cookie(cookieHeader.Name.Value!,
|
||||
cookieHeader.Value.Value,
|
||||
cookieHeader.Path.Value);
|
||||
if (cookieHeader.Expires.HasValue)
|
||||
{
|
||||
cookie.Expires = cookieHeader.Expires.Value.UtcDateTime;
|
||||
}
|
||||
|
||||
CookieContainer.Add(requestUri!, cookie);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bff.Performance.TestInfra;
|
||||
|
||||
public class RequestLoggingHandler(
|
||||
ILogger<RequestLoggingHandler> log,
|
||||
Func<HttpRequestMessage, bool> shouldLog)
|
||||
: DelegatingHandler
|
||||
{
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!shouldLog(request))
|
||||
{
|
||||
return await base.SendAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
var result = await base.SendAsync(request, cancellationToken);
|
||||
|
||||
log.LogInformation("Executing {method} on {url} returned {statuscode} in {ms} ms",
|
||||
request.Method,
|
||||
request.RequestUri,
|
||||
result.StatusCode,
|
||||
stopwatch.ElapsedMilliseconds);
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
log.LogWarning("Executing {method} on {url} was cancelled in {ms} ms",
|
||||
request.Method,
|
||||
request.RequestUri,
|
||||
stopwatch.ElapsedMilliseconds);
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.LogWarning(ex,
|
||||
"Exception while executing {method} on {url} in {ms} ms",
|
||||
request.Method,
|
||||
request.RequestUri,
|
||||
stopwatch.ElapsedMilliseconds);
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
<Folder Name="/bff/" />
|
||||
<Folder Name="/bff/performance/">
|
||||
<Project Path="bff/performance/Bff.Benchmarks/Bff.Benchmarks.csproj" />
|
||||
<Project Path="bff/performance/Bff.Performance/Bff.Performance.csproj"/>
|
||||
</Folder>
|
||||
<Folder Name="/bff/hosts/">
|
||||
<Project Path="bff/hosts/Hosts.AppHost/Hosts.AppHost.csproj" />
|
||||
|
|
@ -26,6 +27,7 @@
|
|||
<Project Path="bff/hosts/Hosts.Bff.MultiFrontend/Hosts.Bff.MultiFrontend.csproj" />
|
||||
<Project Path="bff/hosts/Hosts.IdentityServer/Hosts.IdentityServer.csproj" />
|
||||
<Project Path="bff/hosts/Hosts.ServiceDefaults/Hosts.ServiceDefaults.csproj" />
|
||||
<Project Path="bff/hosts/Hosts.Bff.Performance/Hosts.Bff.Performance.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/bff/hosts/Blazor/" />
|
||||
<Folder Name="/bff/hosts/Blazor/PerComponent/">
|
||||
|
|
|
|||
Loading…
Reference in a new issue